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}, }) }