diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 32b7efc96..0ea84c5c8 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,7 +6,7 @@ - [ ] Acceptance criteria are met - [ ] All open todos and follow ups are defined in a new ticket and justified - [ ] Deviations from the acceptance criteria and design are agreed with the PO and documented. -- [ ] Jest unit tests ensure that components produce expected outputs on different inputs. +- [ ] Vitest unit tests ensure that components produce expected outputs on different inputs. - [ ] Cypress integration tests ensure that login app pages work as expected. The ZITADEL API is mocked. - [ ] No debug or dead code - [ ] My code has no repetitions diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 381501b88..a417bfcdc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,10 +30,10 @@ jobs: with: version: 8 - - name: Setup Node.js 18.x + - name: Setup Node.js 20.x uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: 20.x - uses: pnpm/action-setup@v2 name: Install pnpm diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..21b8f2261 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +.next/ +dist/ +**/src/proto/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 44e0914bd..18038ed32 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,6 +57,7 @@ export $(cat ./apps/login/.env.acceptance | xargs) ### Developing Against Your ZITADEL Cloud Instance Configure your shell by exporting the following environment variables: + ```sh export ZITADEL_API_URL= export ZITADEL_ORG_ID= @@ -93,5 +94,4 @@ In apps/login, these commands also spin up the application and a ZITADEL gRPC AP If you want to run the integration tests standalone against an environment of your choice, navigate to ./apps/login, [configure your shell as you like](# Developing Against Your ZITADEL Cloud Instance) and run `pnpm test:integration:run` or `pnpm test:integration:open`. Then you need to lifecycle the mock process using the command `pnpm mock` or the more fine grained commands `pnpm mock:build`, `pnpm mock:build:nocache`, `pnpm mock:run` and `pnpm mock:destroy`. - That's it! 🎉 diff --git a/README.md b/README.md index 9a3203742..c8b5fa0cd 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,17 @@ # ZITADEL typescript with Turborepo and Changesets -This repository contains all TypeScript and JavaScript packages and applications you need to create your own ZITADEL Login UI. -The repo makes use of the [build system Turbo](https://turbo.build/repo) and the [Changesets CLI for versioning the packages](https://github.com/changesets/changesets). +This repository contains all TypeScript and JavaScript packages and applications you need to create your own ZITADEL +Login UI. +The repo makes use of the [build system Turbo](https://turbo.build/repo) and +the [Changesets CLI for versioning the packages](https://github.com/changesets/changesets). **⚠️ This repo and packages are in alpha state and subject to change ⚠️** The scope of functionality of this repo and packages is limited and under active development. Once the package structure is set and all APIs are fully implemented we'll move this repo to beta state. You can read the [contribution guide](/CONTRIBUTING.md) on how to contribute. -Questions can be raised in our [Discord channel](https://discord.gg/erh5Brh7jE) or as a [GitHub issue](https://github.com/zitadel/typescript/issues). +Questions can be raised in our [Discord channel](https://discord.gg/erh5Brh7jE) or as +a [GitHub issue](https://github.com/zitadel/typescript/issues). ## Developing Your Own ZITADEL Login UI @@ -36,16 +39,18 @@ Each package and app is 100% [TypeScript](https://www.typescriptlang.org/). ### Login -The login is currently in a work in progress state. -The goal is to implement a login UI, using the session API of ZITADEL, which also implements the OIDC Standard and is ready to use for everyone. +The login is currently in a work in progress state. +The goal is to implement a login UI, using the session API of ZITADEL, which also implements the OIDC Standard and is +ready to use for everyone. -In the first phase we want to have a MVP login ready with the OIDC Standard and a basic feature set. In a second step the features will be extended. +In the first phase we want to have a MVP login ready with the OIDC Standard and a basic feature set. In a second step +the features will be extended. This list should show the current implementation state, and also what is missing. You can already use the current state, and extend it with your needs. - [x] Local User Registration (with Password) -- [ ] User Registration and Login with external Provider +- [ ] User Registration and Login with external Provider - [ ] Google - [ ] GitHub - [ ] GitHub Enterprise @@ -67,21 +72,21 @@ You can already use the current state, and extend it with your needs. - [ ] Domain Discovery - [ ] Branding - OIDC Standard - - [ ] Authorization Code Flow with PKCE - - [ ] AuthRequest `hintUserId` - - [ ] AuthRequest `loginHint` - - [ ] AuthRequest `prompt` - - [x] Login - - [x] Select Account - - [ ] Consent - - [ ] Create - - Scopes - - [ ] `openid email profile address`` - - [ ] `offline access` - - [ ] `urn:zitadel:iam:org:idp:id:{idp_id}` - - [ ] `urn:zitadel:iam:org:project:id:zitadel:aud` - - [ ] `urn:zitadel:iam:org:id:{orgid}` - - [ ] AuthRequest UI locales + - [ ] Authorization Code Flow with PKCE + - [ ] AuthRequest `hintUserId` + - [ ] AuthRequest `loginHint` + - [ ] AuthRequest `prompt` + - [x] Login + - [x] Select Account + - [ ] Consent + - [ ] Create + - Scopes + - [ ] `openid email profile address`` + - [ ] `offline access` + - [ ] `urn:zitadel:iam:org:idp:id:{idp_id}` + - [ ] `urn:zitadel:iam:org:project:id:zitadel:aud` + - [ ] `urn:zitadel:iam:org:id:{orgid}` + - [ ] AuthRequest UI locales ## Tooling @@ -103,11 +108,14 @@ You can already use the current state, and extend it with your needs. ## Versioning And Publishing Packages Package publishing has been configured using [Changesets](https://github.com/changesets/changesets). -Here is their [documentation](https://github.com/changesets/changesets#documentation) for more information about the workflow. +Here is their [documentation](https://github.com/changesets/changesets#documentation) for more information about the +workflow. -The [GitHub Action](https://github.com/changesets/action) needs an `NPM_TOKEN` and `GITHUB_TOKEN` in the repository settings. The [Changesets bot](https://github.com/apps/changeset-bot) should also be installed on the GitHub repository. +The [GitHub Action](https://github.com/changesets/action) needs an `NPM_TOKEN` and `GITHUB_TOKEN` in the repository +settings. The [Changesets bot](https://github.com/apps/changeset-bot) should also be installed on the GitHub repository. -Read the [changesets documentation](https://github.com/changesets/changesets/blob/main/docs/automating-changesets.md) for more information about this automation +Read the [changesets documentation](https://github.com/changesets/changesets/blob/main/docs/automating-changesets.md) +for more information about this automation ### NPM @@ -134,8 +142,10 @@ pnpm install ``` then setup the environment for the login application which needs a `.env.local` in `/apps/login`. -Go to your instance and create a service user for the application having the IAM_OWNER manager role. -This user is required to have access to create users on your primary organization and reading policy data so it can be restricted to your personal use case but we'll stick with IAM_OWNER for convenience. Create a PAT and copy the value to paste it under the `ZITADEL_SERVICE_USER_TOKEN` key. +Go to your instance and create a service user for the application having the `IAM_OWNER` manager role. +This user is required to have access to create users on your primary organization and reading policy data so it can be +restricted to your personal use case but we'll stick with `IAM_OWNER` for convenience. Create a PAT and copy the value to +paste it under the `ZITADEL_SERVICE_USER_TOKEN` key. The file should look as follows: ``` @@ -162,7 +172,8 @@ Open the login application with your favorite browser at `localhost:3000`. To deploy your own version on Vercel, navigate to your instance and create a service user. Copy its id from the overview and set it as ZITADEL_SERVICE_USER_ID. -Then create a personal access token (PAT), copy and set it as ZITADEL_SERVICE_USER_TOKEN, then navigate to your instance settings and make sure it gets IAM_OWNER permissions. +Then create a personal access token (PAT), copy and set it as ZITADEL_SERVICE_USER_TOKEN, then navigate to your instance +settings and make sure it gets IAM_OWNER permissions. Finally set your instance url as ZITADEL_API_URL. Make sure to set it without trailing slash. [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fzitadel%2Ftypescript&env=ZITADEL_API_URL,ZITADEL_SERVICE_USER_ID,ZITADEL_SERVICE_USER_TOKEN&root-directory=apps/login&envDescription=Setup%20a%20service%20account%20with%20IAM_OWNER%20membership%20on%20your%20instance%20and%20provide%20its%20id%20and%20personal%20access%20token.&project-name=zitadel-login&repository-name=zitadel-login) diff --git a/apps/login/.eslintrc.js b/apps/login/.eslintrc.js index 9d9f4bdfe..8cf53b3cd 100755 --- a/apps/login/.eslintrc.js +++ b/apps/login/.eslintrc.js @@ -1,4 +1,7 @@ module.exports = { extends: ["next/core-web-vitals"], ignorePatterns: ["external/**/*.ts"], + rules: { + "@next/next/no-html-link-for-pages": "off", + }, }; diff --git a/apps/login/__test__/jest.config.ts b/apps/login/__test__/jest.config.ts deleted file mode 100644 index 10e9f5a33..000000000 --- a/apps/login/__test__/jest.config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Config } from "@jest/types"; -import { pathsToModuleNameMapper } from "ts-jest"; -import { compilerOptions } from "../tsconfig.json"; - -// We make these type imports explicit, so IDEs with their own typescript engine understand them, too. -import type {} from "@testing-library/jest-dom"; - -export default async (): Promise => { - return { - preset: "ts-jest", - transform: { - "^.+\\.tsx?$": ["ts-jest", { tsconfig: "/tsconfig.json" }], - }, - setupFilesAfterEnv: ["@testing-library/jest-dom/extend-expect"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { - prefix: "/../", - }), - testEnvironment: "jsdom", - testRegex: "/__test__/.*\\.test\\.tsx?$", - modulePathIgnorePatterns: ["cypress"], - }; -}; diff --git a/apps/login/__test__/tsconfig.json b/apps/login/__test__/tsconfig.json deleted file mode 100644 index 11ad984ef..000000000 --- a/apps/login/__test__/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "jsx": "react-jsxdev", - "types": ["node", "jest", "@testing-library/jest-dom"] - } -} diff --git a/apps/login/app/(login)/error.tsx b/apps/login/app/(login)/error.tsx deleted file mode 100644 index 614bdbd58..000000000 --- a/apps/login/app/(login)/error.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -import { Boundary } from "#/ui/Boundary"; -import { Button } from "#/ui/Button"; -import React from "react"; - -export default function Error({ error, reset }: any) { - React.useEffect(() => { - console.log("logging error:", error); - }, [error]); - - return ( - -
-
- Error: {error?.message} -
-
- -
-
-
- ); -} diff --git a/apps/login/app/(login)/loginname/page.tsx b/apps/login/app/(login)/loginname/page.tsx deleted file mode 100644 index b85a8a09f..000000000 --- a/apps/login/app/(login)/loginname/page.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { - getBrandingSettings, - getLegalAndSupportSettings, - getLoginSettings, - server, -} from "#/lib/zitadel"; -import DynamicTheme from "#/ui/DynamicTheme"; -import { SignInWithIDP } from "#/ui/SignInWithIDP"; -import UsernameForm from "#/ui/UsernameForm"; -import { - GetActiveIdentityProvidersResponse, - IdentityProvider, - ZitadelServer, - settings, -} from "@zitadel/server"; - -function getIdentityProviders( - server: ZitadelServer, - orgId?: string -): Promise { - const settingsService = settings.getSettings(server); - return settingsService - .getActiveIdentityProviders( - orgId ? { ctx: { orgId } } : { ctx: { instance: true } }, - {} - ) - .then((resp: GetActiveIdentityProvidersResponse) => { - return resp.identityProviders; - }); -} - -export default async function Page({ - searchParams, -}: { - searchParams: Record; -}) { - const loginName = searchParams?.loginName; - const authRequestId = searchParams?.authRequestId; - const organization = searchParams?.organization; - const submit: boolean = searchParams?.submit === "true"; - - const loginSettings = await getLoginSettings(server, organization); - const legal = await getLegalAndSupportSettings(server); - - const identityProviders = await getIdentityProviders(server, organization); - - const host = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : "http://localhost:3000"; - - const branding = await getBrandingSettings(server, organization); - - return ( - -
-

Welcome back!

-

Enter your login data.

- - - - {legal && identityProviders && process.env.ZITADEL_API_URL && ( - - )} -
-
- ); -} diff --git a/apps/login/app/(login)/password/page.tsx b/apps/login/app/(login)/password/page.tsx deleted file mode 100644 index 9e4825749..000000000 --- a/apps/login/app/(login)/password/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { getBrandingSettings, getSession, server } from "#/lib/zitadel"; -import Alert from "#/ui/Alert"; -import DynamicTheme from "#/ui/DynamicTheme"; -import PasswordForm from "#/ui/PasswordForm"; -import UserAvatar from "#/ui/UserAvatar"; -import { getMostRecentCookieWithLoginname } from "#/utils/cookies"; - -export default async function Page({ - searchParams, -}: { - searchParams: Record; -}) { - const { loginName, organization, promptPasswordless, authRequestId, alt } = - searchParams; - const sessionFactors = await loadSession(loginName, organization); - - async function loadSession(loginName?: string, organization?: string) { - const recent = await getMostRecentCookieWithLoginname( - loginName, - organization - ); - - return getSession(server, recent.id, recent.token).then((response) => { - if (response?.session) { - return response.session; - } - }); - } - - const branding = await getBrandingSettings(server, organization); - - return ( - -
-

{sessionFactors?.factors?.user?.displayName ?? "Password"}

-

Enter your password.

- - {!sessionFactors && ( -
- - Could not get the context of the user. Make sure to enter the - username first or provide a loginName as searchParam. - -
- )} - - {sessionFactors && ( - - )} - - -
-
- ); -} diff --git a/apps/login/cypress/integration/login.cy.ts b/apps/login/cypress/integration/login.cy.ts index 91d1cd72d..206d74b4b 100644 --- a/apps/login/cypress/integration/login.cy.ts +++ b/apps/login/cypress/integration/login.cy.ts @@ -111,7 +111,7 @@ describe("login", () => { cy.get('button[type="submit"]').click(); cy.location("pathname", { timeout: 10_000 }).should( "eq", - "/passkey/add" + "/passkey/add", ); }); }); @@ -160,7 +160,7 @@ describe("login", () => { cy.visit("/loginname?loginName=john%40zitadel.com&submit=true"); cy.location("pathname", { timeout: 10_000 }).should( "eq", - "/passkey/login" + "/passkey/login", ); }); }); diff --git a/apps/login/lib/zitadel.ts b/apps/login/lib/zitadel.ts deleted file mode 100644 index edaf9cc36..000000000 --- a/apps/login/lib/zitadel.ts +++ /dev/null @@ -1,500 +0,0 @@ -import { - ZitadelServer, - ZitadelServerOptions, - user, - oidc, - settings, - getServers, - initializeServer, - session, - GetGeneralSettingsResponse, - CreateSessionResponse, - GetBrandingSettingsResponse, - GetPasswordComplexitySettingsResponse, - GetLegalAndSupportSettingsResponse, - AddHumanUserResponse, - BrandingSettings, - ListSessionsResponse, - LegalAndSupportSettings, - PasswordComplexitySettings, - GetSessionResponse, - VerifyEmailResponse, - SetSessionResponse, - SetSessionRequest, - ListUsersResponse, - management, - DeleteSessionResponse, - VerifyPasskeyRegistrationResponse, - LoginSettings, - GetOrgByDomainGlobalResponse, - GetLoginSettingsResponse, - ListAuthenticationMethodTypesResponse, - StartIdentityProviderIntentRequest, - StartIdentityProviderIntentResponse, - RetrieveIdentityProviderIntentRequest, - RetrieveIdentityProviderIntentResponse, - GetAuthRequestResponse, - GetAuthRequestRequest, - CreateCallbackRequest, - CreateCallbackResponse, - RequestChallenges, - TextQueryMethod, - AddHumanUserRequest, -} from "@zitadel/server"; - -export const zitadelConfig: ZitadelServerOptions = { - name: "zitadel login", - apiUrl: process.env.ZITADEL_API_URL ?? "", - token: process.env.ZITADEL_SERVICE_USER_TOKEN ?? "", -}; - -let server: ZitadelServer; - -if (!getServers().length) { - console.log("initialize server"); - server = initializeServer(zitadelConfig); -} - -export async function getBrandingSettings( - server: ZitadelServer, - organization?: string -): Promise { - const settingsService = settings.getSettings(server); - return settingsService - .getBrandingSettings( - { ctx: organization ? { orgId: organization } : { instance: true } }, - {} - ) - .then((resp: GetBrandingSettingsResponse) => resp.settings); -} - -export async function getLoginSettings( - server: ZitadelServer, - orgId?: string -): Promise { - const settingsService = settings.getSettings(server); - return settingsService - .getLoginSettings({ ctx: orgId ? { orgId } : { instance: true } }, {}) - .then((resp: GetLoginSettingsResponse) => resp.settings); -} - -export async function getGeneralSettings( - server: ZitadelServer -): Promise { - const settingsService = settings.getSettings(server); - return settingsService - .getGeneralSettings({}, {}) - .then((resp: GetGeneralSettingsResponse) => resp.supportedLanguages); -} - -export async function getLegalAndSupportSettings( - server: ZitadelServer, - organization?: string -): Promise { - const settingsService = settings.getSettings(server); - return settingsService - .getLegalAndSupportSettings( - { ctx: organization ? { orgId: organization } : { instance: true } }, - {} - ) - .then((resp: GetLegalAndSupportSettingsResponse) => { - return resp.settings; - }); -} - -export async function getPasswordComplexitySettings( - server: ZitadelServer, - organization?: string -): Promise { - const settingsService = settings.getSettings(server); - - return settingsService - .getPasswordComplexitySettings( - organization - ? { ctx: { orgId: organization } } - : { ctx: { instance: true } }, - {} - ) - .then((resp: GetPasswordComplexitySettingsResponse) => resp.settings); -} - -export async function createSessionForLoginname( - server: ZitadelServer, - loginName: string, - password: string | undefined, - challenges: RequestChallenges | undefined -): Promise { - const sessionService = session.getSession(server); - return password - ? sessionService.createSession( - { - checks: { user: { loginName }, password: { password } }, - challenges, - lifetime: { - seconds: 300, - nanos: 0, - }, - }, - {} - ) - : sessionService.createSession( - { - checks: { user: { loginName } }, - challenges, - lifetime: { - seconds: 300, - nanos: 0, - }, - }, - {} - ); -} - -export async function createSessionForUserId( - server: ZitadelServer, - userId: string, - password: string | undefined, - challenges: RequestChallenges | undefined -): Promise { - const sessionService = session.getSession(server); - return password - ? sessionService.createSession( - { - checks: { user: { userId }, password: { password } }, - challenges, - lifetime: { - seconds: 300, - nanos: 0, - }, - }, - {} - ) - : sessionService.createSession( - { - checks: { user: { userId } }, - challenges, - lifetime: { - seconds: 300, - nanos: 0, - }, - }, - {} - ); -} - -export async function createSessionForUserIdAndIdpIntent( - server: ZitadelServer, - userId: string, - idpIntent: { - idpIntentId?: string | undefined; - idpIntentToken?: string | undefined; - } -): Promise { - const sessionService = session.getSession(server); - - return sessionService.createSession( - { - checks: { user: { userId }, idpIntent }, - // lifetime: { - // seconds: 300, - // nanos: 0, - // }, - }, - {} - ); -} - -export async function setSession( - server: ZitadelServer, - sessionId: string, - sessionToken: string, - password: string | undefined, - webAuthN: { credentialAssertionData: any } | undefined, - challenges: RequestChallenges | undefined -): Promise { - const sessionService = session.getSession(server); - - const payload: SetSessionRequest = { - sessionId, - sessionToken, - challenges, - checks: {}, - metadata: {}, - }; - - if (password && payload.checks) { - payload.checks.password = { password }; - } - - if (webAuthN && payload.checks) { - payload.checks.webAuthN = webAuthN; - } - - return sessionService.setSession(payload, {}); -} - -export async function getSession( - server: ZitadelServer, - sessionId: string, - sessionToken: string -): Promise { - const sessionService = session.getSession(server); - return sessionService.getSession({ sessionId, sessionToken }, {}); -} - -export async function deleteSession( - server: ZitadelServer, - sessionId: string, - sessionToken: string -): Promise { - const sessionService = session.getSession(server); - return sessionService.deleteSession({ sessionId, sessionToken }, {}); -} - -export async function listSessions( - server: ZitadelServer, - ids: string[] -): Promise { - const sessionService = session.getSession(server); - const query = { offset: 0, limit: 100, asc: true }; - const queries = [{ idsQuery: { ids } }]; - return sessionService.listSessions({ queries: queries }, {}); -} - -export type AddHumanUserData = { - firstName: string; - lastName: string; - email: string; - password: string | undefined; - organization: string | undefined; -}; - -export async function addHumanUser( - server: ZitadelServer, - { email, firstName, lastName, password, organization }: AddHumanUserData -): Promise { - const userService = user.getUser(server); - - const payload: Partial = { - email: { email }, - username: email, - profile: { givenName: firstName, familyName: lastName }, - }; - - if (organization) { - payload.organization = { orgId: organization }; - } - - return userService.addHumanUser( - password - ? { - ...payload, - password: { password }, - } - : payload, - {} - ); -} - -export async function listUsers( - userName: string, - organizationId: string -): Promise { - const userService = user.getUser(server); - - return userService.listUsers( - { - queries: organizationId - ? [ - { - userNameQuery: { - userName, - method: TextQueryMethod.TEXT_QUERY_METHOD_EQUALS, - }, - }, - { - organizationIdQuery: { - organizationId, - }, - }, - ] - : [ - { - userNameQuery: { - userName, - method: TextQueryMethod.TEXT_QUERY_METHOD_EQUALS, - }, - }, - ], - }, - {} - ); -} - -export async function getOrgByDomain( - domain: string -): Promise { - const mgmtService = management.getManagement(server); - return mgmtService.getOrgByDomainGlobal({ domain }, {}); -} - -export async function startIdentityProviderFlow( - server: ZitadelServer, - { idpId, urls }: StartIdentityProviderIntentRequest -): Promise { - const userService = user.getUser(server); - - return userService.startIdentityProviderIntent({ - idpId, - urls, - }); -} - -export async function retrieveIdentityProviderInformation( - server: ZitadelServer, - { idpIntentId, idpIntentToken }: RetrieveIdentityProviderIntentRequest -): Promise { - const userService = user.getUser(server); - - return userService.retrieveIdentityProviderIntent({ - idpIntentId, - idpIntentToken, - }); -} - -export async function getAuthRequest( - server: ZitadelServer, - { authRequestId }: GetAuthRequestRequest -): Promise { - const oidcService = oidc.getOidc(server); - - return oidcService.getAuthRequest({ - authRequestId, - }); -} - -export async function createCallback( - server: ZitadelServer, - req: CreateCallbackRequest -): Promise { - const oidcService = oidc.getOidc(server); - - return oidcService.createCallback(req); -} - -export async function verifyEmail( - server: ZitadelServer, - userId: string, - verificationCode: string -): Promise { - const userservice = user.getUser(server); - return userservice.verifyEmail( - { - userId, - verificationCode, - }, - {} - ); -} - -/** - * - * @param server - * @param userId the id of the user where the email should be set - * @returns the newly set email - */ -export async function setEmail( - server: ZitadelServer, - userId: string -): Promise { - const userservice = user.getUser(server); - return userservice.setEmail( - { - userId, - }, - {} - ); -} - -/** - * - * @param server - * @param userId the id of the user where the email should be set - * @returns the newly set email - */ -export async function createPasskeyRegistrationLink( - userId: string -): Promise { - const userservice = user.getUser(server); - - return userservice.createPasskeyRegistrationLink({ - userId, - returnCode: {}, - }); -} - -/** - * - * @param server - * @param userId the id of the user where the email should be set - * @returns the newly set email - */ -export async function verifyPasskeyRegistration( - server: ZitadelServer, - passkeyId: string, - passkeyName: string, - publicKeyCredential: - | { - [key: string]: any; - } - | undefined, - userId: string -): Promise { - const userservice = user.getUser(server); - return userservice.verifyPasskeyRegistration( - { - passkeyId, - passkeyName, - publicKeyCredential, - userId, - }, - {} - ); -} - -/** - * - * @param server - * @param userId the id of the user where the email should be set - * @returns the newly set email - */ -export async function registerPasskey( - userId: string, - code: { id: string; code: string }, - domain: string -): Promise { - const userservice = user.getUser(server); - return userservice.registerPasskey({ - userId, - code, - domain, - // authenticator: - }); -} - -/** - * - * @param server - * @param userId the id of the user where the email should be set - * @returns the newly set email - */ -export async function listAuthenticationMethodTypes( - userId: string -): Promise { - const userservice = user.getUser(server); - return userservice.listAuthenticationMethodTypes({ - userId, - }); -} - -export { server }; diff --git a/apps/login/next.config.js b/apps/login/next.config.mjs similarity index 98% rename from apps/login/next.config.js rename to apps/login/next.config.mjs index f919c62bf..4e7107273 100755 --- a/apps/login/next.config.js +++ b/apps/login/next.config.mjs @@ -62,4 +62,4 @@ const nextConfig = { }, }; -module.exports = nextConfig; +export default nextConfig; diff --git a/apps/login/package.json b/apps/login/package.json index 8f4925802..fce88876c 100644 --- a/apps/login/package.json +++ b/apps/login/package.json @@ -5,7 +5,7 @@ "dev": "next dev", "test": "concurrently --timings --kill-others-on-fail 'npm:test:unit' 'npm:test:integration'", "test:watch": "concurrently --kill-others 'npm:test:unit:watch' 'npm:test:integration:watch'", - "test:unit": "jest --config ./__test__/jest.config.ts", + "test:unit": "vitest", "test:unit:watch": "pnpm test:unit --watch", "test:integration": "pnpm mock:build && concurrently --names 'mock,test' --success command-test --kill-others 'pnpm:mock' 'env-cmd -f ./.env.integration start-server-and-test start http://localhost:3000 \"test:integration:run\"'", "test:integration:watch": "concurrently --names 'mock,test' --kill-others 'pnpm:mock' 'env-cmd -f ./.env.integration start-server-and-test dev http://localhost:3000 \"pnpm nodemon -e js,jsx,ts,tsx,css,scss --ignore \\\"__test__/**\\\" --exec \\\"pnpm test:integration:run\\\"\"'", @@ -32,61 +32,57 @@ "*": "prettier --write --ignore-unknown" }, "dependencies": { - "@headlessui/react": "^1.7.14", - "@heroicons/react": "2.0.13", - "@tailwindcss/forms": "0.5.3", - "@vercel/analytics": "^1.0.0", + "@headlessui/react": "^1.7.18", + "@heroicons/react": "2.1.3", + "@tailwindcss/forms": "0.5.7", + "@vercel/analytics": "^1.2.2", "@zitadel/client": "workspace:*", "@zitadel/react": "workspace:*", "@zitadel/server": "workspace:*", - "clsx": "1.2.1", - "moment": "^2.29.4", - "next": "13.4.12", - "next-themes": "^0.2.1", - "nice-grpc": "2.0.1", + "clsx": "2.1.0", + "moment": "^2.30.1", + "next": "14.2.0", + "next-themes": "^0.3.0", + "nice-grpc": "2.1.8", "react": "18.2.0", "react-dom": "18.2.0", - "react-hook-form": "7.39.5", - "sass": "^1.62.0", - "swr": "^2.2.0", - "tinycolor2": "1.4.2" + "react-hook-form": "7.51.3", + "sass": "^1.75.0", + "swr": "^2.2.5", + "tinycolor2": "1.6.0" }, "devDependencies": { - "@bufbuild/buf": "^1.14.0", - "@jest/types": "^29.5.0", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^14.0.0", - "@types/jest": "^29.5.1", - "@types/ms": "0.7.31", - "@types/node": "18.11.9", - "@types/react": "18.2.8", - "@types/react-dom": "18.0.9", - "@types/testing-library__jest-dom": "^5.14.6", - "@types/tinycolor2": "1.4.3", - "@types/uuid": "^9.0.1", + "@bufbuild/buf": "^1.30.1", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/react": "^15.0.1", + "@types/ms": "0.7.34", + "@types/node": "20.12.7", + "@types/react": "18.2.77", + "@types/react-dom": "18.2.25", + "@types/tinycolor2": "1.4.6", + "@types/uuid": "^9.0.8", "@vercel/git-hooks": "1.0.0", + "@vitejs/plugin-react": "^4.2.1", "@zitadel/tsconfig": "workspace:*", - "autoprefixer": "10.4.13", - "concurrently": "^8.1.0", - "cypress": "^12.14.0", - "del-cli": "5.0.0", + "autoprefixer": "10.4.19", + "concurrently": "^8.2.2", + "cypress": "^13.7.3", + "del-cli": "5.1.0", "env-cmd": "^10.1.0", "eslint-config-zitadel": "workspace:*", - "grpc-tools": "1.11.3", - "jest": "^29.5.0", - "jest-environment-jsdom": "^29.5.0", - "jest-silent-reporter": "^0.5.0", - "lint-staged": "13.0.3", - "make-dir-cli": "3.0.0", - "nodemon": "^2.0.22", - "postcss": "8.4.21", - "prettier-plugin-tailwindcss": "0.1.13", - "start-server-and-test": "^2.0.0", - "tailwindcss": "3.2.4", - "ts-jest": "^29.1.0", - "ts-node": "^10.9.1", - "ts-proto": "^1.139.0", - "typescript": "5.0.4", + "grpc-tools": "1.12.4", + "jsdom": "^24.0.0", + "lint-staged": "15.2.2", + "make-dir-cli": "3.1.0", + "nodemon": "^3.1.0", + "postcss": "8.4.38", + "prettier-plugin-tailwindcss": "0.5.13", + "start-server-and-test": "^2.0.3", + "tailwindcss": "3.4.3", + "ts-proto": "^1.171.0", + "typescript": "^5.4.5", + "vite-tsconfig-paths": "^4.3.2", + "vitest": "^1.5.0", "zitadel-tailwind-config": "workspace:*" } } diff --git a/apps/login/readme.md b/apps/login/readme.md index 02bfaf62f..e9510a7cf 100644 --- a/apps/login/readme.md +++ b/apps/login/readme.md @@ -1,11 +1,11 @@ # ZITADEL Login UI -This is going to be our next UI for the hosted login. It's based on Next.js 13 and its introduced `app/` directory. +This is going to be our next UI for the hosted login. It's based on Next.js 13 and its introduced `app/` directory. The Login UI should provide the following functionality: -- **Login API:** Uses the new ZITADEL Login API -- **Server Components:** Making server-first components +- **Login API:** Uses the new ZITADEL Login API +- **Server Components:** Making server-first components ## Running Locally diff --git a/apps/login/app/(login)/accounts/page.tsx b/apps/login/src/app/(login)/accounts/page.tsx similarity index 72% rename from apps/login/app/(login)/accounts/page.tsx rename to apps/login/src/app/(login)/accounts/page.tsx index 6985ea67f..36c46370b 100644 --- a/apps/login/app/(login)/accounts/page.tsx +++ b/apps/login/src/app/(login)/accounts/page.tsx @@ -1,24 +1,30 @@ -import { Session } from "@zitadel/server"; -import { getBrandingSettings, listSessions, server } from "#/lib/zitadel"; -import { getAllSessionCookieIds } from "#/utils/cookies"; +import { Session } from "@zitadel/client/v2beta"; +import { getBrandingSettings, sessionService } 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"; +import SessionsList from "@/ui/SessionsList"; +import DynamicTheme from "@/ui/DynamicTheme"; async function loadSessions(): Promise { const ids = await getAllSessionCookieIds(); - if (ids && ids.length) { - const response = await listSessions( - server, - ids.filter((id: string | undefined) => !!id) - ); - return response?.sessions ?? []; - } else { - console.info("No session cookie found."); + if (ids.length === 0) { return []; } + + const response = await sessionService.listSessions({ + queries: [ + { + query: { + case: "idsQuery", + value: ids.filter((id: string | undefined) => !!id), + }, + }, + ], + }); + + return response.sessions; } export default async function Page({ @@ -31,7 +37,7 @@ export default async function Page({ let sessions = await loadSessions(); - const branding = await getBrandingSettings(server, organization); + const branding = await getBrandingSettings(organization); return ( diff --git a/apps/login/src/app/(login)/error.tsx b/apps/login/src/app/(login)/error.tsx new file mode 100644 index 000000000..51066c5d8 --- /dev/null +++ b/apps/login/src/app/(login)/error.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { Boundary } from "@/ui/Boundary"; +import { Button } from "@/ui/Button"; + +export default function Error(props: { + error: Error | null; + reset: () => void; +}) { + return ( + +
+
+ Error: {props.error?.message} +
+
+ +
+
+
+ ); +} diff --git a/apps/login/app/(login)/idp/[provider]/failure/page.tsx b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx similarity index 70% rename from apps/login/app/(login)/idp/[provider]/failure/page.tsx rename to apps/login/src/app/(login)/idp/[provider]/failure/page.tsx index aada1c524..188aed21b 100644 --- a/apps/login/app/(login)/idp/[provider]/failure/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx @@ -1,16 +1,6 @@ -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"; +import { ProviderSlug } from "@/lib/demos"; +import { getBrandingSettings } from "@/lib/zitadel"; +import DynamicTheme from "@/ui/DynamicTheme"; const PROVIDER_NAME_MAPPING: { [provider: string]: string; @@ -29,7 +19,7 @@ export default async function Page({ const { id, token, authRequestId, organization } = searchParams; const { provider } = params; - const branding = await getBrandingSettings(server, organization); + const branding = await getBrandingSettings(organization); if (provider) { return ( diff --git a/apps/login/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx similarity index 72% rename from apps/login/app/(login)/idp/[provider]/success/page.tsx rename to apps/login/src/app/(login)/idp/[provider]/success/page.tsx index 6aae35bf6..054a9ea54 100644 --- a/apps/login/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -1,59 +1,74 @@ -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 { +import { ProviderSlug } from "@/lib/demos"; +import { getBrandingSettings, userService } from "@/lib/zitadel"; +import Alert, { AlertType } from "@/ui/Alert"; +import DynamicTheme from "@/ui/DynamicTheme"; +import IdpSignin from "@/ui/IdpSignin"; +import { PartialMessage } from "@zitadel/client"; +import type { AddHumanUserRequest, IDPInformation, RetrieveIdentityProviderIntentResponse, - user, IDPLink, -} from "@zitadel/server"; -import { ClientError } from "nice-grpc"; +} from "@zitadel/client/v2beta"; const PROVIDER_MAPPING: { - [provider: string]: (rI: IDPInformation) => Partial; + [provider: string]: ( + rI: IDPInformation, + ) => PartialMessage; } = { [ProviderSlug.GOOGLE]: (idp: IDPInformation) => { - const idpLink: IDPLink = { + const rawInfo = idp.rawInformation?.toJson() as { + User: { + email: string; + name?: string; + given_name?: string; + family_name?: string; + }; + }; + + const idpLink: PartialMessage = { idpId: idp.idpId, userId: idp.userId, userName: idp.userName, }; - const req: Partial = { + + const req: PartialMessage = { username: idp.userName, email: { - email: idp.rawInformation?.User?.email, - isVerified: true, + email: rawInfo.User.email, + verification: { case: "isVerified", value: true }, }, // organisation: Organisation | undefined; profile: { - displayName: idp.rawInformation?.User?.name ?? "", - givenName: idp.rawInformation?.User?.given_name ?? "", - familyName: idp.rawInformation?.User?.family_name ?? "", + displayName: rawInfo.User?.name ?? "", + givenName: rawInfo.User?.given_name ?? "", + familyName: rawInfo.User?.family_name ?? "", }, idpLinks: [idpLink], }; return req; }, [ProviderSlug.GITHUB]: (idp: IDPInformation) => { - const idpLink: IDPLink = { + const rawInfo = idp.rawInformation?.toJson() as { + email: string; + name: string; + }; + const idpLink: PartialMessage = { idpId: idp.idpId, userId: idp.userId, userName: idp.userName, }; - const req: Partial = { + const req: PartialMessage = { username: idp.userName, email: { - email: idp.rawInformation?.email, - isVerified: true, + email: rawInfo.email, + verification: { case: "isVerified", value: true }, }, // organisation: Organisation | undefined; profile: { - displayName: idp.rawInformation?.name ?? "", - givenName: idp.rawInformation?.name ?? "", - familyName: idp.rawInformation?.name ?? "", + displayName: rawInfo.name ?? "", + givenName: rawInfo.name ?? "", + familyName: rawInfo.name ?? "", }, idpLinks: [idpLink], }; @@ -63,21 +78,19 @@ const PROVIDER_MAPPING: { function retrieveIDPIntent( id: string, - token: string + token: string, ): Promise { - const userService = user.getUser(server); return userService.retrieveIdentityProviderIntent( { idpIntentId: id, idpIntentToken: token }, - {} + {}, ); } function createUser( provider: ProviderSlug, - info: IDPInformation + info: IDPInformation, ): Promise { const userData = PROVIDER_MAPPING[provider](info); - const userService = user.getUser(server); return userService.addHumanUser(userData, {}).then((resp) => resp.userId); } @@ -91,7 +104,7 @@ export default async function Page({ const { id, token, authRequestId, organization } = searchParams; const { provider } = params; - const branding = await getBrandingSettings(server, organization); + const branding = await getBrandingSettings(organization); if (provider && id && token) { return retrieveIDPIntent(id, token) @@ -127,7 +140,7 @@ export default async function Page({
); }) - .catch((error: ClientError) => { + .catch((error) => { return (
diff --git a/apps/login/app/(login)/idp/page.tsx b/apps/login/src/app/(login)/idp/page.tsx similarity index 67% rename from apps/login/app/(login)/idp/page.tsx rename to apps/login/src/app/(login)/idp/page.tsx index c1da21788..064027f8e 100644 --- a/apps/login/app/(login)/idp/page.tsx +++ b/apps/login/src/app/(login)/idp/page.tsx @@ -1,27 +1,21 @@ import { getBrandingSettings, getLegalAndSupportSettings, - server, -} from "#/lib/zitadel"; -import DynamicTheme from "#/ui/DynamicTheme"; -import { SignInWithIDP } from "#/ui/SignInWithIDP"; + settingsService, +} from "@/lib/zitadel"; +import DynamicTheme from "@/ui/DynamicTheme"; +import { SignInWithIDP } from "@/ui/SignInWithIDP"; import { GetActiveIdentityProvidersResponse, IdentityProvider, - ZitadelServer, - settings, -} from "@zitadel/server"; +} from "@zitadel/client/v2beta"; +import { makeReqCtx } from "@zitadel/client/v2beta"; function getIdentityProviders( - server: ZitadelServer, - orgId?: string + orgId?: string, ): Promise { - const settingsService = settings.getSettings(server); return settingsService - .getActiveIdentityProviders( - orgId ? { ctx: { orgId } } : { ctx: { instance: true } }, - {} - ) + .getActiveIdentityProviders({ ctx: makeReqCtx(orgId) }) .then((resp: GetActiveIdentityProvidersResponse) => { return resp.identityProviders; }); @@ -35,15 +29,15 @@ export default async function Page({ const authRequestId = searchParams?.authRequestId; const organization = searchParams?.organization; - const legal = await getLegalAndSupportSettings(server, organization); + const legal = await getLegalAndSupportSettings(organization); - const identityProviders = await getIdentityProviders(server, organization); + const identityProviders = await getIdentityProviders(organization); const host = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000"; - const branding = await getBrandingSettings(server, organization); + const branding = await getBrandingSettings(organization); return ( diff --git a/apps/login/src/app/(login)/loginname/page.tsx b/apps/login/src/app/(login)/loginname/page.tsx new file mode 100644 index 000000000..dd91789d5 --- /dev/null +++ b/apps/login/src/app/(login)/loginname/page.tsx @@ -0,0 +1,54 @@ +import { + getBrandingSettings, + getLegalAndSupportSettings, + getLoginSettings, + getIdentityProviders, +} from "@/lib/zitadel"; +import DynamicTheme from "@/ui/DynamicTheme"; +import { SignInWithIDP } from "@/ui/SignInWithIDP"; +import UsernameForm from "@/ui/UsernameForm"; + +export default async function Page(props: { + searchParams: Record; +}) { + const loginName = props.searchParams?.loginName; + const authRequestId = props.searchParams?.authRequestId; + const organization = props.searchParams?.organization; + const submit: boolean = props.searchParams?.submit === "true"; + + const loginSettings = await getLoginSettings(organization); + const legal = await getLegalAndSupportSettings(); + const identityProviders = await getIdentityProviders(organization); + + const host = process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : "http://localhost:3000"; + + const branding = await getBrandingSettings(organization); + + return ( + +
+

Welcome back!

+

Enter your login data.

+ + + + {legal && identityProviders && process.env.ZITADEL_API_URL && ( + + )} +
+
+ ); +} diff --git a/apps/login/app/(login)/mfa/create/page.tsx b/apps/login/src/app/(login)/mfa/create/page.tsx similarity index 85% rename from apps/login/app/(login)/mfa/create/page.tsx rename to apps/login/src/app/(login)/mfa/create/page.tsx index c0ce2fa4d..4fd798f7e 100644 --- a/apps/login/app/(login)/mfa/create/page.tsx +++ b/apps/login/src/app/(login)/mfa/create/page.tsx @@ -1,7 +1,7 @@ "use client"; -import { Button, ButtonVariants } from "#/ui/Button"; -import { TextInput } from "#/ui/Input"; -import UserAvatar from "#/ui/UserAvatar"; +import { Button, ButtonVariants } from "@/ui/Button"; +import { TextInput } from "@/ui/Input"; +import UserAvatar from "@/ui/UserAvatar"; import { useRouter } from "next/navigation"; export default function Page() { diff --git a/apps/login/app/(login)/mfa/page.tsx b/apps/login/src/app/(login)/mfa/page.tsx similarity index 100% rename from apps/login/app/(login)/mfa/page.tsx rename to apps/login/src/app/(login)/mfa/page.tsx diff --git a/apps/login/app/(login)/mfa/set/page.tsx b/apps/login/src/app/(login)/mfa/set/page.tsx similarity index 85% rename from apps/login/app/(login)/mfa/set/page.tsx rename to apps/login/src/app/(login)/mfa/set/page.tsx index 57b7e625d..3ffaf5f06 100644 --- a/apps/login/app/(login)/mfa/set/page.tsx +++ b/apps/login/src/app/(login)/mfa/set/page.tsx @@ -1,7 +1,7 @@ "use client"; -import { Button, ButtonVariants } from "#/ui/Button"; -import { TextInput } from "#/ui/Input"; -import UserAvatar from "#/ui/UserAvatar"; +import { Button, ButtonVariants } from "@/ui/Button"; +import { TextInput } from "@/ui/Input"; +import UserAvatar from "@/ui/UserAvatar"; import { useRouter } from "next/navigation"; export default function Page() { diff --git a/apps/login/app/(login)/passkey/add/page.tsx b/apps/login/src/app/(login)/passkey/add/page.tsx similarity index 78% rename from apps/login/app/(login)/passkey/add/page.tsx rename to apps/login/src/app/(login)/passkey/add/page.tsx index 839ac2225..cf4cb4b13 100644 --- a/apps/login/app/(login)/passkey/add/page.tsx +++ b/apps/login/src/app/(login)/passkey/add/page.tsx @@ -1,9 +1,9 @@ -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"; +import { getBrandingSettings, sessionService } 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"; export default async function Page({ searchParams, @@ -16,15 +16,15 @@ export default async function Page({ const sessionFactors = await loadSession(loginName); async function loadSession(loginName?: string) { - const recent = await getMostRecentCookieWithLoginname( + const recent = await getMostRecentCookieWithLoginName( loginName, - organization + organization, ); - return getSession(server, recent.id, recent.token).then((response) => { - if (response?.session) { - return response.session; - } + const response = await sessionService.getSession({ + sessionId: recent.id, + sessionToken: recent.token, }); + return response?.session; } const title = !!promptPasswordless ? "Authenticate with a passkey" @@ -33,7 +33,7 @@ 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); + const branding = await getBrandingSettings(organization); return ( diff --git a/apps/login/app/(login)/passkey/login/page.tsx b/apps/login/src/app/(login)/passkey/login/page.tsx similarity index 69% rename from apps/login/app/(login)/passkey/login/page.tsx rename to apps/login/src/app/(login)/passkey/login/page.tsx index bcc366465..544543631 100644 --- a/apps/login/app/(login)/passkey/login/page.tsx +++ b/apps/login/src/app/(login)/passkey/login/page.tsx @@ -1,12 +1,12 @@ -import { getBrandingSettings, getSession, server } from "#/lib/zitadel"; -import Alert from "#/ui/Alert"; -import DynamicTheme from "#/ui/DynamicTheme"; -import LoginPasskey from "#/ui/LoginPasskey"; -import UserAvatar from "#/ui/UserAvatar"; +import { getBrandingSettings, sessionService } from "@/lib/zitadel"; +import Alert from "@/ui/Alert"; +import DynamicTheme from "@/ui/DynamicTheme"; +import LoginPasskey from "@/ui/LoginPasskey"; +import UserAvatar from "@/ui/UserAvatar"; import { - getMostRecentCookieWithLoginname, + getMostRecentCookieWithLoginName, getSessionCookieById, -} from "#/utils/cookies"; +} from "@/utils/cookies"; const title = "Authenticate with a passkey"; const description = @@ -26,29 +26,29 @@ export default async function Page({ async function loadSessionByLoginname( loginName?: string, - organization?: string + organization?: string, ) { - const recent = await getMostRecentCookieWithLoginname( + const recent = await getMostRecentCookieWithLoginName( loginName, - organization + organization, ); - return getSession(server, recent.id, recent.token).then((response) => { - if (response?.session) { - return response.session; - } + const response = await sessionService.getSession({ + sessionId: recent.id, + sessionToken: recent.token, }); + return response?.session; } async function loadSessionById(sessionId: string, organization?: string) { const recent = await getSessionCookieById(sessionId, organization); - return getSession(server, recent.id, recent.token).then((response) => { - if (response?.session) { - return response.session; - } + const response = await sessionService.getSession({ + sessionId: recent.id, + sessionToken: recent.token, }); + return response?.session; } - const branding = await getBrandingSettings(server, organization); + const branding = await getBrandingSettings(organization); return ( diff --git a/apps/login/src/app/(login)/password/page.tsx b/apps/login/src/app/(login)/password/page.tsx new file mode 100644 index 000000000..e89142e0e --- /dev/null +++ b/apps/login/src/app/(login)/password/page.tsx @@ -0,0 +1,52 @@ +import { getBrandingSettings, sessionService } from "@/lib/zitadel"; +import Alert from "@/ui/Alert"; +import DynamicTheme from "@/ui/DynamicTheme"; +import PasswordForm from "@/ui/PasswordForm"; +import UserAvatar from "@/ui/UserAvatar"; +import { getMostRecentCookieWithLoginName } from "@/utils/cookies"; + +export default async function Page(props: { + searchParams: Record; +}) { + const recent = await getMostRecentCookieWithLoginName( + props.searchParams.loginName, + props.searchParams.organization, + ); + + const response = await sessionService.getSession({ + sessionId: recent.id, + sessionToken: recent.token, + }); + + const branding = await getBrandingSettings(props.searchParams.organization); + + if (!response.session) { + throw new Error("Session not found"); + } + + return ( + +
+

{response.session.factors?.user?.displayName ?? "Password"}

+

Enter your password.

+ + + + +
+
+ ); +} diff --git a/apps/login/app/(login)/register/page.tsx b/apps/login/src/app/(login)/register/page.tsx similarity index 77% rename from apps/login/app/(login)/register/page.tsx rename to apps/login/src/app/(login)/register/page.tsx index 506b9df07..d9f7ef0f6 100644 --- a/apps/login/app/(login)/register/page.tsx +++ b/apps/login/src/app/(login)/register/page.tsx @@ -2,11 +2,10 @@ import { getBrandingSettings, getLegalAndSupportSettings, getPasswordComplexitySettings, - server, -} from "#/lib/zitadel"; -import DynamicTheme from "#/ui/DynamicTheme"; -import RegisterFormWithoutPassword from "#/ui/RegisterFormWithoutPassword"; -import SetPasswordForm from "#/ui/SetPasswordForm"; +} from "@/lib/zitadel"; +import DynamicTheme from "@/ui/DynamicTheme"; +import RegisterFormWithoutPassword from "@/ui/RegisterFormWithoutPassword"; +import SetPasswordForm from "@/ui/SetPasswordForm"; export default async function Page({ searchParams, @@ -18,13 +17,11 @@ export default async function Page({ const setPassword = !!(firstname && lastname && email); - const legal = await getLegalAndSupportSettings(server, organization); - const passwordComplexitySettings = await getPasswordComplexitySettings( - server, - organization - ); + const legal = await getLegalAndSupportSettings(organization); + const passwordComplexitySettings = + await getPasswordComplexitySettings(organization); - const branding = await getBrandingSettings(server, organization); + const branding = await getBrandingSettings(organization); return setPassword ? ( diff --git a/apps/login/app/(login)/signedin/page.tsx b/apps/login/src/app/(login)/signedin/page.tsx similarity index 54% rename from apps/login/app/(login)/signedin/page.tsx rename to apps/login/src/app/(login)/signedin/page.tsx index 4ee30a6c8..a61765d20 100644 --- a/apps/login/app/(login)/signedin/page.tsx +++ b/apps/login/src/app/(login)/signedin/page.tsx @@ -1,37 +1,41 @@ import { - createCallback, getBrandingSettings, - getSession, - server, -} from "#/lib/zitadel"; -import DynamicTheme from "#/ui/DynamicTheme"; -import UserAvatar from "#/ui/UserAvatar"; -import { getMostRecentCookieWithLoginname } from "#/utils/cookies"; + sessionService, + oidcService, +} from "@/lib/zitadel"; +import DynamicTheme from "@/ui/DynamicTheme"; +import UserAvatar from "@/ui/UserAvatar"; +import { getMostRecentCookieWithLoginName } from "@/utils/cookies"; import { redirect } from "next/navigation"; async function loadSession(loginName: string, authRequestId?: string) { - const recent = await getMostRecentCookieWithLoginname(`${loginName}`); + const recent = await getMostRecentCookieWithLoginName(`${loginName}`); if (authRequestId) { - return createCallback(server, { - authRequestId, - session: { sessionId: recent.id, sessionToken: recent.token }, - }).then(({ callbackUrl }) => { - return redirect(callbackUrl); - }); + return oidcService + .createCallback({ + authRequestId, + callbackKind: { + case: "session", + value: { sessionId: recent.id, sessionToken: recent.token }, + }, + }) + .then(({ callbackUrl }) => { + return redirect(callbackUrl); + }); } - return getSession(server, recent.id, recent.token).then((response) => { - if (response?.session) { - return response.session; - } + const response = await sessionService.getSession({ + sessionId: recent.id, + sessionToken: recent.token, }); + return response?.session; } export default async function Page({ searchParams }: { searchParams: any }) { const { loginName, authRequestId, organization } = searchParams; const sessionFactors = await loadSession(loginName, authRequestId); - const branding = await getBrandingSettings(server, organization); + const branding = await getBrandingSettings(organization); return ( diff --git a/apps/login/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx similarity index 84% rename from apps/login/app/(login)/verify/page.tsx rename to apps/login/src/app/(login)/verify/page.tsx index 32be25309..5bc0cb0eb 100644 --- a/apps/login/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -1,6 +1,6 @@ -import { getBrandingSettings, server } from "#/lib/zitadel"; -import DynamicTheme from "#/ui/DynamicTheme"; -import VerifyEmailForm from "#/ui/VerifyEmailForm"; +import { getBrandingSettings } 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 }) { @@ -15,7 +15,7 @@ export default async function Page({ searchParams }: { searchParams: any }) { passwordset, } = searchParams; - const branding = await getBrandingSettings(server, organization); + const branding = await getBrandingSettings(organization); return ( diff --git a/apps/login/app/api/idp/start/route.ts b/apps/login/src/app/api/idp/start/route.ts similarity index 62% rename from apps/login/app/api/idp/start/route.ts rename to apps/login/src/app/api/idp/start/route.ts index 18a7f61e3..e76a870e6 100644 --- a/apps/login/app/api/idp/start/route.ts +++ b/apps/login/src/app/api/idp/start/route.ts @@ -1,4 +1,4 @@ -import { server, startIdentityProviderFlow } from "#/lib/zitadel"; +import { userService } from "@/lib/zitadel"; import { NextRequest, NextResponse } from "next/server"; export async function POST(request: NextRequest) { @@ -6,13 +6,17 @@ export async function POST(request: NextRequest) { if (body) { let { idpId, successUrl, failureUrl } = body; - return startIdentityProviderFlow(server, { - idpId, - urls: { - successUrl, - failureUrl, - }, - }) + return userService + .startIdentityProviderIntent({ + idpId, + content: { + case: "urls", + value: { + successUrl, + failureUrl, + }, + }, + }) .then((resp) => { return NextResponse.json(resp); }) diff --git a/apps/login/app/api/loginname/route.ts b/apps/login/src/app/api/loginname/route.ts similarity index 77% rename from apps/login/app/api/loginname/route.ts rename to apps/login/src/app/api/loginname/route.ts index 8b559e1f6..3b089b051 100644 --- a/apps/login/app/api/loginname/route.ts +++ b/apps/login/src/app/api/loginname/route.ts @@ -1,5 +1,5 @@ -import { listAuthenticationMethodTypes, listUsers } from "#/lib/zitadel"; -import { createSessionForUserIdAndUpdateCookie } from "#/utils/session"; +import { listUsers, userService } from "@/lib/zitadel"; +import { createSessionForUserIdAndUpdateCookie } from "@/utils/session"; import { NextRequest, NextResponse } from "next/server"; export async function POST(request: NextRequest) { @@ -7,21 +7,20 @@ export async function POST(request: NextRequest) { if (body) { const { loginName, authRequestId, organization } = body; return listUsers(loginName, organization).then((users) => { - if ( - users.details && - users.details.totalResult == 1 && - users.result[0].userId - ) { + if (users.details?.totalResult === BigInt(1) && users.result[0].userId) { const userId = users.result[0].userId; return createSessionForUserIdAndUpdateCookie( userId, undefined, undefined, - authRequestId + authRequestId, ) .then((session) => { if (session.factors?.user?.id) { - return listAuthenticationMethodTypes(session.factors?.user?.id) + return userService + .listAuthenticationMethodTypes({ + userId: session.factors?.user?.id, + }) .then((methods) => { return NextResponse.json({ authMethodTypes: methods.authMethodTypes, @@ -43,7 +42,7 @@ export async function POST(request: NextRequest) { } else { return NextResponse.json( { message: "Could not find user" }, - { status: 404 } + { status: 404 }, ); } }); diff --git a/apps/login/app/api/passkeys/route.ts b/apps/login/src/app/api/passkeys/route.ts similarity index 53% rename from apps/login/app/api/passkeys/route.ts rename to apps/login/src/app/api/passkeys/route.ts index 559902621..0c1525c0d 100644 --- a/apps/login/app/api/passkeys/route.ts +++ b/apps/login/src/app/api/passkeys/route.ts @@ -1,10 +1,5 @@ -import { - createPasskeyRegistrationLink, - getSession, - registerPasskey, - server, -} from "#/lib/zitadel"; -import { getSessionCookieById } from "#/utils/cookies"; +import { sessionService, userService } from "@/lib/zitadel"; +import { getSessionCookieById } from "@/utils/cookies"; import { NextRequest, NextResponse } from "next/server"; export async function POST(request: NextRequest) { @@ -14,11 +9,10 @@ export async function POST(request: NextRequest) { const sessionCookie = await getSessionCookieById(sessionId); - const session = await getSession( - server, - sessionCookie.id, - sessionCookie.token - ); + const session = await sessionService.getSession({ + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }); const domain: string = request.nextUrl.hostname; @@ -26,12 +20,26 @@ export async function POST(request: NextRequest) { if (userId) { // TODO: add org context - return createPasskeyRegistrationLink(userId) + return userService + .createPasskeyRegistrationLink({ + userId, + medium: { + case: "returnCode", + value: {}, + }, + }) .then((resp) => { const code = resp.code; - return registerPasskey(userId, code, domain).then((resp) => { - return NextResponse.json(resp); - }); + return userService + .registerPasskey({ + userId, + code, + domain, + // authenticator: + }) + .then((resp) => { + return NextResponse.json(resp); + }); }) .catch((error) => { console.error("error on creating passkey registration link"); @@ -40,7 +48,7 @@ export async function POST(request: NextRequest) { } else { return NextResponse.json( { details: "could not get session" }, - { status: 500 } + { status: 500 }, ); } } else { diff --git a/apps/login/app/api/passkeys/verify/route.ts b/apps/login/src/app/api/passkeys/verify/route.ts similarity index 69% rename from apps/login/app/api/passkeys/verify/route.ts rename to apps/login/src/app/api/passkeys/verify/route.ts index 1346ecc63..d913d46e8 100644 --- a/apps/login/app/api/passkeys/verify/route.ts +++ b/apps/login/src/app/api/passkeys/verify/route.ts @@ -1,5 +1,5 @@ -import { getSession, server, verifyPasskeyRegistration } from "#/lib/zitadel"; -import { getSessionCookieById } from "#/utils/cookies"; +import { sessionService, userService } from "@/lib/zitadel"; +import { getSessionCookieById } from "@/utils/cookies"; import { NextRequest, NextResponse, userAgent } from "next/server"; export async function POST(request: NextRequest) { @@ -15,22 +15,21 @@ export async function POST(request: NextRequest) { } const sessionCookie = await getSessionCookieById(sessionId); - const session = await getSession( - server, - sessionCookie.id, - sessionCookie.token - ); + const session = await sessionService.getSession({ + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }); const userId = session?.session?.factors?.user?.id; if (userId) { - return verifyPasskeyRegistration( - server, - passkeyId, - passkeyName, - publicKeyCredential, - userId - ) + return userService + .verifyPasskeyRegistration({ + passkeyId, + passkeyName, + publicKeyCredential, + userId, + }) .then((resp) => { return NextResponse.json(resp); }) @@ -40,7 +39,7 @@ export async function POST(request: NextRequest) { } else { return NextResponse.json( { details: "could not get session" }, - { status: 500 } + { status: 500 }, ); } } else { diff --git a/apps/login/app/api/registeruser/route.ts b/apps/login/src/app/api/registeruser/route.ts similarity index 80% rename from apps/login/app/api/registeruser/route.ts rename to apps/login/src/app/api/registeruser/route.ts index 575396143..efd94759f 100644 --- a/apps/login/app/api/registeruser/route.ts +++ b/apps/login/src/app/api/registeruser/route.ts @@ -1,8 +1,5 @@ -import { addHumanUser, server } from "#/lib/zitadel"; -import { - createSessionAndUpdateCookie, - createSessionForUserIdAndUpdateCookie, -} from "#/utils/session"; +import { addHumanUser } from "@/lib/zitadel"; +import { createSessionForUserIdAndUpdateCookie } from "@/utils/session"; import { NextRequest, NextResponse } from "next/server"; export async function POST(request: NextRequest) { @@ -17,7 +14,7 @@ export async function POST(request: NextRequest) { authRequestId, } = body; - return addHumanUser(server, { + return addHumanUser({ email: email, firstName, lastName, @@ -29,7 +26,7 @@ export async function POST(request: NextRequest) { user.userId, password, undefined, - authRequestId + authRequestId, ).then((session) => { return NextResponse.json({ userId: user.userId, diff --git a/apps/login/app/api/resendverifyemail/route.ts b/apps/login/src/app/api/resendverifyemail/route.ts similarity index 83% rename from apps/login/app/api/resendverifyemail/route.ts rename to apps/login/src/app/api/resendverifyemail/route.ts index 15376ee76..ef851706b 100644 --- a/apps/login/app/api/resendverifyemail/route.ts +++ b/apps/login/src/app/api/resendverifyemail/route.ts @@ -1,4 +1,4 @@ -import { setEmail, server } from "#/lib/zitadel"; +import { userService } from "@/lib/zitadel"; import { NextRequest, NextResponse } from "next/server"; export async function POST(request: NextRequest) { @@ -7,7 +7,8 @@ export async function POST(request: NextRequest) { const { userId, code } = body; // replace with resend Mail method once its implemented - return setEmail(server, userId) + return userService + .setEmail({ userId }) .then((resp) => { return NextResponse.json(resp); }) diff --git a/apps/login/app/api/session/route.ts b/apps/login/src/app/api/session/route.ts similarity index 55% rename from apps/login/app/api/session/route.ts rename to apps/login/src/app/api/session/route.ts index 63f1f08e2..967c95cf8 100644 --- a/apps/login/app/api/session/route.ts +++ b/apps/login/src/app/api/session/route.ts @@ -1,17 +1,17 @@ -import { server, deleteSession } from "#/lib/zitadel"; +import { sessionService } from "@/lib/zitadel"; import { SessionCookie, getMostRecentSessionCookie, getSessionCookieById, getSessionCookieByLoginName, removeSessionFromCookie, -} from "#/utils/cookies"; +} from "@/utils/cookies"; import { createSessionAndUpdateCookie, createSessionForIdpAndUpdateCookie, setSessionAndUpdateCookie, -} from "#/utils/session"; -import { RequestChallenges } from "@zitadel/server"; +} from "@/utils/session"; +import { RequestChallenges } from "@zitadel/client/v2beta"; import { NextRequest, NextResponse } from "next/server"; export async function POST(request: NextRequest) { @@ -31,7 +31,7 @@ export async function POST(request: NextRequest) { userId, idpIntent, organization, - authRequestId + authRequestId, ).then((session) => { return NextResponse.json(session); }); @@ -41,7 +41,7 @@ export async function POST(request: NextRequest) { password, undefined, organization, - authRequestId + authRequestId, ).then((session) => { return NextResponse.json(session); }); @@ -49,7 +49,7 @@ export async function POST(request: NextRequest) { } else { return NextResponse.json( { details: "Session could not be created" }, - { status: 500 } + { status: 500 }, ); } } @@ -62,60 +62,55 @@ export async function POST(request: NextRequest) { export async function PUT(request: NextRequest) { const body = await request.json(); - if (body) { - const { - loginName, - sessionId, - organization, - password, - webAuthN, - authRequestId, - } = body; - const challenges: RequestChallenges = body.challenges; - - const recentPromise: Promise = sessionId - ? getSessionCookieById(sessionId).catch((error) => { - return Promise.reject(error); - }) - : loginName - ? getSessionCookieByLoginName(loginName, organization).catch((error) => { - return Promise.reject(error); - }) - : getMostRecentSessionCookie().catch((error) => { - return Promise.reject(error); - }); + if (!body) { + return NextResponse.json( + { details: "Request body is missing" }, + { status: 400 }, + ); + } - const domain: string = request.nextUrl.hostname; + const { + loginName, + sessionId, + organization, + password, + webAuthN, + authRequestId, + } = body; + const challenges: RequestChallenges = body.challenges; - if (challenges && challenges.webAuthN && !challenges.webAuthN.domain) { - challenges.webAuthN.domain = domain; - } + const sessionCookie = sessionId + ? await getSessionCookieById(sessionId) + : loginName + ? await getSessionCookieByLoginName(loginName, organization) + : await getMostRecentSessionCookie(); - return recentPromise - .then((recent) => { - return setSessionAndUpdateCookie( - recent, - password, - webAuthN, - challenges, - authRequestId - ).then((session) => { - return NextResponse.json({ - sessionId: session.id, - factors: session.factors, - challenges: session.challenges, - }); - }); - }) - .catch((error) => { - return NextResponse.json({ details: error }, { status: 500 }); - }); - } else { + if (!sessionCookie) { return NextResponse.json( - { details: "Request body is missing" }, - { status: 400 } + { details: "No session cookie found" }, + { status: 404 }, ); } + + const domain: string = request.nextUrl.hostname; + + if (challenges && challenges.webAuthN && !challenges.webAuthN.domain) { + challenges.webAuthN.domain = domain; + } + + const session = await setSessionAndUpdateCookie( + sessionCookie, + password, + webAuthN, + challenges, + authRequestId, + ); + + return NextResponse.json({ + sessionId: session.id, + factors: session.factors, + challenges: session.challenges, + }); } /** @@ -128,7 +123,8 @@ export async function DELETE(request: NextRequest) { if (id) { const session = await getSessionCookieById(id); - return deleteSession(server, session.id, session.token) + return sessionService + .deleteSession({ sessionId: session.id, sessionToken: session.token }) .then(() => { return removeSessionFromCookie(session) .then(() => { @@ -137,14 +133,14 @@ export async function DELETE(request: NextRequest) { .catch((error) => { return NextResponse.json( { details: "could not set cookie" }, - { status: 500 } + { status: 500 }, ); }); }) .catch((error) => { return NextResponse.json( { details: "could not delete session" }, - { status: 500 } + { status: 500 }, ); }); } else { diff --git a/apps/login/app/api/verifyemail/route.ts b/apps/login/src/app/api/verifyemail/route.ts similarity index 73% rename from apps/login/app/api/verifyemail/route.ts rename to apps/login/src/app/api/verifyemail/route.ts index 1b2ddd29d..0ac424a11 100644 --- a/apps/login/app/api/verifyemail/route.ts +++ b/apps/login/src/app/api/verifyemail/route.ts @@ -1,4 +1,4 @@ -import { server, verifyEmail } from "#/lib/zitadel"; +import { userService } from "@/lib/zitadel"; import { NextRequest, NextResponse } from "next/server"; export async function POST(request: NextRequest) { @@ -6,7 +6,11 @@ export async function POST(request: NextRequest) { if (body) { const { userId, code } = body; - return verifyEmail(server, userId, code) + return userService + .verifyEmail({ + userId, + verificationCode: code, + }) .then((resp) => { return NextResponse.json(resp); }) diff --git a/apps/login/app/layout.tsx b/apps/login/src/app/layout.tsx similarity index 81% rename from apps/login/app/layout.tsx rename to apps/login/src/app/layout.tsx index 2c4768fce..0561ba51d 100644 --- a/apps/login/app/layout.tsx +++ b/apps/login/src/app/layout.tsx @@ -1,15 +1,10 @@ -import "#/styles/globals.scss"; -import { AddressBar } from "#/ui/AddressBar"; -import { GlobalNav } from "#/ui/GlobalNav"; +import "@/styles/globals.scss"; +import { AddressBar } from "@/ui/AddressBar"; +import { GlobalNav } from "@/ui/GlobalNav"; import { Lato } from "next/font/google"; -import { LayoutProviders } from "#/ui/LayoutProviders"; import { Analytics } from "@vercel/analytics/react"; -import ThemeWrapper from "#/ui/ThemeWrapper"; -import { getBrandingSettings } from "#/lib/zitadel"; -import { server } from "../lib/zitadel"; -import { BrandingSettings } from "@zitadel/server"; -import ThemeProvider from "#/ui/ThemeProvider"; -import Theme from "#/ui/Theme"; +import ThemeProvider from "@/ui/ThemeProvider"; +import Theme from "@/ui/Theme"; const lato = Lato({ weight: ["400", "700", "900"], diff --git a/apps/login/app/login/route.ts b/apps/login/src/app/login/route.ts similarity index 74% rename from apps/login/app/login/route.ts rename to apps/login/src/app/login/route.ts index 10516eec3..45854b8cb 100644 --- a/apps/login/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -1,21 +1,21 @@ -import { - createCallback, - getAuthRequest, - getOrgByDomain, - listSessions, - server, -} from "#/lib/zitadel"; -import { SessionCookie, getAllSessions } from "#/utils/cookies"; -import { Session, AuthRequest, Prompt } from "@zitadel/server"; +import { sessionService, managementService, oidcService } from "@/lib/zitadel"; +import { SessionCookie, getAllSessions } from "@/utils/cookies"; +import { Session, AuthRequest, Prompt } from "@zitadel/client/v2beta"; import { NextRequest, NextResponse } from "next/server"; async function loadSessions(ids: string[]): Promise { - const response = await listSessions( - server, - ids.filter((id: string | undefined) => !!id) - ); - - return response?.sessions ?? []; + const response = await sessionService.listSessions({ + queries: [ + { + query: { + case: "idsQuery", + value: { ids: ids.filter((id: string | undefined) => !!id) }, + }, + }, + ], + }); + + return response.sessions; } const ORG_SCOPE_REGEX = /urn:zitadel:iam:org:id:([0-9]+)/; @@ -23,7 +23,7 @@ const ORG_DOMAIN_SCOPE_REGEX = /urn:zitadel:iam:org:domain:primary:(.+)/; // TOD function findSession( sessions: Session[], - authRequest: AuthRequest + authRequest: AuthRequest, ): Session | undefined { if (authRequest.hintUserId) { console.log(`find session for hintUserId: ${authRequest.hintUserId}`); @@ -32,7 +32,7 @@ function findSession( if (authRequest.loginHint) { console.log(`find session for loginHint: ${authRequest.loginHint}`); return sessions.find( - (s) => s.factors?.user?.loginName === authRequest.loginHint + (s) => s.factors?.user?.loginName === authRequest.loginHint, ); } if (sessions.length) { @@ -55,7 +55,7 @@ export async function GET(request: NextRequest) { if (authRequestId && sessionId) { console.log( - `Login with session: ${sessionId} and authRequest: ${authRequestId}` + `Login with session: ${sessionId} and authRequest: ${authRequestId}`, ); let selectedSession = sessions.find((s) => s.id === sessionId); @@ -63,20 +63,21 @@ export async function GET(request: NextRequest) { if (selectedSession && selectedSession.id) { console.log(`Found session ${selectedSession.id}`); const cookie = sessionCookies.find( - (cookie) => cookie.id === selectedSession?.id + (cookie) => cookie.id === selectedSession?.id, ); if (cookie && cookie.id && cookie.token) { console.log(`Found sessioncookie ${cookie.id}`); - const session = { - sessionId: cookie?.id, - sessionToken: cookie?.token, - }; - - const { callbackUrl } = await createCallback(server, { + const { callbackUrl } = await oidcService.createCallback({ authRequestId, - session, + callbackKind: { + case: "session", + value: { + sessionId: cookie?.id, + sessionToken: cookie?.token, + }, + }, }); return NextResponse.redirect(callbackUrl); } @@ -85,12 +86,12 @@ export async function GET(request: NextRequest) { if (authRequestId) { console.log(`Login with authRequest: ${authRequestId}`); - const { authRequest } = await getAuthRequest(server, { authRequestId }); + const { authRequest } = await oidcService.getAuthRequest({ authRequestId }); let organization; if (authRequest?.scope) { const orgScope = authRequest.scope.find((s: string) => - ORG_SCOPE_REGEX.test(s) + ORG_SCOPE_REGEX.test(s), ); if (orgScope) { @@ -98,21 +99,23 @@ export async function GET(request: NextRequest) { organization = matched?.[1] ?? ""; } else { const orgDomainScope = authRequest.scope.find((s: string) => - ORG_DOMAIN_SCOPE_REGEX.test(s) + ORG_DOMAIN_SCOPE_REGEX.test(s), ); if (orgDomainScope) { const matched = ORG_DOMAIN_SCOPE_REGEX.exec(orgDomainScope); const orgDomain = matched?.[1] ?? ""; if (orgDomain) { - const org = await getOrgByDomain(orgDomain); + const org = await managementService.getOrgByDomainGlobal({ + domain: orgDomain, + }); organization = org?.org?.id ?? ""; } } } } - if (authRequest && authRequest.prompt.includes(Prompt.PROMPT_CREATE)) { + if (authRequest && authRequest.prompt.includes(Prompt.CREATE)) { const registerUrl = new URL("/register", request.url); if (authRequest?.id) { registerUrl.searchParams.set("authRequestId", authRequest?.id); @@ -127,7 +130,7 @@ export async function GET(request: NextRequest) { // use existing session and hydrate it for oidc if (authRequest && sessions.length) { // if some accounts are available for selection and select_account is set - if (authRequest.prompt.includes(Prompt.PROMPT_SELECT_ACCOUNT)) { + if (authRequest.prompt.includes(Prompt.SELECT_ACCOUNT)) { const accountsUrl = new URL("/accounts", request.url); if (authRequest?.id) { accountsUrl.searchParams.set("authRequestId", authRequest?.id); @@ -137,7 +140,7 @@ export async function GET(request: NextRequest) { } return NextResponse.redirect(accountsUrl); - } else if (authRequest.prompt.includes(Prompt.PROMPT_LOGIN)) { + } else if (authRequest.prompt.includes(Prompt.LOGIN)) { // if prompt is login const loginNameUrl = new URL("/loginname", request.url); if (authRequest?.id) { @@ -150,36 +153,38 @@ export async function GET(request: NextRequest) { loginNameUrl.searchParams.set("organization", organization); } return NextResponse.redirect(loginNameUrl); - } else if (authRequest.prompt.includes(Prompt.PROMPT_NONE)) { + } else if (authRequest.prompt.includes(Prompt.NONE)) { // NONE prompt - silent authentication let selectedSession = findSession(sessions, authRequest); if (selectedSession && selectedSession.id) { const cookie = sessionCookies.find( - (cookie) => cookie.id === selectedSession?.id + (cookie) => cookie.id === selectedSession?.id, ); if (cookie && cookie.id && cookie.token) { - const session = { - sessionId: cookie?.id, - sessionToken: cookie?.token, - }; - const { callbackUrl } = await createCallback(server, { + const { callbackUrl } = await oidcService.createCallback({ authRequestId, - session, + callbackKind: { + case: "session", + value: { + sessionId: cookie?.id, + sessionToken: cookie?.token, + }, + }, }); return NextResponse.redirect(callbackUrl); } else { return NextResponse.json( { error: "No active session found" }, - { status: 400 } // TODO: check for correct status code + { status: 400 }, // TODO: check for correct status code ); } } else { return NextResponse.json( { error: "No active session found" }, - { status: 400 } // TODO: check for correct status code + { status: 400 }, // TODO: check for correct status code ); } } else { @@ -188,17 +193,19 @@ export async function GET(request: NextRequest) { if (selectedSession && selectedSession.id) { const cookie = sessionCookies.find( - (cookie) => cookie.id === selectedSession?.id + (cookie) => cookie.id === selectedSession?.id, ); if (cookie && cookie.id && cookie.token) { - const session = { - sessionId: cookie?.id, - sessionToken: cookie?.token, - }; - const { callbackUrl } = await createCallback(server, { + const { callbackUrl } = await oidcService.createCallback({ authRequestId, - session, + callbackKind: { + case: "session", + value: { + sessionId: cookie?.id, + sessionToken: cookie?.token, + }, + }, }); return NextResponse.redirect(callbackUrl); } else { @@ -236,7 +243,7 @@ export async function GET(request: NextRequest) { } else { return NextResponse.json( { error: "No authRequestId provided" }, - { status: 500 } + { status: 500 }, ); } } diff --git a/apps/login/app/page.tsx b/apps/login/src/app/page.tsx similarity index 98% rename from apps/login/app/page.tsx rename to apps/login/src/app/page.tsx index 1d4da4610..7bddc8caa 100644 --- a/apps/login/app/page.tsx +++ b/apps/login/src/app/page.tsx @@ -1,4 +1,4 @@ -import { demos } from "#/lib/demos"; +import { demos } from "@/lib/demos"; import Link from "next/link"; export default function Page() { diff --git a/apps/login/lib/demos.ts b/apps/login/src/lib/demos.ts similarity index 100% rename from apps/login/lib/demos.ts rename to apps/login/src/lib/demos.ts diff --git a/apps/login/lib/hooks.ts b/apps/login/src/lib/hooks.ts similarity index 100% rename from apps/login/lib/hooks.ts rename to apps/login/src/lib/hooks.ts diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts new file mode 100644 index 000000000..b87ad0502 --- /dev/null +++ b/apps/login/src/lib/zitadel.ts @@ -0,0 +1,225 @@ +import { + createSessionServiceClient, + createUserServiceClient, + createOIDCServiceClient, + createSettingsServiceClient, + makeReqCtx, + ListUsersRequest, + UserSearchQuery, + IdentityProvider, + CreateSessionResponse, + TextQueryMethod, + GetActiveIdentityProvidersResponse, + GetBrandingSettingsResponse, + GetPasswordComplexitySettingsResponse, + GetLegalAndSupportSettingsResponse, + AddHumanUserResponse, + BrandingSettings, + LegalAndSupportSettings, + PasswordComplexitySettings, + ListUsersResponse, + LoginSettings, + GetLoginSettingsResponse, + RequestChallenges, + AddHumanUserRequest, +} from "@zitadel/client/v2beta"; +import { createManagementServiceClient } from "@zitadel/client/v1"; + +import { PartialMessage } from "@zitadel/client"; +import { createServerTransport } from "@zitadel/server"; + +const transport = createServerTransport( + process.env.ZITADEL_SERVICE_USER_TOKEN!, + { + baseUrl: process.env.ZITADEL_API_URL!, + httpVersion: "2", + }, +); + +export const sessionService = createSessionServiceClient(transport); +export const managementService = createManagementServiceClient(transport); +export const userService = createUserServiceClient(transport); +export const oidcService = createOIDCServiceClient(transport); +export const settingsService = createSettingsServiceClient(transport); + +export async function getBrandingSettings( + orgId?: string, +): Promise { + return settingsService + .getBrandingSettings({ ctx: makeReqCtx(orgId) }) + .then((resp: GetBrandingSettingsResponse) => resp.settings); +} + +export async function getLoginSettings( + orgId?: string, +): Promise { + return settingsService + .getLoginSettings({ ctx: makeReqCtx(orgId) }) + .then((resp: GetLoginSettingsResponse) => resp.settings); +} + +export function getIdentityProviders( + orgId?: string, +): Promise { + return settingsService + .getActiveIdentityProviders({ ctx: makeReqCtx(orgId) }) + .then((resp: GetActiveIdentityProvidersResponse) => { + return resp.identityProviders; + }); +} + +export async function getLegalAndSupportSettings( + orgId?: string, +): Promise { + return settingsService + .getLegalAndSupportSettings({ ctx: makeReqCtx(orgId) }, {}) + .then((resp: GetLegalAndSupportSettingsResponse) => { + return resp.settings; + }); +} + +export async function getPasswordComplexitySettings( + orgId?: string, +): Promise { + return settingsService + .getPasswordComplexitySettings({ ctx: makeReqCtx(orgId) }) + .then((resp: GetPasswordComplexitySettingsResponse) => resp.settings); +} + +export async function createSessionForLoginname( + loginName: string, + password: string | undefined, + challenges: RequestChallenges | undefined, +): Promise { + return password + ? sessionService.createSession( + { + checks: { + user: { + search: { + case: "loginName", + value: loginName, + }, + }, + password: { password }, + }, + challenges, + lifetime: { + seconds: BigInt(300), + nanos: 0, + }, + }, + {}, + ) + : sessionService.createSession( + { + checks: { + user: { + search: { + case: "loginName", + value: loginName, + }, + }, + }, + challenges, + lifetime: { + seconds: BigInt(300), + nanos: 0, + }, + }, + {}, + ); +} + +export async function createSessionForUserIdAndIdpIntent( + userId: string, + idpIntent: { + idpIntentId?: string | undefined; + idpIntentToken?: string | undefined; + }, +): Promise { + return sessionService.createSession( + { + checks: { + user: { + search: { + case: "userId", + value: userId, + }, + }, + idpIntent, + }, + // lifetime: { + // seconds: 300, + // nanos: 0, + // }, + }, + {}, + ); +} + +export type AddHumanUserData = { + firstName: string; + lastName: string; + email: string; + password: string | undefined; + organization: string | undefined; +}; + +export async function addHumanUser({ + email, + firstName, + lastName, + password, + organization, +}: AddHumanUserData): Promise { + const payload: PartialMessage = { + email: { email }, + username: email, + profile: { givenName: firstName, familyName: lastName }, + }; + + if (organization) { + payload.organization = { + org: { case: "orgId", value: organization }, + }; + } + + if (password) { + payload.passwordType = { + case: "password", + value: { + password: password, + }, + }; + } + + return userService.addHumanUser(payload); +} + +export async function listUsers( + userName: string, + organizationId?: string, +): Promise { + const queries = [ + new UserSearchQuery({ + query: { + case: "userNameQuery", + value: { userName, method: TextQueryMethod.EQUALS }, + }, + }), + ]; + + if (organizationId) { + queries.push( + new UserSearchQuery({ + query: { + case: "organizationIdQuery", + value: { organizationId }, + }, + }), + ); + } + + return userService.listUsers(new ListUsersRequest({ queries })); +} diff --git a/apps/login/styles/globals.scss b/apps/login/src/styles/globals.scss similarity index 100% rename from apps/login/styles/globals.scss rename to apps/login/src/styles/globals.scss diff --git a/apps/login/styles/vars.scss b/apps/login/src/styles/vars.scss similarity index 100% rename from apps/login/styles/vars.scss rename to apps/login/src/styles/vars.scss diff --git a/apps/login/ui/AddressBar.tsx b/apps/login/src/ui/AddressBar.tsx similarity index 100% rename from apps/login/ui/AddressBar.tsx rename to apps/login/src/ui/AddressBar.tsx diff --git a/apps/login/ui/Alert.tsx b/apps/login/src/ui/Alert.tsx similarity index 99% rename from apps/login/ui/Alert.tsx rename to apps/login/src/ui/Alert.tsx index c5bf05ea8..41cd3f06a 100644 --- a/apps/login/ui/Alert.tsx +++ b/apps/login/src/ui/Alert.tsx @@ -29,7 +29,7 @@ export default function Alert({ children, type = AlertType.ALERT }: Props) { { [yellow]: type === AlertType.ALERT, [neutral]: type === AlertType.INFO, - } + }, )} > {type === AlertType.ALERT && ( diff --git a/apps/login/ui/AuthenticationMethodRadio.tsx b/apps/login/src/ui/AuthenticationMethodRadio.tsx similarity index 100% rename from apps/login/ui/AuthenticationMethodRadio.tsx rename to apps/login/src/ui/AuthenticationMethodRadio.tsx diff --git a/apps/login/ui/Avatar.tsx b/apps/login/src/ui/Avatar.tsx similarity index 92% rename from apps/login/ui/Avatar.tsx rename to apps/login/src/ui/Avatar.tsx index 096f97de3..bfa5d229a 100644 --- a/apps/login/ui/Avatar.tsx +++ b/apps/login/src/ui/Avatar.tsx @@ -1,6 +1,6 @@ "use client"; -import { ColorShade, getColorHash } from "#/utils/colors"; +import { ColorShade, getColorHash } from "@/utils/colors"; import { useTheme } from "next-themes"; interface AvatarProps { @@ -69,10 +69,10 @@ export function Avatar({ size === "large" ? "h-20 w-20 font-normal" : size === "base" - ? "w-[38px] h-[38px] font-bold" - : size === "small" - ? "w-[32px] h-[32px] font-bold text-[13px]" - : "" + ? "w-[38px] h-[38px] font-bold" + : size === "small" + ? "w-[32px] h-[32px] font-bold text-[13px]" + : "" }`} style={resolvedTheme === "light" ? avatarStyleLight : avatarStyleDark} > diff --git a/apps/login/ui/Boundary.tsx b/apps/login/src/ui/Boundary.tsx similarity index 99% rename from apps/login/ui/Boundary.tsx rename to apps/login/src/ui/Boundary.tsx index ebfa0c6b6..c7487df1a 100644 --- a/apps/login/ui/Boundary.tsx +++ b/apps/login/src/ui/Boundary.tsx @@ -61,7 +61,7 @@ export const Boundary = ({ { "left-3 lg:left-5": size === "small", "left-4 lg:left-9": size === "default", - } + }, )} > {labels.map((label) => { diff --git a/apps/login/ui/Button.tsx b/apps/login/src/ui/Button.tsx similarity index 98% rename from apps/login/ui/Button.tsx rename to apps/login/src/ui/Button.tsx index f76d11ccb..e16a6a698 100644 --- a/apps/login/ui/Button.tsx +++ b/apps/login/src/ui/Button.tsx @@ -35,7 +35,7 @@ export type ButtonProps = DetailedHTMLProps< export const getButtonClasses = ( size: ButtonSizes, variant: ButtonVariants, - color: ButtonColors + color: ButtonColors, ) => clsx({ "box-border font-normal leading-36px text-14px inline-flex items-center rounded-md focus:outline-none transition-colors transition-shadow duration-300": @@ -65,7 +65,7 @@ export const Button = forwardRef( color = ButtonColors.Primary, ...props }, - ref + ref, ) => ( - ) + ), ); diff --git a/apps/login/ui/Checkbox.tsx b/apps/login/src/ui/Checkbox.tsx similarity index 96% rename from apps/login/ui/Checkbox.tsx rename to apps/login/src/ui/Checkbox.tsx index f5e8beb95..c879e1aa9 100644 --- a/apps/login/ui/Checkbox.tsx +++ b/apps/login/src/ui/Checkbox.tsx @@ -17,7 +17,7 @@ export type CheckboxProps = DetailedHTMLProps< }; export const Checkbox = forwardRef( - ( + function Checkbox( { className = "", checked = false, @@ -26,8 +26,8 @@ export const Checkbox = forwardRef( children, ...props }, - ref - ) => { + ref, + ) { const [enabled, setEnabled] = useState(checked); useEffect(() => { @@ -53,7 +53,7 @@ export const Checkbox = forwardRef( "focus:border-gray-500 focus:dark:border-white focus:ring-opacity-40 focus:dark:ring-opacity-40 focus:ring-offset-0 focus:ring-2 dark:focus:ring-offset-0 dark:focus:ring-2 focus:ring-gray-500 focus:dark:ring-white", "h-4 w-4 rounded-sm ring-0 outline-0 checked:ring-0 checked:dark:ring-0 active:border-none active:ring-0", "disabled:bg-gray-500 disabled:text-gray-500 disabled:border-gray-200 disabled:cursor-not-allowed", - className + className, )} {...props} /> @@ -61,5 +61,5 @@ export const Checkbox = forwardRef( {children}
); - } + }, ); diff --git a/apps/login/ui/DefaultTags.tsx b/apps/login/src/ui/DefaultTags.tsx similarity index 100% rename from apps/login/ui/DefaultTags.tsx rename to apps/login/src/ui/DefaultTags.tsx diff --git a/apps/login/ui/DynamicTheme.tsx b/apps/login/src/ui/DynamicTheme.tsx similarity index 94% rename from apps/login/ui/DynamicTheme.tsx rename to apps/login/src/ui/DynamicTheme.tsx index 468e323ea..0f58608c5 100644 --- a/apps/login/ui/DynamicTheme.tsx +++ b/apps/login/src/ui/DynamicTheme.tsx @@ -1,8 +1,8 @@ "use client"; -import { BrandingSettings } from "@zitadel/server"; +import { BrandingSettings } from "@zitadel/client/v2beta"; import React from "react"; -import { Logo } from "#/ui/Logo"; +import { Logo } from "@/ui/Logo"; import ThemeWrapper from "./ThemeWrapper"; import { LayoutProviders } from "./LayoutProviders"; diff --git a/apps/login/ui/ExternalLink.tsx b/apps/login/src/ui/ExternalLink.tsx similarity index 100% rename from apps/login/ui/ExternalLink.tsx rename to apps/login/src/ui/ExternalLink.tsx diff --git a/apps/login/ui/GlobalNav.tsx b/apps/login/src/ui/GlobalNav.tsx similarity index 96% rename from apps/login/ui/GlobalNav.tsx rename to apps/login/src/ui/GlobalNav.tsx index d24864ff0..6a35d0fd2 100644 --- a/apps/login/ui/GlobalNav.tsx +++ b/apps/login/src/ui/GlobalNav.tsx @@ -1,7 +1,7 @@ "use client"; -import { demos, type Item } from "#/lib/demos"; -import { ZitadelLogo } from "#/ui/ZitadelLogo"; +import { demos, type Item } from "@/lib/demos"; +import { ZitadelLogo } from "@/ui/ZitadelLogo"; import Link from "next/link"; import { useSelectedLayoutSegment, usePathname } from "next/navigation"; import clsx from "clsx"; @@ -56,7 +56,7 @@ export function GlobalNav() { "fixed inset-x-0 bottom-0 top-14 mt-px bg-white/80 dark:bg-black/80 backdrop-blur-lg": isOpen, hidden: !isOpen, - } + }, )} >