feat: Dynamically set session cookie domain based on the public URL.

This commit is contained in:
Haitao Pan 2026-02-07 02:23:53 +08:00
parent 3b980a7ff2
commit 7aa99b43d8
4 changed files with 113 additions and 12 deletions

70
accountsvc.log Normal file
View File

@ -0,0 +1,70 @@
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
time=2026-02-07T02:23:47.924+08:00 level=WARN msg="xray sync is disabled in configuration; agent mode will still attempt to manage xray config"
time=2026-02-07T02:23:47.924+08:00 level=INFO msg="configured cors" allowedOrigins="[http://localhost:3000 http://127.0.0.1:3000]"
time=2026-02-07T02:23:47.987+08:00 level=WARN msg="root account bootstrapped from environment variable" email=admin@svc.plus
time=2026-02-07T02:23:48.035+08:00 level=INFO msg="demo read-only user created" username=Demo email=demo@svc.plus
time=2026-02-07T02:23:48.080+08:00 level=INFO msg="sandbox experience user created" email=sandbox@svc.plus
time=2026-02-07T02:23:48.080+08:00 level=INFO msg="token service initialized" auth_enabled=true
time=2026-02-07T02:23:48.082+08:00 level=INFO msg="loaded agents from store" count=1
[GIN-debug] GET /healthz --> account/api.RegisterRoutes.func1 (4 handlers)
[GIN-debug] POST /api/auth/register --> account/api.(*handler).register-fm (4 handlers)
[GIN-debug] POST /api/auth/register/verify --> account/api.(*handler).verifyEmail-fm (4 handlers)
[GIN-debug] POST /api/auth/register/send --> account/api.(*handler).sendEmailVerification-fm (4 handlers)
[GIN-debug] POST /api/auth/login --> account/api.(*handler).login-fm (4 handlers)
[GIN-debug] POST /api/auth/token/exchange --> account/api.(*handler).exchangeToken-fm (4 handlers)
[GIN-debug] GET /api/auth/oauth/login/:provider --> account/api.(*handler).oauthLogin-fm (4 handlers)
[GIN-debug] GET /api/auth/oauth/callback/:provider --> account/api.(*handler).oauthCallback-fm (4 handlers)
[GIN-debug] POST /api/auth/token/refresh --> account/api.(*handler).refreshToken-fm (4 handlers)
[GIN-debug] GET /api/auth/mfa/status --> account/api.(*handler).mfaStatus-fm (4 handlers)
[GIN-debug] GET /api/auth/session --> account/api.(*handler).session-fm (6 handlers)
[GIN-debug] DELETE /api/auth/session --> account/api.(*handler).deleteSession-fm (6 handlers)
[GIN-debug] POST /api/auth/mfa/totp/provision --> account/api.(*handler).provisionTOTP-fm (6 handlers)
[GIN-debug] POST /api/auth/mfa/totp/verify --> account/api.(*handler).verifyTOTP-fm (6 handlers)
[GIN-debug] POST /api/auth/mfa/disable --> account/api.(*handler).disableMFA-fm (6 handlers)
[GIN-debug] POST /api/auth/password/reset --> account/api.(*handler).requestPasswordReset-fm (6 handlers)
[GIN-debug] POST /api/auth/password/reset/confirm --> account/api.(*handler).confirmPasswordReset-fm (6 handlers)
[GIN-debug] GET /api/auth/subscriptions --> account/api.(*handler).listSubscriptions-fm (6 handlers)
[GIN-debug] POST /api/auth/subscriptions --> account/api.(*handler).upsertSubscription-fm (6 handlers)
[GIN-debug] POST /api/auth/subscriptions/cancel --> account/api.(*handler).cancelSubscription-fm (6 handlers)
[GIN-debug] POST /api/auth/config/sync --> account/api.(*handler).syncConfig-fm (6 handlers)
[GIN-debug] GET /api/auth/admin/settings --> account/api.(*handler).getAdminSettings-fm (6 handlers)
[GIN-debug] POST /api/auth/admin/settings --> account/api.(*handler).updateAdminSettings-fm (6 handlers)
[GIN-debug] GET /api/auth/admin/users/metrics --> account/api.(*handler).adminUsersMetrics-fm (6 handlers)
[GIN-debug] POST /api/auth/admin/users --> account/api.(*handler).createCustomUser-fm (6 handlers)
[GIN-debug] POST /api/auth/admin/users/:userId/role --> account/api.(*handler).updateUserRole-fm (6 handlers)
[GIN-debug] DELETE /api/auth/admin/users/:userId/role --> account/api.(*handler).resetUserRole-fm (6 handlers)
[GIN-debug] POST /api/auth/admin/users/:userId/pause --> account/api.(*handler).pauseUser-fm (6 handlers)
[GIN-debug] POST /api/auth/admin/users/:userId/resume --> account/api.(*handler).resumeUser-fm (6 handlers)
[GIN-debug] DELETE /api/auth/admin/users/:userId --> account/api.(*handler).deleteUser-fm (6 handlers)
[GIN-debug] POST /api/auth/admin/users/:userId/renew-uuid --> account/api.(*handler).renewProxyUUID-fm (6 handlers)
[GIN-debug] GET /api/auth/admin/blacklist --> account/api.(*handler).listBlacklist-fm (6 handlers)
[GIN-debug] POST /api/auth/admin/blacklist --> account/api.(*handler).addToBlacklist-fm (6 handlers)
[GIN-debug] DELETE /api/auth/admin/blacklist/:email --> account/api.(*handler).removeFromBlacklist-fm (6 handlers)
[GIN-debug] GET /api/auth/admin/sandbox/binding --> account/api.(*handler).getSandboxBinding-fm (6 handlers)
[GIN-debug] POST /api/auth/admin/sandbox/bind --> account/api.(*handler).bindSandboxNode-fm (6 handlers)
[GIN-debug] GET /api/auth/sandbox/binding --> account/api.(*handler).getSandboxBindingPublic-fm (6 handlers)
[GIN-debug] POST /api/auth/admin/assume --> account/api.(*handler).adminAssume-fm (6 handlers)
[GIN-debug] POST /api/auth/admin/assume/revert --> account/api.(*handler).adminAssumeRevert-fm (6 handlers)
[GIN-debug] GET /api/auth/admin/assume/status --> account/api.(*handler).adminAssumeStatus-fm (6 handlers)
[GIN-debug] GET /api/auth/users --> account/api.(*handler).listUsers-fm (6 handlers)
[GIN-debug] GET /api/internal/public-overview --> account/api.(*handler).internalPublicOverview-fm (5 handlers)
[GIN-debug] GET /api/admin/users/metrics --> account/api.(*handler).adminUsersMetrics-fm (6 handlers)
[GIN-debug] GET /api/admin/agents/status --> account/api.(*handler).adminAgentStatus-fm (6 handlers)
[GIN-debug] POST /api/admin/users --> account/api.(*handler).createCustomUser-fm (6 handlers)
[GIN-debug] POST /api/admin/users/:userId/pause --> account/api.(*handler).pauseUser-fm (6 handlers)
[GIN-debug] POST /api/admin/users/:userId/resume --> account/api.(*handler).resumeUser-fm (6 handlers)
[GIN-debug] DELETE /api/admin/users/:userId --> account/api.(*handler).deleteUser-fm (6 handlers)
[GIN-debug] POST /api/admin/users/:userId/renew-uuid --> account/api.(*handler).renewProxyUUID-fm (6 handlers)
[GIN-debug] GET /api/admin/blacklist --> account/api.(*handler).listBlacklist-fm (6 handlers)
[GIN-debug] POST /api/admin/blacklist --> account/api.(*handler).addToBlacklist-fm (6 handlers)
[GIN-debug] DELETE /api/admin/blacklist/:email --> account/api.(*handler).removeFromBlacklist-fm (6 handlers)
[GIN-debug] GET /api/admin/sandbox/binding --> account/api.(*handler).getSandboxBinding-fm (6 handlers)
[GIN-debug] POST /api/admin/sandbox/bind --> account/api.(*handler).bindSandboxNode-fm (6 handlers)
[GIN-debug] GET /api/agent-server/v1/nodes --> account/api.(*handler).listAgentNodes-fm (4 handlers)
[GIN-debug] GET /api/agent-server/v1/users --> account/api.(*handler).listAgentUsers-fm (4 handlers)
[GIN-debug] POST /api/agent-server/v1/status --> account/api.(*handler).reportAgentStatus-fm (4 handlers)
[GIN-debug] GET /api/agent/nodes --> account/api.(*handler).listAgentNodes-fm (4 handlers)
time=2026-02-07T02:23:48.082+08:00 level=INFO msg="starting account service" addr=127.0.0.1:8080 tls=false

View File

@ -280,6 +280,11 @@ func RegisterRoutes(r *gin.Engine, opts ...Option) {
authGroup.GET("/mfa/status", h.mfaStatus)
// Sandbox binding read endpoint.
// Used by the Console Guest/Demo experience. Must be readable either via a
// normal user session or via the internal service token.
authGroup.GET("/sandbox/binding", h.getSandboxBindingPublic)
// Protected routes requiring authentication
authProtected := authGroup.Group("")
if h.tokenService != nil {
@ -323,9 +328,6 @@ func RegisterRoutes(r *gin.Engine, opts ...Option) {
authProtected.GET("/admin/sandbox/binding", h.getSandboxBinding)
authProtected.POST("/admin/sandbox/bind", h.bindSandboxNode)
// Public read of sandbox binding for demo/sandbox user experience.
authProtected.GET("/sandbox/binding", h.getSandboxBindingPublic)
// Root-only identity switch to sandbox@svc.plus (hard-coded allowlist).
authProtected.POST("/admin/assume", h.adminAssume)
authProtected.POST("/admin/assume/revert", h.adminAssumeRevert)
@ -337,6 +339,7 @@ func RegisterRoutes(r *gin.Engine, opts ...Option) {
internalGroup := r.Group("/api/internal")
internalGroup.Use(auth.InternalAuthMiddleware())
internalGroup.GET("/public-overview", h.internalPublicOverview)
// internalGroup.GET("/sandbox/guest", h.internalSandboxGuest)
// Public /api routes for admin/management (expected by frontend at /api/admin/...)
apiGroup := r.Group("/api")
@ -2735,13 +2738,9 @@ func (h *handler) isReadOnlyAccount(user *store.User) bool {
return true
}
// Default policy: Open default 演示模式 (demo mode) as read-only group.
// If the user is NOT an admin/operator, default to read-only for safety in this specific "open demo" context.
// Modify this logic if you want standard "User" role to be write-capable.
// For now, based on request "开放默认的 演示模式", we assume standard users might be treated as demo visitors.
// However, usually RoleUser should be writable. The prompt says "Only admin@svc.plus can modify config".
// This implies EVERYONE else is read-only.
return true
// Default policy: Allow modification for regular users.
// We only restrict explicitly flagged "demo" or "sandbox" identities or users assigned to a specific "ReadOnly Role".
return false
}
func isRootUser(user *store.User) bool {

View File

@ -0,0 +1,26 @@
package api
import (
"os"
"strings"
"github.com/gin-gonic/gin"
)
const internalServiceTokenHeader = "X-Service-Token"
func isInternalServiceRequest(c *gin.Context) bool {
if c == nil {
return false
}
token := strings.TrimSpace(c.GetHeader(internalServiceTokenHeader))
if token == "" {
return false
}
expected := strings.TrimSpace(os.Getenv("INTERNAL_SERVICE_TOKEN"))
if expected == "" {
return false
}
return token == expected
}

View File

@ -14,8 +14,14 @@ import (
// It is intentionally readable by any authenticated user so demo/sandbox users
// do not depend on localStorage browser state.
func (h *handler) getSandboxBindingPublic(c *gin.Context) {
if _, ok := h.requireAuthenticatedUser(c); !ok {
return
// This endpoint is used by the Console Guest/Demo flow.
// Allow it when either:
// - the caller is an authenticated user (normal case), or
// - the caller is the trusted Console BFF (internal service token).
if !isInternalServiceRequest(c) {
if _, ok := h.requireAuthenticatedUser(c); !ok {
return
}
}
if h.db == nil {