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