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,