mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-08 06:14:42 +00:00
chore: reproducible pipeline with dev containers (#10305)
# Which Problems Are Solved - The previous monorepo in monorepo structure for the login app and its related packages was fragmented, complicated and buggy. - The process for building and testing the login container was inconsistent between local development and CI. - Lack of clear documentation as well as easy and reliable ways for non-frontend developers to reproduce and fix failing PR checks locally. # How the Problems Are Solved - Consolidated the login app and its related npm packages by moving the main package to `apps/login/apps/login` and merging `apps/login/packages/integration` and `apps/login/packages/acceptance` into the main `apps/login` package. - Migrated from Docker Compose-based test setups to dev container-based setups, adding support for multiple dev container configurations: - `.devcontainer/base` - `.devcontainer/turbo-lint-unit` - `.devcontainer/turbo-lint-unit-debug` - `.devcontainer/login-integration` - `.devcontainer/login-integration-debug` - Added npm scripts to run the new dev container setups, enabling exact reproduction of GitHub PR checks locally, and updated the pipeline to use these containers. - Cleaned up Dockerfiles and docker-bake.hcl files to only build the production image for the login app. - Cleaned up compose files to focus on dev environments in dev containers. - Updated `CONTRIBUTING.md` with guidance on running and debugging PR checks locally using the new dev container approach. - Introduced separate Dockerfiles for the login app to distinguish between using published client packages and building clients from local protos. - Ensured the login container is always built in the pipeline for use in integration and acceptance tests. - Updated Makefile and GitHub Actions workflows to use `--frozen-lockfile` for installing pnpm packages, ensuring reproducible installs. - Disabled GitHub release creation by the changeset action. - Refactored the `/build` directory structure for clarity and maintainability. - Added a `clean` command to `docks/package.json`. - Experimentally added `knip` to the `zitadel-client` package for improved linting of dependencies and exports. # Additional Changes - Fixed Makefile commands for consistency and reliability. - Improved the structure and clarity of the `/build` directory to support seamless integration of the login build. - Enhanced documentation and developer experience for running and debugging CI checks locally. # Additional Context - See updated `CONTRIBUTING.md` for new local development and debugging instructions. - These changes are a prerequisite for further improvements to the CI pipeline and local development workflow. - Closes #10276
This commit is contained in:
190
apps/login/acceptance/tests/zitadel.ts
Normal file
190
apps/login/acceptance/tests/zitadel.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { Authenticator } from "@otplib/core";
|
||||
import { createDigest, createRandomBytes } from "@otplib/plugin-crypto";
|
||||
import { keyDecoder, keyEncoder } from "@otplib/plugin-thirty-two"; // use your chosen base32 plugin
|
||||
import axios from "axios";
|
||||
import dotenv from "dotenv";
|
||||
import { request } from "gaxios";
|
||||
import path from "path";
|
||||
import { OtpType, userProps } from "./user";
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") });
|
||||
|
||||
export async function addUser(props: userProps) {
|
||||
const body = {
|
||||
username: props.email,
|
||||
organization: {
|
||||
orgId: props.organization,
|
||||
},
|
||||
profile: {
|
||||
givenName: props.firstName,
|
||||
familyName: props.lastName,
|
||||
},
|
||||
email: {
|
||||
email: props.email,
|
||||
isVerified: true,
|
||||
},
|
||||
phone: {
|
||||
phone: props.phone,
|
||||
isVerified: true,
|
||||
},
|
||||
password: {
|
||||
password: props.password,
|
||||
changeRequired: props.passwordChangeRequired ?? false,
|
||||
},
|
||||
};
|
||||
if (!props.isEmailVerified) {
|
||||
delete body.email.isVerified;
|
||||
}
|
||||
if (!props.isPhoneVerified) {
|
||||
delete body.phone.isVerified;
|
||||
}
|
||||
|
||||
return await listCall(`${process.env.ZITADEL_API_URL}/v2/users/human`, body);
|
||||
}
|
||||
|
||||
export async function removeUserByUsername(username: string) {
|
||||
const resp = await getUserByUsername(username);
|
||||
if (!resp || !resp.result || !resp.result[0]) {
|
||||
return;
|
||||
}
|
||||
await removeUser(resp.result[0].userId);
|
||||
}
|
||||
|
||||
export async function removeUser(id: string) {
|
||||
await deleteCall(`${process.env.ZITADEL_API_URL}/v2/users/${id}`);
|
||||
}
|
||||
|
||||
async function deleteCall(url: string) {
|
||||
try {
|
||||
const response = await axios.delete(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status >= 400 && response.status !== 404) {
|
||||
const error = `HTTP Error: ${response.status} - ${response.statusText}`;
|
||||
console.error(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error making request:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserByUsername(username: string): Promise<any> {
|
||||
const listUsersBody = {
|
||||
queries: [
|
||||
{
|
||||
userNameQuery: {
|
||||
userName: username,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return await listCall(`${process.env.ZITADEL_API_URL}/v2/users`, listUsersBody);
|
||||
}
|
||||
|
||||
async function listCall(url: string, data: any): Promise<any> {
|
||||
try {
|
||||
const response = await axios.post(url, data, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
const error = `HTTP Error: ${response.status} - ${response.statusText}`;
|
||||
console.error(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Error making request:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function activateOTP(userId: string, type: OtpType) {
|
||||
let url = "otp_";
|
||||
switch (type) {
|
||||
case OtpType.sms:
|
||||
url = url + "sms";
|
||||
break;
|
||||
case OtpType.email:
|
||||
url = url + "email";
|
||||
break;
|
||||
}
|
||||
|
||||
await pushCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/${url}`, {});
|
||||
}
|
||||
|
||||
async function pushCall(url: string, data: any) {
|
||||
try {
|
||||
const response = await axios.post(url, data, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
const error = `HTTP Error: ${response.status} - ${response.statusText}`;
|
||||
console.error(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error making request:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function addTOTP(userId: string): Promise<string> {
|
||||
const response = await listCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/totp`, {});
|
||||
const code = totp(response.secret);
|
||||
await pushCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/totp/verify`, { code: code });
|
||||
return response.secret;
|
||||
}
|
||||
|
||||
export function totp(secret: string) {
|
||||
const authenticator = new Authenticator({
|
||||
createDigest,
|
||||
createRandomBytes,
|
||||
keyDecoder,
|
||||
keyEncoder,
|
||||
});
|
||||
// google authenticator usage
|
||||
const token = authenticator.generate(secret);
|
||||
|
||||
// check if token can be used
|
||||
if (!authenticator.verify({ token: token, secret: secret })) {
|
||||
const error = `Generated token could not be verified`;
|
||||
console.error(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function eventualNewUser(id: string) {
|
||||
return request({
|
||||
url: `${process.env.ZITADEL_API_URL}/v2/users/${id}`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
retryConfig: {
|
||||
statusCodesToRetry: [[404, 404]],
|
||||
retry: Number.MAX_SAFE_INTEGER, // totalTimeout limits the number of retries
|
||||
totalTimeout: 10000, // 10 seconds
|
||||
onRetryAttempt: (error) => {
|
||||
console.warn(`Retrying to query new user ${id}: ${error.message}`);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user