feat(admin): session probe, self-hosted signup and the relink cap
Adds GET /auth/me so the admin site's route guards can know who is signed in, POST /auth/signup for self-hosted customers, and max_relinks on the account payload so the UI never hardcodes a rule the backend enforces. Signup follows sitesvc's proven shape: honeypot answered as success, a generic 201 when the address already exists, and nothing usable until the emailed link is opened. Also fixes a lockout found while verifying it. When the verification email failed, the account was rolled back but the customer_users row survived -- an orphan that can never be signed in to and that holds the unique index on email, so every later signup with that address got a cheerful 201 and the customer was locked out of their own address with no visible error. CreateCustomerUser now undoes its own insert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,24 @@ func ownedInstance(c *gin.Context, instanceID string) (*models.Instance, bool) {
|
||||
return &inst, true
|
||||
}
|
||||
|
||||
// getMe reports who the caller is, for route guards in the UI.
|
||||
//
|
||||
// It is deliberately outside RequireCustomer/RequireStaff: the UI needs a
|
||||
// truthful 401 to redirect on, not an error page. It reveals nothing a caller
|
||||
// does not already possess, because it only ever describes their own cookie.
|
||||
func getMe(c *gin.Context) {
|
||||
s := auth.Load(c)
|
||||
if s == nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "not signed in"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"kind": s.Kind,
|
||||
"email": s.Email,
|
||||
"account_id": s.AccountID,
|
||||
})
|
||||
}
|
||||
|
||||
func getAccount(c *gin.Context) {
|
||||
s := auth.Current(c)
|
||||
ctx := c.Request.Context()
|
||||
@@ -58,7 +76,13 @@ func getAccount(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"account": acct, "instances": instances})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"account": acct,
|
||||
"instances": instances,
|
||||
// Sent rather than mirrored in the UI: a hardcoded 3 in TypeScript is a
|
||||
// second source of truth for a rule the backend enforces.
|
||||
"max_relinks": models.MaxRelinksPerTerm,
|
||||
})
|
||||
}
|
||||
|
||||
func linkInstance(c *gin.Context) {
|
||||
|
||||
@@ -30,6 +30,8 @@ func Routes(cfg config.Config) http.Handler {
|
||||
r.POST("/auth/login", auth.HandleCloudLogin) // falls through to customer login
|
||||
r.POST("/auth/logout", auth.HandleLogout)
|
||||
r.GET("/auth/verify", auth.HandleVerify)
|
||||
r.GET("/auth/me", getMe)
|
||||
r.POST("/auth/signup", auth.HandleSignup)
|
||||
|
||||
cust := r.Group("/api")
|
||||
cust.Use(auth.RequireCustomer())
|
||||
|
||||
Reference in New Issue
Block a user