diff --git a/server/internal/auth/local.go b/server/internal/auth/local.go new file mode 100644 index 0000000..d705716 --- /dev/null +++ b/server/internal/auth/local.go @@ -0,0 +1,111 @@ +package auth + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/mrhid6/vantage/server/internal/services" +) + +func SetSessionCookie(c *gin.Context, sessionID string) { + secure := c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" + http.SetCookie(c.Writer, &http.Cookie{ + Name: sessionCookieName, + Value: sessionID, + Path: "/", + HttpOnly: true, + Secure: secure, + SameSite: http.SameSiteLaxMode, + MaxAge: int(sessionTTL.Seconds()), + }) +} + +func HandleLocalLogin(c *gin.Context) { + var body struct { + Email string `json:"email"` + Password string `json:"password"` + } + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "email and password required"}) + return + } + u, err := services.GetUserByEmail(body.Email) + if err != nil || !services.VerifyPassword(u, body.Password) { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"}) + return + } + sessionID, err := SaveSession(c.Request.Context(), &Session{ + UserID: u.UserID, OrgID: u.OrgID, Role: u.Role, Email: u.Email, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "session save failed"}) + return + } + _ = services.TouchLastLogin(u.UserID) + SetSessionCookie(c, sessionID) + c.JSON(http.StatusOK, gin.H{"ok": true}) +} + +func HandleBootstrapStatus(c *gin.Context) { + n, err := services.CountUsers() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"needs_setup": n == 0}) +} + +func HandleBootstrap(c *gin.Context) { + n, err := services.CountUsers() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if n > 0 { + c.JSON(http.StatusConflict, gin.H{"error": "setup already complete"}) + return + } + var body struct { + OrgName string `json:"org_name"` + Email string `json:"email"` + Password string `json:"password"` + } + if err := c.ShouldBindJSON(&body); err != nil || body.OrgName == "" || body.Email == "" || len(body.Password) < 8 { + c.JSON(http.StatusBadRequest, gin.H{"error": "org_name, email, and password (>=8 chars) required"}) + return + } + org, err := services.CreateOrg(body.OrgName) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + u, err := services.CreateUser(org.OrgID, body.Email, body.Password, "owner", "local") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + sessionID, err := SaveSession(c.Request.Context(), &Session{ + UserID: u.UserID, OrgID: u.OrgID, Role: u.Role, Email: u.Email, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "session save failed"}) + return + } + SetSessionCookie(c, sessionID) + c.JSON(http.StatusCreated, gin.H{"org": org, "slug": org.Slug}) +} + +func HandleMe(c *gin.Context) { + cookie, err := c.Request.Cookie(sessionCookieName) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"}) + return + } + sess, err := GetSession(c.Request.Context(), cookie.Value) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "session expired"}) + return + } + org, _ := services.GetOrg(sess.OrgID) + c.JSON(http.StatusOK, gin.H{"user": sess, "org": org}) +} diff --git a/server/internal/auth/oidc.go b/server/internal/auth/oidc.go index 5387ae7..677956b 100644 --- a/server/internal/auth/oidc.go +++ b/server/internal/auth/oidc.go @@ -103,16 +103,7 @@ func HandleCallback(c *gin.Context) { return } - secure := c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" - http.SetCookie(c.Writer, &http.Cookie{ - Name: sessionCookieName, - Value: sessionID, - Path: "/", - HttpOnly: true, - Secure: secure, - SameSite: http.SameSiteLaxMode, - MaxAge: int(sessionTTL.Seconds()), - }) + SetSessionCookie(c, sessionID) frontendURL := os.Getenv("PUBLIC_HOST") if frontendURL == "" { @@ -134,21 +125,3 @@ func HandleLogout(c *gin.Context) { }) c.Redirect(http.StatusFound, "/") } - -func HandleMe(c *gin.Context) { - if !authEnabled { - c.JSON(http.StatusOK, gin.H{"auth_enabled": false}) - return - } - cookie, err := c.Request.Cookie(sessionCookieName) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"}) - return - } - sess, err := GetSession(c.Request.Context(), cookie.Value) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "session expired"}) - return - } - c.JSON(http.StatusOK, sess) -}