2026-05-30 15:42:35 +03:00
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
}
2026-05-30 17:23:20 +03:00
a . renderAuth ( w , r , http . StatusOK , authPageData { Mode : "register" } )
2026-05-30 15:42:35 +03:00
}
func ( a * App ) RegisterPost ( w http . ResponseWriter , r * http . Request ) {
if err := r . ParseForm ( ) ; err != nil {
2026-05-30 17:23:20 +03:00
a . renderAuth ( w , r , http . StatusBadRequest , authPageData { Mode : "register" , Error : "Unable to read form." } )
2026-05-30 15:42:35 +03:00
return
}
user , err := a . authService . CreateBootstrapUser ( r . FormValue ( "username" ) , r . FormValue ( "email" ) , r . FormValue ( "password" ) )
if err != nil {
2026-05-30 17:23:20 +03:00
a . renderAuth ( w , r , http . StatusBadRequest , authPageData { Mode : "register" , Error : err . Error ( ) } )
2026-05-30 15:42:35 +03:00
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
}
2026-05-30 17:23:20 +03:00
a . renderAuth ( w , r , http . StatusOK , authPageData { Mode : "login" , ReturnPath : r . URL . Query ( ) . Get ( "next" ) } )
2026-05-30 15:42:35 +03:00
}
func ( a * App ) LoginPost ( w http . ResponseWriter , r * http . Request ) {
if err := r . ParseForm ( ) ; err != nil {
2026-05-30 17:23:20 +03:00
a . renderAuth ( w , r , http . StatusBadRequest , authPageData { Mode : "login" , Error : "Unable to read form." } )
2026-05-30 15:42:35 +03:00
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" ) )
2026-05-30 17:23:20 +03:00
a . renderAuth ( w , r , http . StatusUnauthorized , authPageData { Mode : "login" , Error : "Invalid email or password." , ReturnPath : next } )
2026-05-30 15:42:35 +03:00
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 ) {
2026-05-30 17:23:20 +03:00
a . renderAuth ( w , r , http . StatusNotFound , authPageData { Mode : "invite" , Error : "This invite is invalid or expired." } )
2026-05-30 15:42:35 +03:00
return
}
2026-05-30 17:23:20 +03:00
a . renderAuth ( w , r , http . StatusOK , authPageData { Mode : "invite" , Token : r . PathValue ( "token" ) , Email : invite . Email , IsReset : invite . UserID != "" } )
2026-05-30 15:42:35 +03:00
}
func ( a * App ) InvitePost ( w http . ResponseWriter , r * http . Request ) {
token := r . PathValue ( "token" )
invite , err := a . authService . InviteByToken ( token )
if err != nil {
2026-05-30 17:23:20 +03:00
a . renderAuth ( w , r , http . StatusNotFound , authPageData { Mode : "invite" , Error : "This invite is invalid or expired." } )
2026-05-30 15:42:35 +03:00
return
}
if err := r . ParseForm ( ) ; err != nil {
2026-05-30 17:23:20 +03:00
a . renderAuth ( w , r , http . StatusBadRequest , authPageData { Mode : "invite" , Token : token , Email : invite . Email , IsReset : invite . UserID != "" , Error : "Unable to read form." } )
2026-05-30 15:42:35 +03:00
return
}
user , err := a . authService . AcceptInvite ( token , r . FormValue ( "username" ) , r . FormValue ( "password" ) )
if err != nil {
2026-05-30 17:23:20 +03:00
a . renderAuth ( w , r , http . StatusBadRequest , authPageData { Mode : "invite" , Token : token , Email : invite . Email , IsReset : invite . UserID != "" , Error : err . Error ( ) } )
2026-05-30 15:42:35 +03:00
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
}
2026-05-30 17:23:20 +03:00
a . renderPage ( w , r , http . StatusOK , "account.html" , web . PageData {
2026-05-30 15:42:35 +03:00
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 )
}
2026-05-30 17:23:20 +03:00
func ( a * App ) renderAuth ( w http . ResponseWriter , r * http . Request , status int , data authPageData ) {
a . renderPage ( w , r , status , "auth.html" , web . PageData {
2026-05-30 15:42:35 +03:00
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
}