222 lines
6.8 KiB
Go
222 lines
6.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/prabhupadasays/spsays-bff/internal/middleware"
|
|
"github.com/prabhupadasays/spsays-bff/internal/service"
|
|
"github.com/prabhupadasays/spsays-bff/pkg/auth"
|
|
"github.com/prabhupadasays/spsays-bff/pkg/response"
|
|
"github.com/prabhupadasays/spsays-bff/pkg/validator"
|
|
)
|
|
|
|
// AuthHandler exposes the auth-related HTTP endpoints.
|
|
//
|
|
// Every handler is a 5-line shim: parse → validate → service → render.
|
|
// All branching (already exists? wrong password? expired link?) lives in
|
|
// AuthService and surfaces here only as sentinel errors mapped to status
|
|
// codes via mapAuthErr.
|
|
type AuthHandler struct {
|
|
svc *service.AuthService
|
|
cookies auth.CookieOptions
|
|
}
|
|
|
|
func NewAuthHandler(svc *service.AuthService, cookies auth.CookieOptions) *AuthHandler {
|
|
return &AuthHandler{svc: svc, cookies: cookies}
|
|
}
|
|
|
|
// --- request shapes ---
|
|
|
|
type signUpReq struct {
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type logInReq struct {
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
type forgotReq struct {
|
|
Email string `json:"email"`
|
|
}
|
|
|
|
type resetReq struct {
|
|
Token string `json:"token"`
|
|
NewPassword string `json:"new_password"`
|
|
}
|
|
|
|
type verifyReq struct {
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
// --- handlers ---
|
|
|
|
// SignUp creates a user, plants the session cookie, returns the user.
|
|
func (h *AuthHandler) SignUp(w http.ResponseWriter, r *http.Request) {
|
|
var req signUpReq
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
response.Error(w, http.StatusBadRequest, "invalid JSON body")
|
|
return
|
|
}
|
|
v := validator.New()
|
|
validator.Required(v, "email", req.Email)
|
|
validator.Email(v, "email", req.Email)
|
|
validator.Required(v, "password", req.Password)
|
|
validator.MinLength(v, "password", req.Password, 8)
|
|
validator.Required(v, "name", req.Name)
|
|
validator.MaxLength(v, "name", req.Name, 120)
|
|
if !v.Valid() {
|
|
response.Error(w, http.StatusBadRequest, v.Error())
|
|
return
|
|
}
|
|
|
|
res, err := h.svc.SignUp(r.Context(), req.Email, req.Password, req.Name)
|
|
if err != nil {
|
|
h.renderAuthErr(w, err)
|
|
return
|
|
}
|
|
auth.Set(w, res.Token, res.Expires, h.cookies)
|
|
response.JSON(w, http.StatusCreated, res.User)
|
|
}
|
|
|
|
// LogIn authenticates an existing user.
|
|
func (h *AuthHandler) LogIn(w http.ResponseWriter, r *http.Request) {
|
|
var req logInReq
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
response.Error(w, http.StatusBadRequest, "invalid JSON body")
|
|
return
|
|
}
|
|
v := validator.New()
|
|
validator.Required(v, "email", req.Email)
|
|
validator.Required(v, "password", req.Password)
|
|
if !v.Valid() {
|
|
response.Error(w, http.StatusBadRequest, v.Error())
|
|
return
|
|
}
|
|
|
|
res, err := h.svc.LogIn(r.Context(), req.Email, req.Password)
|
|
if err != nil {
|
|
h.renderAuthErr(w, err)
|
|
return
|
|
}
|
|
auth.Set(w, res.Token, res.Expires, h.cookies)
|
|
response.JSON(w, http.StatusOK, res.User)
|
|
}
|
|
|
|
// LogOut just clears the cookie. JWTs remain technically valid until expiry,
|
|
// but with the cookie gone the browser can't present them.
|
|
func (h *AuthHandler) LogOut(w http.ResponseWriter, r *http.Request) {
|
|
auth.Clear(w, h.cookies)
|
|
response.JSON(w, http.StatusOK, map[string]bool{"ok": true})
|
|
}
|
|
|
|
// Me returns the authenticated user. Protected by middleware.RequireAuth.
|
|
func (h *AuthHandler) Me(w http.ResponseWriter, r *http.Request) {
|
|
uid := middleware.UserID(r.Context())
|
|
tv := middleware.TokenVersion(r.Context())
|
|
u, err := h.svc.Me(r.Context(), uid, tv)
|
|
if err != nil {
|
|
response.Error(w, http.StatusUnauthorized, "not authenticated")
|
|
return
|
|
}
|
|
response.JSON(w, http.StatusOK, u)
|
|
}
|
|
|
|
// ForgotPassword emails a reset link if the address exists. We always return
|
|
// 200 to avoid revealing whether the email is registered.
|
|
func (h *AuthHandler) ForgotPassword(w http.ResponseWriter, r *http.Request) {
|
|
var req forgotReq
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
response.Error(w, http.StatusBadRequest, "invalid JSON body")
|
|
return
|
|
}
|
|
v := validator.New()
|
|
validator.Required(v, "email", req.Email)
|
|
validator.Email(v, "email", req.Email)
|
|
if !v.Valid() {
|
|
response.Error(w, http.StatusBadRequest, v.Error())
|
|
return
|
|
}
|
|
if err := h.svc.RequestPasswordReset(r.Context(), req.Email); err != nil {
|
|
response.Error(w, http.StatusInternalServerError, "could not process request")
|
|
return
|
|
}
|
|
response.JSON(w, http.StatusOK, map[string]bool{"ok": true})
|
|
}
|
|
|
|
// ResetPassword consumes a reset token and sets a new password.
|
|
func (h *AuthHandler) ResetPassword(w http.ResponseWriter, r *http.Request) {
|
|
var req resetReq
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
response.Error(w, http.StatusBadRequest, "invalid JSON body")
|
|
return
|
|
}
|
|
v := validator.New()
|
|
validator.Required(v, "token", req.Token)
|
|
validator.Required(v, "new_password", req.NewPassword)
|
|
validator.MinLength(v, "new_password", req.NewPassword, 8)
|
|
if !v.Valid() {
|
|
response.Error(w, http.StatusBadRequest, v.Error())
|
|
return
|
|
}
|
|
if err := h.svc.ResetPassword(r.Context(), req.Token, req.NewPassword); err != nil {
|
|
h.renderAuthErr(w, err)
|
|
return
|
|
}
|
|
// Force the user to log in again after a reset (their token_version is now stale).
|
|
auth.Clear(w, h.cookies)
|
|
response.JSON(w, http.StatusOK, map[string]bool{"ok": true})
|
|
}
|
|
|
|
// VerifyEmail flips email_verified_at after the user clicks the link.
|
|
func (h *AuthHandler) VerifyEmail(w http.ResponseWriter, r *http.Request) {
|
|
var req verifyReq
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
response.Error(w, http.StatusBadRequest, "invalid JSON body")
|
|
return
|
|
}
|
|
if req.Token == "" {
|
|
response.Error(w, http.StatusBadRequest, "token is required")
|
|
return
|
|
}
|
|
if err := h.svc.VerifyEmail(r.Context(), req.Token); err != nil {
|
|
h.renderAuthErr(w, err)
|
|
return
|
|
}
|
|
response.JSON(w, http.StatusOK, map[string]bool{"ok": true})
|
|
}
|
|
|
|
// ResendVerification — protected — refires the verification email for the
|
|
// currently-logged-in user.
|
|
func (h *AuthHandler) ResendVerification(w http.ResponseWriter, r *http.Request) {
|
|
uid := middleware.UserID(r.Context())
|
|
if err := h.svc.ResendVerification(r.Context(), uid); err != nil {
|
|
h.renderAuthErr(w, err)
|
|
return
|
|
}
|
|
response.JSON(w, http.StatusOK, map[string]bool{"ok": true})
|
|
}
|
|
|
|
// --- error mapping ---
|
|
|
|
func (h *AuthHandler) renderAuthErr(w http.ResponseWriter, err error) {
|
|
switch {
|
|
case errors.Is(err, service.ErrInvalidCredentials),
|
|
errors.Is(err, service.ErrInvalidOrExpiredLink):
|
|
response.Error(w, http.StatusUnauthorized, err.Error())
|
|
case errors.Is(err, service.ErrEmailAlreadyTaken):
|
|
response.Error(w, http.StatusConflict, err.Error())
|
|
case errors.Is(err, service.ErrEmailAlreadyVerified),
|
|
errors.Is(err, service.ErrWeakPassword):
|
|
response.Error(w, http.StatusBadRequest, err.Error())
|
|
case errors.Is(err, service.ErrUserNotFound):
|
|
response.Error(w, http.StatusNotFound, err.Error())
|
|
default:
|
|
response.Error(w, http.StatusInternalServerError, "something went wrong")
|
|
}
|
|
}
|