mirror of https://github.com/JustKato/FreePad.git
Compare commits
62 Commits
Author | SHA1 | Date |
---|---|---|
|
3a6c796cac | |
|
6376fa9128 | |
|
ca10033ecf | |
|
b68e5c8e88 | |
|
84ccd44fd7 | |
|
0a1bff5cd4 | |
|
7982d564e8 | |
|
becbf30752 | |
|
796a3b07c4 | |
|
5518103575 | |
|
177ab62720 | |
|
5a8ccf20a8 | |
|
a71be11135 | |
|
30dc23c847 | |
|
0a3b5d50f2 | |
|
b4c47ded35 | |
|
6e401a416f | |
|
3b137c5ed6 | |
|
c4f6496e0e | |
|
ee9516a109 | |
|
7dcad9dc31 | |
|
1b1fe59877 | |
|
6cc1628e77 | |
|
bf144c6ecb | |
|
1d3383c8c6 | |
|
4138386fb3 | |
|
cfe2c06dac | |
|
400fd23b3e | |
|
bf1d032e68 | |
|
faff1ab527 | |
|
d056a4d429 | |
|
b710d24a2d | |
|
c3c9aacac3 | |
|
d949b3decb | |
|
662dad90b7 | |
|
1585d3b158 | |
|
1d50efe3c6 | |
|
0f5a352fc6 | |
|
6a8f4f81e5 | |
|
781b4bcf80 | |
|
f748adf132 | |
|
4bfad3ef40 | |
|
11658b4b5e | |
|
685c6ae15f | |
|
97102b98b3 | |
|
22657cc111 | |
|
3dc09cae64 | |
|
70b671c0be | |
|
6177dcecb8 | |
|
aea10baffd | |
|
42eb5add65 | |
|
53066025f0 | |
|
23dd69e060 | |
|
0bc4942924 | |
|
23fa17b840 | |
|
5414dab201 | |
|
8735837cb4 | |
|
7552cd7cd3 | |
|
74af4d1554 | |
|
916bcae961 | |
|
53b35745cd | |
|
725bc4222b |
|
@ -18,3 +18,11 @@ DEV_MODE=0
|
|||
|
||||
# Maximum file storage age in minutes, set to -1 to disable
|
||||
CLEANUP_MAX_AGE=43200 # Default is a month
|
||||
|
||||
# Maximum pad file lenght, this is in characters, a character is one byte.
|
||||
# Default: 524288 ( 500kb )
|
||||
MAXIMUM_PAD_SIZE=524288
|
||||
|
||||
# Your admin access token
|
||||
# If the value is not defined the admin interface will not be available
|
||||
# ADMIN_TOKEN=SUPER_SECRET_ADMIN_TOKEN
|
|
@ -0,0 +1,30 @@
|
|||
# 1.4.0 🖌
|
||||
Syntax highlight has been implemented, as well as a couple security concerns being patched. Thank you to everyone that has helped me out with those
|
||||
|
||||
# 1.3.0 👀
|
||||
Implemented a views system, now everyone can see how many times a pad has been accessed, an auto-save has also been added for those views to file in the `data` dir.
|
||||
|
||||
# 1.2.0 🍥
|
||||
QR Code generation has been implemented, the refresh button has been removed for the sake of keepign things simple and symmetrical. This will generate a QR code on the javascript side.
|
||||
|
||||
I have created this feature mainly for people that are trying to quickly transfer information from their computer to their phone extremely quickly, simply just type your Pad, click on the `QR Code` button and then pull your phone's camera out and get instant access, no longer typing the URL or sending the URL through some messaging app and wastring time.
|
||||
|
||||
# 1.1.1 🗒
|
||||
The freepad version has been added as a header to the response
|
||||
|
||||
# 1.1.0 🛑
|
||||
Implemented a rate-limiting system, quite primitive and basic implementation on my part since it's looking at all requests not just the POST requests, this can be bad news if someone is using the service a ton and won't truly protect from floods as it's ip-based but should offer a level of security better than none.
|
||||
|
||||
# 1.0.0 🖥
|
||||
Initial release of FreePad, this included all of the basic functionality such as:
|
||||
- Homepage
|
||||
- Generating Pads
|
||||
- Real-Time Saving
|
||||
- Download Functionality
|
||||
- Archive Functionality
|
||||
- Refresh Button
|
||||
- Dark/Light Theme Toggles
|
||||
- New look/logo
|
||||
|
||||
# 0.9 🥶
|
||||
Old version of FreePad which depended on a database for storing data, this was later dropped as it was pointless since we are only storing temporary data, so we moved to "v2"
|
26
Dockerfile
26
Dockerfile
|
@ -1,9 +1,27 @@
|
|||
FROM alpine
|
||||
# Importing golang 1.18 to use as a builder for our source
|
||||
FROM golang:1.18 as builder
|
||||
|
||||
LABEL version="1.0.0"
|
||||
# Use the /src directory as a workdir
|
||||
WORKDIR /src
|
||||
|
||||
# Copy the distribution files
|
||||
COPY ./dist /app
|
||||
# Copy the src to /src
|
||||
COPY . ./
|
||||
|
||||
# Download dependencies
|
||||
RUN go mod download
|
||||
|
||||
# Build the executable
|
||||
RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o freepad .
|
||||
|
||||
# Import alpine linux as a base
|
||||
FROM scratch
|
||||
|
||||
LABEL version="1.4.0"
|
||||
|
||||
# Copy the files from the builder to the new image
|
||||
COPY --from=builder /src/freepad /app/freepad
|
||||
COPY --from=builder /src/templates /app/templates
|
||||
COPY --from=builder /src/static /app/static
|
||||
|
||||
# Make /app the work directory
|
||||
WORKDIR /app
|
||||
|
|
21
LICENSE
21
LICENSE
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Kato Twofold
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
87
README.md
87
README.md
|
@ -5,8 +5,8 @@ Quickly create "pads" and share with others
|
|||
[](https://hub.docker.com/r/justkato/freepad)
|
||||
[](https://ko-fi.com/justkato)
|
||||

|
||||
|
||||
[](https://pad.justkato.me/)
|
||||

|
||||
|
||||
# **FreePad**
|
||||
|
||||
|
@ -19,6 +19,29 @@ The project is absolutely free to use, you can extend the code and even contribu
|
|||
|
||||
The current maintainer and creator is `Kato Twofold`
|
||||
|
||||
# 🛑 About reverse proxying 🛑
|
||||
If you are looking to reverse proxy this program, please keep in mind that the websockets have specific settings regarding reverse proxying, I have tried using `Apache2` but to no luck, if someone could give a suggestion as to how to set up my own program on `Apache2` it'd be amazing.
|
||||
On `Nginx` it's rather simple, here is my reverse proxy for the demo at [pad.justkato.me](https://pad.justkato.me/)
|
||||
```nginx
|
||||
server {
|
||||
# Define the basic information such as server name and log location
|
||||
server_name pad.justkato.me
|
||||
access_log logs/pad.justkato.me.access.log main;
|
||||
|
||||
# setup the reverse proxy
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:1626;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# WebSocket support !! Important !!
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
@ -31,3 +54,65 @@ The `.env` file contains all of the available options and you should use it to c
|
|||
If you need any help with any setting you can always open an issue over on github and get help from me.
|
||||
|
||||
If you are barely getting started with hosting your own services, or even Sys admin stuff in general or writing code my suggestion is to just copy `.env` and leave it as is until you get it running with the defaults running fine, afterwards you can play with it a little and who knows, maybe even get to learn something!
|
||||
|
||||

|
||||
|
||||
## Docker `(Recommended)`
|
||||
```bash
|
||||
# Get into a directory to run this
|
||||
mkdir ~/freepad && cd freepad
|
||||
|
||||
# Copy the latest .env and docker-compose.example.yaml files
|
||||
wget -O docker-compose.yaml https://raw.githubusercontent.com/JustKato/FreePad/master/docker-compose.example.yaml
|
||||
wget -O .env https://raw.githubusercontent.com/JustKato/FreePad/master/.env.example
|
||||
|
||||
# Edit your docker-compose.yaml and change the DOMAIN_BASE environment variable
|
||||
vim docker-compose.yaml
|
||||
|
||||
# Edit your .env file and change the variables to your liking
|
||||
vim .env
|
||||
|
||||
# Run the container
|
||||
docker-compose up
|
||||
# Run the container in the background
|
||||
docker-compose up -d
|
||||
```
|
||||
## Distribution
|
||||
[Downloads here](https://github.com/JustKato/FreePad/releases)
|
||||
```bash
|
||||
# Get into a directory to run this
|
||||
mkdir ~/freepad && cd freepad
|
||||
|
||||
# Get the latest distribution from https://github.com/JustKato/FreePad/releases
|
||||
wget -O release.zip https://github.com/JustKato/FreePad/releases/download/main/...
|
||||
|
||||
# Unzip the release
|
||||
unzip release.zip
|
||||
|
||||
# Edit the .env file
|
||||
vim ./.env
|
||||
|
||||
# Run the program
|
||||
./freepad
|
||||
```
|
||||
|
||||
## Building
|
||||
```bash
|
||||
# Clone th erepo
|
||||
git clone https://github.com/JustKato/FreePad.git ~/freepad
|
||||
|
||||
# Get into the directory
|
||||
cd ~/freepad
|
||||
|
||||
# Build using the script
|
||||
./build.sh
|
||||
|
||||
# Copy the environment variable
|
||||
cp .env dist/
|
||||
|
||||
# Go into the program's directory
|
||||
cd dist
|
||||
|
||||
# Run the program
|
||||
./freepad
|
||||
```
|
6
build.sh
6
build.sh
|
@ -2,7 +2,7 @@
|
|||
echo "Building FreePad...\n";
|
||||
|
||||
echo "Removing old build file...";
|
||||
rm dist/freepad 2> /dev/null || true
|
||||
rm dist/freepad* 2> /dev/null || true
|
||||
rm -r dist/static 2> /dev/null || true
|
||||
rm -r dist/templates 2> /dev/null || true
|
||||
rm dist/.env 2> /dev/null || true
|
||||
|
@ -10,6 +10,10 @@ rm dist/.env 2> /dev/null || true
|
|||
# Build
|
||||
echo "Building executable"
|
||||
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o ./dist/freepad .
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o ./dist/freepad-arm64 .
|
||||
CGO_ENABLED=0 GOOS=windows go build -a -installsuffix cgo -o ./dist/freepad.exe .
|
||||
CGO_ENABLED=0 GOOS=darwin go build -a -installsuffix cgo -o ./dist/freepad-darwin .
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -a -installsuffix cgo -o ./dist/freepad-darwin-64 .
|
||||
|
||||
echo "Copying templates"
|
||||
cp -r ./templates ./dist/templates
|
||||
|
|
|
@ -3,13 +3,13 @@ version: '3'
|
|||
services:
|
||||
freepad:
|
||||
# Uncomment the bellow to use the production docker image from the docker repository
|
||||
# image:
|
||||
image: justkato/freepad
|
||||
# Comment the build line if you are just looking to use a docker-compose file
|
||||
build: .
|
||||
# build: .
|
||||
# I don't recommend changing the 8080 as there would be no reason to,
|
||||
# simply change the 3113 port to anything you would like for the container to listen on
|
||||
ports:
|
||||
- 3113:8080
|
||||
- 8080:8080
|
||||
# This will read from your .env variables, in that file you will find the documentation as well
|
||||
environment:
|
||||
- DOMAIN_BASE
|
||||
|
|
3
go.mod
3
go.mod
|
@ -4,6 +4,9 @@ go 1.15
|
|||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.7.7
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/joho/godotenv v1.4.0
|
||||
github.com/mrz1836/go-sanitize v1.1.5
|
||||
github.com/ulule/limiter/v3 v3.10.0
|
||||
)
|
||||
|
|
104
go.sum
104
go.sum
|
@ -1,6 +1,11 @@
|
|||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
|
||||
|
@ -13,13 +18,36 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
|
|||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
|
||||
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
|
@ -30,32 +58,100 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLD
|
|||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mrz1836/go-sanitize v1.1.5 h1:LOywG3ijK/B/D9ik3hsniyIzA1JVZlM2wmp3Q/CBk88=
|
||||
github.com/mrz1836/go-sanitize v1.1.5/go.mod h1:HnnbbJTcBhbr770WyRL4SA95I4FFOnGg/RTLJybsuN8=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ulule/limiter/v3 v3.10.0 h1:C9mx3tgxYnt4pUYKWktZf7aEOVPbRYxR+onNFjQTEp0=
|
||||
github.com/ulule/limiter/v3 v3.10.0/go.mod h1:NqPA/r8QfP7O11iC+95X6gcWJPtRWjKrtOUw07BTvoo=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/JustKato/FreePad/lib/helper"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AdminMiddleware(router *gin.RouterGroup) {
|
||||
|
||||
// Handl
|
||||
router.Use(func(ctx *gin.Context) {
|
||||
|
||||
// Check which route we are accessing
|
||||
fmt.Println(`Accesing: `, ctx.Request.RequestURI)
|
||||
|
||||
// Check if the request is other than the login request
|
||||
if ctx.Request.RequestURI != "/admin/login" {
|
||||
// Check if the user is logged-in
|
||||
|
||||
fmt.Println(`Checking if admin`)
|
||||
|
||||
if !IsAdmin(ctx) {
|
||||
// Not an admin, redirect to homepage
|
||||
ctx.Redirect(http.StatusTemporaryRedirect, "/")
|
||||
ctx.Abort()
|
||||
|
||||
fmt.Println(`Not an admin!`)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func IsAdmin(ctx *gin.Context) bool {
|
||||
adminToken, err := ctx.Cookie("admin_token")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Encode the real token
|
||||
sha512Hasher := sha512.New()
|
||||
sha512Hasher.Write([]byte(helper.GetAdminToken()))
|
||||
hashHexToken := sha512Hasher.Sum(nil)
|
||||
trueToken := hex.EncodeToString(hashHexToken)
|
||||
|
||||
// Check if the user's admin token matches the token
|
||||
if adminToken != "" && adminToken == trueToken {
|
||||
// Yep, it's the admin!
|
||||
return true
|
||||
}
|
||||
|
||||
// Definitely not an admin
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package controllers
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
func ApplyHeaders(router *gin.Engine) {
|
||||
|
||||
router.Use(func(ctx *gin.Context) {
|
||||
// Apply the header
|
||||
ctx.Header("FreePad-Version", "1.4.0")
|
||||
|
||||
// Move on
|
||||
ctx.Next()
|
||||
})
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/JustKato/FreePad/lib/helper"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/ulule/limiter/v3"
|
||||
"github.com/ulule/limiter/v3/drivers/store/memory"
|
||||
|
||||
mgin "github.com/ulule/limiter/v3/drivers/middleware/gin"
|
||||
)
|
||||
|
||||
// Apply the rate limiter to the gin Engine
|
||||
func DoRateLimit(router *gin.Engine) {
|
||||
|
||||
// Initialize the rate limiter
|
||||
rate := limiter.Rate{
|
||||
Period: 5 * time.Minute,
|
||||
Limit: int64(helper.GetApiBanLimit()),
|
||||
}
|
||||
|
||||
// Initialize the memory storage
|
||||
store := memory.NewStore()
|
||||
|
||||
// Initialize the limiter instance
|
||||
instance := limiter.New(store, rate)
|
||||
|
||||
// Create the gin middleware
|
||||
middleWare := mgin.NewMiddleware(instance)
|
||||
|
||||
// use the middleware in gin
|
||||
router.Use(middleWare)
|
||||
}
|
|
@ -28,9 +28,23 @@ func TaskManager() {
|
|||
cleanupInterval = 1
|
||||
}
|
||||
|
||||
fmt.Println("[Task::Cleanup]: Task registered")
|
||||
for range time.Tick(time.Minute * 5) {
|
||||
objects.CleanupPosts(cleanupInterval)
|
||||
}
|
||||
// Run all handlers
|
||||
go cleanupHandler(cleanupInterval)
|
||||
go savePostHandler()
|
||||
|
||||
}
|
||||
|
||||
func savePostHandler() {
|
||||
// Save the views cache
|
||||
fmt.Println("[Task::Save]: File save registered")
|
||||
for range time.NewTicker(time.Minute * 1).C {
|
||||
objects.SavePostViewsCache()
|
||||
}
|
||||
}
|
||||
|
||||
func cleanupHandler(cleanupInterval int) {
|
||||
fmt.Println("[Task::Cleanup]: Cleanup task registered")
|
||||
for range time.NewTicker(time.Minute * 5).C {
|
||||
objects.CleanupPosts(cleanupInterval)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,25 @@ func GetDomainBase() string {
|
|||
return domainBase
|
||||
}
|
||||
|
||||
func GetApiBanLimit() int {
|
||||
banLimit, exists := os.LookupEnv("API_BAN_LIMIT")
|
||||
|
||||
if !exists {
|
||||
os.Setenv("API_BAN_LIMIT", "300")
|
||||
banLimit = "300"
|
||||
}
|
||||
|
||||
// Try and convert the string into an integer
|
||||
rez, err := strconv.Atoi(banLimit)
|
||||
// Check if the conversion has failed
|
||||
if err != nil {
|
||||
// Simply return the default
|
||||
return 300
|
||||
}
|
||||
|
||||
return rez
|
||||
}
|
||||
|
||||
func GetMaximumPadSize() int {
|
||||
// Lookup if the maximum pad size variable exists.
|
||||
maxPadSize, exists := os.LookupEnv("MAXIMUM_PAD_SIZE")
|
||||
|
@ -53,3 +72,18 @@ func GetCacheMapLimit() int {
|
|||
|
||||
return rez
|
||||
}
|
||||
|
||||
// Get the admin token used to authenticate as an admin
|
||||
func GetAdminToken() string {
|
||||
// Get the admin login from the environment
|
||||
adminToken, exists := os.LookupEnv("ADMIN_TOKEN")
|
||||
|
||||
// Check if the admin token was defined
|
||||
if !exists {
|
||||
// The admin token was not defined, disable admin logins
|
||||
return ""
|
||||
}
|
||||
|
||||
// Return the admin token
|
||||
return adminToken
|
||||
}
|
||||
|
|
|
@ -1,21 +1,166 @@
|
|||
package objects
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/JustKato/FreePad/lib/helper"
|
||||
)
|
||||
|
||||
// Initialize the views cache
|
||||
var ViewsCache map[string]uint32 = make(map[string]uint32)
|
||||
|
||||
// Mutex lock for the ViewsCache
|
||||
var viewersLock sync.Mutex
|
||||
|
||||
type Post struct {
|
||||
Name string `json:"name"`
|
||||
LastModified string `json:"last_modified"`
|
||||
Content string `json:"content"`
|
||||
Views uint32 `json:"views"`
|
||||
}
|
||||
|
||||
func (p *Post) Delete() error {
|
||||
filePath := path.Join(getStorageDirectory(), p.Name)
|
||||
|
||||
// Remove the file and return the result
|
||||
return os.Remove(filePath)
|
||||
}
|
||||
|
||||
// Get the path to the views JSON
|
||||
func getViewsFilePath() (string, error) {
|
||||
// Get the path to the storage then append the const name for the storage file
|
||||
filePath := path.Join(getStorageDirectory(), "views_storage.json")
|
||||
|
||||
// Check if the file exists
|
||||
if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) {
|
||||
// Create the file
|
||||
err := os.WriteFile(filePath, []byte(""), 0640)
|
||||
if err != nil {
|
||||
return ``, err
|
||||
}
|
||||
}
|
||||
|
||||
// Return the file path
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
// Load the views cache from file
|
||||
func LoadViewsCache() error {
|
||||
// Get the views file path
|
||||
viewsFilePath, err := getViewsFilePath()
|
||||
if err != nil {
|
||||
// This is now a problem for the upstairs
|
||||
return err
|
||||
}
|
||||
|
||||
// Read the contents of the file as a map
|
||||
f, err := os.ReadFile(viewsFilePath)
|
||||
if err != nil {
|
||||
// This is now a problem for the upstairs
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the contents are valid
|
||||
if len(f) <= 0 && len(string(f)) <= 0 {
|
||||
// The file is completely empty!
|
||||
return nil
|
||||
}
|
||||
|
||||
var parsedData map[string]uint32
|
||||
|
||||
// Parse the data
|
||||
err = json.Unmarshal(f, &parsedData)
|
||||
if err != nil {
|
||||
// This is now a problem for the function caller :D
|
||||
return err
|
||||
}
|
||||
|
||||
storageDir := getStorageDirectory()
|
||||
// Loop through all of the mapped files
|
||||
for fileName := range parsedData {
|
||||
// Grab the path to the file
|
||||
// TODO: Create a generic function that checks if a post exists by name to use here adn in the GetPost method.
|
||||
filePath := path.Join(storageDir, fileName)
|
||||
// Check if the file exists
|
||||
if _, err := os.Stat(filePath); !os.IsNotExist(err) {
|
||||
// Looks like the file does not exist anymore, remove it from the map
|
||||
delete(parsedData, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the current cache
|
||||
ViewsCache = parsedData
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func AddViewToPost(postName string, incrementViews bool) uint32 {
|
||||
// Lock the viewers mapping
|
||||
viewersLock.Lock()
|
||||
|
||||
// Check if the map has any value set to this elem
|
||||
if _, ok := ViewsCache[postName]; !ok {
|
||||
// Set the map value
|
||||
ViewsCache[postName] = 0
|
||||
}
|
||||
|
||||
if incrementViews {
|
||||
// Add to the counter
|
||||
ViewsCache[postName]++
|
||||
}
|
||||
|
||||
// Unlock
|
||||
viewersLock.Unlock()
|
||||
|
||||
// Return the value
|
||||
return ViewsCache[postName]
|
||||
}
|
||||
|
||||
func SavePostViewsCache() error {
|
||||
|
||||
data, err := json.Marshal(ViewsCache)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
viewsFilePath, err := getViewsFilePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(viewsFilePath, os.O_WRONLY|os.O_CREATE, 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Actually close the file
|
||||
defer f.Close()
|
||||
|
||||
// Delete all past content
|
||||
err = f.Truncate(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Reset pointer
|
||||
_, err = f.Seek(0, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write the json into storage
|
||||
_, err = f.Write(data)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the path to the storage directory
|
||||
func getStorageDirectory() string {
|
||||
|
||||
baseStoragePath, exists := os.LookupEnv("PAD_STORAGE_PATH")
|
||||
|
@ -26,7 +171,7 @@ func getStorageDirectory() string {
|
|||
// Check if the base storage path exists
|
||||
if _, err := os.Stat(baseStoragePath); os.IsNotExist(err) {
|
||||
// Looks like the base storage path was NOT set, create the dir
|
||||
err = os.Mkdir(baseStoragePath, 0777)
|
||||
err = os.Mkdir(baseStoragePath, 0640)
|
||||
// Check for errors
|
||||
if err != nil {
|
||||
// No way this sends an error unless it goes horribly wrong.
|
||||
|
@ -38,16 +183,21 @@ func getStorageDirectory() string {
|
|||
return baseStoragePath
|
||||
}
|
||||
|
||||
func GetPost(fileName string) Post {
|
||||
// Get a post from the file system
|
||||
func GetPost(fileName string, incrementViews bool) Post {
|
||||
// Get the base storage directory and make sure it exists
|
||||
storageDir := getStorageDirectory()
|
||||
|
||||
// Generate the file path
|
||||
filePath := fmt.Sprintf("%s%s", storageDir, fileName)
|
||||
|
||||
// Get the post views and add 1 to them
|
||||
postViews := AddViewToPost(fileName, incrementViews)
|
||||
|
||||
p := Post{
|
||||
Name: fileName,
|
||||
Content: "",
|
||||
Views: postViews,
|
||||
LastModified: "Never Before",
|
||||
}
|
||||
|
||||
|
@ -74,6 +224,7 @@ func GetPost(fileName string) Post {
|
|||
return p
|
||||
}
|
||||
|
||||
// Write a post to the file system
|
||||
func WritePost(p Post) error {
|
||||
|
||||
maximumPadSize := helper.GetMaximumPadSize()
|
||||
|
@ -98,13 +249,10 @@ func WritePost(p Post) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
// Cleanup all of the older posts based on the environment settings
|
||||
func CleanupPosts(age int) {
|
||||
// Initialize the files buffer
|
||||
var files []string
|
||||
|
@ -155,5 +303,31 @@ func CleanupPosts(age int) {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func GetAllPosts() []Post {
|
||||
// Initialize the list of posts
|
||||
postList := []Post{}
|
||||
|
||||
// Get the posts storage directory
|
||||
storageDir := getStorageDirectory()
|
||||
|
||||
// Read the directory listing
|
||||
files, err := os.ReadDir(storageDir)
|
||||
// Check if thereh as been an issues with reading the directory contents
|
||||
if err != nil {
|
||||
// Log the error
|
||||
fmt.Println("Error::GetAllPosts:", err)
|
||||
// Return an empty list to have a clean fallback
|
||||
return []Post{}
|
||||
}
|
||||
|
||||
// Go through all of the files
|
||||
for _, v := range files {
|
||||
// Process the file into a pad
|
||||
postList = append(postList, GetPost(v.Name(), false))
|
||||
}
|
||||
|
||||
// Return the post list
|
||||
return postList
|
||||
}
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/JustKato/FreePad/lib/controllers"
|
||||
"github.com/JustKato/FreePad/lib/helper"
|
||||
"github.com/JustKato/FreePad/lib/objects"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"crypto/sha512"
|
||||
)
|
||||
|
||||
var adminLoginToken string = ""
|
||||
|
||||
func AdminRoutes(router *gin.RouterGroup) {
|
||||
|
||||
adminLoginToken = helper.GetAdminToken()
|
||||
|
||||
// Apply the admin middleware for identification
|
||||
controllers.AdminMiddleware(router)
|
||||
|
||||
// Admin login route
|
||||
router.GET("/login", func(ctx *gin.Context) {
|
||||
ctx.HTML(200, "admin_login.html", gin.H{
|
||||
"title": "Login Login",
|
||||
"domain_base": helper.GetDomainBase(),
|
||||
})
|
||||
})
|
||||
|
||||
router.POST("/login", func(ctx *gin.Context) {
|
||||
|
||||
// Get the value of the admin token
|
||||
adminToken := ctx.PostForm("admin-token")
|
||||
|
||||
// Check if the input admin token matches our admin token
|
||||
if adminLoginToken != "" && adminLoginToken == adminToken {
|
||||
|
||||
sha512Hasher := sha512.New()
|
||||
sha512Hasher.Write([]byte(adminToken))
|
||||
|
||||
// Set the cookie to be an admin
|
||||
hashHexToken := sha512Hasher.Sum(nil)
|
||||
hashToken := hex.EncodeToString(hashHexToken)
|
||||
|
||||
// Set the cookie
|
||||
ctx.SetCookie("admin_token", hashToken, 60*60, "/", helper.GetDomainBase(), true, true)
|
||||
|
||||
ctx.Request.Method = "GET"
|
||||
|
||||
// Redirect the user to the admin page
|
||||
ctx.Redirect(http.StatusFound, "/admin/view")
|
||||
return
|
||||
} else {
|
||||
ctx.Request.Method = "GET"
|
||||
|
||||
// Redirect the user to the admin page
|
||||
ctx.Redirect(http.StatusFound, "/admin/login?fail")
|
||||
return
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
router.GET("/delete/:padname", func(ctx *gin.Context) {
|
||||
// Get the pad name that we bout' to delete
|
||||
padName := ctx.Param("padname")
|
||||
|
||||
// Try and get the pad, check if valid
|
||||
pad := objects.GetPost(padName, false)
|
||||
|
||||
// Delete the pad
|
||||
err := pad.Delete()
|
||||
fmt.Println(err)
|
||||
|
||||
// Redirect the user to the admin page
|
||||
ctx.Redirect(http.StatusFound, "/admin/view")
|
||||
})
|
||||
|
||||
// Admin view route
|
||||
router.GET("/view", func(ctx *gin.Context) {
|
||||
|
||||
// Get all of the pads as a listing
|
||||
padList := objects.GetAllPosts()
|
||||
|
||||
ctx.HTML(200, "admin_view.html", gin.H{
|
||||
"title": "Admin",
|
||||
"padList": padList,
|
||||
"domain_base": helper.GetDomainBase(),
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
|
@ -23,6 +24,13 @@ func HomeRoutes(router *gin.Engine) {
|
|||
// Get the post we are looking for.
|
||||
postName := c.Param("post")
|
||||
|
||||
if postName == `views_storage.json` {
|
||||
// Redirect the user to the homepage as this is a reserved keyword
|
||||
c.Redirect(http.StatusPermanentRedirect, "/")
|
||||
// Do not proceed further
|
||||
return
|
||||
}
|
||||
|
||||
// Get the maximum pad size, so that we may notify the client-side to match server-side
|
||||
maximumPadSize := helper.GetMaximumPadSize()
|
||||
|
||||
|
@ -31,15 +39,16 @@ func HomeRoutes(router *gin.Engine) {
|
|||
if err == nil {
|
||||
postName = newPostName
|
||||
}
|
||||
postName = sanitize.AlphaNumeric(postName, true)
|
||||
postName = sanitize.XSS(sanitize.SingleLine(postName))
|
||||
|
||||
post := objects.GetPost(postName)
|
||||
post := objects.GetPost(postName, true)
|
||||
|
||||
c.HTML(200, "page.html", gin.H{
|
||||
"title": postName,
|
||||
"post_content": post.Content,
|
||||
"maximumPadSize": maximumPadSize,
|
||||
"last_modified": post.LastModified,
|
||||
"views": post.Views,
|
||||
"domain_base": helper.GetDomainBase(),
|
||||
})
|
||||
})
|
||||
|
@ -54,11 +63,12 @@ func HomeRoutes(router *gin.Engine) {
|
|||
if err == nil {
|
||||
postName = newPostName
|
||||
}
|
||||
postName = sanitize.AlphaNumeric(postName, true)
|
||||
postName = sanitize.XSS(sanitize.SingleLine(postName))
|
||||
|
||||
p := objects.Post{
|
||||
Name: postName,
|
||||
Content: postContent,
|
||||
Views: 0, // This can just be ignored
|
||||
LastModified: time.Now().Format("02/01/2006 03:04:05 PM"),
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
package socketmanager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/JustKato/FreePad/lib/objects"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var wsUpgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024, // TODO: Make it configurable via the .env file
|
||||
WriteBufferSize: 1024, // TODO: Make it configurable via the .env file
|
||||
}
|
||||
|
||||
// The pad socket map caches all of the existing sockets
|
||||
var padSocketMap map[string]map[string]*websocket.Conn = make(map[string]map[string]*websocket.Conn)
|
||||
|
||||
// TODO: Use generics so that we can take string messages, that'd be nice!
|
||||
type SocketMessage struct {
|
||||
EventType string `json:"eventType"`
|
||||
PadName string `json:"padName"`
|
||||
Message map[string]interface{} `json:"message"`
|
||||
}
|
||||
|
||||
// Bind the websockets to the gin router
|
||||
func BindSocket(router *gin.RouterGroup) {
|
||||
|
||||
router.GET("/get/:pad", func(ctx *gin.Context) {
|
||||
// Get the name of the pad to assign to this socket
|
||||
padName := ctx.Param("pad")
|
||||
// Upgrade the socket connection
|
||||
webSocketUpgrade(ctx, padName)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func webSocketUpgrade(ctx *gin.Context, padName string) {
|
||||
|
||||
conn, err := wsUpgrader.Upgrade(ctx.Writer, ctx.Request, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to set websocket upgrade: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we have any sockets in this padName
|
||||
if _, ok := padSocketMap[padName]; !ok {
|
||||
// Initialize a new map of sockets
|
||||
padSocketMap[padName] = make(map[string]*websocket.Conn)
|
||||
}
|
||||
|
||||
// Give this socket a token
|
||||
socketToken := uuid.NewString()
|
||||
|
||||
// Set the current connection at the socket Token position
|
||||
padSocketMap[padName][socketToken] = conn
|
||||
|
||||
// Somone just connected
|
||||
UpdatePadStatus(padName)
|
||||
|
||||
// Start listening to this socket
|
||||
for {
|
||||
// Try Read the JSON input from the socket
|
||||
_, msg, err := conn.ReadMessage()
|
||||
|
||||
// Check if anything but a read limit was created
|
||||
if err != nil && !errors.Is(err, websocket.ErrReadLimit) {
|
||||
// Remove self from the cache
|
||||
delete(padSocketMap[padName], socketToken)
|
||||
// Somone just disconnected
|
||||
UpdatePadStatus(padName)
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// There has been an error reading the message
|
||||
fmt.Println("Failed to read from the socket but probably still connected")
|
||||
// Skip this cycle
|
||||
continue
|
||||
}
|
||||
|
||||
// Init the variable
|
||||
var p SocketMessage
|
||||
// Try and parse the json
|
||||
err = json.Unmarshal([]byte(msg), &p)
|
||||
if err != nil {
|
||||
// There has been an error reading the message
|
||||
fmt.Println("Failed to parse the JSON", err)
|
||||
// Skip this cycle
|
||||
continue
|
||||
}
|
||||
|
||||
// Pass the message to the proper handlers
|
||||
handleSocketMessage(p, socketToken, padName)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the socket's message
|
||||
func handleSocketMessage(msg SocketMessage, socketToken string, padName string) {
|
||||
|
||||
// Check if this is a pad Update
|
||||
if msg.EventType == `padUpdate` {
|
||||
handlePadUpdate(msg, socketToken, padName)
|
||||
|
||||
// Serialize the message
|
||||
serialized, err := json.Marshal(msg)
|
||||
// Check if there was an error
|
||||
if err != nil {
|
||||
fmt.Println(`Failed to broadcast the padUpdate`, err)
|
||||
// Stop the execution
|
||||
return
|
||||
}
|
||||
|
||||
// Alert all the other pads other than this one.
|
||||
for k, pad := range padSocketMap[padName] {
|
||||
// Check if this is the same socket.
|
||||
if k == socketToken {
|
||||
// Skip self
|
||||
continue
|
||||
}
|
||||
|
||||
// Send the message to the others.
|
||||
pad.WriteMessage(websocket.TextMessage, serialized)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func handlePadUpdate(msg SocketMessage, socketToken string, padName string) {
|
||||
|
||||
// Check if the msg content is valid
|
||||
if _, ok := msg.Message[`content`]; !ok {
|
||||
fmt.Printf("Failed to update pad %s, invalid message\n", padName)
|
||||
return
|
||||
}
|
||||
|
||||
// Check that the content is string
|
||||
newPadContent, ok := msg.Message[`content`].(string)
|
||||
if !ok {
|
||||
fmt.Printf("Type assertion failed for %s, invalid message\n", padName)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the pad
|
||||
pad := objects.GetPost(padName, false)
|
||||
// Update the pad contents
|
||||
pad.Content = newPadContent
|
||||
|
||||
// Save to file
|
||||
objects.WritePost(pad)
|
||||
}
|
||||
|
||||
// Update the current users of the pad about the amount of live viewers.
|
||||
func UpdatePadStatus(padName string) {
|
||||
|
||||
// Grab info about the map's key
|
||||
sockets, ok := padSocketMap[padName]
|
||||
// Check if the pad is set and has sockets connected.
|
||||
if !ok || len(sockets) < 1 {
|
||||
// Quit
|
||||
return
|
||||
}
|
||||
|
||||
// Generate the message
|
||||
msg := SocketMessage{
|
||||
EventType: `statusUpdate`,
|
||||
PadName: padName,
|
||||
Message: gin.H{
|
||||
// Send the current amount of live viewers
|
||||
"currentViewers": len(sockets),
|
||||
},
|
||||
}
|
||||
|
||||
BroadcastMessage(padName, msg)
|
||||
|
||||
}
|
||||
|
||||
func BroadcastMessage(padName string, msg SocketMessage) {
|
||||
|
||||
// Grab info about the map's key
|
||||
sockets, ok := padSocketMap[padName]
|
||||
// Check if the pad is set and has sockets connected.
|
||||
if !ok || len(sockets) < 1 {
|
||||
// Quit
|
||||
return
|
||||
}
|
||||
|
||||
// Get all the participants of the pad group
|
||||
for _, s := range sockets {
|
||||
// Send the message to the socket
|
||||
s.WriteJSON(msg)
|
||||
}
|
||||
|
||||
}
|
22
main.go
22
main.go
|
@ -1,10 +1,13 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/JustKato/FreePad/lib/controllers"
|
||||
"github.com/JustKato/FreePad/lib/objects"
|
||||
"github.com/JustKato/FreePad/lib/routes"
|
||||
"github.com/JustKato/FreePad/lib/socketmanager"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
@ -22,18 +25,37 @@ func main() {
|
|||
// Run the TaskManager
|
||||
go controllers.TaskManager()
|
||||
|
||||
// Load in the views data from storage
|
||||
err := objects.LoadViewsCache()
|
||||
if err != nil {
|
||||
fmt.Println("Failed to load views from cache")
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
// Initialize the router
|
||||
router := gin.Default()
|
||||
|
||||
// Apply the FreePad Headers
|
||||
controllers.ApplyHeaders(router)
|
||||
|
||||
// Read HTML Templates
|
||||
router.LoadHTMLGlob("templates/**/*.html")
|
||||
|
||||
// Load in static path
|
||||
router.Static("/static", "static/")
|
||||
|
||||
// Implement the rate limiter
|
||||
controllers.DoRateLimit(router)
|
||||
|
||||
// Admin Routing
|
||||
routes.AdminRoutes(router.Group("/admin"))
|
||||
|
||||
// Add Routes
|
||||
routes.HomeRoutes(router)
|
||||
|
||||
// Bind the Web Sockets
|
||||
socketmanager.BindSocket(router.Group("/ws"))
|
||||
|
||||
router.Run(":8080")
|
||||
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300&display=swap');
|
||||
|
||||
:root {
|
||||
--color-border-default: #444c56;
|
||||
--color-fg-default: #adbac7;
|
||||
|
@ -35,6 +37,74 @@ main#main-card {
|
|||
right: .5rem;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#pad-content, #textarea-preview {
|
||||
tab-size: 2;
|
||||
|
||||
font-family: 'Roboto Mono', monospace !important;
|
||||
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
#padTitle {
|
||||
padding: .3rem .75rem !important;
|
||||
border-radius: .25rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dark #padTitle {
|
||||
color: #db3384;
|
||||
background-color: rgba(0, 0, 0, 0.10);
|
||||
}
|
||||
|
||||
.light #padTitle {
|
||||
color: #555273;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
#pad-content-area {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
.light .edit-content-text {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.edit-content-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.read-only-content .edit-content-text {
|
||||
margin-top: 1rem;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.read-only-content .view-content-text {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#pad-content-toggler {
|
||||
position: absolute;
|
||||
top: .5rem;
|
||||
right: .5rem;
|
||||
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#textarea-preview {
|
||||
max-height: calc(17rem + 30vh);
|
||||
min-height: 17rem;
|
||||
overflow: auto;
|
||||
|
||||
padding-top: 2rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
textarea:focus,
|
||||
input[type="text"]:focus,
|
||||
|
@ -56,3 +126,27 @@ input[type="color"]:focus,
|
|||
box-shadow: none;
|
||||
outline: 0 none;
|
||||
}
|
||||
|
||||
|
||||
/* ===== Scrollbar CSS ===== */
|
||||
/* Firefox */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #3b3b3b #ffffff00;
|
||||
}
|
||||
|
||||
/* Chrome, Edge, and Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: #ffffff00;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: #3b3b3b;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #ffffff00;
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
/**************************\
|
||||
Basic Modal Styles
|
||||
\**************************/
|
||||
|
||||
.modal {
|
||||
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica, ubuntu, roboto, noto, segoe ui, arial, sans-serif;
|
||||
}
|
||||
|
||||
.modal__overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal__container {
|
||||
background-color: #fff;
|
||||
padding: 30px;
|
||||
max-width: 500px;
|
||||
max-height: 100vh;
|
||||
border-radius: 4px;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.modal__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal__title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.25;
|
||||
color: #555273;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.modal__close {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.modal__header .modal__close:before {
|
||||
content: "\2715";
|
||||
}
|
||||
|
||||
.modal__content {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
line-height: 1.5;
|
||||
color: rgba(0, 0, 0, .8);
|
||||
}
|
||||
|
||||
.modal__btn {
|
||||
font-size: .875rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
padding-top: .5rem;
|
||||
padding-bottom: .5rem;
|
||||
background-color: #e6e6e6;
|
||||
color: rgba(0, 0, 0, .8);
|
||||
border-radius: .25rem;
|
||||
border-style: none;
|
||||
border-width: 0;
|
||||
cursor: pointer;
|
||||
-webkit-appearance: button;
|
||||
text-transform: none;
|
||||
overflow: visible;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
will-change: transform;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
-webkit-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
transition: -webkit-transform .25s ease-out;
|
||||
transition: transform .25s ease-out;
|
||||
transition: transform .25s ease-out, -webkit-transform .25s ease-out;
|
||||
}
|
||||
|
||||
.modal__btn:focus,
|
||||
.modal__btn:hover {
|
||||
-webkit-transform: scale(1.05);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.modal__btn-primary {
|
||||
background-color: #555273;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**************************\
|
||||
Demo Animation Style
|
||||
\**************************/
|
||||
@keyframes mmfadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes mmfadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes mmslideIn {
|
||||
from {
|
||||
transform: translateY(15%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes mmslideOut {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(-10%);
|
||||
}
|
||||
}
|
||||
|
||||
.micromodal-slide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.micromodal-slide.is-open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.micromodal-slide[aria-hidden="false"] .modal__overlay {
|
||||
animation: mmfadeIn .3s cubic-bezier(0.0, 0.0, 0.2, 1);
|
||||
}
|
||||
|
||||
.micromodal-slide[aria-hidden="false"] .modal__container {
|
||||
animation: mmslideIn .3s cubic-bezier(0, 0, .2, 1);
|
||||
}
|
||||
|
||||
.micromodal-slide[aria-hidden="true"] .modal__overlay {
|
||||
animation: mmfadeOut .3s cubic-bezier(0.0, 0.0, 0.2, 1);
|
||||
}
|
||||
|
||||
.micromodal-slide[aria-hidden="true"] .modal__container {
|
||||
animation: mmslideOut .3s cubic-bezier(0, 0, .2, 1);
|
||||
}
|
||||
|
||||
.micromodal-slide .modal__container,
|
||||
.micromodal-slide .modal__overlay {
|
||||
will-change: transform;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
|
@ -3,12 +3,12 @@ function sendMyData(el) {
|
|||
const formData = new FormData();
|
||||
|
||||
// Check if the writing watch was sending something already
|
||||
if ( !!window.writingWatch ) {
|
||||
if (!!window.writingWatch) {
|
||||
// Clear old timeout
|
||||
clearTimeout(window.writingWatch);
|
||||
}
|
||||
|
||||
if ( el.value.length > maximumPadSize ) {
|
||||
if (el.value.length > maximumPadSize) {
|
||||
let err = new Error(`Your Pad is too big! Please keep it limited to ${maximumPadSize} characters!`);
|
||||
alert(err);
|
||||
throw err;
|
||||
|
@ -16,6 +16,13 @@ function sendMyData(el) {
|
|||
|
||||
el.setAttribute(`readonly`, `1`);
|
||||
|
||||
const textareaPreview = document.getElementById(`textarea-preview`)
|
||||
if (!!textareaPreview) {
|
||||
textareaPreview.textContent = el.value;
|
||||
|
||||
hljs.highlightElement(document.getElementById(`textarea-preview`));
|
||||
}
|
||||
|
||||
formData.set("content", el.value);
|
||||
|
||||
updateStatus(`Attempting to save...`, `text-warning`);
|
||||
|
@ -24,22 +31,22 @@ function sendMyData(el) {
|
|||
body: formData,
|
||||
method: "post",
|
||||
})
|
||||
.then( resp => {
|
||||
.then(resp => {
|
||||
resp.json()
|
||||
.then( e => {
|
||||
.then(e => {
|
||||
document.getElementById(`last_modified_`).value = e.pad.last_modified;
|
||||
updateStatus(`Succesfully Saved`, `text-success`);
|
||||
})
|
||||
.catch( err => {
|
||||
.catch(err => {
|
||||
updateStatus(`Failed to Save`, `text-danger`);
|
||||
console.error(err);
|
||||
})
|
||||
})
|
||||
.catch( err => {
|
||||
.catch(err => {
|
||||
updateStatus(`Failed to Save`, `text-danger`);
|
||||
console.error(err);
|
||||
})
|
||||
.finally( () => {
|
||||
.finally(() => {
|
||||
el.removeAttribute(`readonly`);
|
||||
})
|
||||
}
|
||||
|
@ -47,13 +54,13 @@ function sendMyData(el) {
|
|||
function toggleWritingWatch(el) {
|
||||
|
||||
// Check if the writing watch was sending something already
|
||||
if ( !!window.writingWatch ) {
|
||||
if (!!window.writingWatch) {
|
||||
// Clear old timeout
|
||||
clearTimeout(window.writingWatch);
|
||||
}
|
||||
|
||||
// Set a timeout for the action
|
||||
window.writingWatch = setTimeout( () => {
|
||||
window.writingWatch = setTimeout(() => {
|
||||
// Send out the data
|
||||
sendMyData(el)
|
||||
}, 750)
|
||||
|
@ -74,7 +81,7 @@ function getLocalArchives() {
|
|||
let a = localStorage.getItem(`${padTitle}_archives`);
|
||||
|
||||
// Check if we had anything in storage for the archives
|
||||
if ( a == null ) {
|
||||
if (a == null) {
|
||||
// There were nothing in storage
|
||||
return [];
|
||||
}
|
||||
|
@ -82,7 +89,7 @@ function getLocalArchives() {
|
|||
try {
|
||||
// Try and parse the json
|
||||
a = JSON.parse(a);
|
||||
} catch ( err ) {
|
||||
} catch (err) {
|
||||
// Return null of the fail
|
||||
return [];
|
||||
}
|
||||
|
@ -93,7 +100,7 @@ function getLocalArchives() {
|
|||
function storeArchives(archives) {
|
||||
|
||||
// Check if the provided list is an array
|
||||
if ( !Array.isArray(archives) ) return;
|
||||
if (!Array.isArray(archives)) return;
|
||||
|
||||
// Set the current archives
|
||||
localStorage.setItem(`${padTitle}_archives`, JSON.stringify(archives));
|
||||
|
@ -105,13 +112,13 @@ function renderArchivesSelection() {
|
|||
const archivesSelection = document.getElementById(`archives-selection`);
|
||||
const rowTemplate = document.getElementById(`archive-selection-example`);
|
||||
// Clear any old optiosn
|
||||
archivesSelection.querySelectorAll(`.dropdown-item:not(#do-archive-button):not(#archive-selection-example)`).forEach( el => {
|
||||
archivesSelection.querySelectorAll(`.dropdown-item:not(#do-archive-button):not(#archive-selection-example)`).forEach(el => {
|
||||
// Remove the element
|
||||
el.remove();
|
||||
})
|
||||
|
||||
// Get the current list of available archives
|
||||
for ( let a of getLocalArchives() ) {
|
||||
for (let a of getLocalArchives()) {
|
||||
// Clone the template row
|
||||
const row = rowTemplate.cloneNode(true);
|
||||
|
||||
|
@ -130,8 +137,11 @@ function renderArchivesSelection() {
|
|||
|
||||
let resp = confirm("Load contents of pad from memory? This will overwrite the current pad for everyone.");
|
||||
|
||||
if ( !!resp ) {
|
||||
document.getElementById(`pad-content`).value = a.content;
|
||||
if (!!resp) {
|
||||
// Update visually for the client
|
||||
updatePadContent(a.content);
|
||||
// Send the update
|
||||
window.socket.sendPadUpdate();
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -142,7 +152,7 @@ function renderArchivesSelection() {
|
|||
function saveLocalArchive() {
|
||||
let resp = confirm("Save a local copy of the current Pad?");
|
||||
|
||||
if ( !resp ) {
|
||||
if (!resp) {
|
||||
// Do not
|
||||
return;
|
||||
}
|
||||
|
@ -166,11 +176,55 @@ function saveLocalArchive() {
|
|||
|
||||
}
|
||||
|
||||
document.addEventListener(`DOMContentLoaded`, e => {
|
||||
function generateQRCode() {
|
||||
var qrcodeContainer = document.getElementById(`qrcode`)
|
||||
// Remove old contents
|
||||
qrcodeContainer.innerHTML = "";
|
||||
// Add new qr
|
||||
new QRCode(qrcodeContainer, {
|
||||
text: window.location.toString(),
|
||||
width: 256,
|
||||
height: 256,
|
||||
colorDark: "#555273",
|
||||
colorLight: "#ffffff",
|
||||
correctLevel: QRCode.CorrectLevel.H
|
||||
});
|
||||
|
||||
{ // Textarea Focusing
|
||||
// Open the modal
|
||||
MicroModal.show(`qrmodal`)
|
||||
}
|
||||
|
||||
function toggleTextareaPreview() {
|
||||
setTextareaPreview(!document.getElementById(`pad-content-toggler`).classList.contains(`read-only`))
|
||||
}
|
||||
|
||||
// t == true - Read Only
|
||||
// t == false - Edit mode
|
||||
function setTextareaPreview(t = true) {
|
||||
const prev = document.getElementById(`textarea-preview`)
|
||||
const textarea = document.getElementById(`pad-content`);
|
||||
const toggler = document.getElementById(`pad-content-toggler`);
|
||||
const padContentArea = document.getElementById(`pad-content-area`);
|
||||
|
||||
if (t) {
|
||||
// Toggle read only
|
||||
prev.classList.remove(`hidden`)
|
||||
toggler.classList.add(`read-only`);
|
||||
|
||||
padContentArea.classList.add(`read-only-content`);
|
||||
|
||||
prev.scrollTop = prev.scrollHeight;
|
||||
|
||||
textarea.classList.add(`hidden`);
|
||||
} else {
|
||||
// Toggle edit mode
|
||||
prev.classList.add(`hidden`)
|
||||
toggler.classList.remove(`read-only`);
|
||||
|
||||
padContentArea.classList.remove(`read-only-content`);
|
||||
|
||||
|
||||
textarea.classList.remove(`hidden`);
|
||||
// Focus
|
||||
textarea.focus();
|
||||
// Scroll
|
||||
|
@ -179,6 +233,38 @@ document.addEventListener(`DOMContentLoaded`, e => {
|
|||
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
document.addEventListener(`DOMContentLoaded`, e => {
|
||||
|
||||
{ // Textarea Handling
|
||||
const textarea = document.getElementById(`pad-content`);
|
||||
setTextareaPreview(!!textarea.value);
|
||||
|
||||
// Make sure tabs are taken into consideration
|
||||
textarea.addEventListener('keydown', function (e) {
|
||||
if (e.key == 'Tab') {
|
||||
e.preventDefault();
|
||||
const start = this.selectionStart;
|
||||
const end = this.selectionEnd;
|
||||
|
||||
// set textarea value to: text before caret + tab + text after caret
|
||||
this.value = this.value.substring(0, start) +
|
||||
"\t" + this.value.substring(end);
|
||||
|
||||
// put caret at right position again
|
||||
this.selectionStart =
|
||||
this.selectionEnd = start + 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try { // highlights
|
||||
hljs.highlightElement(document.getElementById(`textarea-preview`));
|
||||
} catch ( err ) {
|
||||
console.err(err);
|
||||
}
|
||||
|
||||
{ // Archives
|
||||
renderArchivesSelection()
|
||||
}
|
||||
|
|
|
@ -14,8 +14,14 @@ class Pad {
|
|||
// Create a new blob of the contents of the pad
|
||||
var blob = new Blob([ document.getElementById(`pad-content`).value ], { type: "text/plain;charset=utf-8" });
|
||||
|
||||
let downloadFileName = this.title;
|
||||
if ( !this.title.includes(`.`) ) {
|
||||
// Append a default file format
|
||||
downloadFileName += `.txt`;
|
||||
}
|
||||
|
||||
// Save the blob as
|
||||
saveAs(blob, `${this.title}.txt`);
|
||||
saveAs(blob, `${downloadFileName}`);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,261 @@
|
|||
class PadSocket {
|
||||
|
||||
/**
|
||||
* @type {WebSocket}
|
||||
*/
|
||||
ws = null;
|
||||
/**
|
||||
* @type {String}
|
||||
*/
|
||||
padName = null;
|
||||
|
||||
/**
|
||||
* The actual textarea you write in
|
||||
* @type {HTMLTextAreaElement}
|
||||
*/
|
||||
padContents = null;
|
||||
/**
|
||||
* The <code> of the preview
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
padPreview = null;
|
||||
|
||||
/**
|
||||
* Create a new PadSocket
|
||||
* @param {string} padName The name of the pad
|
||||
* @param {string} connUrl The URL to the websocket
|
||||
*/
|
||||
constructor(padName, connUrl = null) {
|
||||
// Assign the pad name
|
||||
this.padName = padName;
|
||||
|
||||
// Check if a connection URL was mentioned
|
||||
if ( connUrl == null ) {
|
||||
|
||||
let connProtocol = `ws://`;
|
||||
if ( window.location.protocol == `https:` ) {
|
||||
connProtocol = `wss://`;
|
||||
}
|
||||
|
||||
// Try and connect to the local websocket
|
||||
connUrl = connProtocol + window.location.host + `/ws/get/${padName}`;
|
||||
}
|
||||
|
||||
// Connect to the websocket
|
||||
const ws = new WebSocket(connUrl);
|
||||
|
||||
// Bind the onMessage function
|
||||
ws.onmessage = this.handleMessage;
|
||||
|
||||
ws.onopen = () => {
|
||||
updateStatus(`Established`, `text-success`);
|
||||
}
|
||||
|
||||
function onFail() {
|
||||
updateStatus(`Connection Failed`, `text-dangerous`);
|
||||
}
|
||||
|
||||
// Try and reconnect on failure
|
||||
ws.onclose = onFail;
|
||||
ws.onerror = onFail;
|
||||
|
||||
// Assign the websocket
|
||||
this.ws = ws;
|
||||
|
||||
// Get all relevant references from the HTML
|
||||
this.padContents = document.getElementById(`pad-content`);
|
||||
this.padPreview = document.getElementById(`textarea-preview`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Send a message to the server
|
||||
* @param {string} eventType The type of event, this can be anything really, it's just used for routing by the server
|
||||
* @param {Object} message The message to send out to the server, this can only be of format string but JSON is parsed.
|
||||
*/
|
||||
sendMessage = (eventType, message) => {
|
||||
|
||||
if ( this.ws.readyState !== WebSocket.OPEN ) {
|
||||
throw new Error(`The websocket connection is not active`);
|
||||
}
|
||||
|
||||
// Check if the message is a string
|
||||
if ( typeof message !== 'object' ) {
|
||||
// Convert the message into a map[string]interface{}
|
||||
message = {
|
||||
"message": message,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Compress the message, usually we will be sending the whole body of the pad from the client to the server or vice-versa.
|
||||
this.ws.send( JSON.stringify({
|
||||
eventType,
|
||||
padName: this.padName,
|
||||
message,
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the message from the socket based on the message type
|
||||
* @param {MessageEvent} e The websocket message
|
||||
*/
|
||||
handleMessage = ev => {
|
||||
updateStatus(`Catching Message`, `text-white`);
|
||||
|
||||
// Check if the message has valid data
|
||||
if ( !!ev.data ) {
|
||||
// Try and parse the data
|
||||
let parsedData = null;
|
||||
|
||||
try {
|
||||
parsedData = JSON.parse(ev.data);
|
||||
} catch ( err ) {
|
||||
console.error(`Failed to parse the WebSocket data`,err);
|
||||
updateStatus(`Parse Fail`, `text-warning`);
|
||||
}
|
||||
|
||||
if ( !!!parsedData['message'] ) {
|
||||
console.error(`Failed to find the message`)
|
||||
updateStatus(`Message Fail`, `text-warning`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a pad Content Update
|
||||
if ( parsedData['eventType'] === `padUpdate`) {
|
||||
// Pass on the parsed data
|
||||
this.onPadUpdate(parsedData);
|
||||
} // Check if this is a pad Status Update
|
||||
else if ( parsedData['eventType'] === `statusUpdate`) {
|
||||
// Pass on the parsed data
|
||||
this.onStatusUpdate(parsedData);
|
||||
}
|
||||
|
||||
updateStatus(`Established`, `text-success`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whenever a pad update is trigered, run this function
|
||||
* @param {Object} The response from the server
|
||||
*/
|
||||
onPadUpdate = data => {
|
||||
// Check that the content is clear
|
||||
if ( !!data['message']['content'] ) {
|
||||
// Send over the new content to be updated.
|
||||
updatePadContent(data['message']['content']);
|
||||
}
|
||||
}
|
||||
|
||||
onStatusUpdate = data => {
|
||||
// Check that the content is clear
|
||||
if ( !!data['message']['currentViewers'] ) {
|
||||
// Get the amount of viewers reported by the server
|
||||
const viewerCount = Number(data['message']['currentViewers']);
|
||||
// Check if this is a valid number
|
||||
if ( Number.isNaN(viewerCount) ) {
|
||||
// Looks like this is a malformed message
|
||||
return console.error(`Malformed Message`, data);
|
||||
}
|
||||
|
||||
// Send over the new content to be updated.
|
||||
updatePadViewers(viewerCount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sending a pad update for each keystroke to the server.
|
||||
* @param {String} msg The new contents of the pad
|
||||
*/
|
||||
sendPadUpdate = msg => {
|
||||
// Get the contents of the pad
|
||||
const padContents = this.padContents.value;
|
||||
|
||||
// Send the data over the webSocket
|
||||
this.sendMessage(`padUpdate`, {
|
||||
"content": padContents,
|
||||
});
|
||||
|
||||
updatePadContent(padContents, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the contents of the pad
|
||||
* @param {String} newContent
|
||||
*/
|
||||
function updatePadContent(newContent, textArea = true) {
|
||||
// Update the textarea
|
||||
if ( textArea ) {
|
||||
document.getElementById(`pad-content`).value = newContent;
|
||||
}
|
||||
|
||||
// Update the preview
|
||||
const prev = document.getElementById(`textarea-preview`);
|
||||
const shouldScroll = prev.scrollTop >= (prev.scrollHeight - Number(getComputedStyle(prev).height.replace(/px/g, ''))) * 0.98;
|
||||
|
||||
prev.innerHTML = escapeHtml(newContent);
|
||||
|
||||
prev.classList.remove(`language-undefined`);
|
||||
|
||||
prev.classList.forEach( c => {
|
||||
if ( c.indexOf(`language-`) != -1 ) {
|
||||
prev.classList.remove(c);
|
||||
}
|
||||
})
|
||||
|
||||
try { // highlights
|
||||
hljs.highlightElement(document.getElementById(`textarea-preview`));
|
||||
} catch ( err ) {
|
||||
console.err(err);
|
||||
}
|
||||
|
||||
// Check if we should follow the bottom scrolling
|
||||
if (shouldScroll) {
|
||||
prev.scrollTop = prev.scrollHeight;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function updatePadViewers(vc) {
|
||||
// Get the reference to the viewers count inputElement
|
||||
/**
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
const viewerCount = document.getElementById(`currentViewers`);
|
||||
|
||||
// Get the amount of total viewers
|
||||
const totalViews = viewerCount.value.split("|")[1].trim();
|
||||
|
||||
// Set back the real value
|
||||
viewerCount.value = `${vc} | ${totalViews}`;
|
||||
}
|
||||
|
||||
function connectSocket() {
|
||||
// Check if the socket is established
|
||||
if ( !!!window.socket || window.socket.readyState !== WebSocket.OPEN ) {
|
||||
updateStatus(`Connecting...`, `text-warning`);
|
||||
// Connect the socket
|
||||
window.socket = new PadSocket(padTitle);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TODO: Test if this is actually necessary or the DOMContentLoaded event would suffice
|
||||
// wait for the whole window to load
|
||||
window.addEventListener(`load`, e => {
|
||||
connectSocket()
|
||||
})
|
||||
|
||||
|
||||
// lol
|
||||
function escapeHtml(html){
|
||||
const text = document.createTextNode(html);
|
||||
const p = document.createElement('p');
|
||||
|
||||
p.appendChild(text);
|
||||
const content = p.innerHTML;
|
||||
p.remove();
|
||||
|
||||
return content;
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,6 @@
|
|||
/*!
|
||||
* Bootstrap-Dark-5 v1.1.3 (https://vinorodrigues.github.io/bootstrap-dark-5/)
|
||||
* Copyright 2021 Vino Rodrigues
|
||||
* Licensed under MIT (https://github.com/vinorodrigues/bootstrap-dark-5/blob/main/LICENSE.md)
|
||||
*/
|
||||
"use strict";class DarkMode{constructor(){this._hasGDPRConsent=!1,this.cookieExpiry=365,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",(function(){DarkMode.onDOMContentLoaded()})):DarkMode.onDOMContentLoaded()}get inDarkMode(){return DarkMode.getColorScheme()==DarkMode.VALUE_DARK}set inDarkMode(e){this.setDarkMode(e,!1)}get hasGDPRConsent(){return this._hasGDPRConsent}set hasGDPRConsent(e){if(this._hasGDPRConsent=e,e){const e=DarkMode.readCookie(DarkMode.DATA_KEY);e&&(DarkMode.saveCookie(DarkMode.DATA_KEY,"",-1),localStorage.setItem(DarkMode.DATA_KEY,e))}else{const e=localStorage.getItem(DarkMode.DATA_KEY);e&&(localStorage.removeItem(DarkMode.DATA_KEY),DarkMode.saveCookie(DarkMode.DATA_KEY,e))}}get documentRoot(){return document.getElementsByTagName("html")[0]}static saveCookie(e,o="",t=365){let a="";if(t){const e=new Date;e.setTime(e.getTime()+24*t*60*60*1e3),a="; expires="+e.toUTCString()}document.cookie=e+"="+o+a+"; SameSite=Strict; path=/"}saveValue(e,o,t=this.cookieExpiry){this.hasGDPRConsent?DarkMode.saveCookie(e,o,t):localStorage.setItem(e,o)}static readCookie(e){const o=e+"=",t=document.cookie.split(";");for(let e=0;e<t.length;e++){const a=t[e].trim();if(a.startsWith(o))return a.substring(o.length)}return""}readValue(e){if(this.hasGDPRConsent)return DarkMode.readCookie(e);{const o=localStorage.getItem(e);return o||""}}eraseValue(e){this.hasGDPRConsent?this.saveValue(e,"",-1):localStorage.removeItem(e)}getSavedColorScheme(){const e=this.readValue(DarkMode.DATA_KEY);return e||""}getPreferedColorScheme(){return window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?DarkMode.VALUE_DARK:window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches?DarkMode.VALUE_LIGHT:""}setDarkMode(e,o=!0){const t=document.querySelectorAll("[data-"+DarkMode.DATA_SELECTOR+"]");if(0==t.length)e?(this.documentRoot.classList.remove(DarkMode.CLASS_NAME_LIGHT),this.documentRoot.classList.add(DarkMode.CLASS_NAME_DARK)):(this.documentRoot.classList.remove(DarkMode.CLASS_NAME_DARK),this.documentRoot.classList.add(DarkMode.CLASS_NAME_LIGHT));else for(let o=0;o<t.length;o++)t[o].setAttribute("data-"+DarkMode.DATA_SELECTOR,e?DarkMode.VALUE_DARK:DarkMode.VALUE_LIGHT);o&&this.saveValue(DarkMode.DATA_KEY,e?DarkMode.VALUE_DARK:DarkMode.VALUE_LIGHT)}toggleDarkMode(e=!0){let o;const t=document.querySelector("[data-"+DarkMode.DATA_SELECTOR+"]");o=t?t.getAttribute("data-"+DarkMode.DATA_SELECTOR)==DarkMode.VALUE_DARK:this.documentRoot.classList.contains(DarkMode.CLASS_NAME_DARK),this.setDarkMode(!o,e)}resetDarkMode(){this.eraseValue(DarkMode.DATA_KEY);const e=this.getPreferedColorScheme();if(e)this.setDarkMode(e==DarkMode.VALUE_DARK,!1);else{const e=document.querySelectorAll("[data-"+DarkMode.DATA_SELECTOR+"]");if(0==e.length)this.documentRoot.classList.remove(DarkMode.CLASS_NAME_LIGHT),this.documentRoot.classList.remove(DarkMode.CLASS_NAME_DARK);else for(let o=0;o<e.length;o++)e[o].setAttribute("data-"+DarkMode.DATA_SELECTOR,"")}}static getColorScheme(){const e=document.querySelector("[data-"+DarkMode.DATA_SELECTOR+"]");if(e){const o=e.getAttribute("data-"+DarkMode.DATA_SELECTOR);return o==DarkMode.VALUE_DARK||o==DarkMode.VALUE_LIGHT?o:""}return darkmode.documentRoot.classList.contains(DarkMode.CLASS_NAME_DARK)?DarkMode.VALUE_DARK:darkmode.documentRoot.classList.contains(DarkMode.CLASS_NAME_LIGHT)?DarkMode.VALUE_LIGHT:""}static updatePreferedColorSchemeEvent(){let e=darkmode.getSavedColorScheme();e||(e=darkmode.getPreferedColorScheme(),e&&darkmode.setDarkMode(e==DarkMode.VALUE_DARK,!1))}static onDOMContentLoaded(){let e=darkmode.readValue(DarkMode.DATA_KEY);e||(e=DarkMode.getColorScheme(),e||(e=darkmode.getPreferedColorScheme()));const o=e==DarkMode.VALUE_DARK;darkmode.setDarkMode(o,!1),window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",(function(){DarkMode.updatePreferedColorSchemeEvent()}))}}DarkMode.DATA_KEY="bs.prefers-color-scheme",DarkMode.DATA_SELECTOR="bs-color-scheme",DarkMode.VALUE_LIGHT="light",DarkMode.VALUE_DARK="dark",DarkMode.CLASS_NAME_LIGHT="light",DarkMode.CLASS_NAME_DARK="dark";const darkmode=new DarkMode;
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,98 @@
|
|||
/*!
|
||||
Theme: a11y-dark
|
||||
Author: @ericwbailey
|
||||
Maintainer: @ericwbailey
|
||||
|
||||
Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
background: #2b2b2b;
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
/* Comment */
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #d4d0ab;
|
||||
}
|
||||
|
||||
/* Red */
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-regexp,
|
||||
.hljs-deletion {
|
||||
color: #ffa07a;
|
||||
}
|
||||
|
||||
/* Orange */
|
||||
.hljs-number,
|
||||
.hljs-built_in,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params,
|
||||
.hljs-meta,
|
||||
.hljs-link {
|
||||
color: #f5ab35;
|
||||
}
|
||||
|
||||
/* Yellow */
|
||||
.hljs-attribute {
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
/* Green */
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-addition {
|
||||
color: #abe338;
|
||||
}
|
||||
|
||||
/* Blue */
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #00e0e0;
|
||||
}
|
||||
|
||||
/* Purple */
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: #dcc6e0;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media screen and (-ms-high-contrast: active) {
|
||||
.hljs-addition,
|
||||
.hljs-attribute,
|
||||
.hljs-built_in,
|
||||
.hljs-bullet,
|
||||
.hljs-comment,
|
||||
.hljs-link,
|
||||
.hljs-literal,
|
||||
.hljs-meta,
|
||||
.hljs-number,
|
||||
.hljs-params,
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-type,
|
||||
.hljs-quote {
|
||||
color: highlight;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
|||
{{ define "inc/footer.html"}}
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/js/darkmode.min.js"></script>
|
||||
<script src="/static/vendor/bootstrap/darkmode.min.js"></script>
|
||||
<script src="/static/js/main.js"></script>
|
||||
{{ end }}
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
<meta name="color-scheme" content="light dark">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightshade.min.css" rel="stylesheet">
|
||||
<link href="/static/vendor/bootstrap/bootstrap-nightshade.min.css" rel="stylesheet">
|
||||
<!-- Love https://vinorodrigues.github.io/bootstrap-dark-5/ -->
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
{{ template "inc/header.html" .}}
|
||||
|
||||
<body>
|
||||
|
||||
<main id="main-card" class="container rounded mt-5 shadow-sm">
|
||||
<div class="p-3">
|
||||
|
||||
<a href="/" class="logo-container w-100 d-flex mb-4">
|
||||
<img src="/static/img/logo_transparent.png" alt="Logo" style="max-width: 50%; margin: 0 auto;" class="mx-auto">
|
||||
</a>
|
||||
|
||||
<div class="form-group my-4">
|
||||
<form class="search-action input-group" method="post" action="/admin/login">
|
||||
<input autocomplete="off" type="password" class="form-control form-control-lg" name="admin-token" placeholder="Your Admin token" aria-label="Your Admin token" aria-describedby="admin-token-button" id="admin-token">
|
||||
|
||||
<button class="btn btn-primary" type="submit" id="admin-token-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24 " height="24 " fill="currentColor" class="bi bi-box-arrow-in-right" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M6 3.5a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 0-1 0v2A1.5 1.5 0 0 0 6.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-8A1.5 1.5 0 0 0 5 3.5v2a.5.5 0 0 0 1 0v-2z"/>
|
||||
<path fill-rule="evenodd" d="M11.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5H1.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
</form>
|
||||
<small class="text-muted">Access the admin interface for FreePad, this can only be done through the Admin Token.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-muted py-5 border-top text-center">
|
||||
<p class="mb-1">
|
||||
FreePad by <a href="https://justkato.me/">©Kato Twofold</a>
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
FreePad is freely available over on our <a href="https://github.com/JustKato/FreePad">GitHub</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
</main>
|
||||
|
||||
{{ template "inc/theme-toggle.html" .}}
|
||||
</body>
|
||||
|
||||
{{ template "inc/footer.html" .}}
|
|
@ -0,0 +1,94 @@
|
|||
{{ template "inc/header.html" .}}
|
||||
|
||||
<style>
|
||||
|
||||
.pad-instance {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#pad-list {
|
||||
max-height: 30rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.pad-name {
|
||||
max-width: 30%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<body>
|
||||
|
||||
<main id="main-card" class="container rounded mt-5 shadow-sm">
|
||||
<div class="p-3">
|
||||
|
||||
<a href="/" class="logo-container w-100 d-flex mb-4">
|
||||
<img src="/static/img/logo_transparent.png" alt="Logo" style="max-width: 50%; margin: 0 auto;" class="mx-auto">
|
||||
</a>
|
||||
|
||||
<div class="form-group my-4 border-top p-3 border">
|
||||
|
||||
<div class="pad-instance my-2 border-bottom">
|
||||
<div class="pad-name col-5">
|
||||
Pad Name
|
||||
</div>
|
||||
<div class="pad-last-view col-1">
|
||||
Views
|
||||
</div>
|
||||
<div class="pad-last-modified col-4">
|
||||
Create Date
|
||||
</div>
|
||||
<div class="col-2">
|
||||
Actions
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pad-list" >
|
||||
{{ range $indx, $element := .padList }}
|
||||
|
||||
<div class="pad-instance my-2">
|
||||
<div class="pad-name col-5">
|
||||
<a href="/{{ $element.Name }}">
|
||||
{{ $element.Name }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="pad-last-view col-1">
|
||||
{{ $element.Views }}
|
||||
</div>
|
||||
<div class="pad-last-modified col-4">
|
||||
{{ $element.LastModified }}
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<div onclick="doDelete({{ $element.Name }})" class="btn btn-danger">
|
||||
Delete
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
{{ template "inc/theme-toggle.html" .}}
|
||||
</body>
|
||||
|
||||
<script>
|
||||
function doDelete(id) {
|
||||
// Confirm deletion
|
||||
if ( confirm("Confirm pad deletion?") ) {
|
||||
// Do delete
|
||||
window.location.href = `/admin/delete/${id}`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{{ template "inc/footer.html" .}}
|
|
@ -46,6 +46,22 @@
|
|||
just write it in the box above and get to the right page, write anything in
|
||||
and access the same address on any other device to get your info!
|
||||
</p>
|
||||
<small>A couple hints:</small>
|
||||
<p>
|
||||
Pads take into consideration file extensions, use <code>.json</code>,
|
||||
<code>.js</code>, <code>.cpp</code>, <code>.txt</code>, etc... to help
|
||||
parse your type of file
|
||||
</p>
|
||||
<p>
|
||||
The archival feature helps you store information on your local machine! Save your
|
||||
pads and you can always come back and rewrite them exactly as they have been
|
||||
</p>
|
||||
<p>
|
||||
All pads can be publicly edited, so if you choose some common name
|
||||
and someone elses accesses the link they can completely remove/edit
|
||||
what you wrote, not to mention seein that information, so refrain
|
||||
from sharing important data here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{{ template "inc/header.html" .}}
|
||||
|
||||
<style>
|
||||
<link rel="stylesheet" href="/static/css/qr-style.css">
|
||||
|
||||
<style>
|
||||
#pad-content {
|
||||
height: 16rem;
|
||||
}
|
||||
|
@ -13,58 +14,90 @@
|
|||
.dropdown-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
var maximumPadSize = Number({{.maximumPadSize}});
|
||||
var padTitle = {{.title}};
|
||||
var maximumPadSize = Number({{.maximumPadSize }});
|
||||
var padTitle = {{.title }};
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet" href="/static/vendor/hljs/theme.css">
|
||||
|
||||
<body>
|
||||
|
||||
<main id="main-card" class="container rounded mt-5 shadow-sm">
|
||||
<div class="p-3">
|
||||
|
||||
<a href="/" class="logo-container w-100 d-flex mb-4">
|
||||
<img src="/static/img/logo_transparent.png" alt="Logo" style="max-width: 50%; margin: 0 auto;" class="mx-auto">
|
||||
<img src="/static/img/logo_transparent.png" alt="Logo" style="max-width: 50%; margin: 0 auto;"
|
||||
class="mx-auto">
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<h2 class="mb-4">{{.title}}</h2>
|
||||
<h2 class="mb-4" id="padTitle">
|
||||
{{.title}}
|
||||
</h2>
|
||||
|
||||
<div id="pad-content-area">
|
||||
<div class="btn-sm btn" id="pad-content-toggler" onclick="toggleTextareaPreview()">
|
||||
<span class="edit-content-text" title="Edit Content">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16">
|
||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="view-content-text" title="ReadOnly">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eyeglasses" viewBox="0 0 16 16" style="margin-top: 1rem;">
|
||||
<path d="M4 6a2 2 0 1 1 0 4 2 2 0 0 1 0-4zm2.625.547a3 3 0 0 0-5.584.953H.5a.5.5 0 0 0 0 1h.541A3 3 0 0 0 7 8a1 1 0 0 1 2 0 3 3 0 0 0 5.959.5h.541a.5.5 0 0 0 0-1h-.541a3 3 0 0 0-5.584-.953A1.993 1.993 0 0 0 8 6c-.532 0-1.016.208-1.375.547zM14 8a2 2 0 1 1-4 0 2 2 0 0 1 4 0z"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<pre><code id="textarea-preview" class="form-control hidden">{{.post_content}}</code></pre>
|
||||
|
||||
<textarea maxlength="{{.maximumPadSize}}" name="pad-content" id="pad-content" onkeyup="window.socket.sendPadUpdate()"
|
||||
class="form-control hidden">{{.post_content}}</textarea>
|
||||
</div>
|
||||
|
||||
<textarea maxlength="{{.maximumPadSize}}" name="pad-content" id="pad-content" onchange="sendMyData(this)" onkeydown="updateStatus(`Not Saved`, `text-warning`); toggleWritingWatch(this)" class="form-control">{{.post_content}}</textarea>
|
||||
|
||||
<div id="pad-status" class="my-4 row">
|
||||
<div class="col-md-12 col-lg-4 col-xl-4" title="Status">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-reception-3" viewBox="0 0 16 16">
|
||||
<path d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-8zm4 8a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-reception-3" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-8zm4 8a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" class="form-control" readonly value="Loaded" id="loading_status">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 col-lg-4 col-xl-4 mt-4 mt-lg-0 mt-xl-0" title="Current Viewers">
|
||||
<div class="col-md-12 col-lg-4 col-xl-4 mt-4 mt-lg-0 mt-xl-0" title="Current Viewers | Total Views">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
|
||||
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"></path>
|
||||
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"></path>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-eye" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z">
|
||||
</path>
|
||||
<path
|
||||
d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z">
|
||||
</path>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" class="form-control" readonly value="1">
|
||||
<input type="text" class="form-control" readonly value="1 | {{.views}}" id="currentViewers">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 col-lg-4 col-xl-4 mt-4 mt-lg-0 mt-xl-0" title="Last Modified">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-hourglass-split" viewBox="0 0 16 16">
|
||||
<path d="M2.5 15a.5.5 0 1 1 0-1h1v-1a4.5 4.5 0 0 1 2.557-4.06c.29-.139.443-.377.443-.59v-.7c0-.213-.154-.451-.443-.59A4.5 4.5 0 0 1 3.5 3V2h-1a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1h-1v1a4.5 4.5 0 0 1-2.557 4.06c-.29.139-.443.377-.443.59v.7c0 .213.154.451.443.59A4.5 4.5 0 0 1 12.5 13v1h1a.5.5 0 0 1 0 1h-11zm2-13v1c0 .537.12 1.045.337 1.5h6.326c.216-.455.337-.963.337-1.5V2h-7zm3 6.35c0 .701-.478 1.236-1.011 1.492A3.5 3.5 0 0 0 4.5 13s.866-1.299 3-1.48V8.35zm1 0v3.17c2.134.181 3 1.48 3 1.48a3.5 3.5 0 0 0-1.989-3.158C8.978 9.586 8.5 9.052 8.5 8.351z"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-hourglass-split" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M2.5 15a.5.5 0 1 1 0-1h1v-1a4.5 4.5 0 0 1 2.557-4.06c.29-.139.443-.377.443-.59v-.7c0-.213-.154-.451-.443-.59A4.5 4.5 0 0 1 3.5 3V2h-1a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1h-1v1a4.5 4.5 0 0 1-2.557 4.06c-.29.139-.443.377-.443.59v.7c0 .213.154.451.443.59A4.5 4.5 0 0 1 12.5 13v1h1a.5.5 0 0 1 0 1h-11zm2-13v1c0 .537.12 1.045.337 1.5h6.326c.216-.455.337-.963.337-1.5V2h-7zm3 6.35c0 .701-.478 1.236-1.011 1.492A3.5 3.5 0 0 0 4.5 13s.866-1.299 3-1.48V8.35zm1 0v3.17c2.134.181 3 1.48 3 1.48a3.5 3.5 0 0 0-1.989-3.158C8.978 9.586 8.5 9.052 8.5 8.351z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" class="form-control" id="last_modified_" readonly value="{{.last_modified}}">
|
||||
|
@ -75,44 +108,63 @@
|
|||
|
||||
<div id="pad-options" class="row">
|
||||
<div class="col-md-12 col-lg-4 col-xl-4">
|
||||
<button type="button" class="btn btn-secondary btn-md w-100" title="Refresh the contents of the pad" onclick="window.location.reload()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"></path>
|
||||
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"></path>
|
||||
<button type="button" class="btn btn-secondary btn-md w-100" title="Generate a quick QR code of the current page to easily send to other devices, such as your phone." onclick="generateQRCode()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-qr-code" viewBox="0 0 16 16">
|
||||
<path d="M2 2h2v2H2V2Z" />
|
||||
<path d="M6 0v6H0V0h6ZM5 1H1v4h4V1ZM4 12H2v2h2v-2Z" />
|
||||
<path d="M6 10v6H0v-6h6Zm-5 1v4h4v-4H1Zm11-9h2v2h-2V2Z" />
|
||||
<path
|
||||
d="M10 0v6h6V0h-6Zm5 1v4h-4V1h4ZM8 1V0h1v2H8v2H7V1h1Zm0 5V4h1v2H8ZM6 8V7h1V6h1v2h1V7h5v1h-4v1H7V8H6Zm0 0v1H2V8H1v1H0V7h3v1h3Zm10 1h-1V7h1v2Zm-1 0h-1v2h2v-1h-1V9Zm-4 0h2v1h-1v1h-1V9Zm2 3v-1h-1v1h-1v1H9v1h3v-2h1Zm0 0h3v1h-2v1h-1v-2Zm-4-1v1h1v-2H7v1h2Z" />
|
||||
<path d="M7 12h1v3h4v1H7v-4Zm9 2v2h-3v-1h2v-1h1Z" />
|
||||
</svg>
|
||||
Refresh Pad
|
||||
Get QR
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-12 col-lg-4 col-xl-4 mt-4 mt-lg-0 mt-xl-0">
|
||||
<button type="button" class="btn btn-secondary btn-md w-100" title="Download the contents into a text file" onclick="window.pad.downloadPadContents();">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cloud-download" viewBox="0 0 16 16">
|
||||
<path d="M4.406 1.342A5.53 5.53 0 0 1 8 0c2.69 0 4.923 2 5.166 4.579C14.758 4.804 16 6.137 16 7.773 16 9.569 14.502 11 12.687 11H10a.5.5 0 0 1 0-1h2.688C13.979 10 15 8.988 15 7.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 2.825 10.328 1 8 1a4.53 4.53 0 0 0-2.941 1.1c-.757.652-1.153 1.438-1.153 2.055v.448l-.445.049C2.064 4.805 1 5.952 1 7.318 1 8.785 2.23 10 3.781 10H6a.5.5 0 0 1 0 1H3.781C1.708 11 0 9.366 0 7.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383z"></path>
|
||||
<path d="M7.646 15.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 14.293V5.5a.5.5 0 0 0-1 0v8.793l-2.146-2.147a.5.5 0 0 0-.708.708l3 3z"></path>
|
||||
<button type="button" class="btn btn-secondary btn-md w-100"
|
||||
title="Download the contents into a text file" onclick="window.pad.downloadPadContents();">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-cloud-download" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M4.406 1.342A5.53 5.53 0 0 1 8 0c2.69 0 4.923 2 5.166 4.579C14.758 4.804 16 6.137 16 7.773 16 9.569 14.502 11 12.687 11H10a.5.5 0 0 1 0-1h2.688C13.979 10 15 8.988 15 7.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 2.825 10.328 1 8 1a4.53 4.53 0 0 0-2.941 1.1c-.757.652-1.153 1.438-1.153 2.055v.448l-.445.049C2.064 4.805 1 5.952 1 7.318 1 8.785 2.23 10 3.781 10H6a.5.5 0 0 1 0 1H3.781C1.708 11 0 9.366 0 7.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383z">
|
||||
</path>
|
||||
<path
|
||||
d="M7.646 15.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 14.293V5.5a.5.5 0 0 0-1 0v8.793l-2.146-2.147a.5.5 0 0 0-.708.708l3 3z">
|
||||
</path>
|
||||
</svg>
|
||||
Download Pad
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-12 col-lg-4 col-xl-4 mt-4 mt-lg-0 mt-xl-0" title="Archive the current state of the pad">
|
||||
<div class="btn-group w-100" role="group">
|
||||
<button type="button" class="btn btn-secondary btn-md w-100 dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-archive" viewBox="0 0 16 16">
|
||||
<path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>
|
||||
<button type="button" class="btn btn-secondary btn-md w-100 dropdown-toggle"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-archive" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z" />
|
||||
</svg>
|
||||
Archive Pad
|
||||
</button>
|
||||
<ul class="dropdown-menu w-100" id="archives-selection">
|
||||
<li class="dropdown-item" onclick="saveLocalArchive()" id="do-archive-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-archive" viewBox="0 0 16 16">
|
||||
<path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-archive" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z" />
|
||||
</svg>
|
||||
<span class="archive-date">
|
||||
Archive Current
|
||||
</span>
|
||||
</li>
|
||||
<li class="dropdown-item archive-selection" id="archive-selection-example">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-text" viewBox="0 0 16 16">
|
||||
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
|
||||
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-file-earmark-text" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z" />
|
||||
<path
|
||||
d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z" />
|
||||
</svg>
|
||||
<span class="archive-date">
|
||||
DATE
|
||||
|
@ -134,17 +186,47 @@
|
|||
|
||||
</main>
|
||||
|
||||
<!-- START::QRCODE_MODAL -->
|
||||
<div class="modal micromodal-slide" id="qrmodal" aria-hidden="true">
|
||||
<div class="modal__overlay" tabindex="-1" data-micromodal-close>
|
||||
<div class="modal__container" role="dialog" aria-modal="true" aria-labelledby="qrmodal-title">
|
||||
<header class="modal__header">
|
||||
<h2 class="modal__title" id="qrmodal-title">
|
||||
QRCode
|
||||
</h2>
|
||||
<button class="modal__close" aria-label="Close modal" data-micromodal-close></button>
|
||||
</header>
|
||||
<main class="modal__content" id="qrmodal-content">
|
||||
<div id="qrcode"></div>
|
||||
</main>
|
||||
<footer class="modal__footer">
|
||||
<button class="modal__btn" data-micromodal-close aria-label="Close this dialog window">Close</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END::QRCODE_MODAL -->
|
||||
|
||||
{{ template "inc/theme-toggle.html" .}}
|
||||
</body>
|
||||
|
||||
<script src="/static/js/ws.js"></script>
|
||||
<script src="/static/js/fileSaver.js"></script>
|
||||
<script src="/static/js/pad.js"></script>
|
||||
<script src="/static/js/pad-scripts.js"></script>
|
||||
<script src="/static/vendor/hljs/highlight.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
||||
<script src="/static/vendor/bootstrap/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/vendor/qrcodejs/qrcode.min.js"></script>
|
||||
<script src="/static/vendor/micromodal/micromodal.min.js"></script>
|
||||
|
||||
<script>
|
||||
window.pad = new Pad({{.title}}, {{.last_modified}});
|
||||
window.pad = new Pad({{.title }}, {{.last_modified }});
|
||||
|
||||
document.addEventListener(`DOMContentLoaded`, e => {
|
||||
// Initialize the micromodal library
|
||||
MicroModal.init();
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
|
Loading…
Reference in New Issue