Akay Borana commited on
Commit
421b222
·
0 Parent(s):

Initial upload

Browse files
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build stage
2
+ FROM golang:1.25.3-alpine AS builder
3
+
4
+ WORKDIR /build
5
+
6
+ # Copy go mod files
7
+ COPY go.mod go.sum ./
8
+ RUN go mod download
9
+
10
+ # Copy source code
11
+ COPY . .
12
+
13
+ # Build the binary
14
+ RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o patbin .
15
+
16
+ # Runtime stage
17
+ FROM alpine:3.19
18
+
19
+ WORKDIR /app
20
+
21
+ # Install ca-certificates for HTTPS
22
+ RUN apk --no-cache add ca-certificates tzdata
23
+
24
+ # Create non-root user
25
+ RUN adduser -D -g '' appuser
26
+
27
+ # Copy binary from builder
28
+ COPY --from=builder /build/patbin .
29
+
30
+ # Copy static files and templates
31
+ COPY --from=builder /build/static ./static
32
+ COPY --from=builder /build/templates ./templates
33
+
34
+ # Create data directory for SQLite
35
+ RUN mkdir -p /app/data && chown -R appuser:appuser /app
36
+
37
+ # Switch to non-root user
38
+ USER appuser
39
+
40
+ # Environment variables
41
+ ENV PORT=7860
42
+ ENV DB_PATH=/app/data/patbin.db
43
+ ENV GIN_MODE=release
44
+
45
+ # Expose port
46
+ EXPOSE 7860
47
+
48
+ # Health check
49
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
50
+ CMD wget --no-verbose --tries=1 --spider http://localhost:7860/ || exit 1
51
+
52
+ # Run the binary
53
+ CMD ["./patbin"]
README.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: MyPasteBin
3
+ emoji: 📈
4
+ colorFrom: green
5
+ colorTo: green
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
config/config.go ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package config
2
+
3
+ import (
4
+ "os"
5
+ )
6
+
7
+ type Config struct {
8
+ Port string
9
+ JWTSecret string
10
+ DBPath string
11
+ CookieName string
12
+ }
13
+
14
+ func Load() *Config {
15
+ port := os.Getenv("PORT")
16
+ if port == "" {
17
+ port = "8080"
18
+ }
19
+
20
+ jwtSecret := os.Getenv("JWT_SECRET")
21
+ if jwtSecret == "" {
22
+ jwtSecret = "patbin-super-secret-key-change-in-production"
23
+ }
24
+
25
+ dbPath := os.Getenv("DB_PATH")
26
+ if dbPath == "" {
27
+ dbPath = "patbin.db"
28
+ }
29
+
30
+ return &Config{
31
+ Port: port,
32
+ JWTSecret: jwtSecret,
33
+ DBPath: dbPath,
34
+ CookieName: "patbin_token",
35
+ }
36
+ }
database/database.go ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package database
2
+
3
+ import (
4
+ "patbin/models"
5
+
6
+ "github.com/glebarez/sqlite"
7
+ "gorm.io/gorm"
8
+ "gorm.io/gorm/logger"
9
+ )
10
+
11
+ var DB *gorm.DB
12
+
13
+ func Init(dbPath string) error {
14
+ var err error
15
+ DB, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{
16
+ Logger: logger.Default.LogMode(logger.Silent),
17
+ })
18
+ if err != nil {
19
+ return err
20
+ }
21
+
22
+ // Auto migrate models
23
+ err = DB.AutoMigrate(&models.User{}, &models.Paste{})
24
+ if err != nil {
25
+ return err
26
+ }
27
+
28
+ return nil
29
+ }
30
+
31
+ func GetDB() *gorm.DB {
32
+ return DB
33
+ }
go.mod ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module patbin
2
+
3
+ go 1.25.3
4
+
5
+ require (
6
+ github.com/gin-gonic/gin v1.11.0
7
+ github.com/glebarez/sqlite v1.11.0
8
+ github.com/golang-jwt/jwt/v5 v5.3.0
9
+ golang.org/x/crypto v0.46.0
10
+ gorm.io/gorm v1.31.1
11
+ )
12
+
13
+ require (
14
+ github.com/bytedance/gopkg v0.1.3 // indirect
15
+ github.com/bytedance/sonic v1.14.2 // indirect
16
+ github.com/bytedance/sonic/loader v0.4.0 // indirect
17
+ github.com/cloudwego/base64x v0.1.6 // indirect
18
+ github.com/dustin/go-humanize v1.0.1 // indirect
19
+ github.com/gabriel-vasile/mimetype v1.4.12 // indirect
20
+ github.com/gin-contrib/sse v1.1.0 // indirect
21
+ github.com/glebarez/go-sqlite v1.21.2 // indirect
22
+ github.com/go-playground/locales v0.14.1 // indirect
23
+ github.com/go-playground/universal-translator v0.18.1 // indirect
24
+ github.com/go-playground/validator/v10 v10.30.1 // indirect
25
+ github.com/goccy/go-json v0.10.5 // indirect
26
+ github.com/goccy/go-yaml v1.19.1 // indirect
27
+ github.com/google/uuid v1.6.0 // indirect
28
+ github.com/jinzhu/inflection v1.0.0 // indirect
29
+ github.com/jinzhu/now v1.1.5 // indirect
30
+ github.com/json-iterator/go v1.1.12 // indirect
31
+ github.com/klauspost/cpuid/v2 v2.3.0 // indirect
32
+ github.com/leodido/go-urn v1.4.0 // indirect
33
+ github.com/mattn/go-isatty v0.0.20 // indirect
34
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
35
+ github.com/modern-go/reflect2 v1.0.2 // indirect
36
+ github.com/pelletier/go-toml/v2 v2.2.4 // indirect
37
+ github.com/quic-go/qpack v0.6.0 // indirect
38
+ github.com/quic-go/quic-go v0.58.0 // indirect
39
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
40
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
41
+ github.com/ugorji/go/codec v1.3.1 // indirect
42
+ go.uber.org/mock v0.6.0 // indirect
43
+ golang.org/x/arch v0.23.0 // indirect
44
+ golang.org/x/net v0.48.0 // indirect
45
+ golang.org/x/sys v0.39.0 // indirect
46
+ golang.org/x/text v0.32.0 // indirect
47
+ google.golang.org/protobuf v1.36.11 // indirect
48
+ modernc.org/libc v1.22.5 // indirect
49
+ modernc.org/mathutil v1.5.0 // indirect
50
+ modernc.org/memory v1.5.0 // indirect
51
+ modernc.org/sqlite v1.23.1 // indirect
52
+ )
go.sum ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
2
+ github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
3
+ github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
4
+ github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
5
+ github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
6
+ github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
7
+ github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
8
+ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
9
+ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10
+ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
11
+ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12
+ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
13
+ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
14
+ github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
15
+ github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
16
+ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
17
+ github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
18
+ github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
19
+ github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
20
+ github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
21
+ github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
22
+ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
23
+ github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
24
+ github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
25
+ github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
26
+ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
27
+ github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
28
+ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
29
+ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
30
+ github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
31
+ github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
32
+ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
33
+ github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
34
+ github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
35
+ github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
36
+ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
37
+ github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
38
+ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
39
+ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
40
+ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
41
+ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
42
+ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
43
+ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
44
+ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
45
+ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
46
+ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
47
+ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
48
+ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
49
+ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
50
+ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
51
+ github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
52
+ github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
53
+ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
54
+ github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
55
+ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
56
+ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
57
+ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
58
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
59
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
60
+ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
61
+ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
62
+ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
63
+ github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
64
+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
65
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
66
+ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
67
+ github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
68
+ github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
69
+ github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
70
+ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
71
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
72
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
73
+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
74
+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
75
+ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
76
+ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
77
+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
78
+ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
79
+ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
80
+ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
81
+ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
82
+ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
83
+ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
84
+ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
85
+ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
86
+ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
87
+ github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
88
+ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
89
+ go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
90
+ golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
91
+ golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
92
+ golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
93
+ golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
94
+ golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
95
+ golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
96
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
97
+ golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
98
+ golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
99
+ golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
100
+ golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
101
+ google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
102
+ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
103
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
104
+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
105
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
106
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
107
+ gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
108
+ gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
109
+ modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
110
+ modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
111
+ modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
112
+ modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
113
+ modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
114
+ modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
115
+ modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
116
+ modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
handlers/auth.go ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package handlers
2
+
3
+ import (
4
+ "net/http"
5
+ "patbin/config"
6
+ "patbin/database"
7
+ "patbin/middleware"
8
+ "patbin/models"
9
+ "time"
10
+
11
+ "github.com/gin-gonic/gin"
12
+ "github.com/golang-jwt/jwt/v5"
13
+ "golang.org/x/crypto/bcrypt"
14
+ )
15
+
16
+ type AuthHandler struct {
17
+ cfg *config.Config
18
+ }
19
+
20
+ func NewAuthHandler(cfg *config.Config) *AuthHandler {
21
+ return &AuthHandler{cfg: cfg}
22
+ }
23
+
24
+ type RegisterRequest struct {
25
+ Username string `json:"username" binding:"required,min=3,max=50"`
26
+ Password string `json:"password" binding:"required,min=6"`
27
+ }
28
+
29
+ type LoginRequest struct {
30
+ Username string `json:"username" binding:"required"`
31
+ Password string `json:"password" binding:"required"`
32
+ }
33
+
34
+ // Register creates a new user account
35
+ func (h *AuthHandler) Register(c *gin.Context) {
36
+ var req RegisterRequest
37
+ if err := c.ShouldBindJSON(&req); err != nil {
38
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
39
+ return
40
+ }
41
+
42
+ // Check if username exists
43
+ var existingUser models.User
44
+ if result := database.DB.Where("username = ?", req.Username).First(&existingUser); result.Error == nil {
45
+ c.JSON(http.StatusConflict, gin.H{"error": "Username already taken"})
46
+ return
47
+ }
48
+
49
+ // Hash password
50
+ hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
51
+ if err != nil {
52
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process password"})
53
+ return
54
+ }
55
+
56
+ // Create user
57
+ user := models.User{
58
+ Username: req.Username,
59
+ Password: string(hashedPassword),
60
+ CreatedAt: time.Now(),
61
+ }
62
+
63
+ if result := database.DB.Create(&user); result.Error != nil {
64
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
65
+ return
66
+ }
67
+
68
+ // Generate token
69
+ token, err := h.generateToken(user.ID, user.Username)
70
+ if err != nil {
71
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
72
+ return
73
+ }
74
+
75
+ // Set cookie
76
+ c.SetCookie(h.cfg.CookieName, token, 60*60*24*7, "/", "", false, true)
77
+
78
+ c.JSON(http.StatusCreated, gin.H{
79
+ "message": "Registration successful",
80
+ "user": user,
81
+ "token": token,
82
+ })
83
+ }
84
+
85
+ // Login authenticates a user
86
+ func (h *AuthHandler) Login(c *gin.Context) {
87
+ var req LoginRequest
88
+ if err := c.ShouldBindJSON(&req); err != nil {
89
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
90
+ return
91
+ }
92
+
93
+ // Find user
94
+ var user models.User
95
+ if result := database.DB.Where("username = ?", req.Username).First(&user); result.Error != nil {
96
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
97
+ return
98
+ }
99
+
100
+ // Check password
101
+ if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
102
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
103
+ return
104
+ }
105
+
106
+ // Generate token
107
+ token, err := h.generateToken(user.ID, user.Username)
108
+ if err != nil {
109
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
110
+ return
111
+ }
112
+
113
+ // Set cookie
114
+ c.SetCookie(h.cfg.CookieName, token, 60*60*24*7, "/", "", false, true)
115
+
116
+ c.JSON(http.StatusOK, gin.H{
117
+ "message": "Login successful",
118
+ "user": user,
119
+ "token": token,
120
+ })
121
+ }
122
+
123
+ // Logout clears the auth cookie
124
+ func (h *AuthHandler) Logout(c *gin.Context) {
125
+ c.SetCookie(h.cfg.CookieName, "", -1, "/", "", false, true)
126
+ c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"})
127
+ }
128
+
129
+ // GetCurrentUser returns the current authenticated user
130
+ func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
131
+ userID, ok := middleware.GetUserID(c)
132
+ if !ok {
133
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
134
+ return
135
+ }
136
+
137
+ var user models.User
138
+ if result := database.DB.First(&user, userID); result.Error != nil {
139
+ c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
140
+ return
141
+ }
142
+
143
+ c.JSON(http.StatusOK, user)
144
+ }
145
+
146
+ func (h *AuthHandler) generateToken(userID uint, username string) (string, error) {
147
+ claims := &middleware.Claims{
148
+ UserID: userID,
149
+ Username: username,
150
+ RegisteredClaims: jwt.RegisteredClaims{
151
+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)),
152
+ IssuedAt: jwt.NewNumericDate(time.Now()),
153
+ },
154
+ }
155
+
156
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
157
+ return token.SignedString([]byte(h.cfg.JWTSecret))
158
+ }
159
+
160
+ // LoginPage renders the login page
161
+ func (h *AuthHandler) LoginPage(c *gin.Context) {
162
+ c.HTML(http.StatusOK, "login.html", gin.H{
163
+ "title": "Login - Patbin",
164
+ })
165
+ }
166
+
167
+ // RegisterPage renders the registration page
168
+ func (h *AuthHandler) RegisterPage(c *gin.Context) {
169
+ c.HTML(http.StatusOK, "register.html", gin.H{
170
+ "title": "Register - Patbin",
171
+ })
172
+ }
handlers/paste.go ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package handlers
2
+
3
+ import (
4
+ "crypto/rand"
5
+ "encoding/hex"
6
+ "net/http"
7
+ "patbin/database"
8
+ "patbin/middleware"
9
+ "patbin/models"
10
+ "strings"
11
+ "time"
12
+
13
+ "github.com/gin-gonic/gin"
14
+ )
15
+
16
+ type PasteHandler struct{}
17
+
18
+ func NewPasteHandler() *PasteHandler {
19
+ return &PasteHandler{}
20
+ }
21
+
22
+ type CreatePasteRequest struct {
23
+ Title string `json:"title"`
24
+ Content string `json:"content" binding:"required"`
25
+ Language string `json:"language"`
26
+ IsPublic bool `json:"is_public"`
27
+ ExpiresIn string `json:"expires_in"` // "1h", "1d", "1w", "never"
28
+ BurnAfterRead bool `json:"burn_after_read"`
29
+ }
30
+
31
+ type UpdatePasteRequest struct {
32
+ Title string `json:"title"`
33
+ Content string `json:"content"`
34
+ Language string `json:"language"`
35
+ IsPublic *bool `json:"is_public"`
36
+ }
37
+
38
+ // generateID creates a random 8-character hex ID
39
+ func generateID() string {
40
+ bytes := make([]byte, 4)
41
+ rand.Read(bytes)
42
+ return hex.EncodeToString(bytes)
43
+ }
44
+
45
+ // CreatePaste creates a new paste
46
+ func (h *PasteHandler) CreatePaste(c *gin.Context) {
47
+ var req CreatePasteRequest
48
+ if err := c.ShouldBindJSON(&req); err != nil {
49
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Content is required"})
50
+ return
51
+ }
52
+
53
+ const maxContentSize = 512 * 1024 // 512 KB
54
+ if len(req.Content) > maxContentSize {
55
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Content too large (max 512 KB)"})
56
+ return
57
+ }
58
+ var id string
59
+ for {
60
+ id = generateID()
61
+ var existing models.Paste
62
+ if result := database.DB.First(&existing, "id = ?", id); result.Error != nil {
63
+ break
64
+ }
65
+ }
66
+
67
+ paste := models.Paste{
68
+ ID: id,
69
+ Title: req.Title,
70
+ Content: req.Content,
71
+ Language: req.Language,
72
+ IsPublic: req.IsPublic,
73
+ BurnAfterRead: req.BurnAfterRead,
74
+ CreatedAt: time.Now(),
75
+ UpdatedAt: time.Now(),
76
+ }
77
+
78
+ // Set user if authenticated
79
+ if userID, ok := middleware.GetUserID(c); ok {
80
+ paste.UserID = &userID
81
+ }
82
+
83
+ // Set expiration
84
+ if req.ExpiresIn != "" && req.ExpiresIn != "never" {
85
+ var duration time.Duration
86
+ switch req.ExpiresIn {
87
+ case "1h":
88
+ duration = time.Hour
89
+ case "1d":
90
+ duration = 24 * time.Hour
91
+ case "1w":
92
+ duration = 7 * 24 * time.Hour
93
+ case "1m":
94
+ duration = 30 * 24 * time.Hour
95
+ }
96
+ if duration > 0 {
97
+ expiresAt := time.Now().Add(duration)
98
+ paste.ExpiresAt = &expiresAt
99
+ }
100
+ }
101
+
102
+ if result := database.DB.Create(&paste); result.Error != nil {
103
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create paste"})
104
+ return
105
+ }
106
+
107
+ c.JSON(http.StatusCreated, paste)
108
+ }
109
+
110
+ // GetPaste retrieves a paste by ID
111
+ func (h *PasteHandler) GetPaste(c *gin.Context) {
112
+ id := c.Param("id")
113
+
114
+ // Remove extension if present
115
+ if idx := strings.LastIndex(id, "."); idx != -1 {
116
+ id = id[:idx]
117
+ }
118
+
119
+ var paste models.Paste
120
+ if result := database.DB.Preload("User").First(&paste, "id = ?", id); result.Error != nil {
121
+ c.JSON(http.StatusNotFound, gin.H{"error": "Paste not found"})
122
+ return
123
+ }
124
+
125
+ // Check if expired
126
+ if paste.ExpiresAt != nil && paste.ExpiresAt.Before(time.Now()) {
127
+ database.DB.Delete(&paste)
128
+ c.JSON(http.StatusNotFound, gin.H{"error": "Paste has expired"})
129
+ return
130
+ }
131
+
132
+ // Check visibility
133
+ if !paste.IsPublic {
134
+ userID, authenticated := middleware.GetUserID(c)
135
+ if !authenticated || paste.UserID == nil || *paste.UserID != userID {
136
+ c.JSON(http.StatusForbidden, gin.H{"error": "This paste is private"})
137
+ return
138
+ }
139
+ }
140
+
141
+ // Handle burn after read
142
+ if paste.BurnAfterRead && paste.Views > 0 {
143
+ database.DB.Delete(&paste)
144
+ c.JSON(http.StatusNotFound, gin.H{"error": "Paste has been burned after reading"})
145
+ return
146
+ }
147
+
148
+ // Increment views
149
+ database.DB.Model(&paste).Update("views", paste.Views+1)
150
+ paste.Views++
151
+
152
+ c.JSON(http.StatusOK, paste)
153
+ }
154
+
155
+ // UpdatePaste updates an existing paste
156
+ func (h *PasteHandler) UpdatePaste(c *gin.Context) {
157
+ id := c.Param("id")
158
+ userID, ok := middleware.GetUserID(c)
159
+ if !ok {
160
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
161
+ return
162
+ }
163
+
164
+ var paste models.Paste
165
+ if result := database.DB.First(&paste, "id = ?", id); result.Error != nil {
166
+ c.JSON(http.StatusNotFound, gin.H{"error": "Paste not found"})
167
+ return
168
+ }
169
+
170
+ // Check ownership
171
+ if paste.UserID == nil || *paste.UserID != userID {
172
+ c.JSON(http.StatusForbidden, gin.H{"error": "You can only edit your own pastes"})
173
+ return
174
+ }
175
+
176
+ var req UpdatePasteRequest
177
+ if err := c.ShouldBindJSON(&req); err != nil {
178
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
179
+ return
180
+ }
181
+
182
+ updates := map[string]interface{}{
183
+ "updated_at": time.Now(),
184
+ }
185
+
186
+ if req.Title != "" {
187
+ updates["title"] = req.Title
188
+ }
189
+ if req.Content != "" {
190
+ updates["content"] = req.Content
191
+ }
192
+ if req.Language != "" {
193
+ updates["language"] = req.Language
194
+ }
195
+ if req.IsPublic != nil {
196
+ updates["is_public"] = *req.IsPublic
197
+ }
198
+
199
+ if result := database.DB.Model(&paste).Updates(updates); result.Error != nil {
200
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update paste"})
201
+ return
202
+ }
203
+
204
+ database.DB.First(&paste, "id = ?", id)
205
+ c.JSON(http.StatusOK, paste)
206
+ }
207
+
208
+ // DeletePaste deletes a paste
209
+ func (h *PasteHandler) DeletePaste(c *gin.Context) {
210
+ id := c.Param("id")
211
+ userID, ok := middleware.GetUserID(c)
212
+ if !ok {
213
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
214
+ return
215
+ }
216
+
217
+ var paste models.Paste
218
+ if result := database.DB.First(&paste, "id = ?", id); result.Error != nil {
219
+ c.JSON(http.StatusNotFound, gin.H{"error": "Paste not found"})
220
+ return
221
+ }
222
+
223
+ // Check ownership
224
+ if paste.UserID == nil || *paste.UserID != userID {
225
+ c.JSON(http.StatusForbidden, gin.H{"error": "You can only delete your own pastes"})
226
+ return
227
+ }
228
+
229
+ if result := database.DB.Delete(&paste); result.Error != nil {
230
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete paste"})
231
+ return
232
+ }
233
+
234
+ c.JSON(http.StatusOK, gin.H{"message": "Paste deleted successfully"})
235
+ }
236
+
237
+ // GetRawPaste returns the raw content of a paste
238
+ func (h *PasteHandler) GetRawPaste(c *gin.Context) {
239
+ id := c.Param("id")
240
+
241
+ var paste models.Paste
242
+ if result := database.DB.First(&paste, "id = ?", id); result.Error != nil {
243
+ c.String(http.StatusNotFound, "Paste not found")
244
+ return
245
+ }
246
+
247
+ // Check if expired
248
+ if paste.ExpiresAt != nil && paste.ExpiresAt.Before(time.Now()) {
249
+ database.DB.Delete(&paste)
250
+ c.String(http.StatusNotFound, "Paste has expired")
251
+ return
252
+ }
253
+
254
+ // Check visibility
255
+ if !paste.IsPublic {
256
+ userID, authenticated := middleware.GetUserID(c)
257
+ if !authenticated || paste.UserID == nil || *paste.UserID != userID {
258
+ c.String(http.StatusForbidden, "This paste is private")
259
+ return
260
+ }
261
+ }
262
+
263
+ c.Header("Content-Type", "text/plain; charset=utf-8")
264
+ c.String(http.StatusOK, paste.Content)
265
+ }
266
+
267
+ // ForkPaste creates a copy of an existing paste
268
+ func (h *PasteHandler) ForkPaste(c *gin.Context) {
269
+ id := c.Param("id")
270
+
271
+ var original models.Paste
272
+ if result := database.DB.First(&original, "id = ?", id); result.Error != nil {
273
+ c.JSON(http.StatusNotFound, gin.H{"error": "Paste not found"})
274
+ return
275
+ }
276
+
277
+ // Check visibility for forking
278
+ if !original.IsPublic {
279
+ userID, authenticated := middleware.GetUserID(c)
280
+ if !authenticated || original.UserID == nil || *original.UserID != userID {
281
+ c.JSON(http.StatusForbidden, gin.H{"error": "Cannot fork a private paste"})
282
+ return
283
+ }
284
+ }
285
+
286
+ // Generate new ID
287
+ var newID string
288
+ for {
289
+ newID = generateID()
290
+ var existing models.Paste
291
+ if result := database.DB.First(&existing, "id = ?", newID); result.Error != nil {
292
+ break
293
+ }
294
+ }
295
+
296
+ forked := models.Paste{
297
+ ID: newID,
298
+ Title: original.Title + " (Fork)",
299
+ Content: original.Content,
300
+ Language: original.Language,
301
+ IsPublic: true,
302
+ CreatedAt: time.Now(),
303
+ UpdatedAt: time.Now(),
304
+ }
305
+
306
+ // Set user if authenticated
307
+ if userID, ok := middleware.GetUserID(c); ok {
308
+ forked.UserID = &userID
309
+ }
310
+
311
+ if result := database.DB.Create(&forked); result.Error != nil {
312
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fork paste"})
313
+ return
314
+ }
315
+
316
+ c.JSON(http.StatusCreated, forked)
317
+ }
318
+
319
+ // ViewPastePage renders the paste view page
320
+ func (h *PasteHandler) ViewPastePage(c *gin.Context) {
321
+ id := c.Param("id")
322
+ ext := ""
323
+
324
+ // Extract extension for syntax highlighting
325
+ if idx := strings.LastIndex(id, "."); idx != -1 {
326
+ ext = id[idx+1:]
327
+ id = id[:idx]
328
+ }
329
+
330
+ var paste models.Paste
331
+ if result := database.DB.Preload("User").First(&paste, "id = ?", id); result.Error != nil {
332
+ c.HTML(http.StatusNotFound, "error.html", gin.H{
333
+ "title": "Not Found - Patbin",
334
+ "message": "Paste not found",
335
+ })
336
+ return
337
+ }
338
+
339
+ // Check if expired
340
+ if paste.ExpiresAt != nil && paste.ExpiresAt.Before(time.Now()) {
341
+ database.DB.Delete(&paste)
342
+ c.HTML(http.StatusNotFound, "error.html", gin.H{
343
+ "title": "Expired - Patbin",
344
+ "message": "This paste has expired",
345
+ })
346
+ return
347
+ }
348
+
349
+ // Check visibility
350
+ if !paste.IsPublic {
351
+ userID, authenticated := middleware.GetUserID(c)
352
+ if !authenticated || paste.UserID == nil || *paste.UserID != userID {
353
+ c.HTML(http.StatusForbidden, "error.html", gin.H{
354
+ "title": "Private - Patbin",
355
+ "message": "This paste is private",
356
+ })
357
+ return
358
+ }
359
+ }
360
+
361
+ // Handle burn after read
362
+ if paste.BurnAfterRead && paste.Views > 0 {
363
+ database.DB.Delete(&paste)
364
+ c.HTML(http.StatusNotFound, "error.html", gin.H{
365
+ "title": "Burned - Patbin",
366
+ "message": "This paste has been burned after reading",
367
+ })
368
+ return
369
+ }
370
+
371
+ // Increment views
372
+ database.DB.Model(&paste).Update("views", paste.Views+1)
373
+ paste.Views++
374
+
375
+ // Determine language
376
+ language := paste.Language
377
+ if ext != "" {
378
+ language = models.GetLanguageFromExtension(ext)
379
+ }
380
+ if language == "" {
381
+ language = "plaintext"
382
+ }
383
+
384
+ // Count lines
385
+ lines := strings.Count(paste.Content, "\n") + 1
386
+
387
+ // Check if current user owns this paste
388
+ isOwner := false
389
+ if userID, ok := middleware.GetUserID(c); ok && paste.UserID != nil {
390
+ isOwner = *paste.UserID == userID
391
+ }
392
+
393
+ c.HTML(http.StatusOK, "view.html", gin.H{
394
+ "title": paste.Title + " - Patbin",
395
+ "paste": paste,
396
+ "language": language,
397
+ "lines": lines,
398
+ "isOwner": isOwner,
399
+ "ext": ext,
400
+ })
401
+ }
402
+
403
+ // HomePage renders the home page with paste creation form
404
+ func (h *PasteHandler) HomePage(c *gin.Context) {
405
+ c.HTML(http.StatusOK, "index.html", gin.H{
406
+ "title": "Patbin - Modern Pastebin",
407
+ })
408
+ }
409
+
410
+ // EditPastePage renders the paste edit page
411
+ func (h *PasteHandler) EditPastePage(c *gin.Context) {
412
+ id := c.Param("id")
413
+ userID, ok := middleware.GetUserID(c)
414
+ if !ok {
415
+ c.Redirect(http.StatusFound, "/login")
416
+ return
417
+ }
418
+
419
+ var paste models.Paste
420
+ if result := database.DB.First(&paste, "id = ?", id); result.Error != nil {
421
+ c.HTML(http.StatusNotFound, "error.html", gin.H{
422
+ "title": "Not Found - Patbin",
423
+ "message": "Paste not found",
424
+ })
425
+ return
426
+ }
427
+
428
+ // Check ownership
429
+ if paste.UserID == nil || *paste.UserID != userID {
430
+ c.HTML(http.StatusForbidden, "error.html", gin.H{
431
+ "title": "Forbidden - Patbin",
432
+ "message": "You can only edit your own pastes",
433
+ })
434
+ return
435
+ }
436
+
437
+ c.HTML(http.StatusOK, "edit.html", gin.H{
438
+ "title": "Edit - " + paste.Title,
439
+ "paste": paste,
440
+ })
441
+ }
442
+
443
+ // RecentPastes returns recent public pastes
444
+ func (h *PasteHandler) RecentPastes(c *gin.Context) {
445
+ var pastes []models.Paste
446
+ database.DB.Where("is_public = ?", true).
447
+ Order("created_at DESC").
448
+ Limit(20).
449
+ Preload("User").
450
+ Find(&pastes)
451
+
452
+ c.JSON(http.StatusOK, pastes)
453
+ }
handlers/user.go ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package handlers
2
+
3
+ import (
4
+ "net/http"
5
+ "patbin/database"
6
+ "patbin/middleware"
7
+ "patbin/models"
8
+
9
+ "github.com/gin-gonic/gin"
10
+ )
11
+
12
+ type UserHandler struct{}
13
+
14
+ func NewUserHandler() *UserHandler {
15
+ return &UserHandler{}
16
+ }
17
+
18
+ // GetUserProfile returns a user's public pastes
19
+ func (h *UserHandler) GetUserProfile(c *gin.Context) {
20
+ username := c.Param("username")
21
+
22
+ var user models.User
23
+ if result := database.DB.Where("username = ?", username).First(&user); result.Error != nil {
24
+ c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
25
+ return
26
+ }
27
+
28
+ var pastes []models.Paste
29
+ database.DB.Where("user_id = ? AND is_public = ?", user.ID, true).
30
+ Order("created_at DESC").
31
+ Find(&pastes)
32
+
33
+ c.JSON(http.StatusOK, gin.H{
34
+ "user": user,
35
+ "pastes": pastes,
36
+ })
37
+ }
38
+
39
+ // GetUserProfilePage renders the user profile page
40
+ func (h *UserHandler) GetUserProfilePage(c *gin.Context) {
41
+ username := c.Param("username")
42
+
43
+ var user models.User
44
+ if result := database.DB.Where("username = ?", username).First(&user); result.Error != nil {
45
+ c.HTML(http.StatusNotFound, "error.html", gin.H{
46
+ "title": "Not Found - Patbin",
47
+ "message": "User not found",
48
+ })
49
+ return
50
+ }
51
+
52
+ var pastes []models.Paste
53
+ database.DB.Where("user_id = ? AND is_public = ?", user.ID, true).
54
+ Order("created_at DESC").
55
+ Find(&pastes)
56
+
57
+ c.HTML(http.StatusOK, "profile.html", gin.H{
58
+ "title": user.Username + " - Patbin",
59
+ "profileUser": user,
60
+ "pastes": pastes,
61
+ })
62
+ }
63
+
64
+ // GetDashboard returns the current user's dashboard with all their pastes
65
+ func (h *UserHandler) GetDashboard(c *gin.Context) {
66
+ userID, ok := middleware.GetUserID(c)
67
+ if !ok {
68
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
69
+ return
70
+ }
71
+
72
+ var pastes []models.Paste
73
+ database.DB.Where("user_id = ?", userID).
74
+ Order("created_at DESC").
75
+ Find(&pastes)
76
+
77
+ c.JSON(http.StatusOK, gin.H{"pastes": pastes})
78
+ }
79
+
80
+ // GetDashboardPage renders the user's dashboard page
81
+ func (h *UserHandler) GetDashboardPage(c *gin.Context) {
82
+ userID, ok := middleware.GetUserID(c)
83
+ if !ok {
84
+ c.Redirect(http.StatusFound, "/login")
85
+ return
86
+ }
87
+
88
+ username, _ := middleware.GetUsername(c)
89
+
90
+ var pastes []models.Paste
91
+ database.DB.Where("user_id = ?", userID).
92
+ Order("created_at DESC").
93
+ Find(&pastes)
94
+
95
+ // Count stats
96
+ var publicCount, privateCount int64
97
+ database.DB.Model(&models.Paste{}).Where("user_id = ? AND is_public = ?", userID, true).Count(&publicCount)
98
+ database.DB.Model(&models.Paste{}).Where("user_id = ? AND is_public = ?", userID, false).Count(&privateCount)
99
+
100
+ c.HTML(http.StatusOK, "dashboard.html", gin.H{
101
+ "title": "Dashboard - Patbin",
102
+ "username": username,
103
+ "pastes": pastes,
104
+ "publicCount": publicCount,
105
+ "privateCount": privateCount,
106
+ "totalCount": len(pastes),
107
+ })
108
+ }
main.go ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "fmt"
5
+ "html/template"
6
+ "log"
7
+ "patbin/config"
8
+ "patbin/database"
9
+ "patbin/handlers"
10
+ "patbin/middleware"
11
+ "time"
12
+
13
+ "github.com/gin-gonic/gin"
14
+ )
15
+
16
+ func main() {
17
+ cfg := config.Load()
18
+ if err := database.Init(cfg.DBPath); err != nil {
19
+ log.Fatal("Database init failed:", err)
20
+ }
21
+
22
+ gin.SetMode(gin.ReleaseMode)
23
+ r := gin.Default()
24
+
25
+ r.SetFuncMap(template.FuncMap{
26
+ "timeAgo": func(t time.Time) string {
27
+ diff := time.Since(t)
28
+ switch {
29
+ case diff < time.Minute:
30
+ return "just now"
31
+ case diff < time.Hour:
32
+ return fmt.Sprintf("%dm ago", int(diff.Minutes()))
33
+ case diff < 24*time.Hour:
34
+ return fmt.Sprintf("%dh ago", int(diff.Hours()))
35
+ case diff < 7*24*time.Hour:
36
+ return fmt.Sprintf("%dd ago", int(diff.Hours()/24))
37
+ default:
38
+ return t.Format("Jan 2, 2006")
39
+ }
40
+ },
41
+ "formatTime": func(t time.Time) string { return t.Format("Jan 2, 2006 at 3:04 PM") },
42
+ "add": func(a, b int) int { return a + b },
43
+ "iterate": func(n int) []int {
44
+ r := make([]int, n)
45
+ for i := range r {
46
+ r[i] = i
47
+ }
48
+ return r
49
+ },
50
+ })
51
+
52
+ r.LoadHTMLGlob("templates/*")
53
+ r.Static("/static", "./static")
54
+ r.Use(middleware.AuthMiddleware(cfg))
55
+
56
+ authHandler := handlers.NewAuthHandler(cfg)
57
+ pasteHandler := handlers.NewPasteHandler()
58
+ userHandler := handlers.NewUserHandler()
59
+
60
+ r.GET("/", pasteHandler.HomePage)
61
+ r.GET("/login", authHandler.LoginPage)
62
+ r.GET("/register", authHandler.RegisterPage)
63
+
64
+ api := r.Group("/api")
65
+ {
66
+ api.POST("/auth/register", authHandler.Register)
67
+ api.POST("/auth/login", authHandler.Login)
68
+ api.POST("/auth/logout", authHandler.Logout)
69
+ api.GET("/auth/me", authHandler.GetCurrentUser)
70
+ api.POST("/paste", pasteHandler.CreatePaste)
71
+ api.GET("/paste/:id", pasteHandler.GetPaste)
72
+ api.PUT("/paste/:id", middleware.RequireAuth(), pasteHandler.UpdatePaste)
73
+ api.DELETE("/paste/:id", middleware.RequireAuth(), pasteHandler.DeletePaste)
74
+ api.POST("/paste/:id/fork", pasteHandler.ForkPaste)
75
+ api.GET("/pastes/recent", pasteHandler.RecentPastes)
76
+ api.GET("/user/:username", userHandler.GetUserProfile)
77
+ api.GET("/dashboard", middleware.RequireAuth(), userHandler.GetDashboard)
78
+ }
79
+
80
+ r.GET("/dashboard", middleware.RequireAuth(), userHandler.GetDashboardPage)
81
+ r.GET("/u/:username", userHandler.GetUserProfilePage)
82
+ r.GET("/:id/edit", middleware.RequireAuth(), pasteHandler.EditPastePage)
83
+ r.GET("/:id/raw", pasteHandler.GetRawPaste)
84
+ r.GET("/:id", pasteHandler.ViewPastePage)
85
+
86
+ log.Printf("🚀 Patbin running on http://localhost:%s", cfg.Port)
87
+ if err := r.Run(":" + cfg.Port); err != nil {
88
+ log.Fatal("Server failed:", err)
89
+ }
90
+ }
middleware/auth.go ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package middleware
2
+
3
+ import (
4
+ "net/http"
5
+ "patbin/config"
6
+ "strings"
7
+
8
+ "github.com/gin-gonic/gin"
9
+ "github.com/golang-jwt/jwt/v5"
10
+ )
11
+
12
+ type Claims struct {
13
+ UserID uint `json:"user_id"`
14
+ Username string `json:"username"`
15
+ jwt.RegisteredClaims
16
+ }
17
+
18
+ // AuthMiddleware validates JWT token and sets user info in context
19
+ func AuthMiddleware(cfg *config.Config) gin.HandlerFunc {
20
+ return func(c *gin.Context) {
21
+ // Try to get token from cookie first
22
+ tokenString, err := c.Cookie(cfg.CookieName)
23
+ if err != nil {
24
+ // Try Authorization header
25
+ authHeader := c.GetHeader("Authorization")
26
+ if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
27
+ tokenString = strings.TrimPrefix(authHeader, "Bearer ")
28
+ }
29
+ }
30
+
31
+ if tokenString == "" {
32
+ c.Next()
33
+ return
34
+ }
35
+
36
+ // Parse and validate token
37
+ claims := &Claims{}
38
+ token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
39
+ return []byte(cfg.JWTSecret), nil
40
+ })
41
+
42
+ if err != nil || !token.Valid {
43
+ c.Next()
44
+ return
45
+ }
46
+
47
+ // Set user info in context
48
+ c.Set("user_id", claims.UserID)
49
+ c.Set("username", claims.Username)
50
+ c.Set("authenticated", true)
51
+
52
+ c.Next()
53
+ }
54
+ }
55
+
56
+ // RequireAuth ensures user is authenticated
57
+ func RequireAuth() gin.HandlerFunc {
58
+ return func(c *gin.Context) {
59
+ authenticated, exists := c.Get("authenticated")
60
+ if !exists || !authenticated.(bool) {
61
+ // Check if API request or web request
62
+ if strings.HasPrefix(c.Request.URL.Path, "/api/") {
63
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
64
+ } else {
65
+ c.Redirect(http.StatusFound, "/login")
66
+ }
67
+ c.Abort()
68
+ return
69
+ }
70
+ c.Next()
71
+ }
72
+ }
73
+
74
+ // GetUserID returns the authenticated user ID from context
75
+ func GetUserID(c *gin.Context) (uint, bool) {
76
+ userID, exists := c.Get("user_id")
77
+ if !exists {
78
+ return 0, false
79
+ }
80
+ return userID.(uint), true
81
+ }
82
+
83
+ // GetUsername returns the authenticated username from context
84
+ func GetUsername(c *gin.Context) (string, bool) {
85
+ username, exists := c.Get("username")
86
+ if !exists {
87
+ return "", false
88
+ }
89
+ return username.(string), true
90
+ }
models/paste.go ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package models
2
+
3
+ import (
4
+ "time"
5
+ )
6
+
7
+ type Paste struct {
8
+ ID string `gorm:"primaryKey;size:12" json:"id"`
9
+ Title string `gorm:"size:255" json:"title"`
10
+ Content string `gorm:"type:text;not null" json:"content"`
11
+ Language string `gorm:"size:50" json:"language"`
12
+ IsPublic bool `gorm:"default:true" json:"is_public"`
13
+ Views int `gorm:"default:0" json:"views"`
14
+ ExpiresAt *time.Time `json:"expires_at,omitempty"`
15
+ BurnAfterRead bool `gorm:"default:false" json:"burn_after_read"`
16
+ UserID *uint `gorm:"index" json:"user_id,omitempty"`
17
+ User *User `gorm:"constraint:OnDelete:SET NULL" json:"user,omitempty"`
18
+ CreatedAt time.Time `json:"created_at"`
19
+ UpdatedAt time.Time `json:"updated_at"`
20
+ }
21
+
22
+ // Language extension mappings
23
+ var LanguageExtensions = map[string]string{
24
+ "go": "go",
25
+ "py": "python",
26
+ "python": "python",
27
+ "js": "javascript",
28
+ "javascript": "javascript",
29
+ "ts": "typescript",
30
+ "typescript": "typescript",
31
+ "html": "html",
32
+ "css": "css",
33
+ "json": "json",
34
+ "xml": "xml",
35
+ "yaml": "yaml",
36
+ "yml": "yaml",
37
+ "md": "markdown",
38
+ "markdown": "markdown",
39
+ "sql": "sql",
40
+ "sh": "bash",
41
+ "bash": "bash",
42
+ "c": "c",
43
+ "cpp": "cpp",
44
+ "h": "c",
45
+ "hpp": "cpp",
46
+ "java": "java",
47
+ "rs": "rust",
48
+ "rust": "rust",
49
+ "rb": "ruby",
50
+ "ruby": "ruby",
51
+ "php": "php",
52
+ "swift": "swift",
53
+ "kt": "kotlin",
54
+ "kotlin": "kotlin",
55
+ "scala": "scala",
56
+ "r": "r",
57
+ "lua": "lua",
58
+ "perl": "perl",
59
+ "pl": "perl",
60
+ "txt": "plaintext",
61
+ "text": "plaintext",
62
+ }
63
+
64
+ func GetLanguageFromExtension(ext string) string {
65
+ if lang, ok := LanguageExtensions[ext]; ok {
66
+ return lang
67
+ }
68
+ return "plaintext"
69
+ }
models/user.go ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package models
2
+
3
+ import (
4
+ "time"
5
+ )
6
+
7
+ type User struct {
8
+ ID uint `gorm:"primaryKey" json:"id"`
9
+ Username string `gorm:"uniqueIndex;size:50;not null" json:"username"`
10
+ Password string `gorm:"not null" json:"-"`
11
+ CreatedAt time.Time `json:"created_at"`
12
+ Pastes []Paste `gorm:"foreignKey:UserID" json:"pastes,omitempty"`
13
+ }
static/css/style.css ADDED
@@ -0,0 +1,806 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg-primary: #fafafa;
3
+ --bg-secondary: #f5f5f5;
4
+ --bg-tertiary: #e5e5e5;
5
+ --bg-code: #1c1c1c;
6
+ --text-primary: #171717;
7
+ --text-secondary: #525252;
8
+ --text-tertiary: #a3a3a3;
9
+ --accent: #f97316;
10
+ --accent-hover: #ea580c;
11
+ --accent-light: #fff7ed;
12
+ --border: #d4d4d4;
13
+ --success: #22c55e;
14
+ --warning: #f59e0b;
15
+ --error: #ef4444;
16
+ --shadow: 0 1px 3px rgb(0 0 0 / 0.1);
17
+ --radius: 4px;
18
+ --radius-lg: 8px;
19
+ --font-mono: 'JetBrains Mono', monospace;
20
+ }
21
+
22
+ [data-theme="dark"] {
23
+ --bg-primary: #111111;
24
+ --bg-secondary: #181818;
25
+ --bg-tertiary: #222222;
26
+ --bg-code: #111111;
27
+ --text-primary: #e5e5e5;
28
+ --text-secondary: #999999;
29
+ --text-tertiary: #555555;
30
+ --accent: #fb923c;
31
+ --accent-hover: #fdba74;
32
+ --accent-light: #2d1a0a;
33
+ --border: #2d2d2d;
34
+ }
35
+
36
+ *,
37
+ *::before,
38
+ *::after {
39
+ box-sizing: border-box;
40
+ margin: 0;
41
+ padding: 0;
42
+ }
43
+
44
+ ::-webkit-scrollbar {
45
+ width: 10px;
46
+ height: 10px;
47
+ }
48
+
49
+ ::-webkit-scrollbar-track {
50
+ background: var(--bg-secondary);
51
+ }
52
+
53
+ ::-webkit-scrollbar-thumb {
54
+ background: var(--border);
55
+ border-radius: 5px;
56
+ border: 2px solid var(--bg-secondary);
57
+ }
58
+
59
+ ::-webkit-scrollbar-thumb:hover {
60
+ background: var(--accent);
61
+ }
62
+
63
+ * {
64
+ scrollbar-width: thin;
65
+ scrollbar-color: var(--border) var(--bg-secondary);
66
+ }
67
+
68
+ html {
69
+ font-size: 16px;
70
+ }
71
+
72
+ body {
73
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
74
+ background: var(--bg-primary);
75
+ color: var(--text-primary);
76
+ line-height: 1.5;
77
+ min-height: 100vh;
78
+ }
79
+
80
+ a {
81
+ color: var(--accent);
82
+ text-decoration: none;
83
+ }
84
+
85
+ a:hover {
86
+ color: var(--accent-hover);
87
+ }
88
+
89
+ .container {
90
+ max-width: 1400px;
91
+ margin: 0 auto;
92
+ padding: 0 12px;
93
+ }
94
+
95
+ .navbar {
96
+ background: var(--bg-secondary);
97
+ border-bottom: 1px solid var(--border);
98
+ padding: 8px 0;
99
+ position: sticky;
100
+ top: 0;
101
+ z-index: 100;
102
+ }
103
+
104
+ .navbar .container {
105
+ display: flex;
106
+ align-items: center;
107
+ justify-content: space-between;
108
+ }
109
+
110
+ .logo {
111
+ font-size: 1.25rem;
112
+ font-weight: 700;
113
+ color: var(--accent);
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 6px;
117
+ }
118
+
119
+ .logo svg {
120
+ width: 22px;
121
+ height: 22px;
122
+ }
123
+
124
+ .nav-links {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 6px;
128
+ }
129
+
130
+ .btn {
131
+ display: inline-flex;
132
+ align-items: center;
133
+ justify-content: center;
134
+ gap: 4px;
135
+ padding: 6px 12px;
136
+ font-size: 13px;
137
+ font-weight: 500;
138
+ border-radius: var(--radius);
139
+ border: none;
140
+ cursor: pointer;
141
+ transition: all 0.15s;
142
+ text-decoration: none;
143
+ }
144
+
145
+ .btn-primary {
146
+ background: var(--accent);
147
+ color: #fff;
148
+ }
149
+
150
+ .btn-primary:hover {
151
+ background: var(--accent-hover);
152
+ color: #fff;
153
+ }
154
+
155
+ .btn-secondary {
156
+ background: var(--bg-tertiary);
157
+ color: var(--text-primary);
158
+ border: 1px solid var(--border);
159
+ }
160
+
161
+ .btn-secondary:hover {
162
+ background: var(--border);
163
+ }
164
+
165
+ .btn-ghost {
166
+ background: transparent;
167
+ color: var(--text-secondary);
168
+ }
169
+
170
+ .btn-ghost:hover {
171
+ background: var(--bg-tertiary);
172
+ }
173
+
174
+ .btn-danger {
175
+ background: var(--error);
176
+ color: #fff;
177
+ }
178
+
179
+ .btn-sm {
180
+ padding: 4px 8px;
181
+ font-size: 12px;
182
+ }
183
+
184
+ .btn-lg {
185
+ padding: 10px 20px;
186
+ font-size: 14px;
187
+ }
188
+
189
+ .form-group {
190
+ margin-bottom: 12px;
191
+ }
192
+
193
+ .form-label {
194
+ display: block;
195
+ font-size: 13px;
196
+ font-weight: 500;
197
+ margin-bottom: 4px;
198
+ color: var(--text-secondary);
199
+ }
200
+
201
+ .form-input,
202
+ .form-textarea,
203
+ .form-select {
204
+ width: 100%;
205
+ padding: 8px 10px;
206
+ font-size: 14px;
207
+ border: 1px solid var(--border);
208
+ border-radius: var(--radius);
209
+ background: var(--bg-primary);
210
+ color: var(--text-primary);
211
+ }
212
+
213
+ .form-input:focus,
214
+ .form-textarea:focus,
215
+ .form-select:focus {
216
+ outline: none;
217
+ border-color: var(--accent);
218
+ }
219
+
220
+ .form-textarea {
221
+ font-family: var(--font-mono);
222
+ font-size: 13px;
223
+ min-height: 60vh;
224
+ resize: vertical;
225
+ }
226
+
227
+ .form-row {
228
+ display: grid;
229
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
230
+ gap: 12px;
231
+ }
232
+
233
+ .form-checkbox {
234
+ display: flex;
235
+ align-items: center;
236
+ gap: 8px;
237
+ cursor: pointer;
238
+ }
239
+
240
+ .form-checkbox input[type="checkbox"] {
241
+ -webkit-appearance: none;
242
+ appearance: none;
243
+ width: 18px;
244
+ height: 18px;
245
+ border: 2px solid var(--border);
246
+ border-radius: 4px;
247
+ background: var(--bg-primary);
248
+ cursor: pointer;
249
+ position: relative;
250
+ }
251
+
252
+ .form-checkbox input:checked {
253
+ background: var(--accent);
254
+ border-color: var(--accent);
255
+ }
256
+
257
+ .form-checkbox input:checked::after {
258
+ content: '';
259
+ position: absolute;
260
+ top: 2px;
261
+ left: 5px;
262
+ width: 4px;
263
+ height: 8px;
264
+ border: solid #fff;
265
+ border-width: 0 2px 2px 0;
266
+ transform: rotate(45deg);
267
+ }
268
+
269
+ .form-checkbox span {
270
+ font-size: 14px;
271
+ }
272
+
273
+ .card {
274
+ background: var(--bg-secondary);
275
+ border: 1px solid var(--border);
276
+ border-radius: var(--radius-lg);
277
+ padding: 16px;
278
+ }
279
+
280
+ .card-header {
281
+ display: flex;
282
+ align-items: center;
283
+ justify-content: space-between;
284
+ margin-bottom: 12px;
285
+ padding-bottom: 12px;
286
+ border-bottom: 1px solid var(--border);
287
+ }
288
+
289
+ .code-container {
290
+ background: var(--bg-secondary);
291
+ border: 1px solid var(--border);
292
+ border-radius: var(--radius-lg);
293
+ overflow: hidden;
294
+ }
295
+
296
+ [data-theme="dark"] .code-container {
297
+ background: var(--bg-code);
298
+ }
299
+
300
+ .code-header {
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: space-between;
304
+ flex-wrap: wrap;
305
+ gap: 8px;
306
+ padding: 8px 12px;
307
+ background: var(--bg-tertiary);
308
+ border-bottom: 1px solid var(--border);
309
+ }
310
+
311
+ [data-theme="dark"] .code-header {
312
+ background: rgba(0, 0, 0, 0.3);
313
+ border-color: var(--border);
314
+ }
315
+
316
+ .code-title {
317
+ font-size: 13px;
318
+ font-weight: 500;
319
+ display: flex;
320
+ align-items: center;
321
+ gap: 6px;
322
+ }
323
+
324
+ .code-meta {
325
+ display: flex;
326
+ align-items: center;
327
+ gap: 12px;
328
+ font-size: 12px;
329
+ color: var(--text-tertiary);
330
+ }
331
+
332
+ .code-actions {
333
+ display: flex;
334
+ gap: 6px;
335
+ padding: 6px 12px;
336
+ background: var(--bg-tertiary);
337
+ flex-wrap: wrap;
338
+ }
339
+
340
+ [data-theme="dark"] .code-actions {
341
+ background: rgba(0, 0, 0, 0.2);
342
+ }
343
+
344
+ .code-body {
345
+ display: flex;
346
+ overflow-x: auto;
347
+ }
348
+
349
+ .code-body.wrap-text {
350
+ overflow-x: hidden;
351
+ }
352
+
353
+ .code-body.wrap-text .code-content {
354
+ overflow-x: hidden;
355
+ overflow-y: auto;
356
+ }
357
+
358
+ .code-body.wrap-text .code-content pre,
359
+ .code-body.wrap-text .code-content code {
360
+ white-space: pre-wrap !important;
361
+ word-wrap: break-word !important;
362
+ overflow-wrap: break-word !important;
363
+ }
364
+
365
+ .line-numbers {
366
+ flex-shrink: 0;
367
+ padding: 10px 0;
368
+ text-align: right;
369
+ background: var(--bg-tertiary);
370
+ border-right: 1px solid var(--border);
371
+ user-select: none;
372
+ }
373
+
374
+ [data-theme="dark"] .line-numbers {
375
+ background: rgba(0, 0, 0, 0.2);
376
+ }
377
+
378
+ .line-numbers span {
379
+ display: block;
380
+ padding: 0 10px;
381
+ font-family: var(--font-mono);
382
+ font-size: 12px;
383
+ line-height: 1.5;
384
+ color: var(--text-tertiary);
385
+ }
386
+
387
+ .code-content {
388
+ flex: 1;
389
+ padding: 10px 12px;
390
+ overflow-x: auto;
391
+ background: var(--bg-primary);
392
+ }
393
+
394
+ [data-theme="dark"] .code-content {
395
+ background: transparent;
396
+ }
397
+
398
+ .code-content pre {
399
+ margin: 0;
400
+ font-family: var(--font-mono);
401
+ font-size: 13px;
402
+ line-height: 1.5;
403
+ color: var(--text-primary);
404
+ white-space: pre;
405
+ }
406
+
407
+ [data-theme="dark"] .code-content pre {
408
+ color: #c9d1d9;
409
+ }
410
+
411
+ .paste-list {
412
+ display: flex;
413
+ flex-direction: column;
414
+ gap: 8px;
415
+ }
416
+
417
+ .paste-item {
418
+ display: flex;
419
+ align-items: center;
420
+ gap: 12px;
421
+ padding: 10px;
422
+ background: var(--bg-secondary);
423
+ border: 1px solid var(--border);
424
+ border-radius: var(--radius);
425
+ }
426
+
427
+ .paste-item:hover {
428
+ border-color: var(--accent);
429
+ }
430
+
431
+ .paste-icon {
432
+ width: 36px;
433
+ height: 36px;
434
+ border-radius: var(--radius);
435
+ background: var(--accent-light);
436
+ color: var(--accent);
437
+ display: flex;
438
+ align-items: center;
439
+ justify-content: center;
440
+ }
441
+
442
+ .paste-info {
443
+ flex: 1;
444
+ min-width: 0;
445
+ }
446
+
447
+ .paste-name {
448
+ font-weight: 500;
449
+ white-space: nowrap;
450
+ overflow: hidden;
451
+ text-overflow: ellipsis;
452
+ }
453
+
454
+ .paste-details {
455
+ font-size: 12px;
456
+ color: var(--text-tertiary);
457
+ display: flex;
458
+ flex-wrap: wrap;
459
+ gap: 6px;
460
+ }
461
+
462
+ .paste-badge {
463
+ padding: 2px 6px;
464
+ font-size: 11px;
465
+ font-weight: 500;
466
+ border-radius: 99px;
467
+ background: var(--bg-tertiary);
468
+ }
469
+
470
+ .paste-badge.public {
471
+ background: #dcfce7;
472
+ color: #166534;
473
+ }
474
+
475
+ [data-theme="dark"] .paste-badge.public {
476
+ background: #14532d;
477
+ color: #86efac;
478
+ }
479
+
480
+ .paste-badge.private {
481
+ background: #fef3c7;
482
+ color: #92400e;
483
+ }
484
+
485
+ [data-theme="dark"] .paste-badge.private {
486
+ background: #78350f;
487
+ color: #fcd34d;
488
+ }
489
+
490
+ .stats-grid {
491
+ display: grid;
492
+ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
493
+ gap: 10px;
494
+ margin-bottom: 16px;
495
+ }
496
+
497
+ .stat-card {
498
+ background: var(--bg-tertiary);
499
+ border-radius: var(--radius);
500
+ padding: 12px;
501
+ text-align: center;
502
+ }
503
+
504
+ .stat-value {
505
+ font-size: 1.25rem;
506
+ font-weight: 700;
507
+ color: var(--accent);
508
+ }
509
+
510
+ .stat-label {
511
+ font-size: 12px;
512
+ color: var(--text-tertiary);
513
+ }
514
+
515
+ .theme-toggle {
516
+ background: var(--bg-tertiary);
517
+ border: 1px solid var(--border);
518
+ border-radius: var(--radius);
519
+ padding: 6px;
520
+ cursor: pointer;
521
+ display: flex;
522
+ }
523
+
524
+ .theme-toggle svg {
525
+ width: 18px;
526
+ height: 18px;
527
+ color: var(--text-secondary);
528
+ }
529
+
530
+ .theme-toggle .sun {
531
+ display: none;
532
+ }
533
+
534
+ [data-theme="dark"] .theme-toggle .sun {
535
+ display: block;
536
+ }
537
+
538
+ [data-theme="dark"] .theme-toggle .moon {
539
+ display: none;
540
+ }
541
+
542
+ .toast-container {
543
+ position: fixed;
544
+ bottom: 16px;
545
+ right: 16px;
546
+ z-index: 1000;
547
+ display: flex;
548
+ flex-direction: column;
549
+ gap: 6px;
550
+ }
551
+
552
+ .toast {
553
+ background: var(--bg-secondary);
554
+ border: 1px solid var(--border);
555
+ border-radius: var(--radius);
556
+ padding: 10px 14px;
557
+ box-shadow: var(--shadow);
558
+ display: flex;
559
+ align-items: center;
560
+ gap: 8px;
561
+ animation: slideIn 0.3s;
562
+ }
563
+
564
+ .toast.success {
565
+ border-left: 3px solid var(--success);
566
+ }
567
+
568
+ .toast.error {
569
+ border-left: 3px solid var(--error);
570
+ }
571
+
572
+ @keyframes slideIn {
573
+ from {
574
+ opacity: 0;
575
+ transform: translateX(100%);
576
+ }
577
+
578
+ to {
579
+ opacity: 1;
580
+ transform: translateX(0);
581
+ }
582
+ }
583
+
584
+ .page {
585
+ padding: 16px 0;
586
+ }
587
+
588
+ .page-header {
589
+ margin-bottom: 16px;
590
+ }
591
+
592
+ .page-title {
593
+ font-size: 1.25rem;
594
+ font-weight: 600;
595
+ }
596
+
597
+ .page-subtitle {
598
+ color: var(--text-secondary);
599
+ font-size: 14px;
600
+ }
601
+
602
+ .auth-page {
603
+ min-height: calc(100vh - 50px);
604
+ display: flex;
605
+ align-items: center;
606
+ justify-content: center;
607
+ padding: 20px;
608
+ }
609
+
610
+ .auth-card {
611
+ width: 100%;
612
+ max-width: 360px;
613
+ }
614
+
615
+ .auth-header {
616
+ text-align: center;
617
+ margin-bottom: 20px;
618
+ }
619
+
620
+ .auth-footer {
621
+ text-align: center;
622
+ margin-top: 16px;
623
+ font-size: 13px;
624
+ color: var(--text-secondary);
625
+ }
626
+
627
+ .error-page {
628
+ min-height: calc(100vh - 50px);
629
+ display: flex;
630
+ flex-direction: column;
631
+ align-items: center;
632
+ justify-content: center;
633
+ text-align: center;
634
+ padding: 20px;
635
+ }
636
+
637
+ .error-code {
638
+ font-size: 4rem;
639
+ font-weight: 700;
640
+ color: var(--accent);
641
+ line-height: 1;
642
+ }
643
+
644
+ .error-message {
645
+ font-size: 1rem;
646
+ color: var(--text-secondary);
647
+ margin: 12px 0 20px;
648
+ }
649
+
650
+ @media (max-width: 768px) {
651
+ .container {
652
+ padding: 0 8px;
653
+ }
654
+
655
+ .code-header {
656
+ flex-direction: column;
657
+ align-items: flex-start;
658
+ }
659
+
660
+ .form-row {
661
+ grid-template-columns: 1fr;
662
+ }
663
+
664
+ .paste-item {
665
+ flex-direction: column;
666
+ align-items: flex-start;
667
+ }
668
+ }
669
+
670
+ .hidden {
671
+ display: none !important;
672
+ }
673
+
674
+ .flex {
675
+ display: flex;
676
+ }
677
+
678
+ .gap-2 {
679
+ gap: 8px;
680
+ }
681
+
682
+ .gap-3 {
683
+ gap: 12px;
684
+ }
685
+
686
+ .mt-2 {
687
+ margin-top: 8px;
688
+ }
689
+
690
+ .mt-3 {
691
+ margin-top: 12px;
692
+ }
693
+
694
+ .mt-4 {
695
+ margin-top: 16px;
696
+ }
697
+
698
+ .text-center {
699
+ text-align: center;
700
+ }
701
+
702
+ .text-sm {
703
+ font-size: 13px;
704
+ }
705
+
706
+ .text-muted {
707
+ color: var(--text-secondary);
708
+ }
709
+
710
+ .font-mono {
711
+ font-family: var(--font-mono);
712
+ }
713
+
714
+ code[class*="language-"],
715
+ pre[class*="language-"] {
716
+ font-family: var(--font-mono);
717
+ font-size: 13px;
718
+ line-height: 1.5;
719
+ background: none;
720
+ color: var(--text-primary);
721
+ }
722
+
723
+ .token.comment,
724
+ .token.prolog,
725
+ .token.cdata {
726
+ color: #6b7280;
727
+ }
728
+
729
+ .token.punctuation {
730
+ color: #475569;
731
+ }
732
+
733
+ .token.property,
734
+ .token.tag,
735
+ .token.number,
736
+ .token.constant,
737
+ .token.symbol {
738
+ color: #d946ef;
739
+ }
740
+
741
+ .token.selector,
742
+ .token.string,
743
+ .token.char,
744
+ .token.builtin {
745
+ color: #16a34a;
746
+ }
747
+
748
+ .token.operator,
749
+ .token.url {
750
+ color: #0891b2;
751
+ }
752
+
753
+ .token.keyword {
754
+ color: #7c3aed;
755
+ }
756
+
757
+ .token.function,
758
+ .token.class-name {
759
+ color: #ca8a04;
760
+ }
761
+
762
+ .token.regex,
763
+ .token.variable {
764
+ color: #ea580c;
765
+ }
766
+
767
+ [data-theme="dark"] code[class*="language-"],
768
+ [data-theme="dark"] pre[class*="language-"] {
769
+ color: #c9d1d9;
770
+ }
771
+
772
+ [data-theme="dark"] .token.punctuation {
773
+ color: #8b949e;
774
+ }
775
+
776
+ [data-theme="dark"] .token.property,
777
+ [data-theme="dark"] .token.tag,
778
+ [data-theme="dark"] .token.number,
779
+ [data-theme="dark"] .token.constant {
780
+ color: #79c0ff;
781
+ }
782
+
783
+ [data-theme="dark"] .token.selector,
784
+ [data-theme="dark"] .token.string,
785
+ [data-theme="dark"] .token.char {
786
+ color: #a5d6ff;
787
+ }
788
+
789
+ [data-theme="dark"] .token.operator,
790
+ [data-theme="dark"] .token.url {
791
+ color: #ffa657;
792
+ }
793
+
794
+ [data-theme="dark"] .token.keyword {
795
+ color: #ff7b72;
796
+ }
797
+
798
+ [data-theme="dark"] .token.function,
799
+ [data-theme="dark"] .token.class-name {
800
+ color: #d2a8ff;
801
+ }
802
+
803
+ [data-theme="dark"] .token.regex,
804
+ [data-theme="dark"] .token.variable {
805
+ color: #7ee787;
806
+ }
static/js/app.js ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const ThemeManager = {
2
+ init() {
3
+ const saved = localStorage.getItem('theme');
4
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
5
+ this.setTheme(saved || (prefersDark ? 'dark' : 'light'));
6
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
7
+ if (!localStorage.getItem('theme')) this.setTheme(e.matches ? 'dark' : 'light');
8
+ });
9
+ },
10
+ setTheme(t) { document.documentElement.setAttribute('data-theme', t); localStorage.setItem('theme', t); },
11
+ toggle() { this.setTheme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'); }
12
+ };
13
+
14
+ const Toast = {
15
+ container: null,
16
+ init() { this.container = document.createElement('div'); this.container.className = 'toast-container'; document.body.appendChild(this.container); },
17
+ show(msg, type = 'success', dur = 2500) {
18
+ const t = document.createElement('div');
19
+ t.className = `toast ${type}`;
20
+ t.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">${type === 'success' ? '<path d="M20 6L9 17l-5-5"/>' : '<circle cx="12" cy="12" r="10"/><path d="M15 9l-6 6M9 9l6 6"/>'}</svg><span>${msg}</span>`;
21
+ this.container.appendChild(t);
22
+ setTimeout(() => { t.style.animation = 'slideIn 0.3s reverse'; setTimeout(() => t.remove(), 300); }, dur);
23
+ }
24
+ };
25
+
26
+ async function copyToClipboard(text, btn) {
27
+ try {
28
+ await navigator.clipboard.writeText(text);
29
+ Toast.show('Copied!');
30
+ if (btn) { const o = btn.innerHTML; btn.innerHTML = '✓ Copied'; setTimeout(() => btn.innerHTML = o, 1500); }
31
+ } catch { Toast.show('Copy failed', 'error'); }
32
+ }
33
+
34
+ function toggleWrap() {
35
+ const body = document.querySelector('.code-body');
36
+ const btn = document.getElementById('wrap-toggle');
37
+ if (!body) return;
38
+ body.classList.toggle('wrap-text');
39
+ const isWrapped = body.classList.contains('wrap-text');
40
+ localStorage.setItem('wrapText', isWrapped);
41
+ if (btn) btn.classList.toggle('btn-primary', isWrapped);
42
+ }
43
+
44
+ const API = {
45
+ async request(url, opts = {}) {
46
+ const r = await fetch(url, { headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', ...opts });
47
+ const d = await r.json();
48
+ if (!r.ok) throw new Error(d.error || 'Error');
49
+ return d;
50
+ },
51
+ createPaste: (d) => API.request('/api/paste', { method: 'POST', body: JSON.stringify(d) }),
52
+ updatePaste: (id, d) => API.request(`/api/paste/${id}`, { method: 'PUT', body: JSON.stringify(d) }),
53
+ deletePaste: (id) => API.request(`/api/paste/${id}`, { method: 'DELETE' }),
54
+ forkPaste: (id) => API.request(`/api/paste/${id}/fork`, { method: 'POST' }),
55
+ login: (u, p) => API.request('/api/auth/login', { method: 'POST', body: JSON.stringify({ username: u, password: p }) }),
56
+ register: (u, p) => API.request('/api/auth/register', { method: 'POST', body: JSON.stringify({ username: u, password: p }) }),
57
+ logout: () => API.request('/api/auth/logout', { method: 'POST' })
58
+ };
59
+
60
+ function setupPasteForm() {
61
+ const f = document.getElementById('paste-form');
62
+ if (!f) return;
63
+ f.addEventListener('submit', async e => {
64
+ e.preventDefault();
65
+ const btn = f.querySelector('button[type="submit"]');
66
+ try {
67
+ btn.disabled = true; btn.textContent = 'Creating...';
68
+ const paste = await API.createPaste({
69
+ title: f.title.value || 'Untitled', content: f.content.value, language: f.language.value,
70
+ is_public: f.is_public.checked, expires_in: f.expires_in?.value || 'never', burn_after_read: f.burn_after_read?.checked || false
71
+ });
72
+ window.location.href = `/${paste.id}`;
73
+ } catch (err) { Toast.show(err.message, 'error'); btn.disabled = false; btn.textContent = 'Create Paste'; }
74
+ });
75
+ }
76
+
77
+ function setupEditForm() {
78
+ const f = document.getElementById('edit-form');
79
+ if (!f) return;
80
+ f.addEventListener('submit', async e => {
81
+ e.preventDefault();
82
+ const btn = f.querySelector('button[type="submit"]');
83
+ try {
84
+ btn.disabled = true; btn.textContent = 'Saving...';
85
+ await API.updatePaste(f.dataset.pasteId, { title: f.title.value, content: f.content.value, language: f.language.value, is_public: f.is_public.checked });
86
+ Toast.show('Saved!'); setTimeout(() => window.location.href = `/${f.dataset.pasteId}`, 800);
87
+ } catch (err) { Toast.show(err.message, 'error'); btn.disabled = false; btn.textContent = 'Save'; }
88
+ });
89
+ }
90
+
91
+ function setupAuthForms() {
92
+ const login = document.getElementById('login-form');
93
+ const reg = document.getElementById('register-form');
94
+ if (login) login.addEventListener('submit', async e => {
95
+ e.preventDefault();
96
+ const btn = login.querySelector('button[type="submit"]');
97
+ try { btn.disabled = true; await API.login(login.username.value, login.password.value); window.location.href = '/dashboard'; }
98
+ catch (err) { Toast.show(err.message, 'error'); btn.disabled = false; }
99
+ });
100
+ if (reg) reg.addEventListener('submit', async e => {
101
+ e.preventDefault();
102
+ const btn = reg.querySelector('button[type="submit"]');
103
+ try { btn.disabled = true; await API.register(reg.username.value, reg.password.value); window.location.href = '/dashboard'; }
104
+ catch (err) { Toast.show(err.message, 'error'); btn.disabled = false; }
105
+ });
106
+ }
107
+
108
+ function setupDeleteButton() {
109
+ const btn = document.getElementById('delete-paste');
110
+ if (!btn) return;
111
+ btn.addEventListener('click', async () => {
112
+ if (!confirm('Delete this paste?')) return;
113
+ try { await API.deletePaste(btn.dataset.pasteId); Toast.show('Deleted!'); setTimeout(() => window.location.href = '/dashboard', 800); }
114
+ catch (err) { Toast.show(err.message, 'error'); }
115
+ });
116
+ }
117
+
118
+ function setupForkButton() {
119
+ const btn = document.getElementById('fork-paste');
120
+ if (!btn) return;
121
+ btn.addEventListener('click', async () => {
122
+ try { const f = await API.forkPaste(btn.dataset.pasteId); Toast.show('Forked!'); setTimeout(() => window.location.href = `/${f.id}`, 800); }
123
+ catch (err) { Toast.show(err.message, 'error'); }
124
+ });
125
+ }
126
+
127
+ function setupLogout() {
128
+ const btn = document.getElementById('logout-btn');
129
+ if (!btn) return;
130
+ btn.addEventListener('click', async e => { e.preventDefault(); try { await API.logout(); window.location.href = '/'; } catch { } });
131
+ }
132
+
133
+ function setupKeyboardShortcuts() {
134
+ document.addEventListener('keydown', e => {
135
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
136
+ const f = document.querySelector('#paste-form, #edit-form');
137
+ if (f) { e.preventDefault(); f.dispatchEvent(new Event('submit')); }
138
+ }
139
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
140
+ const f = document.getElementById('edit-form');
141
+ if (f) { e.preventDefault(); f.dispatchEvent(new Event('submit')); }
142
+ }
143
+ });
144
+ }
145
+
146
+ function restoreWrapState() {
147
+ if (localStorage.getItem('wrapText') === 'true') {
148
+ const body = document.querySelector('.code-body');
149
+ const btn = document.getElementById('wrap-toggle');
150
+ if (body) body.classList.add('wrap-text');
151
+ if (btn) btn.classList.add('btn-primary');
152
+ }
153
+ }
154
+
155
+ document.addEventListener('DOMContentLoaded', () => {
156
+ ThemeManager.init();
157
+ Toast.init();
158
+ setupPasteForm();
159
+ setupEditForm();
160
+ setupAuthForms();
161
+ setupDeleteButton();
162
+ setupForkButton();
163
+ setupLogout();
164
+ setupKeyboardShortcuts();
165
+ restoreWrapState();
166
+ });
167
+
168
+ window.toggleTheme = () => ThemeManager.toggle();
169
+ window.copyToClipboard = copyToClipboard;
170
+ window.toggleWrap = toggleWrap;
templates/dashboard.html ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{.title}}</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
10
+ <link href="/static/css/style.css" rel="stylesheet">
11
+ </head>
12
+ <body>
13
+ <nav class="navbar">
14
+ <div class="container">
15
+ <a href="/" class="logo">
16
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
17
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
18
+ <polyline points="14 2 14 8 20 8"/>
19
+ <line x1="16" y1="13" x2="8" y2="13"/>
20
+ <line x1="16" y1="17" x2="8" y2="17"/>
21
+ <polyline points="10 9 9 9 8 9"/>
22
+ </svg>
23
+ Patbin
24
+ </a>
25
+ <div class="nav-links">
26
+ <button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme">
27
+ <svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
28
+ <circle cx="12" cy="12" r="5"/>
29
+ <line x1="12" y1="1" x2="12" y2="3"/>
30
+ <line x1="12" y1="21" x2="12" y2="23"/>
31
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
32
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
33
+ <line x1="1" y1="12" x2="3" y2="12"/>
34
+ <line x1="21" y1="12" x2="23" y2="12"/>
35
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
36
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
37
+ </svg>
38
+ <svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
39
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
40
+ </svg>
41
+ </button>
42
+ <a href="/" class="btn btn-primary">
43
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
44
+ <path d="M12 5v14M5 12h14"/>
45
+ </svg>
46
+ New Paste
47
+ </a>
48
+ <a href="#" id="logout-btn" class="btn btn-secondary">Logout</a>
49
+ </div>
50
+ </div>
51
+ </nav>
52
+
53
+ <main class="page">
54
+ <div class="container">
55
+ <div class="page-header">
56
+ <h1 class="page-title">Dashboard</h1>
57
+ <p class="page-subtitle">Welcome back, {{.username}}</p>
58
+ </div>
59
+
60
+ <div class="stats-grid">
61
+ <div class="stat-card">
62
+ <div class="stat-value">{{.totalCount}}</div>
63
+ <div class="stat-label">Total Pastes</div>
64
+ </div>
65
+ <div class="stat-card">
66
+ <div class="stat-value">{{.publicCount}}</div>
67
+ <div class="stat-label">Public</div>
68
+ </div>
69
+ <div class="stat-card">
70
+ <div class="stat-value">{{.privateCount}}</div>
71
+ <div class="stat-label">Private</div>
72
+ </div>
73
+ </div>
74
+
75
+ <div class="card">
76
+ <div class="card-header">
77
+ <h2 class="card-title">Your Pastes</h2>
78
+ <a href="/" class="btn btn-primary btn-sm">
79
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
80
+ <path d="M12 5v14M5 12h14"/>
81
+ </svg>
82
+ New
83
+ </a>
84
+ </div>
85
+
86
+ {{if .pastes}}
87
+ <div class="paste-list">
88
+ {{range .pastes}}
89
+ <a href="/{{.ID}}" class="paste-item">
90
+ <div class="paste-icon">
91
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
92
+ <polyline points="16 18 22 12 16 6"/>
93
+ <polyline points="8 6 2 12 8 18"/>
94
+ </svg>
95
+ </div>
96
+ <div class="paste-info">
97
+ <div class="paste-name">{{if .Title}}{{.Title}}{{else}}Untitled{{end}}</div>
98
+ <div class="paste-details">
99
+ {{if .IsPublic}}
100
+ <span class="paste-badge public">Public</span>
101
+ {{else}}
102
+ <span class="paste-badge private">Private</span>
103
+ {{end}}
104
+ <span>{{if .Language}}{{.Language}}{{else}}plain{{end}}</span>
105
+ <span>{{.Views}} views</span>
106
+ <span>{{formatTime .CreatedAt}}</span>
107
+ </div>
108
+ </div>
109
+ </a>
110
+ {{end}}
111
+ </div>
112
+ {{else}}
113
+ <div class="text-center text-muted" style="padding: 3rem 1rem;">
114
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="margin: 0 auto 1rem; opacity: 0.5;">
115
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
116
+ <polyline points="14 2 14 8 20 8"/>
117
+ </svg>
118
+ <p>No pastes yet</p>
119
+ <a href="/" class="btn btn-primary mt-3">Create your first paste</a>
120
+ </div>
121
+ {{end}}
122
+ </div>
123
+ </div>
124
+ </main>
125
+
126
+ <script src="/static/js/app.js"></script>
127
+ </body>
128
+ </html>
templates/edit.html ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{.title}}</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
8
+ <link href="/static/css/style.css" rel="stylesheet">
9
+ </head>
10
+ <body>
11
+ <nav class="navbar">
12
+ <div class="container">
13
+ <a href="/" class="logo">Patbin</a>
14
+ <div class="nav-links">
15
+ <button class="theme-toggle" onclick="toggleTheme()">
16
+ <svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/></svg>
17
+ <svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
18
+ </button>
19
+ <a href="/" class="btn btn-primary">New Paste</a>
20
+ </div>
21
+ </div>
22
+ </nav>
23
+ <main class="page">
24
+ <div class="container container-narrow">
25
+ <div class="page-header"><h1>Edit Paste</h1></div>
26
+ <form id="edit-form" class="card" data-paste-id="{{.paste.ID}}">
27
+ <div class="form-group">
28
+ <label class="form-label" for="title">Title</label>
29
+ <input type="text" id="title" name="title" class="form-input" value="{{.paste.Title}}">
30
+ </div>
31
+ <div class="form-group">
32
+ <label class="form-label" for="content">Content</label>
33
+ <textarea id="content" name="content" class="form-textarea" required>{{.paste.Content}}</textarea>
34
+ </div>
35
+ <div class="form-row">
36
+ <div class="form-group">
37
+ <label class="form-label" for="language">Language</label>
38
+ <select id="language" name="language" class="form-select">
39
+ <option value="">Auto-detect</option>
40
+ <option value="go" {{if eq .paste.Language "go"}}selected{{end}}>Go</option>
41
+ <option value="python" {{if eq .paste.Language "python"}}selected{{end}}>Python</option>
42
+ <option value="javascript" {{if eq .paste.Language "javascript"}}selected{{end}}>JavaScript</option>
43
+ </select>
44
+ </div>
45
+ <div class="form-group">
46
+ <label class="form-checkbox"><input type="checkbox" name="is_public" {{if .paste.IsPublic}}checked{{end}}><span>Public</span></label>
47
+ </div>
48
+ </div>
49
+ <div class="mt-4 flex gap-3">
50
+ <button type="submit" class="btn btn-primary btn-lg" style="flex:1">Save</button>
51
+ <a href="/{{.paste.ID}}" class="btn btn-secondary btn-lg">Cancel</a>
52
+ </div>
53
+ </form>
54
+ </div>
55
+ </main>
56
+ <script src="/static/js/app.js"></script>
57
+ </body>
58
+ </html>
templates/error.html ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{.title}}</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
8
+ <link href="/static/css/style.css" rel="stylesheet">
9
+ </head>
10
+ <body>
11
+ <nav class="navbar">
12
+ <div class="container">
13
+ <a href="/" class="logo">Patbin</a>
14
+ <div class="nav-links">
15
+ <button class="theme-toggle" onclick="toggleTheme()">
16
+ <svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/></svg>
17
+ <svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
18
+ </button>
19
+ </div>
20
+ </div>
21
+ </nav>
22
+ <main class="error-page">
23
+ <div class="error-code">{{if .code}}{{.code}}{{else}}404{{end}}</div>
24
+ <p class="error-message">{{.message}}</p>
25
+ <a href="/" class="btn btn-primary btn-lg">Go Home</a>
26
+ </main>
27
+ <script src="/static/js/app.js"></script>
28
+ </body>
29
+ </html>
templates/index.html ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{.title}}</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono&display=swap" rel="stylesheet">
9
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
10
+ <link href="/static/css/style.css" rel="stylesheet">
11
+ <style>
12
+ html,body{height:100%;margin:0}
13
+ body{display:flex;flex-direction:column}
14
+ .navbar{flex-shrink:0}
15
+ .editor-wrap{flex:1;display:flex;flex-direction:column;padding:8px;min-height:0;background:var(--bg-tertiary)}
16
+ .editor-box{flex:1;display:flex;flex-direction:column;border:1px solid var(--border);border-radius:8px;overflow:hidden;background:var(--bg-primary);min-height:0}
17
+ .editor-box:focus-within{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-light)}
18
+ .toolbar{display:flex;align-items:center;gap:6px;padding:6px 10px;background:var(--bg-secondary);border-bottom:1px solid var(--border);flex-wrap:wrap}
19
+ .t-input{border:none;background:transparent;font-size:14px;font-weight:500;color:var(--text-primary);padding:4px 0;flex:1;min-width:100px;outline:none}
20
+ .t-input::placeholder{color:var(--text-tertiary)}
21
+ .t-sel{background:var(--bg-primary);border:1px solid var(--border);border-radius:4px;padding:4px 6px;font-size:11px;color:var(--text-primary);cursor:pointer}
22
+ .t-sel:focus{outline:none;border-color:var(--accent)}
23
+ .t-sep{width:1px;height:18px;background:var(--border)}
24
+ .t-opt{display:flex;align-items:center;gap:4px;padding:4px 8px;font-size:11px;border:1px solid var(--border);border-radius:4px;background:var(--bg-primary);color:var(--text-secondary);cursor:pointer}
25
+ .t-opt:hover{color:var(--accent);border-color:var(--accent)}
26
+ .t-opt.on{background:var(--accent);border-color:var(--accent);color:#000}
27
+ .t-opt svg{width:12px;height:12px}
28
+ .t-btn{padding:5px 14px;font-size:12px;font-weight:600;border:1px solid var(--accent);border-radius:4px;background:var(--accent);color:#000;cursor:pointer;margin-left:auto}
29
+ .t-btn:hover{background:var(--accent-hover)}
30
+ .editor-main{flex:1;display:flex;min-height:0}
31
+ .gutter{width:36px;flex-shrink:0;padding:8px 0;background:var(--bg-secondary);border-right:1px solid var(--border);font:11px/1.75 'JetBrains Mono',monospace;color:var(--text-tertiary);text-align:right;overflow-y:auto;user-select:none}
32
+ .gutter div{padding-right:8px}
33
+ .code-area{flex:1;position:relative;display:flex;flex-direction:column;min-width:0}
34
+ .preview{position:absolute;top:0;left:0;right:0;bottom:0;padding:8px 10px;font:13px/1.75 'JetBrains Mono',monospace;overflow:auto;pointer-events:none;white-space:pre;tab-size:4}
35
+ .preview code{background:none!important;padding:0!important}
36
+ .code-ta{flex:1;border:none;resize:none;padding:8px 10px;font:13px/1.75 'JetBrains Mono',monospace;color:var(--text-primary);background:transparent;outline:none;overflow-y:auto;white-space:pre;tab-size:4;position:relative;z-index:1}
37
+ .code-ta::placeholder{color:var(--text-tertiary)}
38
+ .code-ta.hl{color:transparent;caret-color:var(--text-primary)}
39
+ .status{display:flex;align-items:center;gap:12px;padding:4px 10px;background:var(--bg-secondary);border-top:1px solid var(--border);font-size:10px;color:var(--text-tertiary)}
40
+ .status .a{color:var(--accent)}
41
+ .badge{padding:2px 6px;background:var(--accent-light);color:var(--accent);border-radius:3px;font-weight:500}
42
+ .k{background:var(--bg-tertiary);padding:1px 4px;border-radius:2px;font-family:var(--font-mono)}
43
+ @media(max-width:600px){.t-sep{display:none}.t-opt span{display:none}.t-sel{font-size:10px}}
44
+ </style>
45
+ </head>
46
+ <body>
47
+ <nav class="navbar">
48
+ <div class="container">
49
+ <a href="/" class="logo">
50
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:20px;height:20px"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
51
+ Patbin
52
+ </a>
53
+ <div class="nav-links">
54
+ <button class="theme-toggle" onclick="toggleTheme()"><svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/></svg><svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg></button>
55
+ {{if .username}}<a href="/dashboard" class="btn btn-ghost btn-sm">Dashboard</a>{{else}}<a href="/login" class="btn btn-ghost btn-sm">Login</a>{{end}}
56
+ </div>
57
+ </div>
58
+ </nav>
59
+ <form id="paste-form" class="editor-wrap">
60
+ <div class="editor-box">
61
+ <div class="toolbar">
62
+ <input type="text" name="title" class="t-input" placeholder="Untitled" autocomplete="off">
63
+ <select name="language" class="t-sel" id="lang-select">
64
+ <option value="">Language</option>
65
+ <option value="javascript">JavaScript</option>
66
+ <option value="typescript">TypeScript</option>
67
+ <option value="python">Python</option>
68
+ <option value="go">Go</option>
69
+ <option value="rust">Rust</option>
70
+ <option value="java">Java</option>
71
+ <option value="c">C</option>
72
+ <option value="cpp">C++</option>
73
+ <option value="csharp">C#</option>
74
+ <option value="php">PHP</option>
75
+ <option value="ruby">Ruby</option>
76
+ <option value="html">HTML</option>
77
+ <option value="css">CSS</option>
78
+ <option value="json">JSON</option>
79
+ <option value="sql">SQL</option>
80
+ <option value="bash">Bash</option>
81
+ </select>
82
+ <select name="expires_in" class="t-sel">
83
+ <option value="never">Never</option>
84
+ <option value="1h">1h</option>
85
+ <option value="1d">1d</option>
86
+ <option value="1w">1w</option>
87
+ </select>
88
+ <span class="t-sep"></span>
89
+ <button type="button" class="t-opt on" id="pub" onclick="toggleP()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg><span>Public</span></button>
90
+ <button type="button" class="t-opt" id="burn" onclick="toggleB()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22C6 18 3 13 3 10c0-3 2-5 4-6s5 1 5 3c0-2 2.5-4 5-3s4 3 4 6c0 3-3 8-9 12z"/></svg><span>Burn</span></button>
91
+ <input type="hidden" name="is_public" id="is_public" value="true">
92
+ <input type="hidden" name="burn_after_read" id="burn_after_read" value="false">
93
+ <button type="submit" class="t-btn">Create</button>
94
+ </div>
95
+ <div class="editor-main">
96
+ <div class="gutter" id="gut"><div>1</div></div>
97
+ <div class="code-area">
98
+ <div class="preview" id="pre"></div>
99
+ <textarea name="content" class="code-ta" id="ed" placeholder="// Paste code here" required spellcheck="false"></textarea>
100
+ </div>
101
+ </div>
102
+ <div class="status">
103
+ <span id="ln">1 line</span>
104
+ <span id="ch">0 chars</span>
105
+ <span class="badge" id="lg">Plain</span>
106
+ <span style="flex:1"></span>
107
+ <span><span class="k">Tab</span> indent</span>
108
+ <span><span class="k">Ctrl+↵</span> create</span>
109
+ </div>
110
+ </div>
111
+ </form>
112
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
113
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
114
+ <script src="/static/js/app.js"></script>
115
+ <script>
116
+ const ed=document.getElementById('ed'),gut=document.getElementById('gut'),pre=document.getElementById('pre'),ls=document.getElementById('lang-select'),lg=document.getElementById('lg');
117
+ function upd(){const n=ed.value.split('\n').length;gut.innerHTML=Array.from({length:n},(_,i)=>`<div>${i+1}</div>`).join('');document.getElementById('ln').textContent=n+(n===1?' line':' lines');document.getElementById('ch').textContent=ed.value.length+' chars';hl()}
118
+ function hl(){const l=ls.value,c=ed.value||' ';lg.textContent=ls.options[ls.selectedIndex].text.slice(0,10);if(l&&l!=='plaintext'&&Prism.languages[l]){pre.innerHTML=`<code class="language-${l}">${Prism.highlight(c,Prism.languages[l],l)}</code>`;ed.classList.add('hl')}else{pre.innerHTML='';ed.classList.remove('hl')}}
119
+ ed.addEventListener('input',upd);
120
+ ed.addEventListener('scroll',()=>{gut.scrollTop=ed.scrollTop;pre.scrollTop=ed.scrollTop;pre.scrollLeft=ed.scrollLeft});
121
+ ls.addEventListener('change',()=>{Prism.plugins.autoloader.loadLanguages([ls.value],hl);hl()});
122
+ function toggleP(){const b=document.getElementById('pub'),i=document.getElementById('is_public');b.classList.toggle('on');i.value=b.classList.contains('on');b.querySelector('span').textContent=b.classList.contains('on')?'Public':'Private'}
123
+ function toggleB(){const b=document.getElementById('burn'),i=document.getElementById('burn_after_read');b.classList.toggle('on');i.value=b.classList.contains('on')}
124
+ ed.addEventListener('keydown',e=>{if(e.key==='Tab'){e.preventDefault();const s=ed.selectionStart,n=ed.selectionEnd;ed.value=ed.value.substring(0,s)+' '+ed.value.substring(n);ed.selectionStart=ed.selectionEnd=s+4;upd()}});
125
+ upd();
126
+ </script>
127
+ </body>
128
+ </html>
templates/login.html ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{.title}}</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
10
+ <link href="/static/css/style.css" rel="stylesheet">
11
+ </head>
12
+ <body>
13
+ <nav class="navbar">
14
+ <div class="container">
15
+ <a href="/" class="logo">
16
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
17
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
18
+ <polyline points="14 2 14 8 20 8"/>
19
+ <line x1="16" y1="13" x2="8" y2="13"/>
20
+ <line x1="16" y1="17" x2="8" y2="17"/>
21
+ <polyline points="10 9 9 9 8 9"/>
22
+ </svg>
23
+ Patbin
24
+ </a>
25
+ <div class="nav-links">
26
+ <button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme">
27
+ <svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
28
+ <circle cx="12" cy="12" r="5"/>
29
+ <line x1="12" y1="1" x2="12" y2="3"/>
30
+ <line x1="12" y1="21" x2="12" y2="23"/>
31
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
32
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
33
+ <line x1="1" y1="12" x2="3" y2="12"/>
34
+ <line x1="21" y1="12" x2="23" y2="12"/>
35
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
36
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
37
+ </svg>
38
+ <svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
39
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
40
+ </svg>
41
+ </button>
42
+ </div>
43
+ </div>
44
+ </nav>
45
+
46
+ <main class="auth-page">
47
+ <div class="card auth-card">
48
+ <div class="auth-header">
49
+ <h1>Welcome back</h1>
50
+ <p class="text-muted">Sign in to your account</p>
51
+ </div>
52
+
53
+ <form id="login-form">
54
+ <div class="form-group">
55
+ <label class="form-label" for="username">Username</label>
56
+ <input type="text" id="username" name="username" class="form-input" placeholder="Enter username" required autocomplete="username">
57
+ </div>
58
+
59
+ <div class="form-group">
60
+ <label class="form-label" for="password">Password</label>
61
+ <input type="password" id="password" name="password" class="form-input" placeholder="Enter password" required autocomplete="current-password">
62
+ </div>
63
+
64
+ <div class="mt-4">
65
+ <button type="submit" class="btn btn-primary btn-lg" style="width: 100%">Login</button>
66
+ </div>
67
+ </form>
68
+
69
+ <div class="auth-footer">
70
+ Don't have an account? <a href="/register">Sign up</a>
71
+ </div>
72
+ </div>
73
+ </main>
74
+
75
+ <script src="/static/js/app.js"></script>
76
+ </body>
77
+ </html>
templates/profile.html ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{.title}}</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
10
+ <link href="/static/css/style.css" rel="stylesheet">
11
+ </head>
12
+ <body>
13
+ <nav class="navbar">
14
+ <div class="container">
15
+ <a href="/" class="logo">
16
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
17
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
18
+ <polyline points="14 2 14 8 20 8"/>
19
+ <line x1="16" y1="13" x2="8" y2="13"/>
20
+ <line x1="16" y1="17" x2="8" y2="17"/>
21
+ <polyline points="10 9 9 9 8 9"/>
22
+ </svg>
23
+ Patbin
24
+ </a>
25
+ <div class="nav-links">
26
+ <button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme">
27
+ <svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
28
+ <circle cx="12" cy="12" r="5"/>
29
+ <line x1="12" y1="1" x2="12" y2="3"/>
30
+ <line x1="12" y1="21" x2="12" y2="23"/>
31
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
32
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
33
+ <line x1="1" y1="12" x2="3" y2="12"/>
34
+ <line x1="21" y1="12" x2="23" y2="12"/>
35
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
36
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
37
+ </svg>
38
+ <svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
39
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
40
+ </svg>
41
+ </button>
42
+ <a href="/" class="btn btn-primary">
43
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
44
+ <path d="M12 5v14M5 12h14"/>
45
+ </svg>
46
+ New Paste
47
+ </a>
48
+ </div>
49
+ </div>
50
+ </nav>
51
+
52
+ <main class="page">
53
+ <div class="container">
54
+ <div class="page-header">
55
+ <h1 class="page-title">
56
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: -4px; margin-right: 0.5rem;">
57
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
58
+ <circle cx="12" cy="7" r="4"/>
59
+ </svg>
60
+ {{.profileUser.Username}}
61
+ </h1>
62
+ <p class="page-subtitle">Member since {{formatTime .profileUser.CreatedAt}}</p>
63
+ </div>
64
+
65
+ <div class="card">
66
+ <div class="card-header">
67
+ <h2 class="card-title">Public Pastes</h2>
68
+ <span class="text-muted">{{len .pastes}} pastes</span>
69
+ </div>
70
+
71
+ {{if .pastes}}
72
+ <div class="paste-list">
73
+ {{range .pastes}}
74
+ <a href="/{{.ID}}" class="paste-item">
75
+ <div class="paste-icon">
76
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
77
+ <polyline points="16 18 22 12 16 6"/>
78
+ <polyline points="8 6 2 12 8 18"/>
79
+ </svg>
80
+ </div>
81
+ <div class="paste-info">
82
+ <div class="paste-name">{{if .Title}}{{.Title}}{{else}}Untitled{{end}}</div>
83
+ <div class="paste-details">
84
+ <span>{{if .Language}}{{.Language}}{{else}}plain{{end}}</span>
85
+ <span>{{.Views}} views</span>
86
+ <span>{{formatTime .CreatedAt}}</span>
87
+ </div>
88
+ </div>
89
+ </a>
90
+ {{end}}
91
+ </div>
92
+ {{else}}
93
+ <div class="text-center text-muted" style="padding: 3rem 1rem;">
94
+ <p>No public pastes yet</p>
95
+ </div>
96
+ {{end}}
97
+ </div>
98
+ </div>
99
+ </main>
100
+
101
+ <script src="/static/js/app.js"></script>
102
+ </body>
103
+ </html>
templates/register.html ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{.title}}</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
10
+ <link href="/static/css/style.css" rel="stylesheet">
11
+ </head>
12
+ <body>
13
+ <nav class="navbar">
14
+ <div class="container">
15
+ <a href="/" class="logo">
16
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
17
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
18
+ <polyline points="14 2 14 8 20 8"/>
19
+ <line x1="16" y1="13" x2="8" y2="13"/>
20
+ <line x1="16" y1="17" x2="8" y2="17"/>
21
+ <polyline points="10 9 9 9 8 9"/>
22
+ </svg>
23
+ Patbin
24
+ </a>
25
+ <div class="nav-links">
26
+ <button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme">
27
+ <svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
28
+ <circle cx="12" cy="12" r="5"/>
29
+ <line x1="12" y1="1" x2="12" y2="3"/>
30
+ <line x1="12" y1="21" x2="12" y2="23"/>
31
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
32
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
33
+ <line x1="1" y1="12" x2="3" y2="12"/>
34
+ <line x1="21" y1="12" x2="23" y2="12"/>
35
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
36
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
37
+ </svg>
38
+ <svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
39
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
40
+ </svg>
41
+ </button>
42
+ </div>
43
+ </div>
44
+ </nav>
45
+
46
+ <main class="auth-page">
47
+ <div class="card auth-card">
48
+ <div class="auth-header">
49
+ <h1>Create account</h1>
50
+ <p class="text-muted">Join Patbin to manage your pastes</p>
51
+ </div>
52
+
53
+ <form id="register-form">
54
+ <div class="form-group">
55
+ <label class="form-label" for="username">Username</label>
56
+ <input type="text" id="username" name="username" class="form-input" placeholder="Choose a username" required minlength="3" maxlength="50" autocomplete="username">
57
+ <small class="text-muted mt-1" style="display: block; font-size: 0.75rem;">3-50 characters</small>
58
+ </div>
59
+
60
+ <div class="form-group">
61
+ <label class="form-label" for="password">Password</label>
62
+ <input type="password" id="password" name="password" class="form-input" placeholder="Choose a password" required minlength="6" autocomplete="new-password">
63
+ <small class="text-muted mt-1" style="display: block; font-size: 0.75rem;">Minimum 6 characters</small>
64
+ </div>
65
+
66
+ <div class="mt-4">
67
+ <button type="submit" class="btn btn-primary btn-lg" style="width: 100%">Create Account</button>
68
+ </div>
69
+ </form>
70
+
71
+ <div class="auth-footer">
72
+ Already have an account? <a href="/login">Sign in</a>
73
+ </div>
74
+ </div>
75
+ </main>
76
+
77
+ <script src="/static/js/app.js"></script>
78
+ </body>
79
+ </html>
templates/view.html ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{.title}}</title>
7
+ <meta name="description" content="{{if .paste.Title}}{{.paste.Title}}{{else}}Untitled{{end}} - View paste on Patbin">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
11
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
12
+ <link href="/static/css/style.css" rel="stylesheet">
13
+ </head>
14
+ <body>
15
+ <nav class="navbar">
16
+ <div class="container">
17
+ <a href="/" class="logo">
18
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
19
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
20
+ <polyline points="14 2 14 8 20 8"/>
21
+ <line x1="16" y1="13" x2="8" y2="13"/>
22
+ <line x1="16" y1="17" x2="8" y2="17"/>
23
+ <polyline points="10 9 9 9 8 9"/>
24
+ </svg>
25
+ Patbin
26
+ </a>
27
+ <div class="nav-links">
28
+ <button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme">
29
+ <svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
30
+ <circle cx="12" cy="12" r="5"/>
31
+ <line x1="12" y1="1" x2="12" y2="3"/>
32
+ <line x1="12" y1="21" x2="12" y2="23"/>
33
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
34
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
35
+ <line x1="1" y1="12" x2="3" y2="12"/>
36
+ <line x1="21" y1="12" x2="23" y2="12"/>
37
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
38
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
39
+ </svg>
40
+ <svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
41
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
42
+ </svg>
43
+ </button>
44
+ <a href="/" class="btn btn-primary">
45
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
46
+ <path d="M12 5v14M5 12h14"/>
47
+ </svg>
48
+ New Paste
49
+ </a>
50
+ </div>
51
+ </div>
52
+ </nav>
53
+
54
+ <main class="page">
55
+ <div class="container">
56
+ <div class="code-container">
57
+ <div class="code-header">
58
+ <div class="code-title">
59
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
60
+ <polyline points="16 18 22 12 16 6"/>
61
+ <polyline points="8 6 2 12 8 18"/>
62
+ </svg>
63
+ {{if .paste.Title}}{{.paste.Title}}{{else}}Untitled{{end}}
64
+ {{if not .paste.IsPublic}}
65
+ <span class="paste-badge private">
66
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
67
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
68
+ <path d="M7 11V7a5 5 0 0 1 10 0v4"/>
69
+ </svg>
70
+ Private
71
+ </span>
72
+ {{end}}
73
+ </div>
74
+ <div class="code-meta">
75
+ <span title="Language">{{.language}}</span>
76
+ <span title="Lines">{{.lines}} lines</span>
77
+ <span title="Views">{{.paste.Views}} views</span>
78
+ <span title="Created">{{formatTime .paste.CreatedAt}}</span>
79
+ {{if .paste.User}}
80
+ <a href="/u/{{.paste.User.Username}}" style="color: inherit">by {{.paste.User.Username}}</a>
81
+ {{end}}
82
+ </div>
83
+ </div>
84
+ <div class="code-actions">
85
+ <button class="btn btn-secondary btn-sm" onclick="copyToClipboard(document.getElementById('paste-content').textContent, this)">
86
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
87
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
88
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
89
+ </svg>
90
+ Copy
91
+ </button>
92
+ <button class="btn btn-secondary btn-sm" id="wrap-toggle" onclick="toggleWrap()">
93
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
94
+ <path d="M3 6h18M3 12h15a3 3 0 110 6h-6"/>
95
+ <polyline points="10 15 7 18 10 21"/>
96
+ </svg>
97
+ Wrap
98
+ </button>
99
+ <a href="/{{.paste.ID}}/raw" class="btn btn-secondary btn-sm" target="_blank">Raw</a>
100
+ <button class="btn btn-secondary btn-sm" id="fork-paste" data-paste-id="{{.paste.ID}}">
101
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
102
+ <circle cx="12" cy="18" r="3"/>
103
+ <circle cx="6" cy="6" r="3"/>
104
+ <circle cx="18" cy="6" r="3"/>
105
+ <path d="M18 9v1a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V9"/>
106
+ <path d="M12 12v3"/>
107
+ </svg>
108
+ Fork
109
+ </button>
110
+ {{if .isOwner}}
111
+ <a href="/{{.paste.ID}}/edit" class="btn btn-secondary btn-sm">
112
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
113
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
114
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
115
+ </svg>
116
+ Edit
117
+ </a>
118
+ <button class="btn btn-danger btn-sm" id="delete-paste" data-paste-id="{{.paste.ID}}">
119
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
120
+ <polyline points="3 6 5 6 21 6"/>
121
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
122
+ </svg>
123
+ Delete
124
+ </button>
125
+ {{end}}
126
+ </div>
127
+ <div class="code-body">
128
+ <div class="line-numbers">
129
+ {{range $i := iterate .lines}}
130
+ <span>{{add $i 1}}</span>
131
+ {{end}}
132
+ </div>
133
+ <div class="code-content">
134
+ <pre><code id="paste-content" class="language-{{.language}}">{{.paste.Content}}</code></pre>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ </main>
140
+
141
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
142
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
143
+ <script src="/static/js/app.js"></script>
144
+ <script>
145
+ // Re-highlight after page load
146
+ Prism.highlightAll();
147
+ </script>
148
+ </body>
149
+ </html>