package handlers import ( "net/http" "time" "warpbox.dev/backend/libs/services" "warpbox.dev/backend/libs/web" ) const userSessionCookieName = "warpbox_session" type authPageData struct { Mode string Token string Email string IsReset bool Error string ReturnPath string } func (a *App) Register(w http.ResponseWriter, r *http.Request) { available, err := a.authService.BootstrapAvailable() if err != nil { http.Error(w, "unable to check registration", http.StatusInternalServerError) return } if !available { http.Redirect(w, r, "/login", http.StatusSeeOther) return } a.renderAuth(w, r, http.StatusOK, authPageData{Mode: "register"}) } func (a *App) RegisterPost(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { a.renderAuth(w, r, http.StatusBadRequest, authPageData{Mode: "register", Error: "Unable to read form."}) return } user, err := a.authService.CreateBootstrapUser(r.FormValue("username"), r.FormValue("email"), r.FormValue("password")) if err != nil { a.renderAuth(w, r, http.StatusBadRequest, authPageData{Mode: "register", Error: err.Error()}) return } a.logger.Info("first admin created", "source", "auth", "severity", "user_activity", "code", 2401, "user_id", user.ID) a.loginAndRedirect(w, r, user.Email, r.FormValue("password"), "/app") } func (a *App) Login(w http.ResponseWriter, r *http.Request) { if _, ok := a.currentUser(r); ok { http.Redirect(w, r, "/app", http.StatusSeeOther) return } a.renderAuth(w, r, http.StatusOK, authPageData{Mode: "login", ReturnPath: r.URL.Query().Get("next")}) } func (a *App) LoginPost(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { a.renderAuth(w, r, http.StatusBadRequest, authPageData{Mode: "login", Error: "Unable to read form."}) return } next := r.FormValue("next") if next == "" { next = "/app" } user, token, err := a.authService.Login(r.FormValue("email"), r.FormValue("password")) if err != nil { a.logger.Warn("login failed", "source", "auth", "severity", "warn", "code", 4401, "email", r.FormValue("email")) a.renderAuth(w, r, http.StatusUnauthorized, authPageData{Mode: "login", Error: "Invalid email or password.", ReturnPath: next}) return } a.setUserSessionCookie(w, r, token) a.logger.Info("user login", "source", "auth", "severity", "user_activity", "code", 2402, "user_id", user.ID) http.Redirect(w, r, safeReturnPath(next), http.StatusSeeOther) } func (a *App) Logout(w http.ResponseWriter, r *http.Request) { if cookie, err := r.Cookie(userSessionCookieName); err == nil { _ = a.authService.Logout(cookie.Value) } a.clearUserSessionCookie(w) http.Redirect(w, r, "/", http.StatusSeeOther) } func (a *App) Invite(w http.ResponseWriter, r *http.Request) { invite, err := a.authService.InviteByToken(r.PathValue("token")) if err != nil || invite.UsedAt != nil || time.Now().UTC().After(invite.ExpiresAt) { a.renderAuth(w, r, http.StatusNotFound, authPageData{Mode: "invite", Error: "This invite is invalid or expired."}) return } a.renderAuth(w, r, http.StatusOK, authPageData{Mode: "invite", Token: r.PathValue("token"), Email: invite.Email, IsReset: invite.UserID != ""}) } func (a *App) InvitePost(w http.ResponseWriter, r *http.Request) { token := r.PathValue("token") invite, err := a.authService.InviteByToken(token) if err != nil { a.renderAuth(w, r, http.StatusNotFound, authPageData{Mode: "invite", Error: "This invite is invalid or expired."}) return } if err := r.ParseForm(); err != nil { a.renderAuth(w, r, http.StatusBadRequest, authPageData{Mode: "invite", Token: token, Email: invite.Email, IsReset: invite.UserID != "", Error: "Unable to read form."}) return } user, err := a.authService.AcceptInvite(token, r.FormValue("username"), r.FormValue("password")) if err != nil { a.renderAuth(w, r, http.StatusBadRequest, authPageData{Mode: "invite", Token: token, Email: invite.Email, IsReset: invite.UserID != "", Error: err.Error()}) return } a.logger.Info("invite accepted", "source", "auth", "severity", "user_activity", "code", 2403, "user_id", user.ID) a.loginAndRedirect(w, r, user.Email, r.FormValue("password"), "/app") } func (a *App) AccountSettings(w http.ResponseWriter, r *http.Request) { user, ok := a.requireUser(w, r) if !ok { return } a.renderPage(w, r, http.StatusOK, "account.html", web.PageData{ Title: "Account settings", Description: "Manage your Warpbox account.", CurrentUser: a.authService.PublicUser(user), Data: user, }) } func (a *App) ChangePassword(w http.ResponseWriter, r *http.Request) { user, ok := a.requireUser(w, r) if !ok { return } if err := r.ParseForm(); err != nil { http.Redirect(w, r, "/account/settings", http.StatusSeeOther) return } if !services.VerifyPasswordHash(user.PasswordHash, r.FormValue("current_password")) { http.Redirect(w, r, "/account/settings", http.StatusSeeOther) return } if err := a.authService.SetPassword(user.ID, r.FormValue("new_password")); err != nil { http.Redirect(w, r, "/account/settings", http.StatusSeeOther) return } http.Redirect(w, r, "/account/settings", http.StatusSeeOther) } func (a *App) renderAuth(w http.ResponseWriter, r *http.Request, status int, data authPageData) { a.renderPage(w, r, status, "auth.html", web.PageData{ Title: "Account", Description: "Sign in to Warpbox.", Data: data, }) } func (a *App) loginAndRedirect(w http.ResponseWriter, r *http.Request, email, password, path string) { _, token, err := a.authService.Login(email, password) if err != nil { http.Redirect(w, r, "/login", http.StatusSeeOther) return } a.setUserSessionCookie(w, r, token) http.Redirect(w, r, path, http.StatusSeeOther) } func (a *App) currentUser(r *http.Request) (services.User, bool) { cookie, err := r.Cookie(userSessionCookieName) if err != nil { return services.User{}, false } user, _, err := a.authService.UserForSession(cookie.Value) return user, err == nil } func (a *App) requireUser(w http.ResponseWriter, r *http.Request) (services.User, bool) { user, ok := a.currentUser(r) if ok { return user, true } http.Redirect(w, r, "/login?next="+r.URL.Path, http.StatusSeeOther) return services.User{}, false } func (a *App) setUserSessionCookie(w http.ResponseWriter, r *http.Request, token string) { http.SetCookie(w, &http.Cookie{ Name: userSessionCookieName, Value: token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: r.TLS != nil, Expires: time.Now().Add(30 * 24 * time.Hour), }) } func (a *App) clearUserSessionCookie(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{ Name: userSessionCookieName, Value: "", Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: -1, }) } func safeReturnPath(path string) string { if path == "" || path[0] != '/' || len(path) > 1 && path[1] == '/' { return "/app" } return path }