feat(api): auth-хендлеры register/login/logout/me + session cookie

This commit is contained in:
2026-07-03 20:11:00 +07:00
parent a584cf5c37
commit aa0ef1c6a9
4 changed files with 473 additions and 5 deletions
+154
View File
@@ -0,0 +1,154 @@
package api
import (
"context"
"log"
"net/http"
"time"
"github.com/google/uuid"
"github.com/vasyakrg/dns-autoresolver/internal/auth"
)
const sessionCookieName = "session"
// ctxKeyUserID is a private context key carrying the authenticated user's ID.
// Task 4's RequireAuth middleware sets it after validating the session
// cookie; handleMe reads it back.
type ctxKeyUserID struct{}
// userIDFromContext extracts the authenticated user ID set by RequireAuth
// (Task 4). Until that middleware is wired in, tests set it directly via
// context.WithValue.
func userIDFromContext(ctx context.Context) (uuid.UUID, bool) {
id, ok := ctx.Value(ctxKeyUserID{}).(uuid.UUID)
return id, ok
}
func setSessionCookie(w http.ResponseWriter, token string, exp time.Time) {
http.SetCookie(w, &http.Cookie{
Name: sessionCookieName, Value: token, Path: "/",
HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode, Expires: exp,
})
}
func clearSessionCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: sessionCookieName, Value: "", Path: "/",
HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode, MaxAge: -1,
})
}
func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
var req registerRequest
if !decodeBody(w, r, &req) {
return
}
if req.Email == "" || req.Password == "" {
writeErr(w, http.StatusBadRequest, "email and password are required")
return
}
hash, err := auth.HashPassword(req.Password)
if err != nil {
log.Printf("api: hash password failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
u, p, err := a.Auth.RegisterUser(r.Context(), req.Email, hash)
if err != nil {
log.Printf("api: register user failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
token, exp, err := a.Sessions.Create(r.Context(), u.ID)
if err != nil {
log.Printf("api: create session failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
setSessionCookie(w, token, exp)
writeJSON(w, http.StatusOK, toAuthResponse(u, p))
}
// invalidCredentials is deliberately identical for "no such user" and "wrong
// password" — disclosing which one occurred would let an attacker enumerate
// registered emails.
func invalidCredentials(w http.ResponseWriter) {
writeErr(w, http.StatusUnauthorized, "invalid credentials")
}
func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) {
var req loginRequest
if !decodeBody(w, r, &req) {
return
}
u, err := a.Auth.GetUserByEmail(r.Context(), req.Email)
if err != nil {
invalidCredentials(w)
return
}
ok, err := auth.VerifyPassword(u.PasswordHash, req.Password)
if err != nil || !ok {
invalidCredentials(w)
return
}
p, err := a.Auth.GetUserProject(r.Context(), u.ID)
if err != nil {
log.Printf("api: get user project failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
token, exp, err := a.Sessions.Create(r.Context(), u.ID)
if err != nil {
log.Printf("api: create session failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
setSessionCookie(w, token, exp)
writeJSON(w, http.StatusOK, toAuthResponse(u, p))
}
func (a *API) handleLogout(w http.ResponseWriter, r *http.Request) {
if c, err := r.Cookie(sessionCookieName); err == nil && c.Value != "" {
if err := a.Sessions.Destroy(r.Context(), c.Value); err != nil {
log.Printf("api: destroy session failed: %v", err)
}
}
clearSessionCookie(w)
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
// handleMe returns the authenticated caller's identity + default project.
// The user ID comes from the request context, set by Task 4's RequireAuth
// middleware after validating the session cookie (tests set it directly via
// context.WithValue in the interim). AuthStore has no GetUserByID — the
// email field is intentionally left empty here; see task-3-report.md.
func (a *API) handleMe(w http.ResponseWriter, r *http.Request) {
userID, ok := userIDFromContext(r.Context())
if !ok {
writeErr(w, http.StatusUnauthorized, "authentication required")
return
}
p, err := a.Auth.GetUserProject(r.Context(), userID)
if err != nil {
log.Printf("api: get user project failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
writeJSON(w, http.StatusOK, authResponse{
User: userResponse{ID: userID.String()},
Project: projectResponse{ID: p.ID.String(), Name: p.Name},
})
}