Files
Containr/app/backend/internal/api/auth_proxy.go
Tomas DvorakandDevin c16b32f56a feat: owner accounts and in-app platform settings
- First registered account becomes admin (users.is_admin, backfilled
  to earliest account); better-auth mirror path does the same
- Public registration closes after bootstrap; admin can reopen it via
  GET/PUT /api/v1/settings (admin-gated) or SIGNUP_ENABLED env
- In-app settings override env vars; secrets stored in app_settings,
  token values never returned by the API
- Saving a Cloudflare Tunnel token pulls and runs a managed
  cloudflared sidecar on the backend's networks; clearing removes it;
  startup restores it from saved settings
- group_add DOCKER_GID grants the non-root backend user Docker socket
  access; install.sh probes the in-container socket gid
- Settings page gains an owner-only Platform section (registration
  toggle, tunnel token, link to Cloudflare dashboard); sign-up page
  stays reachable while registration is open
- OpenAPI spec + generated types updated; manual user creation now
  requires admin

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-21 18:05:22 +02:00

92 lines
2.2 KiB
Go

package api
import (
"containr/internal/database"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"containr/internal/config"
"github.com/gin-gonic/gin"
)
func setupAuthProxyRoutes(router *gin.Engine, cfg *config.Config, db *database.DB) {
targetURL, err := parseAuthProxyTarget(cfg.BetterAuthProxyURL)
if err != nil {
log.Printf("Warning: auth proxy disabled: %v", err)
return
}
proxy := httputil.NewSingleHostReverseProxy(targetURL)
proxy.ErrorHandler = func(writer http.ResponseWriter, request *http.Request, proxyErr error) {
log.Printf("Auth proxy error: %v", proxyErr)
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusBadGateway)
_, _ = writer.Write([]byte(`{"error":"Auth service unavailable"}`))
}
handler := func(c *gin.Context) {
if shouldBlockPublicBetterAuthSignup(c, cfg, db) {
c.JSON(http.StatusForbidden, gin.H{"error": "Public registration is disabled after bootstrap"})
c.Abort()
return
}
proxy.ServeHTTP(c.Writer, c.Request)
c.Abort()
}
router.Any("/api/auth", handler)
router.Any("/api/auth/*proxyPath", handler)
}
func shouldBlockPublicBetterAuthSignup(c *gin.Context, cfg *config.Config, db *database.DB) bool {
if db == nil || db.DB == nil {
return false
}
if !strings.EqualFold(c.Request.Method, http.MethodPost) {
return false
}
if !strings.HasSuffix(c.Request.URL.Path, "/sign-up/email") {
return false
}
internalToken := strings.TrimSpace(cfg.BetterAuthInternalToken)
if internalToken != "" && c.GetHeader("X-Containr-Auth-Internal") == internalToken {
return false
}
count, err := countLocalUsers(db)
if err != nil {
log.Printf("Failed to inspect signup bootstrap state: %v", err)
return true
}
// Registration is closed once the first account exists unless the owner
// reopens it in Settings (signup_enabled).
return count > 0 && !signupEnabled(db)
}
func parseAuthProxyTarget(rawTarget string) (*url.URL, error) {
trimmed := strings.TrimSpace(rawTarget)
if trimmed == "" {
trimmed = "http://127.0.0.1:3001"
}
targetURL, err := url.Parse(trimmed)
if err != nil {
return nil, err
}
if targetURL.Scheme == "" || targetURL.Host == "" {
return nil, url.InvalidHostError(trimmed)
}
return targetURL, nil
}