package handlers import ( "database/sql" "encoding/json" "net/http" "strings" "time" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" ) const tokenExpiry = 24 * time.Hour // AuthHandler handles login, logout, and /me. type AuthHandler struct { DB *sql.DB JWTSecret []byte } type Claims struct { UserID string `json:"user_id"` Email string `json:"email"` Role string `json:"role"` jwt.RegisteredClaims } type loginRequest struct { Email string `json:"email"` Password string `json:"password"` } type loginResponse struct { Token string `json:"token"` User userResponse `json:"user"` } type userResponse struct { ID string `json:"id"` Email string `json:"email"` Name string `json:"name"` Role string `json:"role"` } // Login godoc: POST /auth/login func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { var req loginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.Email == "" || req.Password == "" { writeError(w, http.StatusBadRequest, "email and password required") return } var ( id string name string role string passwordHash string ) err := h.DB.QueryRowContext(r.Context(), `SELECT id, name, role, password_hash FROM users WHERE email = $1`, req.Email, ).Scan(&id, &name, &role, &passwordHash) if err == sql.ErrNoRows { writeError(w, http.StatusUnauthorized, "invalid credentials") return } if err != nil { writeError(w, http.StatusInternalServerError, "internal error") return } if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)); err != nil { writeError(w, http.StatusUnauthorized, "invalid credentials") return } now := time.Now() claims := Claims{ UserID: id, Email: req.Email, Role: role, RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(tokenExpiry)), Subject: id, }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signed, err := token.SignedString(h.JWTSecret) if err != nil { writeError(w, http.StatusInternalServerError, "could not sign token") return } _, _ = h.DB.ExecContext(r.Context(), `UPDATE users SET last_login = $1 WHERE id = $2`, now, id, ) writeJSON(w, http.StatusOK, loginResponse{ Token: signed, User: userResponse{ID: id, Email: req.Email, Name: name, Role: role}, }) } // Logout godoc: POST /auth/logout // JWT är stateless; klienten kasserar sin token. Servern bekräftar. func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"message": "logged out"}) } // Me godoc: GET /auth/me func (h *AuthHandler) Me(w http.ResponseWriter, r *http.Request) { claims, err := h.claimsFromRequest(r) if err != nil { writeError(w, http.StatusUnauthorized, "unauthorized") return } var u userResponse err = h.DB.QueryRowContext(r.Context(), `SELECT id, email, name, role FROM users WHERE id = $1`, claims.UserID, ).Scan(&u.ID, &u.Email, &u.Name, &u.Role) if err == sql.ErrNoRows { writeError(w, http.StatusUnauthorized, "user not found") return } if err != nil { writeError(w, http.StatusInternalServerError, "internal error") return } writeJSON(w, http.StatusOK, u) } // claimsFromRequest extraherar och validerar JWT från Authorization: Bearer . func (h *AuthHandler) claimsFromRequest(r *http.Request) (*Claims, error) { raw, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ") if !ok || raw == "" { return nil, jwt.ErrTokenMalformed } token, err := jwt.ParseWithClaims(raw, &Claims{}, func(t *jwt.Token) (any, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, jwt.ErrSignatureInvalid } return h.JWTSecret, nil }) if err != nil || !token.Valid { return nil, err } return token.Claims.(*Claims), nil } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) //nolint:errcheck } func writeError(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, map[string]string{"error": msg}) }