Config: Move Portal flag to ClientConfig struct

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-10-15 23:32:54 +02:00
parent 0ef0c79766
commit cb9d8d236a
29 changed files with 40 additions and 39 deletions

View File

@@ -820,7 +820,7 @@ export default class Config {
// isPortal returns true if this is a cluster portal server. // isPortal returns true if this is a cluster portal server.
isPortal() { isPortal() {
return this.feature("portal"); return this.values && this.values.portal;
} }
// isPro returns true if this is team version. // isPro returns true if this is team version.

View File

@@ -192,8 +192,8 @@ export const ItemsPerPage = () => [
{ text: "100", title: "100", value: 100 }, { text: "100", title: "100", value: 100 },
]; ];
export const StartPages = (features) => { export const StartPages = (features, isPortal) => {
if (features.portal) { if (isPortal) {
return [{ value: "default", text: $gettext("Default"), visible: true }]; return [{ value: "default", text: $gettext("Default"), visible: true }];
} }
return [ return [

View File

@@ -64,7 +64,7 @@
<v-select <v-select
v-model="settings.ui.startPage" v-model="settings.ui.startPage"
:disabled="busy" :disabled="busy"
:items="options.StartPages(settings.features)" :items="options.StartPages(settings.features, isPortal)"
tabindex="2" tabindex="2"
item-title="text" item-title="text"
item-value="value" item-value="value"
@@ -437,7 +437,7 @@ export default {
isDemo: this.$config.isDemo(), isDemo: this.$config.isDemo(),
isAdmin: this.$session.isAdmin(), isAdmin: this.$session.isAdmin(),
isSuperAdmin: this.$session.isSuperAdmin(), isSuperAdmin: this.$session.isSuperAdmin(),
isPublic: this.$config.get("public"), isPublic: this.$config.isPublic(),
isPortal: this.$config.isPortal(), isPortal: this.$config.isPortal(),
config: this.$config.values, config: this.$config.values,
settings: new Settings(this.$config.getSettings()), settings: new Settings(this.$config.getSettings()),

View File

@@ -86,7 +86,7 @@ func shouldAttemptJWT(c *gin.Context, token string) bool {
// shouldAllowJWT reports whether the current node configuration permits JWT // shouldAllowJWT reports whether the current node configuration permits JWT
// authentication for the request originating from clientIP. // authentication for the request originating from clientIP.
func shouldAllowJWT(conf *config.Config, clientIP string) bool { func shouldAllowJWT(conf *config.Config, clientIP string) bool {
if conf == nil || conf.IsPortal() { if conf == nil || conf.Portal() {
return false return false
} }

View File

@@ -29,7 +29,7 @@ func ClusterMetrics(router *gin.RouterGroup) {
} }
conf := get.Config() conf := get.Config()
if !conf.IsPortal() { if !conf.Portal() {
AbortFeatureDisabled(c) AbortFeatureDisabled(c)
return return
} }

View File

@@ -57,7 +57,7 @@ func ClusterListNodes(router *gin.RouterGroup) {
conf := get.Config() conf := get.Config()
if !conf.IsPortal() { if !conf.Portal() {
AbortFeatureDisabled(c) AbortFeatureDisabled(c)
return return
} }
@@ -134,7 +134,7 @@ func ClusterGetNode(router *gin.RouterGroup) {
conf := get.Config() conf := get.Config()
if !conf.IsPortal() { if !conf.Portal() {
AbortFeatureDisabled(c) AbortFeatureDisabled(c)
return return
} }
@@ -194,7 +194,7 @@ func ClusterUpdateNode(router *gin.RouterGroup) {
conf := get.Config() conf := get.Config()
if !conf.IsPortal() { if !conf.Portal() {
AbortFeatureDisabled(c) AbortFeatureDisabled(c)
return return
} }
@@ -274,7 +274,7 @@ func ClusterDeleteNode(router *gin.RouterGroup) {
conf := get.Config() conf := get.Config()
if !conf.IsPortal() { if !conf.Portal() {
AbortFeatureDisabled(c) AbortFeatureDisabled(c)
return return
} }

View File

@@ -42,7 +42,7 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
conf := get.Config() conf := get.Config()
// Must be a portal. // Must be a portal.
if !conf.IsPortal() { if !conf.Portal() {
AbortFeatureDisabled(c) AbortFeatureDisabled(c)
return return
} }

View File

@@ -31,7 +31,7 @@ func ClusterSummary(router *gin.RouterGroup) {
conf := get.Config() conf := get.Config()
if !conf.IsPortal() { if !conf.Portal() {
AbortFeatureDisabled(c) AbortFeatureDisabled(c)
return return
} }
@@ -73,7 +73,7 @@ func ClusterHealth(router *gin.RouterGroup) {
c.Header(header.AccessControlAllowOrigin, header.Any) c.Header(header.AccessControlAllowOrigin, header.Any)
// Return error if not a portal node. // Return error if not a portal node.
if !conf.IsPortal() { if !conf.Portal() {
AbortFeatureDisabled(c) AbortFeatureDisabled(c)
return return
} }

View File

@@ -59,7 +59,7 @@ func ClusterGetTheme(router *gin.RouterGroup) {
*/ */
// Abort if this is not a portal server. // Abort if this is not a portal server.
if !conf.IsPortal() { if !conf.Portal() {
AbortFeatureDisabled(c) AbortFeatureDisabled(c)
return return
} }

View File

@@ -32,7 +32,7 @@ func TestAuthJWTCommands(t *testing.T) {
get.SetConfig(conf) get.SetConfig(conf)
conf.RegisterDb() conf.RegisterDb()
require.True(t, conf.IsPortal()) require.True(t, conf.Portal())
manager := get.JWTManager() manager := get.JWTManager()
require.NotNil(t, manager) require.NotNil(t, manager)

View File

@@ -27,7 +27,7 @@ var ClusterHealthCommand = &cli.Command{
func clusterHealthAction(ctx *cli.Context) error { func clusterHealthAction(ctx *cli.Context) error {
return CallWithDependencies(ctx, func(conf *config.Config) error { return CallWithDependencies(ctx, func(conf *config.Config) error {
if !conf.IsPortal() { if !conf.Portal() {
return fmt.Errorf("cluster health is only available on a Portal node") return fmt.Errorf("cluster health is only available on a Portal node")
} }

View File

@@ -38,7 +38,7 @@ var ClusterNodesListCommand = &cli.Command{
func clusterNodesListAction(ctx *cli.Context) error { func clusterNodesListAction(ctx *cli.Context) error {
return CallWithDependencies(ctx, func(conf *config.Config) error { return CallWithDependencies(ctx, func(conf *config.Config) error {
if !conf.IsPortal() { if !conf.Portal() {
return cli.Exit(fmt.Errorf("node listing is only available on a Portal node"), 2) return cli.Exit(fmt.Errorf("node listing is only available on a Portal node"), 2)
} }

View File

@@ -37,7 +37,7 @@ var ClusterNodesModCommand = &cli.Command{
func clusterNodesModAction(ctx *cli.Context) error { func clusterNodesModAction(ctx *cli.Context) error {
return CallWithDependencies(ctx, func(conf *config.Config) error { return CallWithDependencies(ctx, func(conf *config.Config) error {
if !conf.IsPortal() { if !conf.Portal() {
return cli.Exit(fmt.Errorf("node update is only available on a Portal node"), 2) return cli.Exit(fmt.Errorf("node update is only available on a Portal node"), 2)
} }

View File

@@ -31,7 +31,7 @@ var ClusterNodesRemoveCommand = &cli.Command{
func clusterNodesRemoveAction(ctx *cli.Context) error { func clusterNodesRemoveAction(ctx *cli.Context) error {
return CallWithDependencies(ctx, func(conf *config.Config) error { return CallWithDependencies(ctx, func(conf *config.Config) error {
if !conf.IsPortal() { if !conf.Portal() {
return cli.Exit(fmt.Errorf("node delete is only available on a Portal node"), 2) return cli.Exit(fmt.Errorf("node delete is only available on a Portal node"), 2)
} }

View File

@@ -49,7 +49,7 @@ func clusterNodesRotateAction(ctx *cli.Context) error {
// Determine node name. On portal, resolve id->name via registry; otherwise treat key as name. // Determine node name. On portal, resolve id->name via registry; otherwise treat key as name.
name := clean.DNSLabel(key) name := clean.DNSLabel(key)
if conf.IsPortal() { if conf.Portal() {
if r, err := reg.NewClientRegistryWithConfig(conf); err == nil { if r, err := reg.NewClientRegistryWithConfig(conf); err == nil {
if n, err := r.FindByNodeUUID(key); err == nil && n != nil { if n, err := r.FindByNodeUUID(key); err == nil && n != nil {
name = n.Name name = n.Name

View File

@@ -24,7 +24,7 @@ var ClusterNodesShowCommand = &cli.Command{
func clusterNodesShowAction(ctx *cli.Context) error { func clusterNodesShowAction(ctx *cli.Context) error {
return CallWithDependencies(ctx, func(conf *config.Config) error { return CallWithDependencies(ctx, func(conf *config.Config) error {
if !conf.IsPortal() { if !conf.Portal() {
return cli.Exit(fmt.Errorf("node show is only available on a Portal node"), 2) return cli.Exit(fmt.Errorf("node show is only available on a Portal node"), 2)
} }

View File

@@ -24,7 +24,7 @@ var ClusterSummaryCommand = &cli.Command{
func clusterSummaryAction(ctx *cli.Context) error { func clusterSummaryAction(ctx *cli.Context) error {
return CallWithDependencies(ctx, func(conf *config.Config) error { return CallWithDependencies(ctx, func(conf *config.Config) error {
if !conf.IsPortal() { if !conf.Portal() {
return fmt.Errorf("cluster summary is only available on a Portal node") return fmt.Errorf("cluster summary is only available on a Portal node")
} }

View File

@@ -28,7 +28,7 @@ var allowedJWTScope = func() map[string]struct{} {
// requirePortal returns a CLI error when the active configuration is not a portal node. // requirePortal returns a CLI error when the active configuration is not a portal node.
func requirePortal(conf *config.Config) error { func requirePortal(conf *config.Config) error {
if conf == nil || !conf.IsPortal() { if conf == nil || !conf.Portal() {
return cli.Exit(errors.New("command requires a Portal node"), 2) return cli.Exit(errors.New("command requires a Portal node"), 2)
} }
return nil return nil

View File

@@ -62,6 +62,7 @@ type ClientConfig struct {
Trace bool `json:"trace"` Trace bool `json:"trace"`
Test bool `json:"test"` Test bool `json:"test"`
Demo bool `json:"demo"` Demo bool `json:"demo"`
Portal bool `json:"portal"`
Sponsor bool `json:"sponsor"` Sponsor bool `json:"sponsor"`
ReadOnly bool `json:"readonly"` ReadOnly bool `json:"readonly"`
UploadNSFW bool `json:"uploadNSFW"` UploadNSFW bool `json:"uploadNSFW"`
@@ -312,6 +313,7 @@ func (c *Config) ClientPublic() *ClientConfig {
Trace: c.Trace(), Trace: c.Trace(),
Test: c.Test(), Test: c.Test(),
Demo: c.Demo(), Demo: c.Demo(),
Portal: c.Portal(),
Sponsor: c.Sponsor(), Sponsor: c.Sponsor(),
ReadOnly: c.ReadOnly(), ReadOnly: c.ReadOnly(),
Public: c.Public(), Public: c.Public(),
@@ -407,6 +409,7 @@ func (c *Config) ClientShare() *ClientConfig {
Trace: c.Trace(), Trace: c.Trace(),
Test: c.Test(), Test: c.Test(),
Demo: c.Demo(), Demo: c.Demo(),
Portal: c.Portal(),
Sponsor: c.Sponsor(), Sponsor: c.Sponsor(),
ReadOnly: c.ReadOnly(), ReadOnly: c.ReadOnly(),
UploadNSFW: c.UploadNSFW(), UploadNSFW: c.UploadNSFW(),
@@ -510,6 +513,7 @@ func (c *Config) ClientUser(withSettings bool) *ClientConfig {
Trace: c.Trace(), Trace: c.Trace(),
Test: c.Test(), Test: c.Test(),
Demo: c.Demo(), Demo: c.Demo(),
Portal: c.Portal(),
Sponsor: c.Sponsor(), Sponsor: c.Sponsor(),
ReadOnly: c.ReadOnly(), ReadOnly: c.ReadOnly(),
UploadNSFW: c.UploadNSFW(), UploadNSFW: c.UploadNSFW(),

View File

@@ -53,6 +53,7 @@ func TestConfig_ClientConfig(t *testing.T) {
assert.Equal(t, true, cfg.Debug) assert.Equal(t, true, cfg.Debug)
assert.Equal(t, AuthModePublic, cfg.AuthMode) assert.Equal(t, AuthModePublic, cfg.AuthMode)
assert.Equal(t, false, cfg.Demo) assert.Equal(t, false, cfg.Demo)
assert.Equal(t, false, cfg.Portal)
assert.Equal(t, true, cfg.Sponsor) assert.Equal(t, true, cfg.Sponsor)
assert.Equal(t, false, cfg.ReadOnly) assert.Equal(t, false, cfg.ReadOnly)

View File

@@ -84,8 +84,8 @@ func (c *Config) PortalUrl() string {
return c.options.PortalUrl return c.options.PortalUrl
} }
// IsPortal returns true if the configured node type is "portal". // Portal returns true if the configured node type is "portal".
func (c *Config) IsPortal() bool { func (c *Config) Portal() bool {
return c.NodeRole() == cluster.RolePortal return c.NodeRole() == cluster.RolePortal
} }
@@ -123,7 +123,7 @@ func (c *Config) JoinToken() string {
} }
} }
if !c.IsPortal() { if !c.Portal() {
return "" return ""
} }
@@ -182,7 +182,7 @@ func (c *Config) NodeName() string {
} }
// Default: portal nodes → "portal". // Default: portal nodes → "portal".
if c.IsPortal() { if c.Portal() {
return "portal" return "portal"
} }

View File

@@ -94,11 +94,11 @@ func TestConfig_Cluster(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())
// Defaults // Defaults
assert.False(t, c.IsPortal()) assert.False(t, c.Portal())
// Toggle values // Toggle values
c.Options().NodeRole = string(cluster.RolePortal) c.Options().NodeRole = string(cluster.RolePortal)
assert.True(t, c.IsPortal()) assert.True(t, c.Portal())
c.Options().NodeRole = "" c.Options().NodeRole = ""
}) })
t.Run("JWKSUrlSetter", func(t *testing.T) { t.Run("JWKSUrlSetter", func(t *testing.T) {

View File

@@ -21,7 +21,6 @@ type FeatureSettings struct {
People bool `json:"people" yaml:"People"` People bool `json:"people" yaml:"People"`
Places bool `json:"places" yaml:"Places"` Places bool `json:"places" yaml:"Places"`
Private bool `json:"private" yaml:"Private"` Private bool `json:"private" yaml:"Private"`
Portal bool `json:"portal" yaml:"-"`
Ratings bool `json:"ratings" yaml:"Ratings"` Ratings bool `json:"ratings" yaml:"Ratings"`
Reactions bool `json:"reactions" yaml:"Reactions"` Reactions bool `json:"reactions" yaml:"Reactions"`
Review bool `json:"review" yaml:"Review"` Review bool `json:"review" yaml:"Review"`

View File

@@ -97,7 +97,6 @@ func NewSettings(theme, language, timeZone string) *Settings {
Services: true, Services: true,
Account: true, Account: true,
Delete: true, Delete: true,
Portal: false,
}, },
Import: ImportSettings{ Import: ImportSettings{
Path: RootPath, Path: RootPath,

View File

@@ -36,8 +36,6 @@ func (c *Config) initSettings() {
log.Debugf("settings: saved to %s ", settingsFile) log.Debugf("settings: saved to %s ", settingsFile)
} }
c.settings.Features.Portal = c.IsPortal()
i18n.SetDir(c.LocalesPath()) i18n.SetDir(c.LocalesPath())
c.settings.Propagate() c.settings.Propagate()

View File

@@ -22,7 +22,7 @@ var (
func initJWTManager() { func initJWTManager() {
if conf == nil { if conf == nil {
return return
} else if !conf.IsPortal() { } else if !conf.Portal() {
return return
} }
@@ -121,7 +121,7 @@ func IssuePortalJWT(spec jwt.ClaimsSpec) (string, error) {
func IssuePortalJWTForNode(nodeUUID string, scopes []string, ttl time.Duration) (string, error) { func IssuePortalJWTForNode(nodeUUID string, scopes []string, ttl time.Duration) (string, error) {
if conf == nil { if conf == nil {
return "", errors.New("jwt: missing config") return "", errors.New("jwt: missing config")
} else if !conf.IsPortal() { } else if !conf.Portal() {
return "", errors.New("jwt: not supported on nodes") return "", errors.New("jwt: not supported on nodes")
} }

View File

@@ -28,7 +28,7 @@ func registerWellknownRoutes(router *gin.Engine, conf *config.Config) {
// Registers the "/.well-known/jwks.json" endpoint for cluster JWT verification. // Registers the "/.well-known/jwks.json" endpoint for cluster JWT verification.
router.GET(conf.BaseUri("/.well-known/jwks.json"), func(c *gin.Context) { router.GET(conf.BaseUri("/.well-known/jwks.json"), func(c *gin.Context) {
if !conf.IsPortal() { if !conf.Portal() {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return return
} }

View File

@@ -46,7 +46,7 @@ func InitConfig(c *config.Config) error {
role := c.NodeRole() role := c.NodeRole()
// Skip on portal nodes and unknown node types. // Skip on portal nodes and unknown node types.
if c.IsPortal() || (role != cluster.RoleInstance && role != cluster.RoleService) { if c.Portal() || (role != cluster.RoleInstance && role != cluster.RoleService) {
return nil return nil
} }

View File

@@ -4,7 +4,7 @@ import "time"
// BootstrapAutoJoinEnabled indicates whether cluster bootstrap logic is enabled // BootstrapAutoJoinEnabled indicates whether cluster bootstrap logic is enabled
// for nodes by default. Portal nodes ignore this value; gating is decided by // for nodes by default. Portal nodes ignore this value; gating is decided by
// runtime checks (e.g., conf.IsPortal() and conf.NodeRole()). // runtime checks (e.g., conf.Portal() and conf.NodeRole()).
var BootstrapAutoJoinEnabled = true var BootstrapAutoJoinEnabled = true
// BootstrapAutoThemeEnabled indicates whether bootstrap should attempt to // BootstrapAutoThemeEnabled indicates whether bootstrap should attempt to