throw out @zitadel/react @zitadel/next

This commit is contained in:
peintnermax
2024-09-04 15:47:29 +02:00
parent 3fef934a4a
commit 6204cdaaae
63 changed files with 191 additions and 744 deletions

View File

@@ -37,10 +37,8 @@
"@tailwindcss/forms": "0.5.7",
"@vercel/analytics": "^1.2.2",
"@zitadel/client": "workspace:*",
"@zitadel/next": "workspace:*",
"@zitadel/node": "workspace:*",
"@zitadel/proto": "workspace:*",
"@zitadel/react": "workspace:*",
"clsx": "1.2.1",
"copy-to-clipboard": "^3.3.3",
"moment": "^2.29.4",
@@ -57,7 +55,7 @@
"devDependencies": {
"@bufbuild/buf": "^1.36.0",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^16.0.0",
"@testing-library/react": "^16.0.1",
"@types/ms": "0.7.34",
"@types/node": "22.1.0",
"@types/react": "18.3.3",
@@ -65,7 +63,7 @@
"@types/tinycolor2": "1.4.3",
"@types/uuid": "^10.0.0",
"@vercel/git-hooks": "1.0.0",
"@zitadel/prettier-config": "workspace:*",
"@zitadel/prettier-config": "workspace:^",
"@zitadel/tsconfig": "workspace:*",
"autoprefixer": "10.4.20",
"concurrently": "^8.1.0",

View File

@@ -1,9 +1,9 @@
import { getBrandingSettings, listSessions } from "@/lib/zitadel";
import { getAllSessionCookieIds } from "@zitadel/next";
import { UserPlusIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
import SessionsList from "@/ui/SessionsList";
import DynamicTheme from "@/ui/DynamicTheme";
import { getAllSessionCookieIds } from "@/lib/cookies";
async function loadSessions() {
const ids = await getAllSessionCookieIds();

View File

@@ -1,3 +1,5 @@
import { getSessionCookieById } from "@/lib/cookies";
import { loadMostRecentSession } from "@/lib/session";
import {
getBrandingSettings,
getSession,
@@ -9,7 +11,6 @@ import BackButton from "@/ui/BackButton";
import ChooseSecondFactor from "@/ui/ChooseSecondFactor";
import DynamicTheme from "@/ui/DynamicTheme";
import UserAvatar from "@/ui/UserAvatar";
import { getSessionCookieById, loadMostRecentSession } from "@zitadel/next";
export default async function Page({
searchParams,

View File

@@ -1,3 +1,5 @@
import { getSessionCookieById } from "@/lib/cookies";
import { loadMostRecentSession } from "@/lib/session";
import {
getBrandingSettings,
getLoginSettings,
@@ -11,7 +13,6 @@ import BackButton from "@/ui/BackButton";
import ChooseSecondFactorToSetup from "@/ui/ChooseSecondFactorToSetup";
import DynamicTheme from "@/ui/DynamicTheme";
import UserAvatar from "@/ui/UserAvatar";
import { getSessionCookieById, loadMostRecentSession } from "@zitadel/next";
export default async function Page({
searchParams,

View File

@@ -1,9 +1,9 @@
import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, sessionService } from "@/lib/zitadel";
import Alert from "@/ui/Alert";
import DynamicTheme from "@/ui/DynamicTheme";
import LoginOTP from "@/ui/LoginOTP";
import UserAvatar from "@/ui/UserAvatar";
import { loadMostRecentSession } from "@zitadel/next";
export default async function Page({
searchParams,

View File

@@ -13,7 +13,7 @@ import TOTPRegister from "@/ui/TOTPRegister";
import UserAvatar from "@/ui/UserAvatar";
import Link from "next/link";
import { RegisterTOTPResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { loadMostRecentSession } from "@zitadel/next";
import { loadMostRecentSession } from "@/lib/session";
export default async function Page({
searchParams,

View File

@@ -1,9 +1,9 @@
import { loadMostRecentSession } from "@/lib/session";
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 { loadMostRecentSession } from "@zitadel/next";
export default async function Page({
searchParams,

View File

@@ -1,9 +1,10 @@
import { getSessionCookieById } from "@/lib/cookies";
import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getSession, 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 { getSessionCookieById, loadMostRecentSession } from "@zitadel/next";
const title = "Authenticate with a passkey";
const description =

View File

@@ -1,3 +1,4 @@
import { loadMostRecentSession } from "@/lib/session";
import {
getBrandingSettings,
getLoginSettings,
@@ -7,7 +8,6 @@ import Alert from "@/ui/Alert";
import DynamicTheme from "@/ui/DynamicTheme";
import PasswordForm from "@/ui/PasswordForm";
import UserAvatar from "@/ui/UserAvatar";
import { loadMostRecentSession } from "@zitadel/next";
export default async function Page({
searchParams,

View File

@@ -2,12 +2,12 @@ import { createCallback, getBrandingSettings, getSession } from "@/lib/zitadel";
import DynamicTheme from "@/ui/DynamicTheme";
import UserAvatar from "@/ui/UserAvatar";
import { create } from "@zitadel/client";
import { getMostRecentCookieWithLoginname } from "@zitadel/next";
import { redirect } from "next/navigation";
import {
CreateCallbackRequestSchema,
SessionSchema,
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { getMostRecentCookieWithLoginname } from "@/lib/cookies";
async function loadSession(loginName: string, authRequestId?: string) {
const recent = await getMostRecentCookieWithLoginname({ loginName });

View File

@@ -1,9 +1,10 @@
import { getSessionCookieById } from "@/lib/cookies";
import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getSession, 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 { getSessionCookieById, loadMostRecentSession } from "@zitadel/next";
export default async function Page({
searchParams,

View File

@@ -1,9 +1,9 @@
import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, sessionService } from "@/lib/zitadel";
import Alert from "@/ui/Alert";
import DynamicTheme from "@/ui/DynamicTheme";
import RegisterU2F from "@/ui/RegisterU2F";
import UserAvatar from "@/ui/UserAvatar";
import { loadMostRecentSession } from "@zitadel/next";
export default async function Page({
searchParams,

View File

@@ -15,7 +15,6 @@ import {
listSessions,
startIdentityProviderFlow,
} from "@/lib/zitadel";
import { getAllSessions } from "@zitadel/next";
import { NextRequest, NextResponse } from "next/server";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import {
@@ -25,6 +24,7 @@ import {
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { idpTypeToSlug } from "@/lib/idp";
import { create } from "@zitadel/client";
import { getAllSessions } from "@/lib/cookies";
async function loadSessions(ids: string[]): Promise<Session[]> {
const response = await listSessions(

View File

@@ -1,5 +1,5 @@
import { getAllSessions } from "@/lib/cookies";
import { listSessions } from "@/lib/zitadel";
import { getAllSessions } from "@zitadel/next";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { NextRequest, NextResponse } from "next/server";

View File

@@ -0,0 +1,256 @@
"use server";
import { cookies } from "next/headers";
export type Cookie = {
id: string;
token: string;
loginName: string;
organization?: string;
creationDate: string;
expirationDate: string;
changeDate: string;
authRequestId?: string; // if its linked to an OIDC flow
};
type SessionCookie<T> = Cookie & T;
function setSessionHttpOnlyCookie<T>(sessions: SessionCookie<T>[]) {
const cookiesList = cookies();
// @ts-ignore
return cookiesList.set({
name: "sessions",
value: JSON.stringify(sessions),
httpOnly: true,
path: "/",
});
}
export async function addSessionToCookie<T>(session: SessionCookie<T>, cleanup: boolean = false): Promise<any> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
let currentSessions: SessionCookie<T>[] = stringifiedCookie?.value ? JSON.parse(stringifiedCookie?.value) : [];
const index = currentSessions.findIndex((s) => s.loginName === session.loginName);
if (index > -1) {
currentSessions[index] = session;
} else {
currentSessions = [...currentSessions, session];
}
if (cleanup) {
const now = new Date();
const filteredSessions = currentSessions.filter((session) =>
session.expirationDate ? new Date(session.expirationDate) > now : true,
);
return setSessionHttpOnlyCookie(filteredSessions);
} else {
return setSessionHttpOnlyCookie(currentSessions);
}
}
export async function updateSessionCookie<T>(id: string, session: SessionCookie<T>, cleanup: boolean = false): Promise<any> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
const sessions: SessionCookie<T>[] = stringifiedCookie?.value ? JSON.parse(stringifiedCookie?.value) : [session];
const foundIndex = sessions.findIndex((session) => session.id === id);
if (foundIndex > -1) {
sessions[foundIndex] = session;
if (cleanup) {
const now = new Date();
const filteredSessions = sessions.filter((session) =>
session.expirationDate ? new Date(session.expirationDate) > now : true,
);
return setSessionHttpOnlyCookie(filteredSessions);
} else {
return setSessionHttpOnlyCookie(sessions);
}
} else {
throw "updateSessionCookie<T>: session id now found";
}
}
export async function removeSessionFromCookie<T>(session: SessionCookie<T>, cleanup: boolean = false): Promise<any> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
const sessions: SessionCookie<T>[] = stringifiedCookie?.value ? JSON.parse(stringifiedCookie?.value) : [session];
const reducedSessions = sessions.filter((s) => s.id !== session.id);
if (cleanup) {
const now = new Date();
const filteredSessions = reducedSessions.filter((session) =>
session.expirationDate ? new Date(session.expirationDate) > now : true,
);
return setSessionHttpOnlyCookie(filteredSessions);
} else {
return setSessionHttpOnlyCookie(reducedSessions);
}
}
export async function getMostRecentSessionCookie<T>(): Promise<any> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
const sessions: SessionCookie<T>[] = JSON.parse(stringifiedCookie?.value);
const latest = sessions.reduce((prev, current) => {
return new Date(prev.changeDate).getTime() > new Date(current.changeDate).getTime() ? prev : current;
});
return latest;
} else {
return Promise.reject("no session cookie found");
}
}
export async function getSessionCookieById<T>({
sessionId,
organization,
}: {
sessionId: string;
organization?: string;
}): Promise<SessionCookie<T>> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
const sessions: SessionCookie<T>[] = JSON.parse(stringifiedCookie?.value);
const found = sessions.find((s) =>
organization ? s.organization === organization && s.id === sessionId : s.id === sessionId,
);
if (found) {
return found;
} else {
return Promise.reject();
}
} else {
return Promise.reject();
}
}
export async function getSessionCookieByLoginName<T>({
loginName,
organization,
}: {
loginName?: string;
organization?: string;
}): Promise<SessionCookie<T>> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
const sessions: SessionCookie<T>[] = JSON.parse(stringifiedCookie?.value);
const found = sessions.find((s) =>
organization ? s.organization === organization && s.loginName === loginName : s.loginName === loginName,
);
if (found) {
return found;
} else {
return Promise.reject("no cookie found with loginName: " + loginName);
}
} else {
return Promise.reject("no session cookie found");
}
}
/**
*
* @param cleanup when true, removes all expired sessions, default true
* @returns Session Cookies
*/
export async function getAllSessionCookieIds<T>(cleanup: boolean = false): Promise<any> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
const sessions: SessionCookie<T>[] = JSON.parse(stringifiedCookie?.value);
if (cleanup) {
const now = new Date();
return sessions
.filter((session) => (session.expirationDate ? new Date(session.expirationDate) > now : true))
.map((session) => session.id);
} else {
return sessions.map((session) => session.id);
}
} else {
return [];
}
}
/**
*
* @param cleanup when true, removes all expired sessions, default true
* @returns Session Cookies
*/
export async function getAllSessions<T>(cleanup: boolean = false): Promise<SessionCookie<T>[]> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
const sessions: SessionCookie<T>[] = JSON.parse(stringifiedCookie?.value);
if (cleanup) {
const now = new Date();
return sessions.filter((session) => (session.expirationDate ? new Date(session.expirationDate) > now : true));
} else {
return sessions;
}
} else {
return [];
}
}
/**
* Returns most recent session filtered by optinal loginName
* @param loginName optional loginName to filter cookies, if non provided, returns most recent session
* @param organization optional organization to filter cookies
* @returns most recent session
*/
export async function getMostRecentCookieWithLoginname<T>({
loginName,
organization,
}: {
loginName?: string;
organization?: string;
}): Promise<any> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
const sessions: SessionCookie<T>[] = JSON.parse(stringifiedCookie?.value);
let filtered = sessions.filter((cookie) => {
return !!loginName ? cookie.loginName === loginName : true;
});
if (organization) {
filtered = filtered.filter((cookie) => {
return cookie.organization === organization;
});
}
const latest =
filtered && filtered.length
? filtered.reduce((prev, current) => {
return new Date(prev.changeDate).getTime() > new Date(current.changeDate).getTime() ? prev : current;
})
: undefined;
if (latest) {
return latest;
} else {
console.error("sessions", sessions, loginName, organization);
return Promise.reject("Could not get the context or retrieve a session");
}
} else {
return Promise.reject("Could not read session cookie");
}
}

View File

@@ -1,6 +1,6 @@
"use server";
import { loadMostRecentSession } from "@zitadel/next";
import { loadMostRecentSession } from "./session";
import { sessionService, verifyTOTPRegistration } from "./zitadel";
export async function verifyTOTP(

View File

@@ -1,18 +1,17 @@
"use server";
import {
getMostRecentSessionCookie,
getSessionCookieById,
getSessionCookieByLoginName,
} from "@zitadel/next";
import { setSessionAndUpdateCookie } from "@/utils/session";
import { NextRequest, NextResponse } from "next/server";
import {
CheckOTPSchema,
ChecksSchema,
CheckTOTPSchema,
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { create } from "@zitadel/client";
import {
getMostRecentSessionCookie,
getSessionCookieById,
getSessionCookieByLoginName,
} from "../cookies";
export type SetOTPCommand = {
loginName?: string;

View File

@@ -6,12 +6,12 @@ import {
registerPasskey,
verifyPasskeyRegistration,
} from "@/lib/zitadel";
import { getSessionCookieById } from "@zitadel/next";
import { userAgent } from "next/server";
import { create } from "@zitadel/client";
import { VerifyPasskeyRegistrationRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { headers } from "next/headers";
import { RegisterPasskeyResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { getSessionCookieById } from "../cookies";
type VerifyPasskeyCommand = {
passkeyId: string;

View File

@@ -6,12 +6,6 @@ import {
getUserByID,
listAuthenticationMethodTypes,
} from "@/lib/zitadel";
import {
getMostRecentSessionCookie,
getSessionCookieById,
getSessionCookieByLoginName,
removeSessionFromCookie,
} from "@zitadel/next";
import {
createSessionAndUpdateCookie,
createSessionForIdpAndUpdateCookie,
@@ -24,6 +18,12 @@ import {
RequestChallengesSchema,
} from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { create } from "@zitadel/client";
import {
getMostRecentSessionCookie,
getSessionCookieById,
getSessionCookieByLoginName,
removeSessionFromCookie,
} from "../cookies";
type CreateNewSessionCommand = {
userId: string;

View File

@@ -1,11 +1,11 @@
"use server";
import { getSession, registerU2F, verifyU2FRegistration } from "@/lib/zitadel";
import { getSessionCookieById } from "@zitadel/next";
import { userAgent } from "next/server";
import { VerifyU2FRegistrationRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { create } from "@zitadel/client";
import { headers } from "next/headers";
import { getSessionCookieById } from "../cookies";
type RegisterU2FCommand = {
sessionId: string;

View File

@@ -0,0 +1,16 @@
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { GetSessionResponse } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { getMostRecentCookieWithLoginname } from "./cookies";
export async function loadMostRecentSession(
sessionService: any, // TODO: SessionServiceClient,
sessionParams: { loginName?: string; organization?: string },
): Promise<Session | undefined> {
const recent = await getMostRecentCookieWithLoginname({
loginName: sessionParams.loginName,
organization: sessionParams.organization,
});
return sessionService
.getSession({ sessionId: recent.id, sessionToken: recent.token }, {})
.then((resp: GetSessionResponse) => resp.session);
}

View File

@@ -1,5 +1,4 @@
// include styles from the ui package
@import "@zitadel/react/styles.css";
@import "./vars.scss";
@tailwind base;

View File

@@ -3,7 +3,6 @@
import React from "react";
import { Logo } from "@/ui/Logo";
import ThemeWrapper from "./ThemeWrapper";
import { LayoutProviders } from "./LayoutProviders";
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
export default function DynamicTheme({
@@ -15,29 +14,25 @@ export default function DynamicTheme({
}) {
return (
<ThemeWrapper branding={branding}>
{/* <ThemeProvider> */}
<LayoutProviders>
<div className="rounded-lg bg-vc-border-gradient dark:bg-dark-vc-border-gradient p-px shadow-lg shadow-black/5 dark:shadow-black/20 mb-10">
<div className="rounded-lg bg-background-light-400 dark:bg-background-dark-500 px-8 py-12">
<div className="mx-auto flex flex-col items-center space-y-4">
<div className="relative">
{branding && (
<Logo
lightSrc={branding.lightTheme?.logoUrl}
darkSrc={branding.darkTheme?.logoUrl}
height={150}
width={150}
/>
)}
</div>
<div className="w-full">{children}</div>
<div className="flex flex-row justify-between"></div>
<div className="rounded-lg bg-vc-border-gradient dark:bg-dark-vc-border-gradient p-px shadow-lg shadow-black/5 dark:shadow-black/20 mb-10">
<div className="rounded-lg bg-background-light-400 dark:bg-background-dark-500 px-8 py-12">
<div className="mx-auto flex flex-col items-center space-y-4">
<div className="relative">
{branding && (
<Logo
lightSrc={branding.lightTheme?.logoUrl}
darkSrc={branding.darkTheme?.logoUrl}
height={150}
width={150}
/>
)}
</div>
<div className="w-full">{children}</div>
<div className="flex flex-row justify-between"></div>
</div>
</div>
</LayoutProviders>
{/* </ThemeProvider> */}
</div>
</ThemeWrapper>
);
}

View File

@@ -1,6 +1,5 @@
"use client";
import { ZitadelReactProvider } from "@zitadel/react";
import { useTheme } from "next-themes";
type Props = {
@@ -12,8 +11,6 @@ export function LayoutProviders({ children }: Props) {
const isDark = resolvedTheme === "dark";
return (
<div className={`${isDark ? "ui-dark" : "ui-light"} `}>
<ZitadelReactProvider dark={isDark}>{children}</ZitadelReactProvider>
</div>
<div className={`${isDark ? "ui-dark" : "ui-light"} `}>{children}</div>
);
}

View File

@@ -1,18 +1,16 @@
"use client";
import { ReactNode, useState } from "react";
import {
SignInWithGitlab,
SignInWithAzureAD,
SignInWithGoogle,
SignInWithGithub,
} from "@zitadel/react";
import { useRouter } from "next/navigation";
import Alert from "./Alert";
import { IdentityProvider } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { idpTypeToSlug } from "@/lib/idp";
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { startIDPFlow } from "@/lib/server/idp";
import { SignInWithGithub } from "./idps/SignInWithGithub";
import { SignInWithAzureAD } from "./idps/SignInWithAzureAD";
import { SignInWithGoogle } from "./idps/SignInWithGoogle";
import { SignInWithGitlab } from "./idps/SignInWithGitlab";
export interface SignInWithIDPProps {
children?: ReactNode;

View File

@@ -0,0 +1,40 @@
"use client";
import { ReactNode, forwardRef } from "react";
import { IdpButtonClasses, SignInWithIdentityProviderProps } from "./classes";
export const SignInWithAzureAD = forwardRef<
HTMLButtonElement,
SignInWithIdentityProviderProps
>(
({ children, className = "", name = "", ...props }, ref): ReactNode => (
<button
type="button"
ref={ref}
className={`${IdpButtonClasses} ${className}`}
{...props}
>
<div className="h-12 p-[10px] w-12 flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="21"
height="21"
viewBox="0 0 21 21"
className="w-full h-full"
>
<path fill="#f25022" d="M1 1H10V10H1z"></path>
<path fill="#00a4ef" d="M1 11H10V20H1z"></path>
<path fill="#7fba00" d="M11 1H20V10H11z"></path>
<path fill="#ffb900" d="M11 11H20V20H11z"></path>
</svg>
</div>
{children ? (
children
) : (
<span className="ml-4">{name ? name : "Sign in with AzureAD"}</span>
)}
</button>
),
);
SignInWithAzureAD.displayName = "SignInWithAzureAD";

View File

@@ -0,0 +1,58 @@
"use client";
import { ReactNode, forwardRef } from "react";
import { IdpButtonClasses, SignInWithIdentityProviderProps } from "./classes";
export const SignInWithGithub = forwardRef<
HTMLButtonElement,
SignInWithIdentityProviderProps
>(
({ children, className = "", name = "", ...props }, ref): ReactNode => (
<button
type="button"
ref={ref}
className={`${IdpButtonClasses} ${className}`}
{...props}
>
<div className="h-8 w-8 mx-2 flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="1024"
height="1024"
fill="none"
viewBox="0 0 1024 1024"
className="hidden dark:block"
>
<path
fill="#fafafa"
fillRule="evenodd"
d="M512 0C229.12 0 0 229.12 0 512c0 226.56 146.56 417.92 350.08 485.76 25.6 4.48 35.2-10.88 35.2-24.32 0-12.16-.64-52.48-.64-95.36-128.64 23.68-161.92-31.36-172.16-60.16-5.76-14.72-30.72-60.16-52.48-72.32-17.92-9.6-43.52-33.28-.64-33.92 40.32-.64 69.12 37.12 78.72 52.48 46.08 77.44 119.68 55.68 149.12 42.24 4.48-33.28 17.92-55.68 32.64-68.48-113.92-12.8-232.96-56.96-232.96-252.8 0-55.68 19.84-101.76 52.48-137.6-5.12-12.8-23.04-65.28 5.12-135.68 0 0 42.88-13.44 140.8 52.48 40.96-11.52 84.48-17.28 128-17.28 43.52 0 87.04 5.76 128 17.28 97.92-66.56 140.8-52.48 140.8-52.48 28.16 70.4 10.24 122.88 5.12 135.68 32.64 35.84 52.48 81.28 52.48 137.6 0 196.48-119.68 240-233.6 252.8 18.56 16 34.56 46.72 34.56 94.72 0 68.48-.64 123.52-.64 140.8 0 13.44 9.6 29.44 35.2 24.32C877.44 929.92 1024 737.92 1024 512 1024 229.12 794.88 0 512 0z"
clipRule="evenodd"
></path>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1024"
height="1024"
fill="none"
viewBox="0 0 1024 1024"
className="block dark:hidden"
>
<path
fill="#1B1F23"
fillRule="evenodd"
d="M512 0C229.12 0 0 229.12 0 512c0 226.56 146.56 417.92 350.08 485.76 25.6 4.48 35.2-10.88 35.2-24.32 0-12.16-.64-52.48-.64-95.36-128.64 23.68-161.92-31.36-172.16-60.16-5.76-14.72-30.72-60.16-52.48-72.32-17.92-9.6-43.52-33.28-.64-33.92 40.32-.64 69.12 37.12 78.72 52.48 46.08 77.44 119.68 55.68 149.12 42.24 4.48-33.28 17.92-55.68 32.64-68.48-113.92-12.8-232.96-56.96-232.96-252.8 0-55.68 19.84-101.76 52.48-137.6-5.12-12.8-23.04-65.28 5.12-135.68 0 0 42.88-13.44 140.8 52.48 40.96-11.52 84.48-17.28 128-17.28 43.52 0 87.04 5.76 128 17.28 97.92-66.56 140.8-52.48 140.8-52.48 28.16 70.4 10.24 122.88 5.12 135.68 32.64 35.84 52.48 81.28 52.48 137.6 0 196.48-119.68 240-233.6 252.8 18.56 16 34.56 46.72 34.56 94.72 0 68.48-.64 123.52-.64 140.8 0 13.44 9.6 29.44 35.2 24.32C877.44 929.92 1024 737.92 1024 512 1024 229.12 794.88 0 512 0z"
clipRule="evenodd"
></path>
</svg>
</div>
{children ? (
children
) : (
<span className="ml-4">{name ? name : "Sign in with GitHub"}</span>
)}
</button>
),
);
SignInWithGithub.displayName = "SignInWithGithub";

View File

@@ -0,0 +1,25 @@
import { afterEach, describe, expect, test } from "vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { SignInWithGitlab } from "./SignInWithGitlab";
afterEach(cleanup);
describe("<SignInWithGitlab />", () => {
test("renders without crashing", () => {
const { container } = render(<SignInWithGitlab />);
expect(container.firstChild).toBeDefined();
});
test("displays the default text", () => {
render(<SignInWithGitlab />);
const signInText = screen.getByText(/Sign in with Gitlab/i);
expect(signInText).toBeInTheDocument();
});
test("displays the given text", () => {
render(<SignInWithGitlab name={"Gitlab"} />);
const signInText = screen.getByText(/Gitlab/i);
expect(signInText).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,51 @@
"use client";
import { ReactNode, forwardRef } from "react";
import { IdpButtonClasses, SignInWithIdentityProviderProps } from "./classes";
export const SignInWithGitlab = forwardRef<
HTMLButtonElement,
SignInWithIdentityProviderProps
>(
({ children, className = "", name = "", ...props }, ref): ReactNode => (
<button
type="button"
ref={ref}
className={`${IdpButtonClasses} ${className}`}
{...props}
>
<div className="h-12 w-12 flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width={25}
height={24}
fill="none"
>
<path
fill="#e24329"
d="m24.507 9.5-.034-.09L21.082.562a.896.896 0 0 0-1.694.091l-2.29 7.01H7.825L5.535.653a.898.898 0 0 0-1.694-.09L.451 9.411.416 9.5a6.297 6.297 0 0 0 2.09 7.278l.012.01.03.022 5.16 3.867 2.56 1.935 1.554 1.176a1.051 1.051 0 0 0 1.268 0l1.555-1.176 2.56-1.935 5.197-3.89.014-.01A6.297 6.297 0 0 0 24.507 9.5z"
/>
<path
fill="#fc6d26"
d="m24.507 9.5-.034-.09a11.44 11.44 0 0 0-4.56 2.051l-7.447 5.632 4.742 3.584 5.197-3.89.014-.01A6.297 6.297 0 0 0 24.507 9.5z"
/>
<path
fill="#fca326"
d="m7.707 20.677 2.56 1.935 1.555 1.176a1.051 1.051 0 0 0 1.268 0l1.555-1.176 2.56-1.935-4.743-3.584-4.755 3.584z"
/>
<path
fill="#fc6d26"
d="M5.01 11.461a11.43 11.43 0 0 0-4.56-2.05L.416 9.5a6.297 6.297 0 0 0 2.09 7.278l.012.01.03.022 5.16 3.867 4.745-3.584-7.444-5.632z"
/>
</svg>
</div>
{children ? (
children
) : (
<span className="ml-4">{name ? name : "Sign in with GitLab"}</span>
)}
</button>
),
);
SignInWithGitlab.displayName = "SignInWithGitlab";

View File

@@ -0,0 +1,25 @@
import { afterEach, describe, expect, test } from "vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { SignInWithGoogle } from "./SignInWithGoogle";
afterEach(cleanup);
describe("<SignInWithGoogle />", () => {
test("renders without crashing", () => {
const { container } = render(<SignInWithGoogle />);
expect(container.firstChild).toBeDefined();
});
test("displays the default text", () => {
render(<SignInWithGoogle />);
const signInText = screen.getByText(/Sign in with Google/i);
expect(signInText).toBeInTheDocument();
});
test("displays the given text", () => {
render(<SignInWithGoogle name={"Google"} />);
const signInText = screen.getByText(/Google/i);
expect(signInText).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,64 @@
"use client";
import { ReactNode, forwardRef } from "react";
import { IdpButtonClasses, SignInWithIdentityProviderProps } from "./classes";
export const SignInWithGoogle = forwardRef<
HTMLButtonElement,
SignInWithIdentityProviderProps
>(
({ children, className = "", name = "", ...props }, ref): ReactNode => (
<button
type="button"
ref={ref}
className={`${IdpButtonClasses} ${className}`}
{...props}
>
<div className="h-12 w-12 flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve"
id="Capa_1"
viewBox="0 0 150 150"
>
<style>
{
".st0{fill:#1a73e8}.st1{fill:#ea4335}.st2{fill:#4285f4}.st3{fill:#fbbc04}.st4{fill:#34a853}.st5{fill:#4caf50}.st6{fill:#1e88e5}.st7{fill:#e53935}.st8{fill:#c62828}.st9{fill:#fbc02d}.st10{fill:#1565c0}.st11{fill:#2e7d32}.st16{clip-path:url(#SVGID_2_)}.st17{fill:#188038}.st18,.st19{opacity:.2;fill:#fff;enable-background:new}.st19{opacity:.3;fill:#0d652d}.st20{clip-path:url(#SVGID_4_)}.st21{opacity:.3;fill:url(#_45_shadow_1_);enable-background:new}.st22{clip-path:url(#SVGID_6_)}.st23{fill:#fa7b17}.st24,.st25,.st26{opacity:.3;fill:#174ea6;enable-background:new}.st25,.st26{fill:#a50e0e}.st26{fill:#e37400}.st27{fill:url(#Finish_mask_1_)}.st28{fill:#fff}.st29{fill:#0c9d58}.st30,.st31{opacity:.2;fill:#004d40;enable-background:new}.st31{fill:#3e2723}.st32{fill:#ffc107}.st33{fill:#1a237e;enable-background:new}.st33,.st34{opacity:.2}.st35{fill:#1a237e}.st36{fill:url(#SVGID_7_)}.st37{fill:#fbbc05}.st38{clip-path:url(#SVGID_9_);fill:#e53935}.st39{clip-path:url(#SVGID_11_);fill:#fbc02d}.st40{clip-path:url(#SVGID_13_);fill:#e53935}.st41{clip-path:url(#SVGID_15_);fill:#fbc02d}"
}
</style>
<path
d="M120 76.1c0-3.1-.3-6.3-.8-9.3H75.9v17.7h24.8c-1 5.7-4.3 10.7-9.2 13.9l14.8 11.5C115 101.8 120 90 120 76.1z"
style={{
fill: "#4280ef",
}}
/>
<path
d="M75.9 120.9c12.4 0 22.8-4.1 30.4-11.1L91.5 98.4c-4.1 2.8-9.4 4.4-15.6 4.4-12 0-22.1-8.1-25.8-18.9L34.9 95.6c7.8 15.5 23.6 25.3 41 25.3z"
style={{
fill: "#34a353",
}}
/>
<path
d="M50.1 83.8c-1.9-5.7-1.9-11.9 0-17.6L34.9 54.4c-6.5 13-6.5 28.3 0 41.2l15.2-11.8z"
style={{
fill: "#f6b704",
}}
/>
<path
d="M75.9 47.3c6.5-.1 12.9 2.4 17.6 6.9L106.6 41c-8.3-7.8-19.3-12-30.7-11.9-17.4 0-33.2 9.8-41 25.3l15.2 11.8c3.7-10.9 13.8-18.9 25.8-18.9z"
style={{
fill: "#e54335",
}}
/>
</svg>
</div>
{children ? (
children
) : (
<span className="ml-4">{name ? name : "Sign in with Google"}</span>
)}
</button>
),
);
SignInWithGoogle.displayName = "SignInWithGoogle";

View File

@@ -0,0 +1,17 @@
import { ButtonHTMLAttributes, DetailedHTMLProps } from "react";
export const IdpButtonClasses =
"transition-all w-full cursor-pointer flex flex-row items-center bg-background-light-400 text-text-light-500 dark:bg-background-dark-500 dark:text-text-dark-500 border border-divider-light hover:border-black dark:border-divider-dark hover:dark:border-white focus:border-primary-light-500 focus:dark:border-primary-dark-500 outline-none rounded-md px-4 text-sm";
export interface SignInWithIDPProps {
children?: React.ReactNode;
orgId?: string;
}
export type SignInWithIdentityProviderProps = DetailedHTMLProps<
ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> & {
name?: string;
e2e?: string;
};

View File

@@ -6,7 +6,6 @@ import {
getSession,
setSession,
} from "@/lib/zitadel";
import { addSessionToCookie, updateSessionCookie } from "@zitadel/next";
import {
Challenges,
RequestChallenges,
@@ -18,6 +17,7 @@ import {
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { timestampDate, toDate } from "@zitadel/client";
import { create } from "@zitadel/client";
import { addSessionToCookie, updateSessionCookie } from "@/lib/cookies";
type CustomCookieData = {
id: string;

View File

@@ -6,36 +6,16 @@
"dependsOn": ["^build"]
},
"test": {
"dependsOn": [
"@zitadel/node#build",
"@zitadel/client#build",
"@zitadel/react#build",
"@zitadel/next#build"
]
"dependsOn": ["@zitadel/node#build", "@zitadel/client#build"]
},
"test:integration": {
"dependsOn": [
"@zitadel/node#build",
"@zitadel/client#build",
"@zitadel/react#build",
"@zitadel/next#build"
]
"dependsOn": ["@zitadel/node#build", "@zitadel/client#build"]
},
"test:unit": {
"dependsOn": [
"@zitadel/node#build",
"@zitadel/client#build",
"@zitadel/react#build",
"@zitadel/next#build"
]
"dependsOn": ["@zitadel/node#build", "@zitadel/client#build"]
},
"test:watch": {
"dependsOn": [
"@zitadel/node#build",
"@zitadel/client#build",
"@zitadel/react#build",
"@zitadel/next#build"
]
"dependsOn": ["@zitadel/node#build", "@zitadel/client#build"]
}
}
}