mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
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>
131 lines
3.4 KiB
Go
131 lines
3.4 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"containr/internal/database"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func requireAuthenticatedUserID(c *gin.Context) (string, bool) {
|
|
userIDValue, exists := c.Get("user_id")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
|
return "", false
|
|
}
|
|
|
|
userID, ok := userIDValue.(string)
|
|
if !ok || userID == "" {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user context"})
|
|
return "", false
|
|
}
|
|
|
|
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
|
|
}
|