Files
boc/aamos-admin-upgrade/backend/handlers/auth.go
T
Bernt 6de2455917 v1.2.0: Add Global Markets footer, translated to 9 languages
- Added GLOBAL_MARKETS_TITLE to all translation files
- Updated footer with 12 markets (4 active + 8 upcoming)
- Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi
- Built and deployed to production
- CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
2026-07-08 19:56:03 +00:00

170 lines
4.2 KiB
Go

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