diff --git a/server/go.mod b/server/go.mod
index 4f4666764d..850e8e9236 100644
--- a/server/go.mod
+++ b/server/go.mod
@@ -21,6 +21,7 @@ require (
github.com/hallazzang/echo-compose v1.0.1
github.com/jarcoal/httpmock v1.3.1
github.com/joho/godotenv v1.5.1
+ github.com/k0kubun/pp/v3 v3.2.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/labstack/echo/v4 v4.11.4
github.com/oapi-codegen/runtime v1.1.1
diff --git a/server/go.sum b/server/go.sum
index 4e772bb001..89fe8e9046 100644
--- a/server/go.sum
+++ b/server/go.sum
@@ -243,6 +243,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
github.com/jpillora/opts v1.2.3 h1:Q0YuOM7y0BlunHJ7laR1TUxkUA7xW8A2rciuZ70xs8g=
github.com/jpillora/opts v1.2.3/go.mod h1:7p7X/vlpKZmtaDFYKs956EujFqA6aCrOkcCaS6UBcR4=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
+github.com/k0kubun/pp/v3 v3.2.0 h1:h33hNTZ9nVFNP3u2Fsgz8JXiF5JINoZfFq4SvKJwNcs=
+github.com/k0kubun/pp/v3 v3.2.0/go.mod h1:ODtJQbQcIRfAD3N+theGCV1m/CBxweERz2dapdz1EwA=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
diff --git a/server/internal/app/config.go b/server/internal/app/config.go
index e72f4e5945..4a8284ef85 100644
--- a/server/internal/app/config.go
+++ b/server/internal/app/config.go
@@ -7,6 +7,7 @@ import (
"strings"
"github.com/joho/godotenv"
+ "github.com/k0kubun/pp/v3"
"github.com/kelseyhightower/envconfig"
"github.com/reearth/reearth-cms/server/internal/infrastructure/aws"
"github.com/reearth/reearth-cms/server/internal/infrastructure/gcp"
@@ -17,51 +18,55 @@ import (
const configPrefix = "REEARTH_CMS"
+func init() {
+ pp.Default.SetColoringEnabled(false)
+}
+
type Config struct {
- Port string `default:"8080" envconfig:"PORT"`
- ServerHost string
- Host string `default:"http://localhost:8080"`
- Dev bool
- Host_Web string
- GraphQL GraphQLConfig
- Origins []string
- DB string `default:"mongodb://localhost"`
- Mailer string
- SMTP SMTPConfig
- SendGrid SendGridConfig
- SignupSecret string
- GCS GCSConfig
- S3 S3Config
- Task gcp.TaskConfig
- AWSTask aws.TaskConfig
- AssetBaseURL string
- Web map[string]string
- Web_Config JSON
- Web_Disabled bool
+ Port string `default:"8080" envconfig:"PORT"`
+ ServerHost string `pp:",omitempty"`
+ Host string `default:"http://localhost:8080"`
+ Dev bool `pp:",omitempty"`
+ Host_Web string `pp:",omitempty"`
+ GraphQL GraphQLConfig `pp:",omitempty"`
+ Origins []string `pp:",omitempty"`
+ DB string `default:"mongodb://localhost"`
+ Mailer string `pp:",omitempty"`
+ SMTP SMTPConfig `pp:",omitempty"`
+ SendGrid SendGridConfig `pp:",omitempty"`
+ SignupSecret string `pp:",omitempty"`
+ GCS GCSConfig `pp:",omitempty"`
+ S3 S3Config `pp:",omitempty"`
+ Task gcp.TaskConfig `pp:",omitempty"`
+ AWSTask aws.TaskConfig `pp:",omitempty"`
+ AssetBaseURL string `pp:",omitempty"`
+ Web map[string]string `pp:",omitempty"`
+ Web_Config JSON `pp:",omitempty"`
+ Web_Disabled bool `pp:",omitempty"`
// auth
- Auth AuthConfigs
- Auth0 Auth0Config
- Cognito CognitoConfig
- Auth_ISS string
- Auth_AUD string
- Auth_ALG *string
- Auth_TTL *int
- Auth_ClientID *string
- Auth_JWKSURI *string
+ Auth AuthConfigs `pp:",omitempty"`
+ Auth0 Auth0Config `pp:",omitempty"`
+ Cognito CognitoConfig `pp:",omitempty"`
+ Auth_ISS string `pp:",omitempty"`
+ Auth_AUD string `pp:",omitempty"`
+ Auth_ALG *string `pp:",omitempty"`
+ Auth_TTL *int `pp:",omitempty"`
+ Auth_ClientID *string `pp:",omitempty"`
+ Auth_JWKSURI *string `pp:",omitempty"`
// auth for m2m
- AuthM2M AuthM2MConfig
+ AuthM2M AuthM2MConfig `pp:",omitempty"`
- DB_Account string
- DB_Users []appx.NamedURI
+ DB_Account string `pp:",omitempty"`
+ DB_Users []appx.NamedURI `pp:",omitempty"`
}
type AuthConfig struct {
- ISS string
- AUD []string
- ALG *string
- TTL *int
- ClientID *string
- JWKSURI *string
+ ISS string `pp:",omitempty"`
+ AUD []string `pp:",omitempty"`
+ ALG *string `pp:",omitempty"`
+ TTL *int `pp:",omitempty"`
+ ClientID *string `pp:",omitempty"`
+ JWKSURI *string `pp:",omitempty"`
}
type GraphQLConfig struct {
@@ -71,50 +76,50 @@ type GraphQLConfig struct {
type AuthConfigs []AuthConfig
type Auth0Config struct {
- Domain string
- Audience string
- ClientID string
- ClientSecret string
- WebClientID string
+ Domain string `pp:",omitempty"`
+ Audience string `pp:",omitempty"`
+ ClientID string `pp:",omitempty"`
+ ClientSecret string `pp:",omitempty"`
+ WebClientID string `pp:",omitempty"`
}
type CognitoConfig struct {
- UserPoolID string
- Region string
- ClientID string
+ UserPoolID string `pp:",omitempty"`
+ Region string `pp:",omitempty"`
+ ClientID string `pp:",omitempty"`
}
type SendGridConfig struct {
- Email string
- Name string
- API string
+ Email string `pp:",omitempty"`
+ Name string `pp:",omitempty"`
+ API string `pp:",omitempty"`
}
type SMTPConfig struct {
- Host string
- Port string
- SMTPUsername string
- Email string
- Password string
+ Host string `pp:",omitempty"`
+ Port string `pp:",omitempty"`
+ SMTPUsername string `pp:",omitempty"`
+ Email string `pp:",omitempty"`
+ Password string `pp:",omitempty"`
}
type GCSConfig struct {
- BucketName string
- PublicationCacheControl string
+ BucketName string `pp:",omitempty"`
+ PublicationCacheControl string `pp:",omitempty"`
}
type S3Config struct {
- BucketName string
- PublicationCacheControl string
+ BucketName string `pp:",omitempty"`
+ PublicationCacheControl string `pp:",omitempty"`
}
type AuthM2MConfig struct {
- ISS string
- AUD []string
- ALG *string
- TTL *int
- Email string
- JWKSURI *string
+ ISS string `pp:",omitempty"`
+ AUD []string `pp:",omitempty"`
+ ALG *string `pp:",omitempty"`
+ TTL *int `pp:",omitempty"`
+ Email string `pp:",omitempty"`
+ JWKSURI *string `pp:",omitempty"`
}
func (c *Config) Auths() (res AuthConfigs) {
@@ -290,13 +295,26 @@ func ReadConfig(debug bool) (*Config, error) {
}
func (c *Config) Print() string {
- s := fmt.Sprintf("%+v", c)
- for _, secret := range []string{c.DB, c.Auth0.ClientSecret} {
+ s := pp.Sprint(c)
+
+ for _, secret := range c.secrets() {
if secret == "" {
continue
}
s = strings.ReplaceAll(s, secret, "***")
}
+
+ return s
+}
+
+func (c *Config) secrets() []string {
+ s := []string{
+ c.DB,
+ c.Auth0.ClientSecret,
+ }
+ for _, d := range c.DB_Users {
+ s = append(s, d.URI)
+ }
return s
}
diff --git a/server/internal/infrastructure/gcp/config.go b/server/internal/infrastructure/gcp/config.go
index c4552c70df..09c246f883 100644
--- a/server/internal/infrastructure/gcp/config.go
+++ b/server/internal/infrastructure/gcp/config.go
@@ -1,11 +1,11 @@
package gcp
type TaskConfig struct {
- GCPProject string
- GCPRegion string
- Topic string
- GCSHost string
- GCSBucket string
+ GCPProject string `pp:",omitempty"`
+ GCPRegion string `pp:",omitempty"`
+ Topic string `pp:",omitempty"`
+ GCSHost string `pp:",omitempty"`
+ GCSBucket string `pp:",omitempty"`
DecompressorImage string `default:"reearth/reearth-cms-decompressor"`
DecompressorTopic string `default:"decompress"`
DecompressorGzipExt string `default:"gml"`
diff --git a/web/.gitignore b/web/.gitignore
index af242f0d5c..2b6ad9731d 100644
--- a/web/.gitignore
+++ b/web/.gitignore
@@ -25,6 +25,8 @@ dist-ssr
/storybook-static
/coverage
+/reearth-config.json
+.env*
#amplify-do-not-edit-begin
amplify/\#current-cloud-backend
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 35aa6c5ced..77855b64f4 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -33,8 +33,10 @@ import { Provider as I18nProvider } from "@reearth-cms/i18n";
const router = createBrowserRouter(
createRoutesFromElements(
<>
- } />
+ } />
+ } />
}>
+ } />
} />
} />
} />
diff --git a/web/src/auth/Auth0Auth.ts b/web/src/auth/Auth0Auth.ts
index 56dabff473..2abc6323aa 100644
--- a/web/src/auth/Auth0Auth.ts
+++ b/web/src/auth/Auth0Auth.ts
@@ -1,5 +1,7 @@
import { useAuth0 } from "@auth0/auth0-react";
+import { logOutFromTenant } from "@reearth-cms/config";
+
import AuthHook from "./AuthHook";
export const errorKey = "reeartherror";
@@ -21,12 +23,17 @@ export const useAuth0Auth = (): AuthHook => {
isLoading,
error: error?.message ?? null,
getAccessToken: () => getAccessTokenSilently(),
- login: () => loginWithRedirect(),
- logout: () =>
- logout({
+ login: () => {
+ logOutFromTenant();
+ return loginWithRedirect();
+ },
+ logout: () => {
+ logOutFromTenant();
+ return logout({
returnTo: error
? `${window.location.origin}?${errorKey}=${encodeURIComponent(error?.message)}`
: window.location.origin,
- }),
+ });
+ },
};
};
diff --git a/web/src/auth/AuthProvider.tsx b/web/src/auth/AuthProvider.tsx
index 5096719338..56d8c9b1c2 100644
--- a/web/src/auth/AuthProvider.tsx
+++ b/web/src/auth/AuthProvider.tsx
@@ -1,5 +1,7 @@
import { Auth0Provider } from "@auth0/auth0-react";
-import React, { createContext, ReactNode } from "react";
+import React, { createContext, ReactNode, useState } from "react";
+
+import { getAuthInfo, getSignInCallbackUrl, logInToTenant } from "@reearth-cms/config";
import { useAuth0Auth } from "./Auth0Auth";
import AuthHook from "./AuthHook";
@@ -18,12 +20,16 @@ const CognitoWrapper = ({ children }: { children: ReactNode }) => {
};
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
- const authProvider = window.REEARTH_CONFIG?.authProvider;
+ const [authInfo] = useState(() => {
+ logInToTenant(); // note that it includes side effect
+ return getAuthInfo();
+ });
+ const authProvider = authInfo?.authProvider;
if (authProvider === "auth0") {
- const domain = window.REEARTH_CONFIG?.auth0Domain;
- const clientId = window.REEARTH_CONFIG?.auth0ClientId;
- const audience = window.REEARTH_CONFIG?.auth0Audience;
+ const domain = authInfo?.auth0Domain;
+ const clientId = authInfo?.auth0ClientId;
+ const audience = authInfo?.auth0Audience;
return domain && clientId ? (
= ({ children
useRefreshTokens
scope="openid profile email"
cacheLocation="localstorage"
- redirectUri={window.location.origin}>
+ redirectUri={getSignInCallbackUrl()}>
{children}
) : null;
@@ -44,5 +50,5 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
return {children};
}
- return <>{children}>; // or some default fallback
+ return null;
};
diff --git a/web/src/auth/CognitoAuth.ts b/web/src/auth/CognitoAuth.ts
index bcfe763b46..fbd81a7989 100644
--- a/web/src/auth/CognitoAuth.ts
+++ b/web/src/auth/CognitoAuth.ts
@@ -1,6 +1,8 @@
import { Auth } from "@aws-amplify/auth";
import { useState, useEffect } from "react";
+import { logOutFromTenant } from "@reearth-cms/config";
+
import AuthHook from "./AuthHook";
export const useCognitoAuth = (): AuthHook => {
@@ -32,10 +34,12 @@ export const useCognitoAuth = (): AuthHook => {
};
const login = () => {
+ logOutFromTenant();
Auth.federatedSignIn();
};
const logout = async () => {
+ logOutFromTenant();
try {
await Auth.signOut();
setUser(null);
diff --git a/web/src/aws-config.ts b/web/src/aws-config.ts
deleted file mode 100644
index 568db987ea..0000000000
--- a/web/src/aws-config.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Amplify } from "aws-amplify";
-
-const authProvider = window.REEARTH_CONFIG?.authProvider;
-if (authProvider === "cognito") {
- const cognitoRegion = window.REEARTH_CONFIG?.cognitoRegion;
- const cognitoUserPoolId = window.REEARTH_CONFIG?.cognitoUserPoolId;
- const cognitoUserPoolWebClientId = window.REEARTH_CONFIG?.cognitoUserPoolWebClientId;
- const cognitoOauthScope = window.REEARTH_CONFIG?.cognitoOauthScope?.split(", ");
- const cognitoOauthDomain = window.REEARTH_CONFIG?.cognitoOauthDomain;
- const cognitoOauthRedirectSignIn = window.REEARTH_CONFIG?.cognitoOauthRedirectSignIn;
- const cognitoOauthRedirectSignOut = window.REEARTH_CONFIG?.cognitoOauthRedirectSignOut;
- const cognitoOauthResponseType = window.REEARTH_CONFIG?.cognitoOauthResponseType;
-
- const config = {
- Auth: {
- region: cognitoRegion,
- userPoolId: cognitoUserPoolId,
- userPoolWebClientId: cognitoUserPoolWebClientId,
- oauth: {
- scope: cognitoOauthScope,
- domain: cognitoOauthDomain,
- redirectSignIn: cognitoOauthRedirectSignIn,
- redirectSignOut: cognitoOauthRedirectSignOut,
- responseType: cognitoOauthResponseType,
- },
- },
- };
-
- Amplify.configure(config);
-}
diff --git a/web/src/components/pages/RootPage/index.tsx b/web/src/components/pages/RootPage/index.tsx
index 1a493628b8..72aa3e9a08 100644
--- a/web/src/components/pages/RootPage/index.tsx
+++ b/web/src/components/pages/RootPage/index.tsx
@@ -18,10 +18,10 @@ const RootPage: React.FC = () => {
if (isAuthenticated) {
if (data?.me?.id) {
if (currentWorkspaceId && currentUserId === data.me.id) {
- navigate(`workspace/${currentWorkspaceId}`);
+ navigate(`/workspace/${currentWorkspaceId || ""}`);
} else {
setCurrentWorkspaceId(undefined);
- navigate("workspace");
+ navigate("/workspace");
}
}
}
diff --git a/web/src/config/authInfo.ts b/web/src/config/authInfo.ts
new file mode 100644
index 0000000000..220415941c
--- /dev/null
+++ b/web/src/config/authInfo.ts
@@ -0,0 +1,79 @@
+import { type CognitoParams } from "./aws";
+
+import { config } from ".";
+
+const tenantKey = "reearth_tennant";
+
+export type AuthInfo = {
+ auth0ClientId?: string;
+ auth0Domain?: string;
+ auth0Audience?: string;
+ authProvider?: string;
+ cognito?: CognitoParams;
+} & CognitoParams;
+
+export function getAuthInfo(conf = config()): AuthInfo | undefined {
+ return getMultitenantAuthInfo(conf) || defaultAuthInfo(conf);
+}
+
+export function getSignInCallbackUrl() {
+ const tenantName = getTenantName();
+ if (tenantName) {
+ // multi-tenant
+ return `${window.location.origin}/auth/${tenantName}`;
+ }
+ return window.location.origin;
+}
+
+export function logInToTenant() {
+ const tenantName = getLogginInTenantName();
+ if (tenantName) {
+ window.localStorage.setItem(tenantKey, tenantName);
+ }
+}
+
+export function logOutFromTenant() {
+ window.localStorage.removeItem(tenantKey);
+}
+
+function defaultAuthInfo(conf = config()): AuthInfo | undefined {
+ if (!conf) return;
+ return {
+ auth0Audience: conf.auth0Audience,
+ auth0ClientId: conf.auth0ClientId,
+ auth0Domain: conf.auth0Domain,
+ authProvider: conf.authProvider || "auth0",
+ cognito: conf.cognito,
+ };
+}
+
+function getMultitenantAuthInfo(conf = config()): AuthInfo | undefined {
+ if (!conf?.multiTenant) return;
+ const name = getTenantName();
+ if (name) {
+ const tenant = conf.multiTenant[name];
+ if (tenant && !tenant.authProvider) {
+ tenant.authProvider = "auth0";
+ }
+ return tenant;
+ }
+ return;
+}
+
+function getTenantName(): string | null {
+ const loggingInTenantName = getLogginInTenantName();
+ if (loggingInTenantName) {
+ return loggingInTenantName;
+ }
+ return window.localStorage.getItem(tenantKey);
+}
+
+function getLogginInTenantName(): string | null {
+ const path = window.location.pathname;
+ // /auth/
+ if (path.startsWith("/auth/")) {
+ const name = path.split("/")[2];
+ return name || null;
+ }
+ return null;
+}
diff --git a/web/src/config/aws.ts b/web/src/config/aws.ts
new file mode 100644
index 0000000000..a8baff7a55
--- /dev/null
+++ b/web/src/config/aws.ts
@@ -0,0 +1,40 @@
+import { Amplify } from "aws-amplify";
+
+export type CognitoParams = {
+ cognitoRegion?: string;
+ cognitoUserPoolId?: string;
+ cognitoUserPoolWebClientId?: string;
+ cognitoOauthScope?: string;
+ cognitoOauthDomain?: string;
+ cognitoOauthRedirectSignIn?: string;
+ cognitoOauthRedirectSignOut?: string;
+ cognitoOauthResponseType?: string;
+};
+
+export function configureCognito(cognito: CognitoParams) {
+ const cognitoRegion = cognito.cognitoRegion;
+ const cognitoUserPoolId = cognito.cognitoUserPoolId;
+ const cognitoUserPoolWebClientId = cognito.cognitoUserPoolWebClientId;
+ const cognitoOauthScope = cognito.cognitoOauthScope?.split(", ");
+ const cognitoOauthDomain = cognito.cognitoOauthDomain;
+ const cognitoOauthRedirectSignIn = cognito.cognitoOauthRedirectSignIn;
+ const cognitoOauthRedirectSignOut = cognito.cognitoOauthRedirectSignOut;
+ const cognitoOauthResponseType = cognito.cognitoOauthResponseType;
+
+ const config = {
+ Auth: {
+ region: cognitoRegion,
+ userPoolId: cognitoUserPoolId,
+ userPoolWebClientId: cognitoUserPoolWebClientId,
+ oauth: {
+ scope: cognitoOauthScope,
+ domain: cognitoOauthDomain,
+ redirectSignIn: cognitoOauthRedirectSignIn,
+ redirectSignOut: cognitoOauthRedirectSignOut,
+ responseType: cognitoOauthResponseType,
+ },
+ },
+ };
+
+ Amplify.configure(config);
+}
diff --git a/web/src/config.ts b/web/src/config/index.ts
similarity index 54%
rename from web/src/config.ts
rename to web/src/config/index.ts
index 9799fecb11..5867f6f97b 100644
--- a/web/src/config.ts
+++ b/web/src/config/index.ts
@@ -1,22 +1,16 @@
+import { type AuthInfo, getAuthInfo } from "./authInfo";
+import { configureCognito } from "./aws";
+
+export { getAuthInfo, getSignInCallbackUrl, logInToTenant, logOutFromTenant } from "./authInfo";
+
export type Config = {
api: string;
- auth0ClientId?: string;
- auth0Domain?: string;
- auth0Audience?: string;
- cognitoRegion?: string;
- cognitoUserPoolId?: string;
- cognitoUserPoolWebClientId?: string;
- cognitoOauthScope?: string;
- cognitoOauthDomain?: string;
- cognitoOauthRedirectSignIn?: string;
- cognitoOauthRedirectSignOut?: string;
- cognitoOauthResponseType?: string;
- authProvider?: string;
logoUrl?: string;
coverImageUrl?: string;
cesiumIonAccessToken?: string;
editorUrl: string;
-};
+ multiTenant?: Record;
+} & AuthInfo;
const env = import.meta.env;
@@ -39,6 +33,9 @@ export default async function loadConfig() {
...defaultConfig,
...(await (await fetch("/reearth_config.json")).json()),
};
+
+ const authInfo = getAuthInfo(window.REEARTH_CONFIG);
+ if (authInfo?.authProvider === "cognito") configureCognito(authInfo.cognito ?? authInfo);
}
export function config(): Config | undefined {
@@ -51,25 +48,7 @@ export function e2eAccessToken(): string | undefined {
declare global {
interface Window {
- REEARTH_CONFIG?: {
- api: string;
- auth0ClientId?: string;
- auth0Domain?: string;
- auth0Audience?: string;
- cognitoRegion?: string;
- cognitoUserPoolId?: string;
- cognitoUserPoolWebClientId?: string;
- cognitoOauthScope?: string;
- cognitoOauthDomain?: string;
- cognitoOauthRedirectSignIn?: string;
- cognitoOauthRedirectSignOut?: string;
- cognitoOauthResponseType?: string;
- authProvider?: string;
- logoUrl?: string;
- coverImageUrl?: string;
- cesiumIonAccessToken?: string;
- editorUrl: string;
- };
+ REEARTH_CONFIG?: Config;
REEARTH_E2E_ACCESS_TOKEN?: string;
}
}
diff --git a/web/src/main.tsx b/web/src/main.tsx
index cf558bf250..865e6e292c 100644
--- a/web/src/main.tsx
+++ b/web/src/main.tsx
@@ -11,7 +11,6 @@ import "./index.css";
try {
await loadConfig();
} finally {
- await import("./aws-config");
const element = document.getElementById("root");
if (element) {
const root = ReactDOM.createRoot(element);
diff --git a/web/vite.config.ts b/web/vite.config.ts
index 10a18989d4..91f3b30266 100644
--- a/web/vite.config.ts
+++ b/web/vite.config.ts
@@ -70,8 +70,18 @@ function config(): Plugin {
return {
name: "reearth-config",
async configureServer(server) {
+ const envs = loadEnv(
+ server.config.mode,
+ server.config.envDir ?? process.cwd(),
+ server.config.envPrefix,
+ );
+ const remoteReearthConfig = envs.REEARTH_WEB_CONFIG_URL
+ ? await (await fetch(envs.REEARTH_WEB_CONFIG_URL)).json()
+ : {};
const configRes = JSON.stringify(
{
+ ...remoteReearthConfig,
+ api: "http://localhost:8080/api",
...readEnv("REEARTH_CMS", {
source: loadEnv(
server.config.mode,