diff --git a/apps/login/app/(login)/accounts/page.tsx b/apps/login/app/(login)/accounts/page.tsx index 5761512df14..6985ea67fd5 100644 --- a/apps/login/app/(login)/accounts/page.tsx +++ b/apps/login/app/(login)/accounts/page.tsx @@ -1,9 +1,10 @@ import { Session } from "@zitadel/server"; -import { listSessions, server } from "#/lib/zitadel"; +import { getBrandingSettings, listSessions, server } from "#/lib/zitadel"; import { getAllSessionCookieIds } from "#/utils/cookies"; import { UserPlusIcon } from "@heroicons/react/24/outline"; import Link from "next/link"; import SessionsList from "#/ui/SessionsList"; +import DynamicTheme from "#/ui/DynamicTheme"; async function loadSessions(): Promise { const ids = await getAllSessionCookieIds(); @@ -26,34 +27,39 @@ export default async function Page({ searchParams: Record; }) { const authRequestId = searchParams?.authRequestId; + const organization = searchParams?.organization; let sessions = await loadSessions(); - return ( -
-

Accounts

-

Use your ZITADEL Account

+ const branding = await getBrandingSettings(server, organization); -
- - -
-
- + return ( + +
+

Accounts

+

Use your ZITADEL Account

+ +
+ + +
+
+ +
+ Add another account
- Add another account -
- + +
-
+ ); } diff --git a/apps/login/app/(login)/idp/[provider]/failure/page.tsx b/apps/login/app/(login)/idp/[provider]/failure/page.tsx new file mode 100644 index 00000000000..aada1c52493 --- /dev/null +++ b/apps/login/app/(login)/idp/[provider]/failure/page.tsx @@ -0,0 +1,61 @@ +import { ProviderSlug } from "#/lib/demos"; +import { getBrandingSettings, server } from "#/lib/zitadel"; +import Alert, { AlertType } from "#/ui/Alert"; +import DynamicTheme from "#/ui/DynamicTheme"; +import IdpSignin from "#/ui/IdpSignin"; +import { + AddHumanUserRequest, + IDPInformation, + RetrieveIdentityProviderIntentResponse, + user, + IDPLink, +} from "@zitadel/server"; +import { ClientError } from "nice-grpc"; + +const PROVIDER_NAME_MAPPING: { + [provider: string]: string; +} = { + [ProviderSlug.GOOGLE]: "Google", + [ProviderSlug.GITHUB]: "GitHub", +}; + +export default async function Page({ + searchParams, + params, +}: { + searchParams: Record; + params: { provider: ProviderSlug }; +}) { + const { id, token, authRequestId, organization } = searchParams; + const { provider } = params; + + const branding = await getBrandingSettings(server, organization); + + if (provider) { + return ( + +
+

Login failure

+
+ An error signing in with{" "} + {PROVIDER_NAME_MAPPING[provider] + ? PROVIDER_NAME_MAPPING[provider] + : provider}{" "} + happened! +
+ + {/* + {} + */} +
+
+ ); + } else { + return ( +
+

Register

+

No id and token received!

+
+ ); + } +} diff --git a/apps/login/app/(login)/idp/[provider]/success/page.tsx b/apps/login/app/(login)/idp/[provider]/success/page.tsx index 511adf16d87..6aae35bf68b 100644 --- a/apps/login/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/app/(login)/idp/[provider]/success/page.tsx @@ -1,15 +1,14 @@ import { ProviderSlug } from "#/lib/demos"; -import { server } from "#/lib/zitadel"; +import { getBrandingSettings, server } from "#/lib/zitadel"; import Alert, { AlertType } from "#/ui/Alert"; +import DynamicTheme from "#/ui/DynamicTheme"; import IdpSignin from "#/ui/IdpSignin"; -import { createSessionForIdpAndUpdateCookie } from "#/utils/session"; import { AddHumanUserRequest, IDPInformation, RetrieveIdentityProviderIntentResponse, user, IDPLink, - Session, } from "@zitadel/server"; import { ClientError } from "nice-grpc"; @@ -89,9 +88,11 @@ export default async function Page({ searchParams: Record; params: { provider: ProviderSlug }; }) { - const { id, token, authRequestId } = searchParams; + const { id, token, authRequestId, organization } = searchParams; const { provider } = params; + const branding = await getBrandingSettings(server, organization); + if (provider && id && token) { return retrieveIDPIntent(id, token) .then((resp) => { @@ -100,40 +101,46 @@ export default async function Page({ // handle login if (userId) { return ( -
-

Login successful

-
You have successfully been loggedIn!
+ +
+

Login successful

+
You have successfully been loggedIn!
- -
+ +
+ ); } else { // handle register return createUser(provider, idpInformation) .then((userId) => { return ( -
-

Register successful

-
You have successfully been registered!
-
+ +
+

Register successful

+
You have successfully been registered!
+
+
); }) .catch((error: ClientError) => { return ( -
-

Register failed

-
- { - - {JSON.stringify(error.message)} - - } + +
+

Register failed

+
+ { + + {JSON.stringify(error.message)} + + } +
-
+ ); }); } @@ -143,16 +150,18 @@ export default async function Page({ }) .catch((error) => { return ( -
-

An error occurred

-
- { - - {JSON.stringify(error.message)} - - } + +
+

An error occurred

+
+ { + + {JSON.stringify(error.message)} + + } +
-
+ ); }); } else { diff --git a/apps/login/app/(login)/idp/page.tsx b/apps/login/app/(login)/idp/page.tsx index 04661a6b36b..c1da21788e0 100644 --- a/apps/login/app/(login)/idp/page.tsx +++ b/apps/login/app/(login)/idp/page.tsx @@ -1,4 +1,9 @@ -import { getLegalAndSupportSettings, server } from "#/lib/zitadel"; +import { + getBrandingSettings, + getLegalAndSupportSettings, + server, +} from "#/lib/zitadel"; +import DynamicTheme from "#/ui/DynamicTheme"; import { SignInWithIDP } from "#/ui/SignInWithIDP"; import { GetActiveIdentityProvidersResponse, @@ -28,30 +33,35 @@ export default async function Page({ searchParams: Record; }) { const authRequestId = searchParams?.authRequestId; + const organization = searchParams?.organization; - const legal = await getLegalAndSupportSettings(server); + const legal = await getLegalAndSupportSettings(server, organization); - // TODO if org idps should be shown replace emptystring with the orgId. - const identityProviders = await getIdentityProviders(server, ""); + const identityProviders = await getIdentityProviders(server, organization); const host = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000"; - return ( -
-

Register

-

- Select one of the following providers to register -

+ const branding = await getBrandingSettings(server, organization); - {legal && identityProviders && process.env.ZITADEL_API_URL && ( - - )} -
+ return ( + +
+

Register

+

+ Select one of the following providers to register +

+ + {legal && identityProviders && process.env.ZITADEL_API_URL && ( + + )} +
+
); } diff --git a/apps/login/app/(login)/loginname/page.tsx b/apps/login/app/(login)/loginname/page.tsx index 79cdfe1fd04..b85a8a09f66 100644 --- a/apps/login/app/(login)/loginname/page.tsx +++ b/apps/login/app/(login)/loginname/page.tsx @@ -6,9 +6,7 @@ import { } from "#/lib/zitadel"; import DynamicTheme from "#/ui/DynamicTheme"; import { SignInWithIDP } from "#/ui/SignInWithIDP"; -import ThemeWrapper from "#/ui/ThemeWrapper"; import UsernameForm from "#/ui/UsernameForm"; -import { BrandingSettings } from "@zitadel/server"; import { GetActiveIdentityProvidersResponse, IdentityProvider, @@ -71,6 +69,7 @@ export default async function Page({ host={host} identityProviders={identityProviders} authRequestId={authRequestId} + organization={organization} > )}
diff --git a/apps/login/app/(login)/passkey/add/page.tsx b/apps/login/app/(login)/passkey/add/page.tsx index 08a4c36dcd5..129ecd88565 100644 --- a/apps/login/app/(login)/passkey/add/page.tsx +++ b/apps/login/app/(login)/passkey/add/page.tsx @@ -1,5 +1,6 @@ -import { getSession, server } from "#/lib/zitadel"; +import { getBrandingSettings, getSession, server } from "#/lib/zitadel"; import Alert, { AlertType } from "#/ui/Alert"; +import DynamicTheme from "#/ui/DynamicTheme"; import RegisterPasskey from "#/ui/RegisterPasskey"; import UserAvatar from "#/ui/UserAvatar"; import { getMostRecentCookieWithLoginname } from "#/utils/cookies"; @@ -31,48 +32,52 @@ export default async function Page({ ? "When set up, you will be able to authenticate without a password." : "Your device will ask for your fingerprint, face, or screen lock"; + const branding = await getBrandingSettings(server, organization); + return ( -
-

{title}

+ +
+

{title}

- {sessionFactors && ( - - )} -

{description}

+ {sessionFactors && ( + + )} +

{description}

- - - A passkey is an authentication method on a device like your - fingerprint, Apple FaceID or similar. - - Passwordless Authentication - - - + + + A passkey is an authentication method on a device like your + fingerprint, Apple FaceID or similar. + + Passwordless Authentication + + + - {!sessionFactors && ( -
- - Could not get the context of the user. Make sure to enter the - username first or provide a loginName as searchParam. - -
- )} + {!sessionFactors && ( +
+ + Could not get the context of the user. Make sure to enter the + username first or provide a loginName as searchParam. + +
+ )} - {sessionFactors?.id && ( - - )} -
+ {sessionFactors?.id && ( + + )} +
+ ); } diff --git a/apps/login/app/(login)/register/page.tsx b/apps/login/app/(login)/register/page.tsx index 646ed2296c4..506b9df0717 100644 --- a/apps/login/app/(login)/register/page.tsx +++ b/apps/login/app/(login)/register/page.tsx @@ -1,8 +1,10 @@ import { + getBrandingSettings, getLegalAndSupportSettings, getPasswordComplexitySettings, server, } from "#/lib/zitadel"; +import DynamicTheme from "#/ui/DynamicTheme"; import RegisterFormWithoutPassword from "#/ui/RegisterFormWithoutPassword"; import SetPasswordForm from "#/ui/SetPasswordForm"; @@ -16,40 +18,46 @@ export default async function Page({ const setPassword = !!(firstname && lastname && email); - const legal = await getLegalAndSupportSettings(server); + const legal = await getLegalAndSupportSettings(server, organization); const passwordComplexitySettings = await getPasswordComplexitySettings( server, organization ); + const branding = await getBrandingSettings(server, organization); + return setPassword ? ( -
-

Set Password

-

Set the password for your account

+ +
+

Set Password

+

Set the password for your account

- {legal && passwordComplexitySettings && ( - - )} -
+ {legal && passwordComplexitySettings && ( + + )} +
+ ) : ( -
-

Register

-

Create your ZITADEL account.

+ +
+

Register

+

Create your ZITADEL account.

- {legal && passwordComplexitySettings && ( - - )} -
+ {legal && passwordComplexitySettings && ( + + )} +
+ ); } diff --git a/apps/login/app/(login)/signedin/page.tsx b/apps/login/app/(login)/signedin/page.tsx index 740840df28a..4ee30a6c8d1 100644 --- a/apps/login/app/(login)/signedin/page.tsx +++ b/apps/login/app/(login)/signedin/page.tsx @@ -1,4 +1,10 @@ -import { createCallback, getSession, server } from "#/lib/zitadel"; +import { + createCallback, + getBrandingSettings, + getSession, + server, +} from "#/lib/zitadel"; +import DynamicTheme from "#/ui/DynamicTheme"; import UserAvatar from "#/ui/UserAvatar"; import { getMostRecentCookieWithLoginname } from "#/utils/cookies"; import { redirect } from "next/navigation"; @@ -22,19 +28,23 @@ async function loadSession(loginName: string, authRequestId?: string) { } export default async function Page({ searchParams }: { searchParams: any }) { - const { loginName, authRequestId } = searchParams; + const { loginName, authRequestId, organization } = searchParams; const sessionFactors = await loadSession(loginName, authRequestId); - return ( -
-

{`Welcome ${sessionFactors?.factors?.user?.displayName}`}

-

You are signed in.

+ const branding = await getBrandingSettings(server, organization); - -
+ return ( + +
+

{`Welcome ${sessionFactors?.factors?.user?.displayName}`}

+

You are signed in.

+ + +
+
); } diff --git a/apps/login/app/(login)/signup/page.tsx b/apps/login/app/(login)/signup/page.tsx deleted file mode 100644 index 0762ff32249..00000000000 --- a/apps/login/app/(login)/signup/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { - getLegalAndSupportSettings, - getPasswordComplexitySettings, - server, -} from "#/lib/zitadel"; -import RegisterForm from "#/ui/RegisterForm"; - -export default async function Page() { - const legal = await getLegalAndSupportSettings(server); - const passwordComplexitySettings = await getPasswordComplexitySettings( - server - ); - - return ( -
-

Register

-

Create your ZITADEL account.

- - {legal && passwordComplexitySettings && ( - - )} -
- ); -} diff --git a/apps/login/app/(login)/verify/page.tsx b/apps/login/app/(login)/verify/page.tsx index b074dfe8cfb..591c3fd9e2f 100644 --- a/apps/login/app/(login)/verify/page.tsx +++ b/apps/login/app/(login)/verify/page.tsx @@ -1,28 +1,36 @@ +import { getBrandingSettings, server } from "#/lib/zitadel"; +import DynamicTheme from "#/ui/DynamicTheme"; import VerifyEmailForm from "#/ui/VerifyEmailForm"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; export default async function Page({ searchParams }: { searchParams: any }) { - const { userID, code, submit, orgID, loginname, passwordset } = searchParams; + const { userID, code, submit, organization, loginname, passwordset } = + searchParams; + + const branding = await getBrandingSettings(server, organization); return ( -
-

Verify user

-

- Enter the Code provided in the verification email. -

+ +
+

Verify user

+

+ Enter the Code provided in the verification email. +

- {userID ? ( - - ) : ( -
- - No userId provided! -
- )} -
+ {userID ? ( + + ) : ( +
+ + No userId provided! +
+ )} +
+ ); } diff --git a/apps/login/app/api/registeruser/route.ts b/apps/login/app/api/registeruser/route.ts index 81ac8148feb..d0ed88f47fa 100644 --- a/apps/login/app/api/registeruser/route.ts +++ b/apps/login/app/api/registeruser/route.ts @@ -4,13 +4,21 @@ import { NextRequest, NextResponse } from "next/server"; export async function POST(request: NextRequest) { const body = await request.json(); if (body) { - const { email, password, firstName, lastName } = body; + const { + email, + password, + firstName, + lastName, + organization, + authRequestId, + } = body; return addHumanUser(server, { email: email, firstName, lastName, password: password ? password : undefined, + organization, }) .then((userId) => { return NextResponse.json({ userId }); diff --git a/apps/login/app/login/route.ts b/apps/login/app/login/route.ts index 3d76dd89619..a5531c518d8 100644 --- a/apps/login/app/login/route.ts +++ b/apps/login/app/login/route.ts @@ -89,9 +89,11 @@ export async function GET(request: NextRequest) { if ( authRequest?.scope && - authRequest.scope.find((s) => ORG_SCOPE_REGEX.test(s)) + authRequest.scope.find((s: string) => ORG_SCOPE_REGEX.test(s)) ) { - const orgId = authRequest.scope.find((s) => ORG_SCOPE_REGEX.test(s)); + const orgId = authRequest.scope.find((s: string) => + ORG_SCOPE_REGEX.test(s) + ); if (orgId) { const matched = ORG_SCOPE_REGEX.exec(orgId); diff --git a/apps/login/lib/zitadel.ts b/apps/login/lib/zitadel.ts index 58a412e035c..69c29abdfb8 100644 --- a/apps/login/lib/zitadel.ts +++ b/apps/login/lib/zitadel.ts @@ -87,11 +87,15 @@ export async function getGeneralSettings( } export async function getLegalAndSupportSettings( - server: ZitadelServer + server: ZitadelServer, + organization?: string ): Promise { const settingsService = settings.getSettings(server); return settingsService - .getLegalAndSupportSettings({}, {}) + .getLegalAndSupportSettings( + { ctx: organization ? { orgId: organization } : { instance: true } }, + {} + ) .then((resp: GetLegalAndSupportSettingsResponse) => { return resp.settings; }); @@ -261,11 +265,12 @@ export type AddHumanUserData = { lastName: string; email: string; password: string | undefined; + organization: string | undefined; }; export async function addHumanUser( server: ZitadelServer, - { email, firstName, lastName, password }: AddHumanUserData + { email, firstName, lastName, password, organization }: AddHumanUserData ): Promise { const userService = user.getUser(server); @@ -274,6 +279,11 @@ export async function addHumanUser( username: email, profile: { givenName: firstName, familyName: lastName }, }; + + if (organization) { + payload.organization = { orgId: organization }; + } + return userService .addHumanUser( password diff --git a/apps/login/ui/RegisterForm.tsx b/apps/login/ui/RegisterForm.tsx index 6357299a1ce..5e95f2efeda 100644 --- a/apps/login/ui/RegisterForm.tsx +++ b/apps/login/ui/RegisterForm.tsx @@ -32,11 +32,13 @@ type Inputs = type Props = { legal: LegalAndSupportSettings; passwordComplexitySettings: PasswordComplexitySettings; + organization?: string; }; export default function RegisterForm({ legal, passwordComplexitySettings, + organization, }: Props) { const { register, handleSubmit, watch, formState } = useForm({ mode: "onBlur", @@ -58,6 +60,7 @@ export default function RegisterForm({ password: values.password, firstName: values.firstname, lastName: values.lastname, + organization: organization, }), }); @@ -72,7 +75,13 @@ export default function RegisterForm({ function submitAndLink(value: Inputs): Promise { return submitRegister(value).then((resp: any) => { - return router.push(`/verify?userID=${resp.userId}`); + const params: any = { userID: resp.userId }; + + if (organization) { + params.organization = organization; + } + + return router.push(`/verify?` + new URLSearchParams(params)); }); } diff --git a/apps/login/ui/RegisterFormWithoutPassword.tsx b/apps/login/ui/RegisterFormWithoutPassword.tsx index 46d1589bfde..c9c93bcaa94 100644 --- a/apps/login/ui/RegisterFormWithoutPassword.tsx +++ b/apps/login/ui/RegisterFormWithoutPassword.tsx @@ -53,6 +53,7 @@ export default function RegisterFormWithoutPassword({ email: values.email, firstName: values.firstname, lastName: values.lastname, + organization: organization, }), }); setLoading(false); @@ -88,16 +89,30 @@ export default function RegisterFormWithoutPassword({ value: Inputs, withPassword: boolean = false ) { + const registerParams: any = value; + + if (organization) { + registerParams.organization = organization; + } + return withPassword - ? router.push(`/register?` + new URLSearchParams(value)) + ? router.push(`/register?` + new URLSearchParams(registerParams)) : submitAndRegister(value) .then((resp: any) => { createSessionWithLoginName(value.email).then(({ factors }) => { setError(""); - return router.push( - `/passkey/add?` + - new URLSearchParams({ loginName: factors.user.loginName }) - ); + + const params: any = { loginName: factors.user.loginName }; + + if (organization) { + params.organization = organization; + } + + if (authRequestId) { + params.authRequestId = authRequestId; + } + + return router.push(`/passkey/add?` + new URLSearchParams(params)); }); }) .catch((errorDetails: Error) => { diff --git a/apps/login/ui/SignInWithIDP.tsx b/apps/login/ui/SignInWithIDP.tsx index 843d4f460de..043f595d580 100644 --- a/apps/login/ui/SignInWithIDP.tsx +++ b/apps/login/ui/SignInWithIDP.tsx @@ -16,6 +16,7 @@ export interface SignInWithIDPProps { host: string; identityProviders: any[]; authRequestId?: string; + organization?: string; startIDPFlowPath?: (idpId: string) => string; } @@ -26,6 +27,7 @@ export function SignInWithIDP({ host, identityProviders, authRequestId, + organization, startIDPFlowPath = START_IDP_FLOW_PATH, }: SignInWithIDPProps) { const [loading, setLoading] = useState(false); @@ -35,6 +37,16 @@ export function SignInWithIDP({ async function startFlow(idpId: string, provider: ProviderSlug) { setLoading(true); + const params = new URLSearchParams(); + + if (authRequestId) { + params.set("authRequestId", authRequestId); + } + + if (organization) { + params.set("organization", organization); + } + const res = await fetch("/api/idp/start", { method: "POST", headers: { @@ -42,11 +54,10 @@ export function SignInWithIDP({ }, body: JSON.stringify({ idpId, - successUrl: authRequestId - ? `${host}/idp/${provider}/success?` + - new URLSearchParams({ authRequestId }) - : `${host}/idp/${provider}/success`, - failureUrl: `${host}/idp/${provider}/failure`, + successUrl: + `${host}/idp/${provider}/success?` + new URLSearchParams(params), + failureUrl: + `${host}/idp/${provider}/failure?` + new URLSearchParams(params), }), }); diff --git a/apps/login/ui/VerifyEmailForm.tsx b/apps/login/ui/VerifyEmailForm.tsx index b4b81dd50df..c9463e9e719 100644 --- a/apps/login/ui/VerifyEmailForm.tsx +++ b/apps/login/ui/VerifyEmailForm.tsx @@ -16,9 +16,15 @@ type Props = { userId: string; code: string; submit: boolean; + organization?: string; }; -export default function VerifyEmailForm({ userId, code, submit }: Props) { +export default function VerifyEmailForm({ + userId, + code, + submit, + organization, +}: Props) { const { register, handleSubmit, formState } = useForm({ mode: "onBlur", defaultValues: { @@ -50,6 +56,7 @@ export default function VerifyEmailForm({ userId, code, submit }: Props) { body: JSON.stringify({ code: values.code, userId, + organization, }), });