mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +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
@@ -3,7 +3,10 @@ package api
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"containr/internal/database"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func requireAuthenticatedUserID(c *gin.Context) (string, bool) {
|
||||
@@ -21,3 +24,107 @@ func requireAuthenticatedUserID(c *gin.Context) (string, bool) {
|
||||
|
||||
return userID, true
|
||||
}
|
||||
|
||||
// optionalUserID returns the resolved user id or "" for anonymous callers on
|
||||
// OptionalAuth routes. Never writes a response.
|
||||
func optionalUserID(c *gin.Context) string {
|
||||
userIDValue, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
userID, _ := userIDValue.(string)
|
||||
return userID
|
||||
}
|
||||
|
||||
// optionalUserUUID is the uuid variant of optionalUserID; anonymous callers
|
||||
// get uuid.Nil, which matches no owner_id and therefore only sees approved
|
||||
// public resources.
|
||||
func optionalUserUUID(c *gin.Context) uuid.UUID {
|
||||
userID := optionalUserID(c)
|
||||
parsed, err := uuid.Parse(userID)
|
||||
if err != nil {
|
||||
return uuid.Nil
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
// contextIsAdmin reports whether the resolved caller is a platform admin.
|
||||
func contextIsAdmin(c *gin.Context) bool {
|
||||
return c.GetBool("is_admin")
|
||||
}
|
||||
|
||||
// projectReadAccess reports (exists, allowedToRead). Approved projects are
|
||||
// public; unapproved ones are visible to the owner, project members, and
|
||||
// platform admins only.
|
||||
func projectReadAccess(c *gin.Context, db *database.DB, projectID uuid.UUID) (bool, bool) {
|
||||
var isApproved bool
|
||||
var ownerID string
|
||||
err := db.QueryRow(
|
||||
`SELECT is_approved, owner_id::text FROM projects WHERE id = $1`,
|
||||
projectID,
|
||||
).Scan(&isApproved, &ownerID)
|
||||
if err != nil {
|
||||
return false, false
|
||||
}
|
||||
if isApproved || contextIsAdmin(c) {
|
||||
return true, true
|
||||
}
|
||||
userID := optionalUserID(c)
|
||||
if userID == "" {
|
||||
return true, false
|
||||
}
|
||||
if ownerID == userID {
|
||||
return true, true
|
||||
}
|
||||
var member bool
|
||||
if err := db.QueryRow(
|
||||
`SELECT EXISTS(SELECT 1 FROM project_members WHERE project_id = $1 AND user_id = $2)`,
|
||||
projectID, userID,
|
||||
).Scan(&member); err == nil && member {
|
||||
return true, true
|
||||
}
|
||||
return true, false
|
||||
}
|
||||
|
||||
// projectManageAccess reports (exists, allowedToOperate). Unlike
|
||||
// projectReadAccess, approval does NOT grant access — only the owner,
|
||||
// project members, and platform admins may reach secret-adjacent data
|
||||
// such as variables and runtime logs.
|
||||
func projectManageAccess(c *gin.Context, db *database.DB, projectID uuid.UUID) (bool, bool) {
|
||||
var ownerID string
|
||||
err := db.QueryRow(
|
||||
`SELECT owner_id::text FROM projects WHERE id = $1`,
|
||||
projectID,
|
||||
).Scan(&ownerID)
|
||||
if err != nil {
|
||||
return false, false
|
||||
}
|
||||
if contextIsAdmin(c) {
|
||||
return true, true
|
||||
}
|
||||
userID := optionalUserID(c)
|
||||
if userID == "" {
|
||||
return true, false
|
||||
}
|
||||
if ownerID == userID {
|
||||
return true, true
|
||||
}
|
||||
var member bool
|
||||
if err := db.QueryRow(
|
||||
`SELECT EXISTS(SELECT 1 FROM project_members WHERE project_id = $1 AND user_id = $2)`,
|
||||
projectID, userID,
|
||||
).Scan(&member); err == nil && member {
|
||||
return true, true
|
||||
}
|
||||
return true, false
|
||||
}
|
||||
|
||||
// serviceProjectID resolves the project that owns a service.
|
||||
func serviceProjectID(db *database.DB, serviceID uuid.UUID) (uuid.UUID, bool) {
|
||||
var projectID uuid.UUID
|
||||
err := db.QueryRow(`SELECT project_id FROM services WHERE id = $1`, serviceID).Scan(&projectID)
|
||||
if err != nil {
|
||||
return uuid.Nil, false
|
||||
}
|
||||
return projectID, true
|
||||
}
|
||||
Reference in new issue
Block a user