mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
feat: public platform, admin console, templates, landing docs
Projects become publicly browsable after admin approval; management and secret-adjacent reads (logs, variables, exec, connection URLs) require owner/member/admin auth, and platform controls sit behind RequireAdmin. - Backend: OptionalAuth middleware, read/authed/admin route groups, projects.is_approved + service_templates.owner_id migration, visibility-aware sqlc queries, admin overview/users/approval endpoints, EnsureAdminAccount bootstrap (ADMIN_* env), 30 seeded templates with owner CRUD, managed-database connection URLs gated owner+admin, fix DB.Stats() infinite recursion. - Frontend: expandable persisted sidebar + clickable user menu, public landing page with searchable docs browser (GitHub sync, bundled snapshot + localStorage fallback), DocsBrowser shared with /docs, template create/edit/JSON-upload for owned templates, database create dialog, admin console (stats, approvals, project edit/delete, user admin toggles), HA/Security/Usage polish with dead CSS tokens mapped to real theme vars, anonymous-safe gating on every mutation control. - Tests: safe window.localStorage access fixes happy-dom crashes in PlatformShell and canvas storage. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
8f5789bf93
commit
8b6214a0f5
62 files changed
+4986
-1133
No files matched your search
@@ -69,41 +69,72 @@ func RequestID() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// resolveRequestUser authenticates the request via Bearer JWT or the Better
|
||||
// Auth session cookie. On success it returns the local user id and email.
|
||||
// tokenErr/hasToken mirror the legacy error contract for Auth.
|
||||
func resolveRequestUser(c *gin.Context, jwtSecret string, sessionVerifier *betterAuthSessionVerifier) (userID, email, tokenErr string, hasToken, ok bool) {
|
||||
tokenString, tokenErr, hasToken := extractJWTToken(c)
|
||||
if tokenString != "" {
|
||||
if claims, valid := validateJWTClaims(tokenString, jwtSecret); valid {
|
||||
userIDClaim, exists := claims["user_id"]
|
||||
if exists {
|
||||
id := strings.TrimSpace(fmt.Sprint(userIDClaim))
|
||||
if _, err := uuid.Parse(id); err == nil {
|
||||
if emailClaim, exists := claims["email"]; exists && emailClaim != nil {
|
||||
email = strings.TrimSpace(fmt.Sprint(emailClaim))
|
||||
}
|
||||
return id, email, "", hasToken, true
|
||||
}
|
||||
}
|
||||
tokenErr = "Invalid token claims"
|
||||
} else if tokenErr == "" {
|
||||
tokenErr = "Invalid token"
|
||||
}
|
||||
}
|
||||
|
||||
if sessionVerifier != nil {
|
||||
if id, mail, verified := sessionVerifier.resolveUser(c); verified {
|
||||
return id, mail, "", hasToken, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", tokenErr, hasToken, false
|
||||
}
|
||||
|
||||
// setAuthenticatedContext stores the resolved identity plus the admin flag on
|
||||
// the gin context so handlers and RequireAdmin can rely on it.
|
||||
func setAuthenticatedContext(c *gin.Context, userID, email string) {
|
||||
c.Set("user_id", userID)
|
||||
c.Set("email", email)
|
||||
c.Set("is_admin", loadIsAdmin(c, userID))
|
||||
}
|
||||
|
||||
func loadIsAdmin(c *gin.Context, userID string) bool {
|
||||
dbValue, exists := c.Get("db")
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
db, ok := dbValue.(*database.DB)
|
||||
if !ok || db == nil || db.DB == nil {
|
||||
return false
|
||||
}
|
||||
var isAdmin bool
|
||||
if err := db.QueryRow(`SELECT is_admin FROM users WHERE id = $1`, userID).Scan(&isAdmin); err != nil {
|
||||
return false
|
||||
}
|
||||
return isAdmin
|
||||
}
|
||||
|
||||
// Auth middleware for JWT authentication
|
||||
func Auth(jwtSecret string) gin.HandlerFunc {
|
||||
sessionVerifier := newBetterAuthSessionVerifier()
|
||||
|
||||
return func(c *gin.Context) {
|
||||
tokenString, tokenErr, hasToken := extractJWTToken(c)
|
||||
if tokenString != "" {
|
||||
if claims, valid := validateJWTClaims(tokenString, jwtSecret); valid {
|
||||
userIDClaim, exists := claims["user_id"]
|
||||
if exists {
|
||||
userID := strings.TrimSpace(fmt.Sprint(userIDClaim))
|
||||
if _, err := uuid.Parse(userID); err == nil {
|
||||
email := ""
|
||||
if emailClaim, ok := claims["email"]; ok && emailClaim != nil {
|
||||
email = strings.TrimSpace(fmt.Sprint(emailClaim))
|
||||
}
|
||||
c.Set("user_id", userID)
|
||||
c.Set("email", email)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
tokenErr = "Invalid token claims"
|
||||
} else if tokenErr == "" {
|
||||
tokenErr = "Invalid token"
|
||||
}
|
||||
}
|
||||
|
||||
if sessionVerifier != nil {
|
||||
if userID, email, ok := sessionVerifier.resolveUser(c); ok {
|
||||
c.Set("user_id", userID)
|
||||
c.Set("email", email)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
userID, email, tokenErr, hasToken, ok := resolveRequestUser(c, jwtSecret, sessionVerifier)
|
||||
if ok {
|
||||
setAuthenticatedContext(c, userID, email)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if tokenErr != "" {
|
||||
@@ -121,6 +152,38 @@ func Auth(jwtSecret string) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// OptionalAuth resolves identity when a valid credential is present but never
|
||||
// rejects the request — anonymous callers simply get no user context. Read
|
||||
// endpoints hang off this so the public site can browse approved resources.
|
||||
func OptionalAuth(jwtSecret string) gin.HandlerFunc {
|
||||
sessionVerifier := newBetterAuthSessionVerifier()
|
||||
|
||||
return func(c *gin.Context) {
|
||||
if userID, email, _, _, ok := resolveRequestUser(c, jwtSecret, sessionVerifier); ok {
|
||||
setAuthenticatedContext(c, userID, email)
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAdmin rejects requests whose resolved user is not a platform admin.
|
||||
// Must run after Auth (or OptionalAuth) so user_id/is_admin are populated.
|
||||
func RequireAdmin() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if _, exists := c.Get("user_id"); !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if !c.GetBool("is_admin") {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
type betterAuthSessionVerifier struct {
|
||||
internalURL string
|
||||
internalToken string
|
||||
|
||||
Reference in new issue
Block a user