Files
warpbox-dev/backend/libs/handlers/auth.go
Daniel Legt d77f164900
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m8s
feat: add upload policies, daily limits, and storage quotas
- Add environment variables to configure anonymous uploads, daily upload caps, and default user storage limits.
- Update config loader to parse and validate the new settings.
- Implement backend logic to track daily usage and active storage per user.
- Update README and `.env.example` to document the new settings and admin panels.
2026-05-30 17:23:20 +03:00

212 lines
6.8 KiB
Go

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
}