This commit is contained in:
Florian Forster
2025-08-04 22:36:41 -07:00
parent 0f9f8697e2
commit 7ecfd8759b
39 changed files with 493 additions and 1486 deletions

View File

@@ -15,6 +15,11 @@
},
"nx": {
"targets": {
"release": {
"docker": {
"repositoryName": "zitadel/console"
}
},
"generate": {
"outputs": [
"{projectRoot}/src/app/proto/generated/**"
@@ -27,7 +32,7 @@
"dependsOn": [
"generate",
"@zitadel/client:build"
]
]
}
}
},

View File

@@ -1,36 +1,24 @@
FROM node:20-alpine AS base
# syntax=docker.io/docker/dockerfile:1
FROM node:22-alpine
FROM base AS build
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack prepare pnpm@9.1.2 --activate && \
apk update && apk add --no-cache && \
rm -rf /var/cache/apk/*
WORKDIR /app
COPY pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm fetch --frozen-lockfile
COPY package.json ./
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile --prod
COPY . .
RUN pnpm build:login:standalone
FROM scratch AS build-out
COPY --from=build /app/.next/standalone /
COPY --from=build /app/.next/static /.next/static
COPY --from=build /app/public /public
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY public ./public
COPY .next/standalone ./
COPY .next/static ./.next/static
RUN ls .next/static
FROM base AS login-standalone
WORKDIR /runtime
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# If /.env-file/.env is mounted into the container, its variables are made available to the server before it starts up.
RUN mkdir -p /.env-file && touch /.env-file/.env && chown -R nextjs:nodejs /.env-file
COPY ./scripts/ ./
COPY --chown=nextjs:nodejs --from=build-out / ./
USER nextjs
ENV HOSTNAME="0.0.0.0"
EXPOSE 3000
ENV PORT=3000
# TODO: Check healthy, not ready
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD ["/bin/sh", "-c", "node ./healthcheck.js http://localhost:${PORT}/ui/v2/login/healthy"]
ENTRYPOINT ["./entrypoint.sh"]
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

View File

@@ -1,3 +0,0 @@
target "release" {
platforms = ["linux/amd64", "linux/arm64"]
}

View File

@@ -1,25 +0,0 @@
variable "LOGIN_TAG" {
default = "zitadel-login:local"
}
group "default" {
targets = ["login-standalone"]
}
# The release target is overwritten in docker-bake-release.hcl
# It makes sure the image is built for multiple platforms.
# By default the platforms property is empty, so images are only built for the current bake runtime platform.
target "release" {}
target "docker-metadata-action" {
# In the pipeline, this target is overwritten by the docker metadata action.
tags = ["${LOGIN_TAG}"]
}
# We run integration and acceptance tests against the next standalone server for docker.
target "login-standalone" {
inherits = [
"docker-metadata-action",
"release",
]
}

View File

@@ -1,10 +0,0 @@
module.exports = {
root: true,
// Use basic ESLint config since the login app has its own detailed config
extends: ["eslint:recommended"],
settings: {
next: {
rootDir: ["apps/*/"],
},
},
};

View File

@@ -1,2 +0,0 @@
screenshots
videos

View File

@@ -1 +0,0 @@
side-effects-cache=false

View File

@@ -1,15 +0,0 @@
FROM bufbuild/buf:1.54.0 AS proto-files
RUN buf export https://github.com/envoyproxy/protoc-gen-validate.git --path validate --output /proto-files && \
buf export https://github.com/grpc-ecosystem/grpc-gateway.git --path protoc-gen-openapiv2 --output /proto-files && \
buf export https://github.com/googleapis/googleapis.git --path protos/zitadelgoogle/api/annotations.proto --path google/api/http.proto --path google/api/field_behavior.proto --output /proto-files && \
buf export https://github.com/zitadel/zitadel.git --path ./proto/zitadel --output /proto-files
FROM golang:1.20.5-alpine3.18 AS mock-zitadel
RUN go install github.com/eliobischof/grpc-mock/cmd/grpc-mock@01b09f60db1b501178af59bed03b2c22661df48c
COPY mocked-services.cfg .
COPY initial-stubs initial-stubs
COPY --from=proto-files /proto-files/ ./
ENTRYPOINT [ "sh", "-c", "grpc-mock -v 1 -proto $(tr '\n' ',' < ./mocked-services.cfg) -stub-dir ./initial-stubs -mock-addr :22222" ]

View File

@@ -1,66 +0,0 @@
[
{
"service": "zitadel.settings.v2.SettingsService",
"method": "GetBrandingSettings",
"out": {
"data": {}
}
},
{
"service": "zitadel.settings.v2.SettingsService",
"method": "GetSecuritySettings",
"out": {
"data": {}
}
},
{
"service": "zitadel.settings.v2.SettingsService",
"method": "GetLegalAndSupportSettings",
"out": {
"data": {
"settings": {
"tosLink": "http://whatever.com/help",
"privacyPolicyLink": "http://whatever.com/help",
"helpLink": "http://whatever.com/help"
}
}
}
},
{
"service": "zitadel.settings.v2.SettingsService",
"method": "GetActiveIdentityProviders",
"out": {
"data": {
"identityProviders": [
{
"id": "123",
"name": "Hubba bubba",
"type": 10
}
]
}
}
},
{
"service": "zitadel.settings.v2.SettingsService",
"method": "GetPasswordComplexitySettings",
"out": {
"data": {
"settings": {
"minLength": 8,
"requiresUppercase": true,
"requiresLowercase": true,
"requiresNumber": true,
"requiresSymbol": true
}
}
}
},
{
"service": "zitadel.settings.v2.SettingsService",
"method": "GetHostedLoginTranslation",
"out": {
"data": {}
}
}
]

View File

@@ -1,7 +0,0 @@
zitadel/user/v2/user_service.proto
zitadel/org/v2/org_service.proto
zitadel/session/v2/session_service.proto
zitadel/settings/v2/settings_service.proto
zitadel/management.proto
zitadel/auth.proto
zitadel/admin.proto

View File

@@ -1,14 +0,0 @@
import { defineConfig } from "cypress";
export default defineConfig({
reporter: "list",
e2e: {
baseUrl: process.env.LOGIN_BASE_URL || "http://localhost:3001/ui/v2/login",
specPattern: "integration/**/*.cy.{js,jsx,ts,tsx}",
supportFile: "support/e2e.{js,jsx,ts,tsx}",
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
});

View File

@@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -1,110 +0,0 @@
import { stub } from "../support/e2e";
describe("verify invite", () => {
beforeEach(() => {
stub("zitadel.org.v2.OrganizationService", "ListOrganizations", {
data: {
details: {
totalResult: 1,
},
result: [{ id: "256088834543534543" }],
},
});
stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", {
data: {
authMethodTypes: [], // user with no auth methods was invited
},
});
stub("zitadel.user.v2.UserService", "GetUserByID", {
data: {
user: {
userId: "221394658884845598",
state: 1,
username: "john@zitadel.com",
loginNames: ["john@zitadel.com"],
preferredLoginName: "john@zitadel.com",
human: {
userId: "221394658884845598",
state: 1,
username: "john@zitadel.com",
loginNames: ["john@zitadel.com"],
preferredLoginName: "john@zitadel.com",
profile: {
givenName: "John",
familyName: "Doe",
avatarUrl: "https://zitadel.com/avatar.jpg",
},
email: {
email: "john@zitadel.com",
isVerified: false,
},
},
},
},
});
stub("zitadel.session.v2.SessionService", "CreateSession", {
data: {
details: {
sequence: 859,
changeDate: new Date("2024-04-04T09:40:55.577Z"),
resourceOwner: "220516472055706145",
},
sessionId: "221394658884845598",
sessionToken: "SDMc7DlYXPgwRJ-Tb5NlLqynysHjEae3csWsKzoZWLplRji0AYY3HgAkrUEBqtLCvOayLJPMd0ax4Q",
challenges: undefined,
},
});
stub("zitadel.session.v2.SessionService", "GetSession", {
data: {
session: {
id: "221394658884845598",
creationDate: new Date("2024-04-04T09:40:55.577Z"),
changeDate: new Date("2024-04-04T09:40:55.577Z"),
sequence: 859,
factors: {
user: {
id: "221394658884845598",
loginName: "john@zitadel.com",
},
password: undefined,
webAuthN: undefined,
intent: undefined,
},
metadata: {},
},
},
});
stub("zitadel.settings.v2.SettingsService", "GetLoginSettings", {
data: {
settings: {
passkeysType: 1,
allowUsernamePassword: true,
},
},
});
});
it.only("shows authenticators after successful invite verification", () => {
stub("zitadel.user.v2.UserService", "VerifyInviteCode");
cy.visit("/verify?userId=221394658884845598&code=abc&invite=true");
cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl + "/authenticator/set");
});
it("shows an error if invite code validation failed", () => {
stub("zitadel.user.v2.UserService", "VerifyInviteCode", {
code: 3,
error: "error validating code",
});
// TODO: Avoid uncaught exception in application
cy.once("uncaught:exception", () => false);
cy.visit("/verify?userId=221394658884845598&code=abc&invite=true");
cy.contains("Could not verify invite", { timeout: 10_000 });
});
});

View File

@@ -1,172 +0,0 @@
import { stub } from "../support/e2e";
describe("login", () => {
beforeEach(() => {
stub("zitadel.org.v2.OrganizationService", "ListOrganizations", {
data: {
details: {
totalResult: 1,
},
result: [{ id: "256088834543534543" }],
},
});
stub("zitadel.session.v2.SessionService", "CreateSession", {
data: {
details: {
sequence: 859,
changeDate: new Date("2024-04-04T09:40:55.577Z"),
resourceOwner: "220516472055706145",
},
sessionId: "221394658884845598",
sessionToken: "SDMc7DlYXPgwRJ-Tb5NlLqynysHjEae3csWsKzoZWLplRji0AYY3HgAkrUEBqtLCvOayLJPMd0ax4Q",
challenges: undefined,
},
});
stub("zitadel.session.v2.SessionService", "GetSession", {
data: {
session: {
id: "221394658884845598",
creationDate: new Date("2024-04-04T09:40:55.577Z"),
changeDate: new Date("2024-04-04T09:40:55.577Z"),
sequence: 859,
factors: {
user: {
id: "221394658884845598",
loginName: "john@zitadel.com",
},
password: undefined,
webAuthN: undefined,
intent: undefined,
},
metadata: {},
},
},
});
stub("zitadel.settings.v2.SettingsService", "GetLoginSettings", {
data: {
settings: {
passkeysType: 1,
allowUsernamePassword: true,
},
},
});
});
describe("password login", () => {
beforeEach(() => {
stub("zitadel.user.v2.UserService", "ListUsers", {
data: {
details: {
totalResult: 1,
},
result: [
{
userId: "221394658884845598",
state: 1,
username: "john@zitadel.com",
loginNames: ["john@zitadel.com"],
preferredLoginName: "john@zitadel.com",
human: {
userId: "221394658884845598",
state: 1,
username: "john@zitadel.com",
loginNames: ["john@zitadel.com"],
preferredLoginName: "john@zitadel.com",
profile: {
givenName: "John",
familyName: "Doe",
avatarUrl: "https://zitadel.com/avatar.jpg",
},
email: {
email: "john@zitadel.com",
isVerified: true,
},
},
},
],
},
});
stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", {
data: {
authMethodTypes: [1], // 1 for password authentication
},
});
});
it("should redirect a user with password authentication to /password", () => {
cy.visit("/loginname?loginName=john%40zitadel.com&submit=true");
cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl + "/password");
});
describe("with passkey prompt", () => {
beforeEach(() => {
stub("zitadel.session.v2.SessionService", "SetSession", {
data: {
details: {
sequence: 859,
changeDate: "2023-07-04T07:58:20.126Z",
resourceOwner: "220516472055706145",
},
sessionToken: "SDMc7DlYXPgwRJ-Tb5NlLqynysHjEae3csWsKzoZWLplRji0AYY3HgAkrUEBqtLCvOayLJPMd0ax4Q",
challenges: undefined,
},
});
});
// it("should prompt a user to setup passwordless authentication if passkey is allowed in the login settings", () => {
// cy.visit("/loginname?loginName=john%40zitadel.com&submit=true");
// cy.location("pathname", { timeout: 10_000 }).should("eq", "/password");
// cy.get('input[type="password"]').focus().type("MyStrongPassword!1");
// cy.get('button[type="submit"]').click();
// cy.location("pathname", { timeout: 10_000 }).should(
// "eq",
// "/passkey/set",
// );
// });
});
});
describe("passkey login", () => {
beforeEach(() => {
stub("zitadel.user.v2.UserService", "ListUsers", {
data: {
details: {
totalResult: 1,
},
result: [
{
userId: "221394658884845598",
state: 1,
username: "john@zitadel.com",
loginNames: ["john@zitadel.com"],
preferredLoginName: "john@zitadel.com",
human: {
userId: "221394658884845598",
state: 1,
username: "john@zitadel.com",
loginNames: ["john@zitadel.com"],
preferredLoginName: "john@zitadel.com",
profile: {
givenName: "John",
familyName: "Doe",
avatarUrl: "https://zitadel.com/avatar.jpg",
},
email: {
email: "john@zitadel.com",
isVerified: true,
},
},
},
],
},
});
stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", {
data: {
authMethodTypes: [2], // 2 for passwordless authentication
},
});
});
it("should redirect a user with passwordless authentication to /passkey", () => {
cy.visit("/loginname?loginName=john%40zitadel.com&submit=true");
cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl + "/passkey");
});
});
});

View File

@@ -1,21 +0,0 @@
import { stub } from "../support/e2e";
const IDP_URL = "https://example.com/idp/url";
describe("register idps", () => {
beforeEach(() => {
stub("zitadel.user.v2.UserService", "StartIdentityProviderIntent", {
data: {
authUrl: IDP_URL,
},
});
});
it("should redirect the user to the correct url", () => {
cy.visit("/idp");
cy.get('button[e2e="google"]').click();
cy.origin(IDP_URL, { args: IDP_URL }, (url) => {
cy.location("href", { timeout: 10_000 }).should("eq", url);
});
});
});

View File

@@ -1,73 +0,0 @@
import { stub } from "../support/e2e";
describe("register", () => {
beforeEach(() => {
stub("zitadel.org.v2.OrganizationService", "ListOrganizations", {
data: {
details: {
totalResult: 1,
},
result: [{ id: "256088834543534543" }],
},
});
stub("zitadel.settings.v2.SettingsService", "GetLoginSettings", {
data: {
settings: {
passkeysType: 1,
allowRegister: true,
allowUsernamePassword: true,
defaultRedirectUri: "",
},
},
});
stub("zitadel.user.v2.UserService", "AddHumanUser", {
data: {
userId: "221394658884845598",
},
});
stub("zitadel.session.v2.SessionService", "CreateSession", {
data: {
details: {
sequence: 859,
changeDate: new Date("2024-04-04T09:40:55.577Z"),
resourceOwner: "220516472055706145",
},
sessionId: "221394658884845598",
sessionToken: "SDMc7DlYXPgwRJ-Tb5NlLqynysHjEae3csWsKzoZWLplRji0AYY3HgAkrUEBqtLCvOayLJPMd0ax4Q",
challenges: undefined,
},
});
stub("zitadel.session.v2.SessionService", "GetSession", {
data: {
session: {
id: "221394658884845598",
creationDate: new Date("2024-04-04T09:40:55.577Z"),
changeDate: new Date("2024-04-04T09:40:55.577Z"),
sequence: 859,
factors: {
user: {
id: "221394658884845598",
loginName: "john@zitadel.com",
},
password: undefined,
webAuthN: undefined,
intent: undefined,
},
metadata: {},
},
},
});
});
it("should redirect a user who selects passwordless on register to /passkey/set", () => {
cy.visit("/register");
cy.get('input[data-testid="firstname-text-input"]').focus().type("John");
cy.get('input[data-testid="lastname-text-input"]').focus().type("Doe");
cy.get('input[data-testid="email-text-input"]').focus().type("john@zitadel.com");
cy.get('input[type="checkbox"][value="privacypolicy"]').check();
cy.get('input[type="checkbox"][value="tos"]').check();
cy.get('button[type="submit"]').click();
cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl + "/passkey/set");
});
});

View File

@@ -1,95 +0,0 @@
import { stub } from "../support/e2e";
describe("verify email", () => {
beforeEach(() => {
stub("zitadel.org.v2.OrganizationService", "ListOrganizations", {
data: {
details: {
totalResult: 1,
},
result: [{ id: "256088834543534543" }],
},
});
stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", {
data: {
authMethodTypes: [1], // set one method such that we know that the user was not invited
},
});
stub("zitadel.user.v2.UserService", "SendEmailCode");
stub("zitadel.user.v2.UserService", "GetUserByID", {
data: {
user: {
userId: "221394658884845598",
state: 1,
username: "john@zitadel.com",
loginNames: ["john@zitadel.com"],
preferredLoginName: "john@zitadel.com",
human: {
userId: "221394658884845598",
state: 1,
username: "john@zitadel.com",
loginNames: ["john@zitadel.com"],
preferredLoginName: "john@zitadel.com",
profile: {
givenName: "John",
familyName: "Doe",
avatarUrl: "https://zitadel.com/avatar.jpg",
},
email: {
email: "john@zitadel.com",
isVerified: false, // email is not verified yet
},
},
},
},
});
stub("zitadel.session.v2.SessionService", "CreateSession", {
data: {
details: {
sequence: 859,
changeDate: new Date("2024-04-04T09:40:55.577Z"),
resourceOwner: "220516472055706145",
},
sessionId: "221394658884845598",
sessionToken: "SDMc7DlYXPgwRJ-Tb5NlLqynysHjEae3csWsKzoZWLplRji0AYY3HgAkrUEBqtLCvOayLJPMd0ax4Q",
challenges: undefined,
},
});
stub("zitadel.session.v2.SessionService", "GetSession", {
data: {
session: {
id: "221394658884845598",
creationDate: new Date("2024-04-04T09:40:55.577Z"),
changeDate: new Date("2024-04-04T09:40:55.577Z"),
sequence: 859,
factors: {
user: {
id: "221394658884845598",
loginName: "john@zitadel.com",
},
password: undefined,
webAuthN: undefined,
intent: undefined,
},
metadata: {},
},
},
});
});
it("shows an error if email code validation failed", () => {
stub("zitadel.user.v2.UserService", "VerifyEmail", {
code: 3,
error: "error validating code",
});
// TODO: Avoid uncaught exception in application
cy.once("uncaught:exception", () => false);
cy.visit("/verify?userId=221394658884845598&code=abc");
cy.contains("Could not verify email", { timeout: 10_000 });
});
});

View File

@@ -1,29 +0,0 @@
const url = Cypress.env("CORE_MOCK_STUBS_URL") || "http://mock-zitadel:22220/v1/stubs";
function removeStub(service: string, method: string) {
return cy.request({
url,
method: "DELETE",
qs: {
service,
method,
},
});
}
export function stub(service: string, method: string, out?: any) {
removeStub(service, method);
return cy.request({
url,
method: "POST",
body: {
stubs: [
{
service,
method,
out,
},
],
},
});
}

View File

@@ -1,8 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress", "node"]
},
"include": ["**/*.ts"]
}

View File

@@ -22,6 +22,11 @@
"test:acceptance:setup:dev": "cd ../.. && make login_test_acceptance_setup_dev"
},
"nx": {
"release": {
"docker": {
"repositoryName": "zitadel/login"
}
},
"targets": {
"build": {
"outputs": [

View File

@@ -1,22 +0,0 @@
{
"extends": ["//"],
"tasks": {
"build": {
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"dependsOn": ["@zitadel/client#build"]
},
"build:login:standalone": {
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"dependsOn": ["@zitadel/client#build"]
},
"dev": {
"dependsOn": ["@zitadel/client#build"]
},
"test": {
"dependsOn": ["@zitadel/client#build"]
},
"test:unit": {
"dependsOn": ["@zitadel/client#build"]
}
}
}