118 lines
2.6 KiB
Go
118 lines
2.6 KiB
Go
|
|
package handlers
|
||
|
|
|
||
|
|
import (
|
||
|
|
"database/sql"
|
||
|
|
"encoding/json"
|
||
|
|
"net/http"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/golang-jwt/jwt/v5"
|
||
|
|
"golang.org/x/crypto/bcrypt"
|
||
|
|
)
|
||
|
|
|
||
|
|
const tokenExpiry = 24 * time.Hour
|
||
|
|
|
||
|
|
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 userResponse struct {
|
||
|
|
ID string `json:"id"`
|
||
|
|
Email string `json:"email"`
|
||
|
|
Name string `json:"name"`
|
||
|
|
Role string `json:"role"`
|
||
|
|
}
|
||
|
|
|
||
|
|
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 boc_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
|
||
|
|
}
|
||
|
|
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
|
|
"token": signed,
|
||
|
|
"user": userResponse{
|
||
|
|
ID: id,
|
||
|
|
Email: req.Email,
|
||
|
|
Name: name,
|
||
|
|
Role: role,
|
||
|
|
},
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *AuthHandler) Me(w http.ResponseWriter, r *http.Request) {
|
||
|
|
claims, ok := r.Context().Value("user").(*Claims)
|
||
|
|
if !ok {
|
||
|
|
writeError(w, http.StatusUnauthorized, "unauthorized")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
|
|
"id": claims.UserID,
|
||
|
|
"email": claims.Email,
|
||
|
|
"role": claims.Role,
|
||
|
|
})
|
||
|
|
}
|