feat: auth handlers and session management

This commit is contained in:
2026-08-26 23:16:08 +08:00
parent 2852097b26
commit af6aba3d37
4 changed files with 229 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
package server
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"net/http"
"time"
)
type loginRequest struct {
Token string `json:"token"`
}
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
s.mu.Lock()
if lockTime, locked := s.loginLockout[ip]; locked && time.Now().Before(lockTime) {
s.mu.Unlock()
http.Error(w, "Too many attempts, locked for 60s", http.StatusForbidden)
return
}
s.mu.Unlock()
var req loginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if req.Token != s.accessToken {
s.mu.Lock()
s.loginAttempts[ip]++
if s.loginAttempts[ip] >= 5 {
s.loginLockout[ip] = time.Now().Add(60 * time.Second)
s.loginAttempts[ip] = 0
}
s.mu.Unlock()
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
s.mu.Lock()
delete(s.loginAttempts, ip)
delete(s.loginLockout, ip)
s.mu.Unlock()
sessionID := generateSessionID()
s.mu.Lock()
s.sessions[sessionID] = time.Now().Unix()
s.mu.Unlock()
http.SetCookie(w, &http.Cookie{
Name: "ydropbox_session",
Value: sessionID,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: true,
MaxAge: 7 * 24 * 60 * 60,
})
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{})
}
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("ydropbox_session")
if err != nil {
http.Error(w, "Not logged in", http.StatusUnauthorized)
return
}
s.mu.Lock()
delete(s.sessions, cookie.Value)
s.mu.Unlock()
http.SetCookie(w, &http.Cookie{
Name: "ydropbox_session",
Value: "",
Path: "/",
MaxAge: -1,
})
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{})
}
func generateSessionID() string {
b := make([]byte, 32)
rand.Read(b)
return hex.EncodeToString(b)
}
+50
View File
@@ -0,0 +1,50 @@
package server
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestLoginSuccess(t *testing.T) {
s := &Server{
accessToken: "secret123",
sessions: make(map[string]int64),
loginAttempts: make(map[string]int),
loginLockout: make(map[string]time.Time),
}
req := httptest.NewRequest("POST", "/api/login", bytes.NewBufferString(`{"token":"secret123"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
s.handleLogin(w, req)
if w.Code != http.StatusOK {
t.Errorf("Status = %d, want %d", w.Code, http.StatusOK)
}
if len(w.Header().Values("Set-Cookie")) == 0 {
t.Error("Expected Set-Cookie header")
}
}
func TestLoginFailure(t *testing.T) {
s := &Server{
accessToken: "secret123",
sessions: make(map[string]int64),
loginAttempts: make(map[string]int),
loginLockout: make(map[string]time.Time),
}
req := httptest.NewRequest("POST", "/api/login", bytes.NewBufferString(`{"token":"wrong"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
s.handleLogin(w, req)
if w.Code != http.StatusUnauthorized {
t.Errorf("Status = %d, want %d", w.Code, http.StatusUnauthorized)
}
}
+47
View File
@@ -0,0 +1,47 @@
package server
import (
"net/http"
)
func (s *Server) requireAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("ydropbox_session")
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
s.mu.Lock()
_, exists := s.sessions[cookie.Value]
s.mu.Unlock()
if !exists {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next(w, r)
}
}
func (s *Server) requireAuthPage(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("ydropbox_session")
if err != nil {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
s.mu.Lock()
_, exists := s.sessions[cookie.Value]
s.mu.Unlock()
if !exists {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
next(w, r)
}
}
+38
View File
@@ -0,0 +1,38 @@
package server
import (
"crypto/rand"
"encoding/hex"
"sync"
"time"
"yoresee_dropbox/internal/store"
)
type Server struct {
store *store.Store
accessToken string
sessions map[string]int64
shareSessions map[string]string
loginAttempts map[string]int
loginLockout map[string]time.Time
mu sync.Mutex
workspace string
}
func New(store *store.Store, accessToken string, workspace string) *Server {
return &Server{
store: store,
accessToken: accessToken,
sessions: make(map[string]int64),
shareSessions: make(map[string]string),
loginAttempts: make(map[string]int),
loginLockout: make(map[string]time.Time),
workspace: workspace,
}
}
func generateUUID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}