Files
AnthoLume/api/v1/auth.go
2026-03-22 17:21:32 -04:00

180 lines
4.8 KiB
Go

package v1
import (
"context"
"crypto/md5"
"encoding/json"
"fmt"
"net/http"
"time"
argon2 "github.com/alexedwards/argon2id"
"github.com/gorilla/sessions"
log "github.com/sirupsen/logrus"
)
// authData represents session authentication data
type authData struct {
UserName string
IsAdmin bool
AuthHash string
}
// withAuth wraps a handler with session authentication
func (s *Server) withAuth(handler http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
auth, ok := s.getSession(r)
if !ok {
writeJSONError(w, http.StatusUnauthorized, "Unauthorized")
return
}
ctx := context.WithValue(r.Context(), "auth", auth)
handler(w, r.WithContext(ctx))
}
}
// getSession retrieves auth data from the session cookie
func (s *Server) getSession(r *http.Request) (auth authData, ok bool) {
// Get session from cookie store
store := sessions.NewCookieStore([]byte(s.cfg.CookieAuthKey))
if s.cfg.CookieEncKey != "" {
if len(s.cfg.CookieEncKey) == 16 || len(s.cfg.CookieEncKey) == 32 {
store = sessions.NewCookieStore([]byte(s.cfg.CookieAuthKey), []byte(s.cfg.CookieEncKey))
} else {
log.Error("invalid cookie encryption key (must be 16 or 32 bytes)")
return authData{}, false
}
}
session, err := store.Get(r, "token")
if err != nil {
return authData{}, false
}
// Get session values
authorizedUser := session.Values["authorizedUser"]
isAdmin := session.Values["isAdmin"]
expiresAt := session.Values["expiresAt"]
authHash := session.Values["authHash"]
if authorizedUser == nil || isAdmin == nil || expiresAt == nil || authHash == nil {
return authData{}, false
}
auth = authData{
UserName: authorizedUser.(string),
IsAdmin: isAdmin.(bool),
AuthHash: authHash.(string),
}
// Validate auth hash
ctx := r.Context()
correctAuthHash, err := s.getUserAuthHash(ctx, auth.UserName)
if err != nil || correctAuthHash != auth.AuthHash {
return authData{}, false
}
return auth, true
}
// getUserAuthHash retrieves the user's auth hash from DB or cache
func (s *Server) getUserAuthHash(ctx context.Context, username string) (string, error) {
user, err := s.db.Queries.GetUser(ctx, username)
if err != nil {
return "", err
}
return *user.AuthHash, nil
}
// apiLogin handles POST /api/v1/auth/login
func (s *Server) apiLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSONError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSONError(w, http.StatusBadRequest, "Invalid JSON")
return
}
if req.Username == "" || req.Password == "" {
writeJSONError(w, http.StatusBadRequest, "Invalid credentials")
return
}
// MD5 - KOSync compatibility
password := fmt.Sprintf("%x", md5.Sum([]byte(req.Password)))
// Verify credentials
user, err := s.db.Queries.GetUser(r.Context(), req.Username)
if err != nil {
writeJSONError(w, http.StatusUnauthorized, "Invalid credentials")
return
}
if match, err := argon2.ComparePasswordAndHash(password, *user.Pass); err != nil || !match {
writeJSONError(w, http.StatusUnauthorized, "Invalid credentials")
return
}
// Create session
store := sessions.NewCookieStore([]byte(s.cfg.CookieAuthKey))
if s.cfg.CookieEncKey != "" {
if len(s.cfg.CookieEncKey) == 16 || len(s.cfg.CookieEncKey) == 32 {
store = sessions.NewCookieStore([]byte(s.cfg.CookieAuthKey), []byte(s.cfg.CookieEncKey))
}
}
session, _ := store.Get(r, "token")
session.Values["authorizedUser"] = user.ID
session.Values["isAdmin"] = user.Admin
session.Values["expiresAt"] = time.Now().Unix() + (60 * 60 * 24 * 7)
session.Values["authHash"] = *user.AuthHash
if err := session.Save(r, w); err != nil {
writeJSONError(w, http.StatusInternalServerError, "Failed to create session")
return
}
writeJSON(w, http.StatusOK, LoginResponse{
Username: user.ID,
IsAdmin: user.Admin,
})
}
// apiLogout handles POST /api/v1/auth/logout
func (s *Server) apiLogout(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSONError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
store := sessions.NewCookieStore([]byte(s.cfg.CookieAuthKey))
session, _ := store.Get(r, "token")
session.Values = make(map[any]any)
if err := session.Save(r, w); err != nil {
writeJSONError(w, http.StatusInternalServerError, "Failed to logout")
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "logged out"})
}
// apiGetMe handles GET /api/v1/auth/me
func (s *Server) apiGetMe(w http.ResponseWriter, r *http.Request) {
auth, ok := r.Context().Value("auth").(authData)
if !ok {
writeJSONError(w, http.StatusUnauthorized, "Unauthorized")
return
}
writeJSON(w, http.StatusOK, UserData{
Username: auth.UserName,
IsAdmin: auth.IsAdmin,
})
}