feat(api): auth-хендлеры register/login/logout/me + session cookie
This commit is contained in:
@@ -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},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user