You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

117 lines
2.8 KiB

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))
})
}