merge main

This commit is contained in:
Elio Bischof
2025-07-01 17:16:25 +02:00
10 changed files with 171 additions and 67 deletions

View File

@@ -0,0 +1,71 @@
services:
zitadel:
user: "${ZITADEL_DEV_UID}"
image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:02617cf17fdde849378c1a6b5254bbfb2745b164}"
command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --config /zitadel.yaml --steps /zitadel.yaml'
ports:
- "8080:8080"
volumes:
- ./pat:/pat
- ./zitadel.yaml:/zitadel.yaml
depends_on:
db:
condition: "service_healthy"
extra_hosts:
- "localhost:host-gateway"
db:
restart: "always"
image: postgres:17.0-alpine3.19
environment:
- POSTGRES_USER=zitadel
- PGUSER=zitadel
- POSTGRES_DB=zitadel
- POSTGRES_HOST_AUTH_METHOD=trust
command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all -c shared_buffers=1GB -c work_mem=16MB -c effective_io_concurrency=100 -c wal_level=minimal -c archive_mode=off -c max_wal_senders=0
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: "10s"
timeout: "30s"
retries: 5
start_period: "20s"
ports:
- 5432:5432
wait_for_zitadel:
image: curlimages/curl:8.00.1
command: /bin/sh -c "until curl -s -o /dev/null -i -f http://zitadel:8080/debug/ready; do echo 'waiting' && sleep 1; done; echo 'ready' && sleep 5;" || false
depends_on:
- zitadel
setup:
user: "${ZITADEL_DEV_UID}"
container_name: setup
image: acceptance-setup:latest
environment:
PAT_FILE: /pat/zitadel-admin-sa.pat
ZITADEL_API_INTERNAL_URL: http://zitadel:8080
WRITE_ENVIRONMENT_FILE: /apps/login/.env.local
WRITE_TEST_ENVIRONMENT_FILE: /acceptance/tests/.env.local
SINK_EMAIL_INTERNAL_URL: http://sink:3333/email
SINK_SMS_INTERNAL_URL: http://sink:3333/sms
SINK_NOTIFICATION_URL: http://localhost:3333/notification
volumes:
- "./pat:/pat"
- "../apps/login:/apps/login"
- "../acceptance/tests:/acceptance/tests"
depends_on:
wait_for_zitadel:
condition: "service_completed_successfully"
sink:
image: golang:1.24-alpine
container_name: sink
command: go run /sink/main.go -port '3333' -email '/email' -sms '/sms' -notification '/notification'
ports:
- 3333:3333
volumes:
- "./sink:/sink"
depends_on:
setup:
condition: "service_completed_successfully"

View File

@@ -28,6 +28,6 @@ declare namespace NodeJS {
* Optional: custom request headers to be added to every request * Optional: custom request headers to be added to every request
* Split by comma, key value pairs separated by colon * Split by comma, key value pairs separated by colon
*/ */
CUSTOM_REQUEST_HEADERS: string; CUSTOM_REQUEST_HEADERS?: string;
} }
} }

View File

@@ -217,7 +217,7 @@ export async function GET(request: NextRequest) {
params.set("organization", organization); params.set("organization", organization);
} }
return startIdentityProviderFlow({ let url: string | null = await startIdentityProviderFlow({
serviceUrl, serviceUrl,
idpId, idpId,
urls: { urls: {
@@ -228,14 +228,21 @@ export async function GET(request: NextRequest) {
`${origin}/idp/${provider}/failure?` + `${origin}/idp/${provider}/failure?` +
new URLSearchParams(params), new URLSearchParams(params),
}, },
}).then((resp) => {
if (
resp.nextStep.value &&
typeof resp.nextStep.value === "string"
) {
return NextResponse.redirect(resp.nextStep.value);
}
}); });
if (!url) {
return NextResponse.json(
{ error: "Could not start IDP flow" },
{ status: 500 },
);
}
if (url.startsWith("/")) {
// if the url is a relative path, construct the absolute url
url = constructUrl(request, url).toString();
}
return NextResponse.redirect(url);
} }
} }
} }

View File

@@ -16,7 +16,7 @@ export default getRequestConfig(async () => {
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const i18nOrganization = _headers.get("x-zitadel-i18n-organization") || ""; // You may need to set this header in middleware const i18nOrganization = _headers.get("x-zitadel-i18n-organization") || ""; // You may need to set this header in middleware
console.log("i18nOrganization:", i18nOrganization);
let translations: JsonObject | {} = {}; let translations: JsonObject | {} = {};
try { try {
const i18nJSON = await getHostedLoginTranslation({ const i18nJSON = await getHostedLoginTranslation({

View File

@@ -1,20 +1,16 @@
"use server"; "use server";
import { createServerTransport } from "@zitadel/client/node";
import { createUserServiceClient } from "@zitadel/client/v2"; import { createUserServiceClient } from "@zitadel/client/v2";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { getSessionCookieById } from "./cookies"; import { getSessionCookieById } from "./cookies";
import { getServiceUrlFromHeaders } from "./service-url"; import { getServiceUrlFromHeaders } from "./service-url";
import { getSession } from "./zitadel"; import { createServerTransport, getSession } from "./zitadel";
const transport = async (serviceUrl: string, token: string) => {
return createServerTransport(token, {
baseUrl: serviceUrl,
});
};
const myUserService = async (serviceUrl: string, sessionToken: string) => { const myUserService = async (serviceUrl: string, sessionToken: string) => {
const transportPromise = await transport(serviceUrl, sessionToken); const transportPromise = await createServerTransport(
sessionToken,
serviceUrl,
);
return createUserServiceClient(transportPromise); return createUserServiceClient(transportPromise);
}; };

View File

@@ -74,22 +74,20 @@ export type StartIDPFlowCommand = {
async function startIDPFlow(command: StartIDPFlowCommand) { async function startIDPFlow(command: StartIDPFlowCommand) {
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
return startIdentityProviderFlow({ const url = await startIdentityProviderFlow({
serviceUrl: command.serviceUrl, serviceUrl: command.serviceUrl,
idpId: command.idpId, idpId: command.idpId,
urls: { urls: {
successUrl: `${command.host.includes("localhost") ? "http://" : "https://"}${command.host}${basePath}${command.successUrl}`, successUrl: `${command.host.includes("localhost") ? "http://" : "https://"}${command.host}${basePath}${command.successUrl}`,
failureUrl: `${command.host.includes("localhost") ? "http://" : "https://"}${command.host}${basePath}${command.failureUrl}`, failureUrl: `${command.host.includes("localhost") ? "http://" : "https://"}${command.host}${basePath}${command.failureUrl}`,
}, },
}).then((response) => {
if (
response &&
response.nextStep.case === "authUrl" &&
response?.nextStep.value
) {
return { redirect: response.nextStep.value };
}
}); });
if (!url) {
return { error: "Could not start IDP flow" };
}
return { redirect: url };
} }
type CreateNewSessionCommand = { type CreateNewSessionCommand = {

View File

@@ -102,7 +102,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const resp = await startIdentityProviderFlow({ const url = await startIdentityProviderFlow({
serviceUrl, serviceUrl,
idpId: identityProviders[0].id, idpId: identityProviders[0].id,
urls: { urls: {
@@ -115,9 +115,11 @@ export async function sendLoginname(command: SendLoginnameCommand) {
}, },
}); });
if (resp?.nextStep.case === "authUrl") { if (!url) {
return { redirect: resp.nextStep.value }; return { error: "Could not start IDP flow" };
} }
return { redirect: url };
} }
}; };
@@ -166,7 +168,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const resp = await startIdentityProviderFlow({ const url = await startIdentityProviderFlow({
serviceUrl, serviceUrl,
idpId: idp.id, idpId: idp.id,
urls: { urls: {
@@ -179,9 +181,11 @@ export async function sendLoginname(command: SendLoginnameCommand) {
}, },
}); });
if (resp?.nextStep.case === "authUrl") { if (!url) {
return { redirect: resp.nextStep.value }; return { error: "Could not start IDP flow" };
} }
return { redirect: url };
} }
}; };

View File

@@ -17,7 +17,6 @@ import {
setUserPassword, setUserPassword,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { ConnectError, create } from "@zitadel/client"; import { ConnectError, create } from "@zitadel/client";
import { createServerTransport } from "@zitadel/client/node";
import { createUserServiceClient } from "@zitadel/client/v2"; import { createUserServiceClient } from "@zitadel/client/v2";
import { import {
Checks, Checks,
@@ -39,6 +38,7 @@ import {
checkPasswordChangeRequired, checkPasswordChangeRequired,
checkUserVerification, checkUserVerification,
} from "../verify-helper"; } from "../verify-helper";
import { createServerTransport } from "../zitadel";
type ResetPasswordCommand = { type ResetPasswordCommand = {
loginName: string; loginName: string;
@@ -428,9 +428,7 @@ export async function checkSessionAndSetPassword({
}); });
} else { } else {
const transport = async (serviceUrl: string, token: string) => { const transport = async (serviceUrl: string, token: string) => {
return createServerTransport(token, { return createServerTransport(token, serviceUrl);
baseUrl: serviceUrl,
});
}; };
const myUserService = async (serviceUrl: string, sessionToken: string) => { const myUserService = async (serviceUrl: string, sessionToken: string) => {

View File

@@ -1,5 +1,4 @@
import { createClientFor } from "@zitadel/client"; import { createClientFor } from "@zitadel/client";
import { createServerTransport } from "@zitadel/client/node";
import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb"; import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb";
import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb"; import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb";
@@ -8,6 +7,7 @@ import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_servic
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { systemAPIToken } from "./api"; import { systemAPIToken } from "./api";
import { createServerTransport } from "./zitadel";
type ServiceClass = type ServiceClass =
| typeof IdentityProviderService | typeof IdentityProviderService
@@ -43,24 +43,7 @@ export async function createServiceForHost<T extends ServiceClass>(
throw new Error("No token found"); throw new Error("No token found");
} }
const transport = createServerTransport(token, { const transport = createServerTransport(token, serviceUrl);
baseUrl: serviceUrl,
interceptors: !process.env.CUSTOM_REQUEST_HEADERS
? undefined
: [
(next) => {
return (req) => {
process.env.CUSTOM_REQUEST_HEADERS.split(",").forEach(
(header) => {
const kv = header.split(":");
req.header.set(kv[0], kv[1]);
},
);
return next(req);
};
},
],
});
return createClientFor<T>(service)(transport); return createClientFor<T>(service)(transport);
} }

View File

@@ -1,4 +1,5 @@
import { Client, create, Duration } from "@zitadel/client"; import { Client, create, Duration } from "@zitadel/client";
import { createServerTransport as libCreateServerTransport } from "@zitadel/client/node";
import { makeReqCtx } from "@zitadel/client/v2"; import { makeReqCtx } from "@zitadel/client/v2";
import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb"; import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb";
import { import {
@@ -23,7 +24,10 @@ import {
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
import { SendEmailVerificationCodeSchema } from "@zitadel/proto/zitadel/user/v2/email_pb"; import { SendEmailVerificationCodeSchema } from "@zitadel/proto/zitadel/user/v2/email_pb";
import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb"; import type {
FormData,
RedirectURLsJson,
} from "@zitadel/proto/zitadel/user/v2/idp_pb";
import { import {
NotificationType, NotificationType,
SendPasswordResetLinkSchema, SendPasswordResetLinkSchema,
@@ -88,7 +92,6 @@ export async function getHostedLoginTranslation({
{}, {},
) )
.then((resp) => { .then((resp) => {
console.log(resp);
return resp.translations ? resp.translations : undefined; return resp.translations ? resp.translations : undefined;
}); });
@@ -964,18 +967,37 @@ export async function startIdentityProviderFlow({
serviceUrl: string; serviceUrl: string;
idpId: string; idpId: string;
urls: RedirectURLsJson; urls: RedirectURLsJson;
}) { }): Promise<string | null> {
const userService: Client<typeof UserService> = await createServiceForHost( const userService: Client<typeof UserService> = await createServiceForHost(
UserService, UserService,
serviceUrl, serviceUrl,
); );
return userService.startIdentityProviderIntent({ return userService
.startIdentityProviderIntent({
idpId, idpId,
content: { content: {
case: "urls", case: "urls",
value: urls, value: urls,
}, },
})
.then((resp) => {
if (resp.nextStep.case === "authUrl" && resp.nextStep.value) {
return resp.nextStep.value;
} else if (resp.nextStep.case === "formData" && resp.nextStep.value) {
const formData: FormData = resp.nextStep.value;
const redirectUrl = "/saml-post";
const params = new URLSearchParams({ url: formData.url });
Object.entries(formData.fields).forEach(([k, v]) => {
params.append(k, v);
});
return `${redirectUrl}?${params.toString()}`;
} else {
return null;
}
}); });
} }
@@ -1476,3 +1498,28 @@ export async function listAuthenticationMethodTypes({
userId, userId,
}); });
} }
export function createServerTransport(token: string, baseUrl: string) {
return libCreateServerTransport(token, {
baseUrl,
interceptors: !process.env.CUSTOM_REQUEST_HEADERS
? undefined
: [
(next) => {
return (req) => {
process.env
.CUSTOM_REQUEST_HEADERS!.split(",")
.forEach((header) => {
const kv = header.split(":");
if (kv.length === 2) {
req.header.set(kv[0].trim(), kv[1].trim());
} else {
console.warn(`Skipping malformed header: ${header}`);
}
});
return next(req);
};
},
],
});
}