login via magic link

This commit is contained in:
juancwu 2026-01-04 19:24:01 -05:00
commit 94a05b0433
22 changed files with 815 additions and 122 deletions

View file

@ -1,17 +1,24 @@
package handler
import (
"log/slog"
"net/http"
"strings"
"time"
"git.juancwu.dev/juancwu/budgit/internal/service"
"git.juancwu.dev/juancwu/budgit/internal/ui"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/toast"
"git.juancwu.dev/juancwu/budgit/internal/ui/pages"
"git.juancwu.dev/juancwu/budgit/internal/validation"
)
type authHandler struct {
authService *service.AuthService
}
func NewAuthHandler() *authHandler {
return &authHandler{}
func NewAuthHandler(authService *service.AuthService) *authHandler {
return &authHandler{authService: authService}
}
func (h *authHandler) AuthPage(w http.ResponseWriter, r *http.Request) {
@ -21,3 +28,62 @@ func (h *authHandler) AuthPage(w http.ResponseWriter, r *http.Request) {
func (h *authHandler) PasswordPage(w http.ResponseWriter, r *http.Request) {
ui.Render(w, r, pages.AuthPassword(""))
}
func (h *authHandler) SendMagicLink(w http.ResponseWriter, r *http.Request) {
email := strings.TrimSpace(r.FormValue("email"))
if email == "" {
ui.Render(w, r, pages.Auth("Email is required"))
return
}
err := validation.ValidateEmail(email)
if err != nil {
ui.Render(w, r, pages.Auth("Please provide a valid email address"))
return
}
err = h.authService.SendMagicLink(email)
if err != nil {
slog.Warn("magic link send failed", "error", err, "email", email)
}
if r.URL.Query().Get("resend") == "true" {
ui.RenderOOB(w, r, toast.Toast(toast.Props{
Title: "Magic link sent",
Description: "Check your email for a new magic link",
Variant: toast.VariantSuccess,
Icon: true,
Dismissible: true,
Duration: 5000,
}), "beforeend:#toast-container")
return
}
ui.Render(w, r, pages.MagicLinkSent(email))
}
func (h *authHandler) VerifyMagicLink(w http.ResponseWriter, r *http.Request) {
tokenString := r.PathValue("token")
user, err := h.authService.VerifyMagicLink(tokenString)
if err != nil {
slog.Warn("magic link verification failed", "error", err, "token", tokenString)
ui.Render(w, r, pages.Auth("Invalid or expired magic link. Please try again."))
return
}
jwtToken, err := h.authService.GenerateJWT(user)
if err != nil {
slog.Error("failed to generate JWT", "error", err, "user_id", user.ID)
ui.Render(w, r, pages.Auth("An error occurred. Please try again."))
return
}
h.authService.SetJWTCookie(w, jwtToken, time.Now().Add(7*24*time.Hour))
// TODO: check for onboarding
slog.Info("user logged via magic link", "user_id", user.ID, "email", user.Email)
http.Redirect(w, r, "/app/dashboard", http.StatusSeeOther)
}