Compare commits

..

62 Commits
main ... master

Author SHA1 Message Date
Daniel Legt 3a6c796cac * Reverse Proxy Warning 2022-06-14 23:59:34 +03:00
Daniel Legt 6376fa9128 * Removed the header pass-through 2022-06-13 22:19:49 +03:00
Daniel Legt ca10033ecf Merge branch 'master' of https://github.com/JustKato/FreePad 2022-06-07 02:42:03 +03:00
Daniel Legt b68e5c8e88 REVERT 2022-06-07 02:41:31 +03:00
Daniel Legt 84ccd44fd7 Merge branch 'master' of https://github.com/JustKato/FreePad 2022-06-07 02:26:58 +03:00
Daniel Legt 0a1bff5cd4 * Branch reset 2022-06-07 02:26:55 +03:00
Daniel Legt 7982d564e8 * 2022-06-07 02:25:31 +03:00
Daniel Legt becbf30752 . 2022-06-07 02:04:21 +03:00
Daniel Legt 796a3b07c4 * Debug on live 2022-06-07 01:53:47 +03:00
Daniel Legt 5518103575 * Cleanup 2022-06-07 01:44:48 +03:00
Daniel Legt 177ab62720 * Hopefully a fix 2022-06-07 01:40:25 +03:00
Daniel Legt 5a8ccf20a8 * Quick protocol fix client-side 2022-06-07 01:13:45 +03:00
Daniel Legt a71be11135
Merge pull request #17 from JustKato/feature/web-sockets
Feature/web sockets
2022-06-07 00:56:44 +03:00
Daniel Legt 30dc23c847 * Dynamic updates for highlights 2022-06-07 00:53:25 +03:00
Daniel Legt 0a3b5d50f2 * Default to scroll at the bottom of content 2022-06-07 00:39:05 +03:00
Daniel Legt b4c47ded35 * Better text update follow 2022-06-07 00:37:35 +03:00
Daniel Legt 6e401a416f * Fixed light pad title style 2022-06-07 00:29:30 +03:00
Daniel Legt 3b137c5ed6 * Fixed invisible edit button on Light Mode 2022-06-07 00:28:53 +03:00
Daniel Legt c4f6496e0e * Restyled the Pad title 2022-06-07 00:27:51 +03:00
Daniel Legt ee9516a109 * Fixed annoying alignment of edit/view 2022-06-07 00:20:26 +03:00
Daniel Legt 7dcad9dc31 * Archive Fix 2022-06-07 00:16:27 +03:00
Daniel Legt 1b1fe59877 * Fixed preview not updating 2022-06-07 00:04:49 +03:00
Daniel Legt 6cc1628e77 Real time WebSocket pasting
+ Pad update moved to sockets
+ Pad status updates instantly update
TODO: Fix live text highlight
2022-06-07 00:02:31 +03:00
Daniel Legt bf144c6ecb * Fixed possible bug 2022-06-06 22:50:20 +03:00
Daniel Legt 1d3383c8c6 * Removed deprecated variable 2022-06-06 22:49:55 +03:00
Daniel Legt 4138386fb3 * Some code notes 2022-06-04 16:36:30 +03:00
Daniel Legt cfe2c06dac WebSockets Work started
+ Implemented Gorilla Sockets
+ Implemented a javascript class
+ Golang Struct introduced
+ JSON parsing from the websocket
+ Fail handlers
2022-06-04 16:32:53 +03:00
Daniel Legt 400fd23b3e
Merge pull request #15 from JustKato/feature/admin-interface
Everything seems stable! Been tested just by myself, so if some other people could do some security testing, it'd be awesome!
2022-06-04 13:24:19 +03:00
Daniel Legt bf1d032e68 + Deletion implemented 2022-06-03 23:15:20 +03:00
Daniel Legt faff1ab527 + Deletion Confirmation 2022-06-03 22:59:44 +03:00
Daniel Legt d056a4d429 * Removed debug line 2022-06-03 22:56:53 +03:00
Daniel Legt b710d24a2d * Previous commit 2022-06-03 22:56:25 +03:00
Daniel Legt c3c9aacac3 + Admin interface
+ Pad Listing
@TODO: Add pagination
2022-06-03 22:56:19 +03:00
Daniel Legt d949b3decb Working on the admin interface
+ Implemented login token
+ Routing
+ Admin controller
+ Login Page
* Updated `.env` example
2022-06-02 23:53:32 +03:00
Daniel Legt 662dad90b7
Merge pull request #13 from JustKato/feature/dockerFileBuild
Dockerfile Build Improvements
2022-06-01 21:30:04 +03:00
Daniel Legt 1585d3b158 * Replaced alpine with Scratch
* Changed comments
* Used /src instead of /app twice
2022-06-01 21:28:57 +03:00
Daniel Legt 1d50efe3c6 Dockerfile Build Improvements 2022-06-01 21:24:03 +03:00
Daniel Legt 0f5a352fc6 * Docker Compose example updated 2022-06-01 18:43:35 +03:00
Daniel Legt 6a8f4f81e5 * Corrected view button position 2022-06-01 18:31:59 +03:00
Daniel Legt 781b4bcf80
Merge pull request #12 from JustKato/feature/syntax_highlight
Feature/syntax highlight
2022-06-01 18:29:48 +03:00
Daniel Legt f748adf132 * Version Bumb 2022-06-01 18:29:24 +03:00
Daniel Legt 4bfad3ef40 Changelog and Version bump 2022-06-01 18:29:17 +03:00
Daniel Legt 11658b4b5e Extra Text
+ Added extra text to the home
2022-06-01 18:28:17 +03:00
Daniel Legt 685c6ae15f * Optimization ( Thanks Tecchie088 ) 2022-06-01 18:10:10 +03:00
Daniel Legt 97102b98b3 * Fixed Security flaw ( Thanks @SleepingProcess ) 2022-06-01 18:08:17 +03:00
Daniel Legt 22657cc111 * Fixed Sanitization 2022-06-01 18:07:33 +03:00
Daniel Legt 3dc09cae64 # Updated Looks, Syntax Highlights and more
* Moved all CDN libraries into /static/vendor
+ Implemented syntax highlight with hljs
* Updated the sanitization to include `.`
2022-06-01 17:43:05 +03:00
Daniel Legt 70b671c0be + Pad Types::Downloads 2022-06-01 17:00:23 +03:00
Daniel Legt 6177dcecb8 Appearance updates
+ Improved scroll bar look
+ Implemented a preview mode
2022-06-01 16:58:09 +03:00
Daniel Legt aea10baffd * Changelog
* Version
2022-06-01 13:28:36 +03:00
Daniel Legt 42eb5add65 * Changed the frequency of the views save to one minute 2022-06-01 13:27:25 +03:00
Daniel Legt 53066025f0 Implemented a views counter
+ Implemented views for the post struct
+ Views functions
+ Views storage file
2022-06-01 13:26:54 +03:00
Daniel Legt 23dd69e060 + Added some comments 2022-06-01 11:32:12 +03:00
Daniel Legt 0bc4942924 QR Codes
+ QR Code generation
+ Added micromodal
* Bumped version by 0.0.8
* Updated DockerFile
* Updated Changelog
2022-05-22 20:42:19 +03:00
Daniel Legt 23fa17b840 * Updated Versioning 2022-05-22 20:15:51 +03:00
Daniel Legt 5414dab201 + FreePad version in ContentHeaders 2022-05-22 20:15:11 +03:00
Daniel Legt 8735837cb4 + Implemented Changelog
+ Started mentioning versioning in Dockerfile
2022-05-22 19:58:47 +03:00
Daniel Legt 7552cd7cd3 * Updated the license 2022-05-22 19:42:49 +03:00
Kato 74af4d1554
Merge pull request #7 from JustKato/rate-limiting
Working on the API Rate limiting
2022-05-22 19:41:32 +03:00
Daniel Legt 916bcae961 + Working on the API Rate limiting 2022-05-22 19:38:12 +03:00
Daniel Legt 53b35745cd * Readme visual changes 2022-05-21 17:45:34 +03:00
Daniel Legt 725bc4222b Updated visuals
+ New banner
* fixed .env missing variable
* Build script now builds to all platforms
+ ReadMe instructions for running
2022-05-21 17:41:43 +03:00
38 changed files with 3175 additions and 100 deletions

View File

@ -17,4 +17,12 @@ API_BAN_LIMIT=300
DEV_MODE=0
# Maximum file storage age in minutes, set to -1 to disable
CLEANUP_MAX_AGE=43200 # Default is a month
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

30
Changelog.md Normal file
View File

@ -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"

View File

@ -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
View File

@ -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.

View File

@ -5,8 +5,8 @@ Quickly create "pads" and share with others
[![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://hub.docker.com/r/justkato/freepad)
[![Ko-Fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/justkato)
![Go](https://img.shields.io/badge/go-%2300ADD8.svg?style=for-the-badge&logo=go&logoColor=white)
[![demo](https://img.shields.io/badge/Demo-Check%20out%20the%20functionality-orange)](https://pad.justkato.me/)
![MariaDB](https://img.shields.io/badge/MariaDB-003545?style=for-the-badge&logo=mariadb&logoColor=white)
# **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";
}
}
```
![Gopher](static/img/banner_prerequisites.png)
@ -30,4 +53,66 @@ 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!
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!
![Gopher](static/img/banner_how_to_run.png)
## 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
```

View File

@ -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

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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
}

View File

@ -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()
})
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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(),
})
})
}

View File

@ -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"),
}

View File

@ -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
View File

@ -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")
}

View File

@ -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,
@ -55,4 +125,28 @@ input[type="color"]:focus,
border-color: none;
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;
}

174
static/css/qr-style.css Normal file
View File

@ -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

View File

@ -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,36 +31,36 @@ function sendMyData(el) {
body: formData,
method: "post",
})
.then( resp => {
resp.json()
.then( e => {
document.getElementById(`last_modified_`).value = e.pad.last_modified;
updateStatus(`Succesfully Saved`, `text-success`);
.then(resp => {
resp.json()
.then(e => {
document.getElementById(`last_modified_`).value = e.pad.last_modified;
updateStatus(`Succesfully Saved`, `text-success`);
})
.catch(err => {
updateStatus(`Failed to Save`, `text-danger`);
console.error(err);
})
})
.catch( err => {
.catch(err => {
updateStatus(`Failed to Save`, `text-danger`);
console.error(err);
})
})
.catch( err => {
updateStatus(`Failed to Save`, `text-danger`);
console.error(err);
})
.finally( () => {
el.removeAttribute(`readonly`);
})
.finally(() => {
el.removeAttribute(`readonly`);
})
}
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);
@ -129,9 +136,12 @@ function renderArchivesSelection() {
row.addEventListener(`click`, e => {
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
const textarea = document.getElementById(`pad-content`);
// 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()
}

View File

@ -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}`);
}
}

261
static/js/ws.js Normal file
View File

@ -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

View File

@ -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;

1173
static/vendor/hljs/highlight.min.js vendored Normal file

File diff suppressed because one or more lines are too long

98
static/vendor/hljs/theme.css vendored Normal file
View File

@ -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

1
static/vendor/qrcodejs/qrcode.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -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 }}

View File

@ -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>

View File

@ -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" .}}

View File

@ -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" .}}

View File

@ -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>

View File

@ -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>