package handlers import ( "context" "net/http" "os" "strings" "time" "person-site/internal/models" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" ) type AuthHandler struct { UserStore *models.UserStore JWTSecret []byte AdminUser string AdminPass string } func NewAuthHandler(us *models.UserStore) *AuthHandler { jwtSecret := os.Getenv("JWT_SECRET") if jwtSecret == "" { panic("JWT_SECRET environment variable is required") } adminUser := os.Getenv("ADMIN_USER") if adminUser == "" { panic("ADMIN_USER environment variable is required") } adminPass := os.Getenv("ADMIN_PASS") if adminPass == "" { panic("ADMIN_PASS environment variable is required") } return &AuthHandler{ UserStore: us, JWTSecret: []byte(jwtSecret), AdminUser: adminUser, AdminPass: adminPass, } } func (h *AuthHandler) SeedAdmin() error { hash, err := bcrypt.GenerateFromPassword([]byte(h.AdminPass), 12) if err != nil { return err } return h.UserStore.SeedAdmin(h.AdminUser, string(hash)) } func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { var req struct { Username string `json:"username"` Password string `json:"password"` } if err := Decode(r, &req); err != nil { Error(w, 400, "invalid request body") return } user, err := h.UserStore.GetByUsername(req.Username) if err != nil || user == nil { Error(w, 401, "invalid credentials") return } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { Error(w, 401, "invalid credentials") return } now := time.Now().UTC() claims := jwt.RegisteredClaims{ Subject: user.Username, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenStr, err := token.SignedString(h.JWTSecret) if err != nil { Error(w, 500, "failed to generate token") return } JSON(w, 200, map[string]interface{}{ "token": tokenStr, "expires_at": claims.ExpiresAt.Format(time.RFC3339), }) } func (h *AuthHandler) Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/api/admin/login" { next.ServeHTTP(w, r) return } tokenStr := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") if tokenStr == "" { Error(w, 401, "missing authorization header") return } claims := &jwt.RegisteredClaims{} token, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) { return h.JWTSecret, nil }) if err != nil || !token.Valid { Error(w, 401, "invalid or expired token") return } ctx := context.WithValue(r.Context(), "username", claims.Subject) next.ServeHTTP(w, r.WithContext(ctx)) }) }