6de2455917
- 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
170 lines
4.2 KiB
Go
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})
|
|
}
|