Skip to content

Commit e82a6bd

Browse files
committed
feat: add simple password gate with session cookie (env SNAPKIT_PASSWORD)
1 parent f5d0a6b commit e82a6bd

3 files changed

Lines changed: 140 additions & 0 deletions

File tree

deploy.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ ssh -o ServerAliveInterval=5 $SERVER "docker rm -f snapkit 2>/dev/null; docker r
2121
--network dokploy-network \
2222
-v $APP_DIR/brands:/app/brands \
2323
-v /opt/snapkit-data:/app/data \
24+
-e SNAPKIT_PASSWORD=\${SNAPKIT_PASSWORD:-snapkit2026} \
2425
--restart unless-stopped \
2526
--label 'traefik.enable=true' \
2627
--label 'traefik.http.routers.snapkit.rule=Host(\`snapkit.vibery.app\`)' \

server/handlers/auth.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package handlers
2+
3+
import (
4+
"crypto/rand"
5+
"encoding/hex"
6+
"net/http"
7+
"os"
8+
"sync"
9+
"time"
10+
11+
"github.com/gin-gonic/gin"
12+
)
13+
14+
// Simple password gate: env SNAPKIT_PASSWORD, session cookie, 30-day expiry
15+
16+
var (
17+
sessions = map[string]time.Time{}
18+
sessionsMu sync.RWMutex
19+
)
20+
21+
const sessionMaxAge = 30 * 24 * 60 * 60 // 30 days in seconds
22+
23+
func getPassword() string {
24+
if p := os.Getenv("SNAPKIT_PASSWORD"); p != "" {
25+
return p
26+
}
27+
return "snapkit2026"
28+
}
29+
30+
func generateSessionToken() string {
31+
b := make([]byte, 32)
32+
rand.Read(b)
33+
return hex.EncodeToString(b)
34+
}
35+
36+
func isValidSession(token string) bool {
37+
sessionsMu.RLock()
38+
defer sessionsMu.RUnlock()
39+
exp, ok := sessions[token]
40+
return ok && time.Now().Before(exp)
41+
}
42+
43+
// AuthMiddleware checks for valid session cookie, skips login/static routes
44+
func AuthMiddleware() gin.HandlerFunc {
45+
return func(c *gin.Context) {
46+
path := c.Request.URL.Path
47+
48+
// Skip auth for login routes and static assets
49+
if path == "/login" || path == "/auth/login" {
50+
c.Next()
51+
return
52+
}
53+
54+
token, err := c.Cookie("snapkit_session")
55+
if err != nil || !isValidSession(token) {
56+
// API requests get 401, page requests redirect to login
57+
if len(path) > 4 && path[:5] == "/api/" {
58+
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
59+
return
60+
}
61+
c.Redirect(http.StatusFound, "/login")
62+
c.Abort()
63+
return
64+
}
65+
c.Next()
66+
}
67+
}
68+
69+
// HandleLogin serves the login page
70+
func HandleLogin(c *gin.Context) {
71+
c.Header("Content-Type", "text/html; charset=utf-8")
72+
c.String(http.StatusOK, loginHTML)
73+
}
74+
75+
// HandleLoginPost validates password and sets session cookie
76+
func HandleLoginPost(c *gin.Context) {
77+
password := c.PostForm("password")
78+
if password != getPassword() {
79+
c.Header("Content-Type", "text/html; charset=utf-8")
80+
c.String(http.StatusOK, loginHTMLError)
81+
return
82+
}
83+
84+
token := generateSessionToken()
85+
sessionsMu.Lock()
86+
sessions[token] = time.Now().Add(time.Duration(sessionMaxAge) * time.Second)
87+
sessionsMu.Unlock()
88+
89+
c.SetCookie("snapkit_session", token, sessionMaxAge, "/", "", false, true)
90+
c.Redirect(http.StatusFound, "/")
91+
}
92+
93+
const loginHTML = `<!DOCTYPE html>
94+
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
95+
<title>SnapKit Login</title>
96+
<style>
97+
*{margin:0;padding:0;box-sizing:border-box}
98+
body{font-family:system-ui,sans-serif;background:#f5f3f0;display:flex;align-items:center;justify-content:center;min-height:100vh}
99+
.card{background:#fff;border-radius:12px;padding:2.5rem;width:320px;box-shadow:0 2px 12px rgba(0,0,0,.08)}
100+
h1{font-size:1.25rem;margin-bottom:1.5rem;color:#333}
101+
input{width:100%;padding:10px 14px;border:1px solid #ddd;border-radius:8px;font-size:15px;margin-bottom:1rem;outline:none}
102+
input:focus{border-color:#c75b39}
103+
button{width:100%;padding:10px;background:#c75b39;color:#fff;border:none;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer}
104+
button:hover{background:#a94a2e}
105+
.err{color:#c75b39;font-size:13px;margin-bottom:1rem}
106+
</style></head><body>
107+
<div class="card"><h1>SnapKit</h1>
108+
<form method="POST" action="/auth/login">
109+
<input type="password" name="password" placeholder="Password" autofocus required>
110+
<button type="submit">Enter</button>
111+
</form></div></body></html>`
112+
113+
const loginHTMLError = `<!DOCTYPE html>
114+
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
115+
<title>SnapKit Login</title>
116+
<style>
117+
*{margin:0;padding:0;box-sizing:border-box}
118+
body{font-family:system-ui,sans-serif;background:#f5f3f0;display:flex;align-items:center;justify-content:center;min-height:100vh}
119+
.card{background:#fff;border-radius:12px;padding:2.5rem;width:320px;box-shadow:0 2px 12px rgba(0,0,0,.08)}
120+
h1{font-size:1.25rem;margin-bottom:1.5rem;color:#333}
121+
input{width:100%;padding:10px 14px;border:1px solid #ddd;border-radius:8px;font-size:15px;margin-bottom:1rem;outline:none}
122+
input:focus{border-color:#c75b39}
123+
button{width:100%;padding:10px;background:#c75b39;color:#fff;border:none;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer}
124+
button:hover{background:#a94a2e}
125+
.err{color:#c75b39;font-size:13px;margin-bottom:1rem}
126+
</style></head><body>
127+
<div class="card"><h1>SnapKit</h1>
128+
<p class="err">Wrong password</p>
129+
<form method="POST" action="/auth/login">
130+
<input type="password" name="password" placeholder="Password" autofocus required>
131+
<button type="submit">Enter</button>
132+
</form></div></body></html>`

server/main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ func main() {
4040
AllowCredentials: true,
4141
}))
4242

43+
// Auth routes (before middleware)
44+
r.GET("/login", handlers.HandleLogin)
45+
r.POST("/auth/login", handlers.HandleLoginPost)
46+
47+
// Password gate (skip /login, /auth/login, static assets)
48+
r.Use(handlers.AuthMiddleware())
49+
4350
// Static files
4451
r.Static("/brands", brandsDir)
4552
r.Static("/uploads", filepath.Join(dataDir, "uploads"))

0 commit comments

Comments
 (0)