All checks were successful
Tests / unit-tests (push) Successful in 51s
- Login sa session cookie autentifikacijom - Lista projekata iz filesystem-a - Chat sa Claude CLI preko WebSocket-a - Streaming NDJSON parsiranje iz CLI stdout-a - Sesija zivi nezavisno od browsera (reconnect replay) - Sidebar sa .md fajlovima i markdown renderovanjem - Dark tema, htmx + Go templates - 47 unit testova Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
128 lines
2.5 KiB
Go
128 lines
2.5 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
sessionCookieName = "cwc_session"
|
|
sessionMaxAge = 24 * time.Hour
|
|
)
|
|
|
|
type Session struct {
|
|
Token string
|
|
Username string
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
type SessionManager struct {
|
|
sessions map[string]*Session
|
|
secret string
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func NewSessionManager(secret string) *SessionManager {
|
|
return &SessionManager{
|
|
sessions: make(map[string]*Session),
|
|
secret: secret,
|
|
}
|
|
}
|
|
|
|
func (sm *SessionManager) Create(username string) *Session {
|
|
token := generateToken()
|
|
sig := sm.sign(token)
|
|
signedToken := token + "." + sig
|
|
|
|
sess := &Session{
|
|
Token: signedToken,
|
|
Username: username,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
sm.mu.Lock()
|
|
sm.sessions[signedToken] = sess
|
|
sm.mu.Unlock()
|
|
|
|
return sess
|
|
}
|
|
|
|
func (sm *SessionManager) Get(token string) *Session {
|
|
sm.mu.RLock()
|
|
sess, ok := sm.sessions[token]
|
|
sm.mu.RUnlock()
|
|
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if time.Since(sess.CreatedAt) > sessionMaxAge {
|
|
sm.Delete(token)
|
|
return nil
|
|
}
|
|
return sess
|
|
}
|
|
|
|
func (sm *SessionManager) Delete(token string) {
|
|
sm.mu.Lock()
|
|
delete(sm.sessions, token)
|
|
sm.mu.Unlock()
|
|
}
|
|
|
|
func (sm *SessionManager) sign(token string) string {
|
|
h := hmac.New(sha256.New, []byte(sm.secret))
|
|
h.Write([]byte(token))
|
|
return hex.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
func generateToken() string {
|
|
b := make([]byte, 32)
|
|
rand.Read(b)
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
// SetSessionCookie sets the session cookie on the response.
|
|
func SetSessionCookie(w http.ResponseWriter, sess *Session) {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: sessionCookieName,
|
|
Value: sess.Token,
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
SameSite: http.SameSiteLaxMode,
|
|
MaxAge: int(sessionMaxAge.Seconds()),
|
|
})
|
|
}
|
|
|
|
// ClearSessionCookie removes the session cookie.
|
|
func ClearSessionCookie(w http.ResponseWriter) {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: sessionCookieName,
|
|
Value: "",
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
MaxAge: -1,
|
|
})
|
|
}
|
|
|
|
// AuthMiddleware protects routes that require authentication.
|
|
func AuthMiddleware(sm *SessionManager, next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
cookie, err := r.Cookie(sessionCookieName)
|
|
if err != nil {
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
sess := sm.Get(cookie.Value)
|
|
if sess == nil {
|
|
ClearSessionCookie(w)
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|