Merge pull request #11 from zitadel/username

feat: v2 alpha service, login with username password, email verification
This commit is contained in:
Max Peintner
2023-05-25 07:01:30 +02:00
committed by GitHub
58 changed files with 1606 additions and 457 deletions

View File

@@ -0,0 +1,103 @@
import { Session } from "#/../../packages/zitadel-server/dist";
import { listSessions, server } from "#/lib/zitadel";
import Alert from "#/ui/Alert";
import { Avatar } from "#/ui/Avatar";
import { getAllSessionIds } from "#/utils/cookies";
import { UserPlusIcon, XCircleIcon } from "@heroicons/react/24/outline";
import moment from "moment";
import Link from "next/link";
async function loadSessions(): Promise<Session[]> {
const ids = await getAllSessionIds();
if (ids && ids.length) {
const response = await listSessions(
server,
ids.filter((id: string | undefined) => !!id)
);
return response?.sessions ?? [];
} else {
return [];
}
}
export default async function Page() {
const sessions = await loadSessions();
return (
<div className="flex flex-col items-center space-y-4">
<h1>Accounts</h1>
<p className="ztdl-p mb-6 block">Use your ZITADEL Account</p>
<div className="flex flex-col w-full space-y-2">
{sessions ? (
sessions
.filter((session) => session?.factors?.user?.loginName)
.map((session, index) => {
const validPassword = session?.factors?.password?.verifiedAt;
return (
<Link
key={"session-" + index}
href={
validPassword
? `/signedin?` +
new URLSearchParams({
loginName: session.factors?.user?.loginName as string,
})
: `/password?` +
new URLSearchParams({
loginName: session.factors?.user?.loginName as string,
})
}
className="group flex flex-row items-center bg-background-light-400 dark:bg-background-dark-400 border border-divider-light hover:shadow-lg dark:hover:bg-white/10 py-2 px-4 rounded-md transition-all"
>
<div className="pr-4">
<Avatar
size="small"
loginName={session.factors?.user?.loginName as string}
name={session.factors?.user?.displayName ?? ""}
/>
</div>
<div className="flex flex-col">
<span className="">
{session.factors?.user?.displayName}
</span>
<span className="text-xs opacity-80">
{session.factors?.user?.loginName}
</span>
{validPassword && (
<span className="text-xs opacity-80">
{moment(new Date(validPassword)).fromNow()}
</span>
)}
</div>
<span className="flex-grow"></span>
<div className="relative flex flex-row items-center">
{validPassword ? (
<div className="absolute h-2 w-2 bg-green-500 rounded-full mx-2 transform right-0 group-hover:right-6 transition-all"></div>
) : (
<div className="absolute h-2 w-2 bg-red-500 rounded-full mx-2 transform right-0 group-hover:right-6 transition-all"></div>
)}
<XCircleIcon className="hidden group-hover:block h-5 w-5 transition-all opacity-50 hover:opacity-100" />
</div>
</Link>
);
})
) : (
<Alert>No Sessions available!</Alert>
)}
<Link href="/username">
<div className="flex flex-row items-center py-3 px-4 hover:bg-black/10 dark:hover:bg-white/10 rounded-md transition-all">
<div className="w-8 h-8 mr-4 flex flex-row justify-center items-center rounded-full bg-black/5 dark:bg-white/5">
<UserPlusIcon className="h-5 w-5" />
</div>
<span className="text-sm">Add another account</span>
</div>
</Link>
</div>
</div>
);
}

View File

@@ -10,9 +10,9 @@ export default function Error({ error, reset }: any) {
}, [error]);
return (
<Boundary labels={["Home page Error UI"]} color="pink">
<Boundary labels={["Home page Error UI"]} color="red">
<div className="space-y-4">
<div className="text-sm text-pink-500">
<div className="text-sm text-red-500 dark:text-red-500">
<strong className="font-bold">Error:</strong> {error?.message}
</div>
<div>

View File

@@ -1,15 +1,32 @@
import { ZitadelLogo } from "#/ui/ZitadelLogo";
import { BrandingSettings } from "@zitadel/server";
import React from "react";
import { getBrandingSettings, server } from "#/lib/zitadel";
import { Logo } from "#/ui/Logo";
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const branding = await getBrandingSettings(server);
let partial: Partial<BrandingSettings> | undefined;
if (branding) {
partial = {
lightTheme: branding?.lightTheme,
darkTheme: branding?.darkTheme,
};
}
return (
<div className="mx-auto flex flex-col items-center space-y-4">
<div className="relative">
<ZitadelLogo height={70} width={180} />
{branding && (
<Logo
lightSrc={branding.lightTheme?.logoUrl}
darkSrc={branding.darkTheme?.logoUrl}
height={150}
width={150}
/>
)}
</div>
<div className="w-full">{children}</div>

View File

@@ -12,7 +12,11 @@ export default function Page() {
<h1>Password</h1>
<p className="ztdl-p mb-6 block">Enter your password.</p>
<UserAvatar name="max@zitadel.com"></UserAvatar>
<UserAvatar
showDropdown
displayName="Max Peintner"
loginName="max@zitadel.com"
></UserAvatar>
<div className="w-full">
<TextInput type="password" label="Password" />

View File

@@ -12,8 +12,11 @@ export default function Page() {
<h1>Password</h1>
<p className="ztdl-p mb-6 block">Enter your password.</p>
<UserAvatar name="max@zitadel.com"></UserAvatar>
<UserAvatar
showDropdown
displayName="Max Peintner"
loginName="max@zitadel.com"
></UserAvatar>
<div className="w-full">
<TextInput type="password" label="Password" />
</div>

View File

@@ -12,7 +12,11 @@ export default function Page() {
<h1>Password</h1>
<p className="ztdl-p mb-6 block">Enter your password.</p>
<UserAvatar name="max@zitadel.com"></UserAvatar>
<UserAvatar
showDropdown
displayName="Max Peintner"
loginName="max@zitadel.com"
></UserAvatar>
<div className="w-full">
<TextInput type="password" label="Password" />

View File

@@ -1,32 +1,50 @@
"use client";
import { Button, ButtonVariants } from "#/ui/Button";
import { TextInput } from "#/ui/Input";
import { getSession, server } from "#/lib/zitadel";
import Alert from "#/ui/Alert";
import PasswordForm from "#/ui/PasswordForm";
import UserAvatar from "#/ui/UserAvatar";
import { useRouter } from "next/navigation";
import { getMostRecentCookieWithLoginname } from "#/utils/cookies";
export default function Page() {
const router = useRouter();
export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
}) {
const { loginName } = searchParams;
const sessionFactors = await loadSession(loginName);
async function loadSession(loginName?: string) {
const recent = await getMostRecentCookieWithLoginname(loginName);
return getSession(server, recent.id, recent.token).then((response) => {
if (response?.session) {
return response.session;
}
});
}
return (
<div className="flex flex-col items-center space-y-4">
<h1>Password</h1>
<h1>{sessionFactors?.factors?.user?.displayName ?? "Password"}</h1>
<p className="ztdl-p mb-6 block">Enter your password.</p>
<UserAvatar name="max@zitadel.com"></UserAvatar>
{!sessionFactors && (
<div className="py-4">
<Alert>
Could not get the context of the user. Make sure to enter the
username first or provide a loginName as searchParam.
</Alert>
</div>
)}
<div className="w-full">
<TextInput type="password" label="Password" />
</div>
{sessionFactors && (
<UserAvatar
loginName={loginName ?? sessionFactors.factors?.user?.loginName ?? ""}
displayName={sessionFactors.factors?.user?.displayName}
showDropdown
></UserAvatar>
)}
<div className="flex w-full flex-row items-center justify-between">
<Button
onClick={() => router.back()}
variant={ButtonVariants.Secondary}
>
back
</Button>
<Button variant={ButtonVariants.Primary}>continue</Button>
</div>
<PasswordForm loginName={loginName} />
</div>
);
}

View File

@@ -12,7 +12,11 @@ export default function Page() {
<h1>Password</h1>
<p className="ztdl-p mb-6 block">Enter your password.</p>
<UserAvatar name="max@zitadel.cloud"></UserAvatar>
<UserAvatar
showDropdown
displayName="Max Peintner"
loginName="max@zitadel.com"
></UserAvatar>
<div className="w-full">
<TextInput type="password" label="Password" />

View File

@@ -12,7 +12,11 @@ export default function Page() {
<h1>Password</h1>
<p className="ztdl-p mb-6 block">Enter your password.</p>
<UserAvatar name="max@zitadel.com"></UserAvatar>
<UserAvatar
showDropdown
displayName="Max Peintner"
loginName="max@zitadel.com"
></UserAvatar>
<div className="w-full">
<TextInput type="password" label="Password" />

View File

@@ -1,23 +1,25 @@
import {
getPasswordComplexityPolicy,
getPrivacyPolicy,
getLegalAndSupportSettings,
getPasswordComplexitySettings,
server,
} from "#/lib/zitadel";
import RegisterForm from "#/ui/RegisterForm";
export default async function Page() {
const privacyPolicy = await getPrivacyPolicy(server);
const passwordComplexityPolicy = await getPasswordComplexityPolicy(server);
const legal = await getLegalAndSupportSettings(server);
const passwordComplexitySettings = await getPasswordComplexitySettings(
server
);
return (
<div className="flex flex-col items-center space-y-4">
<h1>Register</h1>
<p className="ztdl-p">Create your ZITADEL account.</p>
{privacyPolicy && passwordComplexityPolicy && (
{legal && passwordComplexitySettings && (
<RegisterForm
privacyPolicy={privacyPolicy}
passwordComplexityPolicy={passwordComplexityPolicy}
legal={legal}
passwordComplexitySettings={passwordComplexitySettings}
></RegisterForm>
)}
</div>

View File

@@ -1,7 +1,5 @@
import { Button, ButtonVariants } from "#/ui/Button";
import { NextPage, NextPageContext } from "next";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
type Props = {
searchParams: { [key: string]: string | string[] | undefined };

View File

@@ -0,0 +1,31 @@
import { getSession, server } from "#/lib/zitadel";
import UserAvatar from "#/ui/UserAvatar";
import { getMostRecentCookieWithLoginname } from "#/utils/cookies";
async function loadSession(loginName: string) {
const recent = await getMostRecentCookieWithLoginname(`${loginName}`);
return getSession(server, recent.id, recent.token).then((response) => {
if (response?.session) {
return response.session;
}
});
}
export default async function Page({ searchParams }: { searchParams: any }) {
const { loginName } = searchParams;
const sessionFactors = await loadSession(loginName);
return (
<div className="flex flex-col items-center space-y-4">
<h1>{`Welcome ${sessionFactors?.factors?.user?.displayName}`}</h1>
<p className="ztdl-p mb-6 block">You are signed in.</p>
<UserAvatar
loginName={loginName ?? sessionFactors?.factors?.user?.loginName}
displayName={sessionFactors?.factors?.user?.displayName}
showDropdown
></UserAvatar>
</div>
);
}

View File

@@ -1,42 +1,12 @@
"use client";
import { Button, ButtonVariants } from "#/ui/Button";
import IdentityProviders from "#/ui/IdentityProviders";
import { TextInput } from "#/ui/Input";
import { useRouter } from "next/navigation";
import UsernameForm from "#/ui/UsernameForm";
export default function Page() {
const router = useRouter();
function submit() {
router.push("/password");
}
return (
<div className="flex flex-col items-center space-y-4">
<h1>Welcome back!</h1>
<p className="ztdl-p">Enter your login data.</p>
<form className="w-full" onSubmit={() => submit()}>
<div className="block">
<TextInput label="Loginname" />
</div>
<div>
<IdentityProviders />
</div>
<div className="mt-8 flex w-full flex-row items-center justify-between">
<Button type="button" variant={ButtonVariants.Secondary}>
back
</Button>
<Button
type="submit"
variant={ButtonVariants.Primary}
onClick={() => submit()}
>
continue
</Button>
</div>
</form>
<UsernameForm />
</div>
);
}

View File

@@ -0,0 +1,28 @@
import VerifyEmailForm from "#/ui/VerifyEmailForm";
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
export default async function Page({ searchParams }: { searchParams: any }) {
const { userID, code, submit, orgID, loginname, passwordset } = searchParams;
return (
<div className="flex flex-col items-center space-y-4">
<h1>Verify user</h1>
<p className="ztdl-p mb-6 block">
Enter the Code provided in the verification email.
</p>
{userID ? (
<VerifyEmailForm
userId={userID}
code={code}
submit={submit === "true"}
/>
) : (
<div className="w-full flex flex-row items-center justify-center border border-yellow-600/40 dark:border-yellow-500/20 bg-yellow-200/30 text-yellow-600 dark:bg-yellow-700/20 dark:text-yellow-200 rounded-md py-2 scroll-px-40">
<ExclamationTriangleIcon className="h-5 w-5 mr-2" />
<span className="text-center text-sm">No userId provided!</span>
</div>
)}
</div>
);
}

View File

@@ -2,13 +2,12 @@ import "#/styles/globals.scss";
import { AddressBar } from "#/ui/AddressBar";
import { GlobalNav } from "#/ui/GlobalNav";
import { Lato } from "next/font/google";
import Byline from "#/ui/Byline";
import { LayoutProviders } from "#/ui/LayoutProviders";
import { Analytics } from "@vercel/analytics/react";
import ThemeWrapper from "#/ui/ThemeWrapper";
import { getBranding } from "#/lib/zitadel";
import { getBrandingSettings } from "#/lib/zitadel";
import { server } from "../lib/zitadel";
import { LabelPolicyColors } from "#/utils/colors";
import { BrandingSettings } from "@zitadel/server";
const lato = Lato({
weight: ["400", "700", "900"],
@@ -25,26 +24,23 @@ export default async function RootLayout({
// later only shown with dev mode enabled
const showNav = true;
const branding = await getBranding(server);
let partialPolicy: LabelPolicyColors | undefined;
console.log(branding);
const branding = await getBrandingSettings(server);
let partial: Partial<BrandingSettings> | undefined;
if (branding) {
partialPolicy = {
backgroundColor: branding?.backgroundColor,
backgroundColorDark: branding?.backgroundColorDark,
primaryColor: branding?.primaryColor,
primaryColorDark: branding?.primaryColorDark,
warnColor: branding?.warnColor,
warnColorDark: branding?.warnColorDark,
fontColor: branding?.fontColor,
fontColorDark: branding?.fontColorDark,
partial = {
lightTheme: branding?.lightTheme,
darkTheme: branding?.darkTheme,
};
}
let domain = process.env.ZITADEL_API_URL;
domain = domain ? domain.replace("https://", "") : "acme.com";
return (
<html lang="en" className={`${lato.className}`} suppressHydrationWarning>
<head />
<body>
<ThemeWrapper branding={partialPolicy}>
<ThemeWrapper branding={partial}>
<LayoutProviders>
<div className="h-screen overflow-y-scroll bg-background-light-600 dark:bg-background-dark-600 bg-[url('/grid-light.svg')] dark:bg-[url('/grid-dark.svg')]">
{showNav && <GlobalNav />}
@@ -54,7 +50,7 @@ export default async function RootLayout({
{showNav && (
<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">
<div className="rounded-lg bg-background-light-400 dark:bg-background-dark-500">
<AddressBar />
<AddressBar domain={domain} />
</div>
</div>
)}
@@ -64,16 +60,6 @@ export default async function RootLayout({
{children}
</div>
</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 ${
showNav ? "" : "max-w-[440px] w-full fixed bottom-4"
}`}
>
<div className="rounded-lg bg-background-light-400 dark:bg-background-dark-500">
<Byline />
</div>
</div>
</div>
</div>
</div>

View File

@@ -4,11 +4,9 @@ import Link from "next/link";
export default function Page() {
return (
<div className="space-y-8">
<h1 className="text-xl font-medium text-gray-800 dark:text-gray-300">
Pages
</h1>
<h1 className="text-xl font-medium">Pages</h1>
<div className="space-y-10 text-white">
<div className="space-y-10">
{demos.map((section) => {
return (
<div key={section.name} className="space-y-5">
@@ -21,14 +19,12 @@ export default function Page() {
<Link
href={`/${item.slug}`}
key={item.name}
className="bg-background-light-400 dark:bg-background-dark-400 group block space-y-1.5 rounded-lg px-5 py-3 hover:bg-background-light-500 hover:dark:bg-background-dark-300 hover:shadow-lg border border-divider-light dark:border-divider-dark transition-all "
className="bg-background-light-400 dark:bg-background-dark-400 group block space-y-1.5 rounded-md px-5 py-3 hover:shadow-lg hover:dark:bg-white/10 border border-divider-light dark:border-divider-dark transition-all "
>
<div className="font-medium text-gray-600 dark:text-gray-200 group-hover:text-gray-900 dark:group-hover:text-gray-300">
{item.name}
</div>
<div className="font-medium">{item.name}</div>
{item.description ? (
<div className="line-clamp-3 text-sm text-gray-500 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-gray-300">
<div className="line-clamp-3 text-sm text-text-light-secondary-500 dark:text-text-dark-secondary-500">
{item.description}
</div>
) : null}

View File

@@ -0,0 +1,20 @@
import { setEmail, server } from "#/lib/zitadel";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
if (body) {
const { userId, code } = body;
// replace with resend Mail method once its implemented
return setEmail(server, userId)
.then((resp) => {
return NextResponse.json(resp);
})
.catch((error) => {
return NextResponse.json(error, { status: 500 });
});
} else {
return NextResponse.error();
}
}

View File

@@ -0,0 +1,124 @@
import { createSession, getSession, server, setSession } from "#/lib/zitadel";
import {
SessionCookie,
addSessionToCookie,
getMostRecentSessionCookie,
updateSessionCookie,
} from "#/utils/cookies";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
if (body) {
const { loginName } = body;
const createdSession = await createSession(server, loginName);
if (createdSession) {
return getSession(
server,
createdSession.sessionId,
createdSession.sessionToken
).then((response) => {
if (response?.session && response.session?.factors?.user?.loginName) {
const sessionCookie: SessionCookie = {
id: createdSession.sessionId,
token: createdSession.sessionToken,
changeDate: response.session.changeDate?.toString() ?? "",
loginName: response.session?.factors?.user?.loginName ?? "",
};
return addSessionToCookie(sessionCookie).then(() => {
return NextResponse.json({ factors: response?.session?.factors });
});
} else {
return NextResponse.json(
{
details:
"could not get session or session does not have loginName",
},
{ status: 500 }
);
}
});
} else {
return NextResponse.error();
}
} else {
return NextResponse.json(
{ details: "Session could not be created" },
{ status: 500 }
);
}
}
/**
*
* @param request password for the most recent session
* @returns the updated most recent Session with the added password
*/
export async function PUT(request: NextRequest) {
const body = await request.json();
if (body) {
const { password } = body;
const recent = await getMostRecentSessionCookie();
return setSession(server, recent.id, recent.token, password)
.then((session) => {
if (session) {
const sessionCookie: SessionCookie = {
id: recent.id,
token: session.sessionToken,
changeDate: session.details?.changeDate?.toString() ?? "",
loginName: recent.loginName,
};
return getSession(server, sessionCookie.id, sessionCookie.token).then(
(response) => {
if (
response?.session &&
response.session.factors?.user?.loginName
) {
const { session } = response;
const newCookie: SessionCookie = {
id: sessionCookie.id,
token: sessionCookie.token,
changeDate: session.changeDate?.toString() ?? "",
loginName: session.factors?.user?.loginName ?? "",
};
return updateSessionCookie(sessionCookie.id, newCookie)
.then(() => {
return NextResponse.json({ factors: session.factors });
})
.catch((error) => {
return NextResponse.json(
{ details: "could not set cookie" },
{ status: 500 }
);
});
} else {
return NextResponse.json(
{
details:
"could not get session or session does not have loginName",
},
{ status: 500 }
);
}
}
);
} else {
return NextResponse.json(
{ details: "Session not be set" },
{ status: 500 }
);
}
})
.catch((error) => {
console.error("erasd", error);
return NextResponse.json(error, { status: 500 });
});
} else {
return NextResponse.error();
}
}

View File

@@ -0,0 +1,19 @@
import { server, verifyEmail } from "#/lib/zitadel";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
if (body) {
const { userId, code } = body;
return verifyEmail(server, userId, code)
.then((resp) => {
return NextResponse.json(resp);
})
.catch((error) => {
return NextResponse.json(error, { status: 500 });
});
} else {
return NextResponse.error();
}
}

View File

@@ -19,35 +19,40 @@ export const demos: { name: string; items: Item[] }[] = [
description: "The page to request a users password",
},
{
name: "Set Password",
slug: "password/set",
description: "The page to set a users password",
},
{
name: "MFA",
slug: "mfa",
description: "The page to request a users mfa method",
},
{
name: "MFA Set",
slug: "mfa/set",
description: "The page to set a users mfa method",
},
{
name: "MFA Create",
slug: "mfa/create",
description: "The page to create a users mfa method",
},
{
name: "Passwordless",
slug: "passwordless",
description: "The page to login a user with his passwordless device",
},
{
name: "Passwordless Create",
slug: "passwordless/create",
description: "The page to add a users passwordless device",
name: "Accounts",
slug: "accounts",
description: "List active and inactive sessions",
},
// {
// name: "Set Password",
// slug: "password/set",
// description: "The page to set a users password",
// },
// {
// name: "MFA",
// slug: "mfa",
// description: "The page to request a users mfa method",
// },
// {
// name: "MFA Set",
// slug: "mfa/set",
// description: "The page to set a users mfa method",
// },
// {
// name: "MFA Create",
// slug: "mfa/create",
// description: "The page to create a users mfa method",
// },
// {
// name: "Passwordless",
// slug: "passwordless",
// description: "The page to login a user with his passwordless device",
// },
// {
// name: "Passwordless Create",
// slug: "passwordless/create",
// description: "The page to add a users passwordless device",
// },
],
},
{
@@ -58,6 +63,11 @@ export const demos: { name: string; items: Item[] }[] = [
slug: "register",
description: "Create your ZITADEL account",
},
{
name: "Verify email",
slug: "verify",
description: "Verify your account with an email code",
},
],
},
];

View File

@@ -1,17 +1,25 @@
import {
management,
ZitadelServer,
ZitadelServerOptions,
getManagement,
orgMetadata,
getServer,
user,
settings,
getServers,
LabelPolicy,
initializeServer,
PrivacyPolicy,
PasswordComplexityPolicy,
session,
GetGeneralSettingsResponse,
CreateSessionResponse,
GetBrandingSettingsResponse,
GetPasswordComplexitySettingsResponse,
GetLegalAndSupportSettingsResponse,
AddHumanUserResponse,
BrandingSettings,
ListSessionsResponse,
LegalAndSupportSettings,
PasswordComplexitySettings,
GetSessionResponse,
VerifyEmailResponse,
SetSessionResponse,
} from "@zitadel/server";
// import { getAuth } from "@zitadel/server/auth";
export const zitadelConfig: ZitadelServerOptions = {
name: "zitadel login",
@@ -26,46 +34,83 @@ if (!getServers().length) {
server = initializeServer(zitadelConfig);
}
export function getBranding(
export function getBrandingSettings(
server: ZitadelServer
): Promise<LabelPolicy | undefined> {
const mgmt = getManagement(server);
return mgmt
.getLabelPolicy(
{},
{
// metadata: orgMetadata(process.env.ZITADEL_ORG_ID ?? "")
}
)
.then((resp) => resp.policy);
): Promise<BrandingSettings | undefined> {
const settingsService = settings.getSettings(server);
return settingsService
.getBrandingSettings({}, {})
.then((resp: GetBrandingSettingsResponse) => resp.settings);
}
export function getPrivacyPolicy(
export function getGeneralSettings(
server: ZitadelServer
): Promise<PrivacyPolicy | undefined> {
const mgmt = getManagement(server);
return mgmt
.getPrivacyPolicy(
{},
{
// metadata: orgMetadata(process.env.ZITADEL_ORG_ID ?? "")
}
)
.then((resp) => resp.policy);
): Promise<string[] | undefined> {
const settingsService = settings.getSettings(server);
return settingsService
.getGeneralSettings({}, {})
.then((resp: GetGeneralSettingsResponse) => resp.supportedLanguages);
}
export function getPasswordComplexityPolicy(
export function getLegalAndSupportSettings(
server: ZitadelServer
): Promise<PasswordComplexityPolicy | undefined> {
const mgmt = getManagement(server);
return mgmt
.getPasswordComplexityPolicy(
{},
{
// metadata: orgMetadata(process.env.ZITADEL_ORG_ID ?? "")
}
)
.then((resp) => resp.policy);
): Promise<LegalAndSupportSettings | undefined> {
const settingsService = settings.getSettings(server);
return settingsService
.getLegalAndSupportSettings({}, {})
.then((resp: GetLegalAndSupportSettingsResponse) => {
return resp.settings;
});
}
export function getPasswordComplexitySettings(
server: ZitadelServer
): Promise<PasswordComplexitySettings | undefined> {
const settingsService = settings.getSettings(server);
return settingsService
.getPasswordComplexitySettings({}, {})
.then((resp: GetPasswordComplexitySettingsResponse) => resp.settings);
}
export function createSession(
server: ZitadelServer,
loginName: string
): Promise<CreateSessionResponse | undefined> {
const sessionService = session.getSession(server);
return sessionService.createSession({ checks: { user: { loginName } } }, {});
}
export function setSession(
server: ZitadelServer,
sessionId: string,
sessionToken: string,
password: string
): Promise<SetSessionResponse | undefined> {
const sessionService = session.getSession(server);
return sessionService.setSession(
{ sessionId, sessionToken, checks: { password: { password } } },
{}
);
}
export function getSession(
server: ZitadelServer,
sessionId: string,
sessionToken: string
): Promise<GetSessionResponse | undefined> {
const sessionService = session.getSession(server);
return sessionService.getSession({ sessionId, sessionToken }, {});
}
export function listSessions(
server: ZitadelServer,
ids: string[]
): Promise<ListSessionsResponse | undefined> {
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 = {
@@ -74,27 +119,56 @@ export type AddHumanUserData = {
email: string;
password: string;
};
export function addHumanUser(
server: ZitadelServer,
{ email, firstName, lastName, password }: AddHumanUserData
): Promise<string> {
const mgmt = getManagement(server);
const mgmt = user.getUser(server);
return mgmt
.addHumanUser(
{
email: { email, isEmailVerified: false },
userName: email,
email: { email },
username: email,
profile: { firstName, lastName },
initialPassword: password,
password: { password },
},
{
// metadata: orgMetadata(process.env.ZITADEL_ORG_ID ?? "")
}
{}
)
.then((resp) => {
console.log("added user", resp.userId);
.then((resp: AddHumanUserResponse) => {
return resp.userId;
});
}
export function verifyEmail(
server: ZitadelServer,
userId: string,
verificationCode: string
): Promise<VerifyEmailResponse> {
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 function setEmail(server: ZitadelServer, userId: string): Promise<any> {
const userservice = user.getUser(server);
return userservice.setEmail(
{
userId,
},
{}
);
}
export { server };

View File

@@ -3,20 +3,13 @@ const nextConfig = {
reactStrictMode: true, // Recommended for the `pages` directory, default in `app`.
swcMinify: true,
experimental: {
// Required:
appDir: true,
serverActions: true,
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "zitadel.com",
port: "",
pathname: "/**",
},
{
protocol: "https",
hostname: "zitadel.cloud",
hostname: process.env.ZITADEL_API_URL.replace("https://", ""),
port: "",
pathname: "/**",
},

View File

@@ -27,7 +27,8 @@
"@zitadel/server": "workspace:*",
"clsx": "1.2.1",
"date-fns": "2.29.3",
"next": "13.3.2-canary.2",
"moment": "^2.29.4",
"next": "13.4.2",
"next-themes": "^0.2.1",
"nice-grpc": "2.0.1",
"react": "18.2.0",
@@ -45,7 +46,6 @@
"@types/tinycolor2": "1.4.3",
"@vercel/git-hooks": "1.0.0",
"@zitadel/tsconfig": "workspace:*",
"zitadel-tailwind-config": "workspace:*",
"autoprefixer": "10.4.13",
"del-cli": "5.0.0",
"eslint-config-zitadel": "workspace:*",
@@ -56,6 +56,7 @@
"prettier-plugin-tailwindcss": "0.1.13",
"tailwindcss": "3.2.4",
"ts-proto": "^1.139.0",
"typescript": "4.8.4"
"typescript": "4.8.4",
"zitadel-tailwind-config": "workspace:*"
}
}

View File

@@ -50,6 +50,28 @@ module.exports = {
theme: {
extend: {
colors,
animation: {
shake: "shake .8s cubic-bezier(.36,.07,.19,.97) both;",
},
keyframes: {
shake: {
"10%, 90%": {
transform: "translate3d(-1px, 0, 0)",
},
"20%, 80%": {
transform: "translate3d(2px, 0, 0)",
},
"30%, 50%, 70%": {
transform: "translate3d(-4px, 0, 0)",
},
"40%, 60%": {
transform: "translate3d(4px, 0, 0)",
},
},
},
},
},
plugins: [require("@tailwindcss/forms")],

View File

@@ -3,7 +3,11 @@
import React from "react";
import { usePathname } from "next/navigation";
export function AddressBar() {
type Props = {
domain: string;
};
export function AddressBar({ domain }: Props) {
const pathname = usePathname();
return (
@@ -24,7 +28,7 @@ export function AddressBar() {
</div>
<div className="flex space-x-1 text-sm font-medium">
<div>
<span className="px-2 text-gray-500">acme.com</span>
<span className="px-2 text-gray-500">{domain}</span>
</div>
{pathname ? (
<>

14
apps/login/ui/Alert.tsx Normal file
View File

@@ -0,0 +1,14 @@
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
type Props = {
children: React.ReactNode;
};
export default function Alert({ children }: Props) {
return (
<div className="flex flex-row items-center justify-center border border-yellow-600/40 dark:border-yellow-500/20 bg-yellow-200/30 text-yellow-600 dark:bg-yellow-700/20 dark:text-yellow-200 rounded-md py-2 scroll-px-40">
<ExclamationTriangleIcon className="flex-shrink-0 h-5 w-5 mr-2 ml-2" />
<span className="text-center text-sm">{children}</span>
</div>
);
}

View File

@@ -1,35 +1,27 @@
import { Color, ColorShade, getColorHash } from "#/utils/colors";
import { useTheme } from "next-themes";
import { FC } from "react";
"use client";
export enum AvatarSize {
SMALL = "small",
BASE = "base",
LARGE = "large",
}
import { ColorShade, getColorHash } from "#/utils/colors";
import { useTheme } from "next-themes";
interface AvatarProps {
name: string | null | undefined;
loginName: string;
imageUrl?: string;
size?: AvatarSize;
size?: "small" | "base" | "large";
shadow?: boolean;
}
export const Avatar: FC<AvatarProps> = ({
size = AvatarSize.BASE,
name,
loginName,
imageUrl,
shadow,
}) => {
const { resolvedTheme } = useTheme();
function getInitials(name: string, loginName: string) {
let credentials = "";
if (name) {
const split = name.split(" ");
const initials = split[0].charAt(0) + (split[1] ? split[1].charAt(0) : "");
credentials = initials;
if (split) {
const initials =
split[0].charAt(0) + (split[1] ? split[1].charAt(0) : "");
credentials = initials;
} else {
credentials = name.charAt(0);
}
} else {
const username = loginName.split("@")[0];
let separator = "_";
@@ -44,6 +36,19 @@ export const Avatar: FC<AvatarProps> = ({
credentials = initials;
}
return credentials;
}
export function Avatar({
size = "base",
name,
loginName,
imageUrl,
shadow,
}: AvatarProps) {
const { resolvedTheme } = useTheme();
const credentials = getInitials(name ?? loginName, loginName);
const color: ColorShade = getColorHash(loginName);
const avatarStyleDark = {
@@ -61,12 +66,12 @@ export const Avatar: FC<AvatarProps> = ({
className={`w-full h-full flex-shrink-0 flex justify-center items-center cursor-default pointer-events-none group-focus:outline-none group-focus:ring-2 transition-colors duration-200 dark:group-focus:ring-offset-blue bg-primary-light-500 text-primary-light-contrast-500 hover:bg-primary-light-400 hover:dark:bg-primary-dark-500 group-focus:ring-primary-light-200 dark:group-focus:ring-primary-dark-400 dark:bg-primary-dark-300 dark:text-primary-dark-contrast-300 dark:text-blue rounded-full ${
shadow ? "shadow" : ""
} ${
size === AvatarSize.LARGE
size === "large"
? "h-20 w-20 font-normal"
: size === AvatarSize.BASE
: size === "base"
? "w-[38px] h-[38px] font-bold"
: size === AvatarSize.SMALL
? "w-[32px] h-[32px] font-bold"
: size === "small"
? "w-[32px] h-[32px] font-bold text-[13px]"
: ""
}`}
style={resolvedTheme === "light" ? avatarStyleLight : avatarStyleDark}
@@ -78,13 +83,11 @@ export const Avatar: FC<AvatarProps> = ({
/>
) : (
<span
className={`uppercase ${
size === AvatarSize.LARGE ? "text-xl" : "text-13px"
}`}
className={`uppercase ${size === "large" ? "text-xl" : "text-13px"}`}
>
{credentials}
</span>
)}
</div>
);
};
}

View File

@@ -8,15 +8,16 @@ const Label = ({
}: {
children: React.ReactNode;
animateRerendering?: boolean;
color?: "default" | "pink" | "blue" | "violet" | "cyan" | "orange";
color?: "default" | "pink" | "blue" | "violet" | "cyan" | "orange" | "red";
}) => {
return (
<div
className={clsx("rounded-full px-1.5 shadow-[0_0_1px_3px_black]", {
className={clsx("rounded-full px-1.5", {
"bg-gray-800 text-gray-500": color === "default",
"bg-pink-500 text-pink-100": color === "pink",
"bg-blue-500 text-blue-100": color === "blue",
"bg-cyan-500 text-cyan-100": color === "cyan",
"bg-red-500 text-red-100": color === "red",
"bg-violet-500 text-violet-100": color === "violet",
"bg-orange-500 text-orange-100": color === "orange",
"animate-[highlight_1s_ease-in-out_1]": animateRerendering,
@@ -36,7 +37,7 @@ export const Boundary = ({
children: React.ReactNode;
labels?: string[];
size?: "small" | "default";
color?: "default" | "pink" | "blue" | "violet" | "cyan" | "orange";
color?: "default" | "pink" | "blue" | "violet" | "cyan" | "orange" | "red";
animateRerendering?: boolean;
}) => {
return (
@@ -48,6 +49,7 @@ export const Boundary = ({
"border-pink-500": color === "pink",
"border-blue-500": color === "blue",
"border-cyan-500": color === "cyan",
"border-red-500": color === "red",
"border-violet-500": color === "violet",
"border-orange-500": color === "orange",
"animate-[rerender_1s_ease-in-out_1] text-pink-500": animateRerendering,
@@ -55,7 +57,7 @@ export const Boundary = ({
>
<div
className={clsx(
"absolute -top-2.5 flex space-x-1 text-[9px] uppercase leading-4 tracking-widest",
"absolute -top-2 flex space-x-1 text-[9px] uppercase leading-4 tracking-widest",
{
"left-3 lg:left-5": size === "small",
"left-4 lg:left-9": size === "default",

View File

@@ -1,13 +0,0 @@
import Theme from "./Theme";
export default function Byline() {
return (
<div className="flex items-center justify-between w-full p-3.5 lg:px-5 lg:py-3">
<div className="flex items-center space-x-1.5">
<div className="text-sm text-gray-600">By</div>
<div className="text-sm font-semibold">ZITADEL</div>
</div>
<Theme />
</div>
);
}

View File

@@ -7,13 +7,14 @@ import { useSelectedLayoutSegment, usePathname } from "next/navigation";
import clsx from "clsx";
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/solid";
import { useState } from "react";
import Theme from "./Theme";
export function GlobalNav() {
const [isOpen, setIsOpen] = useState(false);
const close = () => setIsOpen(false);
return (
<div className="fixed top-0 z-10 flex w-full flex-col border-b border-divider-light dark:border-divider-dark bg-background-light-700 dark:bg-background-dark-700 lg:bottom-0 lg:z-auto lg:w-72 lg:border-r">
<div className="fixed top-0 z-10 flex w-full flex-col border-b border-divider-light dark:border-divider-dark bg-white/80 dark:bg-black/80 lg:bottom-0 lg:z-auto lg:w-72 lg:border-r">
<div className="flex h-14 items-center py-4 px-4 lg:h-auto">
<Link
href="/"
@@ -29,27 +30,34 @@ export function GlobalNav() {
</h2>
</Link>
</div>
<button
type="button"
className="group absolute right-0 top-0 flex h-14 items-center space-x-2 px-4 lg:hidden"
onClick={() => setIsOpen(!isOpen)}
>
<div className="font-medium text-text-light-secondary-500 group-hover:text-text-light-500 dark:text-text-dark-secondary-500 dark:group-hover:text-text-dark-500">
Menu
</div>
{isOpen ? (
<XMarkIcon className="block w-6 " />
) : (
<Bars3Icon className="block w-6 " />
)}
</button>
<div className="absolute right-0 top-0 flex flex-row items-center lg:hidden">
<Theme />
<button
type="button"
className="group flex h-14 items-center space-x-2 px-4"
onClick={() => setIsOpen(!isOpen)}
>
<div className="font-medium text-text-light-secondary-500 group-hover:text-text-light-500 dark:text-text-dark-secondary-500 dark:group-hover:text-text-dark-500">
Menu
</div>
{isOpen ? (
<XMarkIcon className="block w-6 " />
) : (
<Bars3Icon className="block w-6 " />
)}
</button>
</div>
<div
className={clsx("overflow-y-auto lg:static lg:block", {
"fixed inset-x-0 bottom-0 top-14 mt-px bg-background-light-500 dark:bg-background-dark-500":
isOpen,
hidden: !isOpen,
})}
className={clsx(
"overflow-y-auto lg:static lg:flex lg:flex-col justify-between h-full",
{
"fixed inset-x-0 bottom-0 top-14 mt-px bg-white/80 dark:bg-black/80 backdrop-blur-lg":
isOpen,
hidden: !isOpen,
}
)}
>
<nav
className={`space-y-6 px-4 py-5 ${
@@ -72,6 +80,10 @@ export function GlobalNav() {
);
})}
</nav>
<div className="flex flex-row p-4">
<Theme />
</div>
</div>
</div>
);

37
apps/login/ui/Logo.tsx Normal file
View File

@@ -0,0 +1,37 @@
import Image from "next/image";
type Props = {
darkSrc?: string;
lightSrc?: string;
height?: number;
width?: number;
};
export function Logo({ lightSrc, darkSrc, height = 40, width = 147.5 }: Props) {
return (
<>
{darkSrc && (
<div className="hidden dark:flex">
<Image
height={height}
width={width}
src={darkSrc}
alt="logo"
priority={true}
/>
</div>
)}
{lightSrc && (
<div className="flex dark:hidden">
<Image
height={height}
width={width}
priority={true}
src={lightSrc}
alt="logo"
/>
</div>
)}
</>
);
}

View File

@@ -4,10 +4,10 @@ import {
symbolValidator,
upperCaseValidator,
} from "#/utils/validators";
import { PasswordComplexityPolicy } from "@zitadel/server";
import { PasswordComplexitySettings } from "@zitadel/server";
type Props = {
passwordComplexityPolicy: PasswordComplexityPolicy;
passwordComplexitySettings: PasswordComplexitySettings;
password: string;
equals: boolean;
};
@@ -48,11 +48,11 @@ const desc =
"text-14px leading-4 text-input-light-label dark:text-input-dark-label";
export default function PasswordComplexity({
passwordComplexityPolicy,
passwordComplexitySettings,
password,
equals,
}: Props) {
const hasMinLength = password?.length >= passwordComplexityPolicy.minLength;
const hasMinLength = password?.length >= passwordComplexitySettings.minLength;
const hasSymbol = symbolValidator(password);
const hasNumber = numberValidator(password);
const hasUppercase = upperCaseValidator(password);
@@ -63,7 +63,7 @@ export default function PasswordComplexity({
<div className="flex flex-row items-center">
{hasMinLength ? check : cross}
<span className={desc}>
Password length {passwordComplexityPolicy.minLength}
Password length {passwordComplexitySettings.minLength}
</span>
</div>
<div className="flex flex-row items-center">

View File

@@ -0,0 +1,101 @@
"use client";
import { useState } from "react";
import { Button, ButtonVariants } from "./Button";
import { TextInput } from "./Input";
import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
import { Spinner } from "./Spinner";
import Alert from "./Alert";
type Inputs = {
password: string;
};
type Props = {
loginName?: string;
};
export default function PasswordForm({ loginName }: Props) {
const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur",
});
const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const router = useRouter();
async function submitPassword(values: Inputs) {
setError("");
setLoading(true);
const res = await fetch("/session", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
password: values.password,
}),
});
const response = await res.json();
setLoading(false);
if (!res.ok) {
setError(response.details);
return Promise.reject(response.details);
}
return response;
}
function submitPasswordAndContinue(value: Inputs): Promise<boolean | void> {
return submitPassword(value).then((resp: any) => {
return router.push(`/accounts`);
});
}
const { errors } = formState;
return (
<form className="w-full">
<div className={`${error && "transform-gpu animate-shake"}`}>
<TextInput
type="password"
autoComplete="password"
{...register("password", { required: "This field is required" })}
label="Password"
// error={errors.username?.message as string}
/>
{loginName && (
<input type="hidden" name="loginName" value={loginName} />
)}
</div>
{error && (
<div className="py-4">
<Alert>{error}</Alert>
</div>
)}
<div className="mt-8 flex w-full flex-row items-center">
{/* <Button type="button" variant={ButtonVariants.Secondary}>
back
</Button> */}
<span className="flex-grow"></span>
<Button
type="submit"
className="self-end"
variant={ButtonVariants.Primary}
disabled={loading || !formState.isValid}
onClick={handleSubmit(submitPasswordAndContinue)}
>
{loading && <Spinner className="h-5 w-5 mr-2" />}
continue
</Button>
</div>
</form>
);
}

View File

@@ -2,10 +2,10 @@
import React, { useState } from "react";
import Link from "next/link";
import { Checkbox } from "./Checkbox";
import { PrivacyPolicy } from "@zitadel/server";
import { LegalAndSupportSettings } from "@zitadel/server";
type Props = {
privacyPolicy: PrivacyPolicy;
legal: LegalAndSupportSettings;
onChange: (allAccepted: boolean) => void;
};
@@ -14,7 +14,7 @@ type AcceptanceState = {
privacyPolicyAccepted: boolean;
};
export function PrivacyPolicyCheckboxes({ privacyPolicy, onChange }: Props) {
export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) {
const [acceptanceState, setAcceptanceState] = useState<AcceptanceState>({
tosAccepted: false,
privacyPolicyAccepted: false,
@@ -24,9 +24,9 @@ export function PrivacyPolicyCheckboxes({ privacyPolicy, onChange }: Props) {
<>
<p className="flex flex-row items-center text-text-light-secondary-500 dark:text-text-dark-secondary-500 mt-4 text-sm">
To register you must agree to the terms and conditions
{privacyPolicy?.helpLink && (
{legal?.helpLink && (
<span>
<Link href={privacyPolicy.helpLink} target="_blank">
<Link href={legal.helpLink} target="_blank">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
@@ -45,7 +45,7 @@ export function PrivacyPolicyCheckboxes({ privacyPolicy, onChange }: Props) {
</span>
)}
</p>
{privacyPolicy?.tosLink && (
{legal?.tosLink && (
<div className="mt-4 flex items-center">
<Checkbox
className="mr-4"
@@ -62,18 +62,14 @@ export function PrivacyPolicyCheckboxes({ privacyPolicy, onChange }: Props) {
<div className="mr-4 w-[28rem]">
<p className="text-sm text-text-light-500 dark:text-text-dark-500">
Agree&nbsp;
<Link
href={privacyPolicy.tosLink}
className="underline"
target="_blank"
>
<Link href={legal.tosLink} className="underline" target="_blank">
Terms of Service
</Link>
</p>
</div>
</div>
)}
{privacyPolicy?.privacyLink && (
{legal?.privacyPolicyLink && (
<div className="mt-4 flex items-center">
<Checkbox
className="mr-4"
@@ -91,7 +87,7 @@ export function PrivacyPolicyCheckboxes({ privacyPolicy, onChange }: Props) {
<p className="text-sm text-text-light-500 dark:text-text-dark-500">
Agree&nbsp;
<Link
href={privacyPolicy.privacyLink}
href={legal.privacyPolicyLink}
className="underline"
target="_blank"
>

View File

@@ -1,6 +1,9 @@
"use client";
import { PasswordComplexityPolicy, PrivacyPolicy } from "@zitadel/server";
import {
LegalAndSupportSettings,
PasswordComplexitySettings,
} from "@zitadel/server";
import PasswordComplexity from "./PasswordComplexity";
import { useState } from "react";
import { Button, ButtonVariants } from "./Button";
@@ -27,13 +30,13 @@ type Inputs =
| FieldValues;
type Props = {
privacyPolicy: PrivacyPolicy;
passwordComplexityPolicy: PasswordComplexityPolicy;
legal: LegalAndSupportSettings;
passwordComplexitySettings: PasswordComplexitySettings;
};
export default function RegisterForm({
privacyPolicy,
passwordComplexityPolicy,
legal,
passwordComplexitySettings,
}: Props) {
const { register, handleSubmit, watch, formState } = useForm<Inputs>({
mode: "onBlur",
@@ -69,7 +72,7 @@ export default function RegisterForm({
function submitAndLink(value: Inputs): Promise<boolean | void> {
return submitRegister(value).then((resp: any) => {
return router.push(`/register/success?userid=${resp.userId}`);
return router.push(`/verify?userID=${resp.userId}`);
});
}
@@ -81,19 +84,19 @@ export default function RegisterForm({
const [tosAndPolicyAccepted, setTosAndPolicyAccepted] = useState(false);
const hasMinLength =
passwordComplexityPolicy &&
watchPassword?.length >= passwordComplexityPolicy.minLength;
passwordComplexitySettings &&
watchPassword?.length >= passwordComplexitySettings.minLength;
const hasSymbol = symbolValidator(watchPassword);
const hasNumber = numberValidator(watchPassword);
const hasUppercase = upperCaseValidator(watchPassword);
const hasLowercase = lowerCaseValidator(watchPassword);
const policyIsValid =
passwordComplexityPolicy &&
(passwordComplexityPolicy.hasLowercase ? hasLowercase : true) &&
(passwordComplexityPolicy.hasNumber ? hasNumber : true) &&
(passwordComplexityPolicy.hasUppercase ? hasUppercase : true) &&
(passwordComplexityPolicy.hasSymbol ? hasSymbol : true) &&
passwordComplexitySettings &&
(passwordComplexitySettings.requiresLowercase ? hasLowercase : true) &&
(passwordComplexitySettings.requiresNumber ? hasNumber : true) &&
(passwordComplexitySettings.requiresUppercase ? hasUppercase : true) &&
(passwordComplexitySettings.requiresSymbol ? hasSymbol : true) &&
hasMinLength;
return (
@@ -155,17 +158,17 @@ export default function RegisterForm({
</div>
</div>
{passwordComplexityPolicy && (
{passwordComplexitySettings && (
<PasswordComplexity
passwordComplexityPolicy={passwordComplexityPolicy}
passwordComplexitySettings={passwordComplexitySettings}
password={watchPassword}
equals={!!watchPassword && watchPassword === watchConfirmPassword}
/>
)}
{privacyPolicy && (
{legal && (
<PrivacyPolicyCheckboxes
privacyPolicy={privacyPolicy}
legal={legal}
onChange={setTosAndPolicyAccepted}
/>
)}

View File

@@ -1,11 +1,10 @@
"use client";
import { Switch } from "@headlessui/react";
import { MoonIcon, SunIcon } from "@heroicons/react/24/outline";
import React, { useEffect, useState } from "react";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { MoonIcon, SunIcon } from "@heroicons/react/24/outline";
export default function Theme() {
function Theme() {
const { resolvedTheme, setTheme } = useTheme();
const [mounted, setMounted] = useState<boolean>(false);
@@ -21,28 +20,27 @@ export default function Theme() {
}
return (
<Switch
checked={isDark}
onChange={(checked) => setTheme(checked ? "dark" : "light")}
className={`${
isDark
? "!bg-gray-800 dark:bg-background-dark-400"
: "!bg-gray-200 dark:bg-background-dark-400"
}
relative inline-flex h-4 w-9 items-center rounded-full`}
<div
className={`relative grid grid-cols-2 rounded-full border border-divider-light dark:border-divider-dark p-1`}
>
<div
aria-hidden="true"
className={`${
isDark ? "translate-x-5" : "translate-x-0"
} flex flex-row items-center justify-center h-4 w-4 transform rounded-full bg-white transition-all shadow dark:bg-background-dark-500 ring-1 ring-[#00000020] dark:ring-[#ffffff20] ring-offset-1 ring-offset-[#ffffff50] dark:ring-offset-[#00000005]`}
<button
className={`h-8 w-8 rounded-full flex flex-row items-center justify-center hover:opacity-100 transition-all ${
isDark ? "bg-black/10 dark:bg-white/10" : "opacity-60"
}`}
onClick={() => setTheme("dark")}
>
{isDark ? (
<MoonIcon className="dark:text-amber-500 h-4 w-4" />
) : (
<SunIcon className="text-amber-500 h-4 w-4" />
)}
</div>
</Switch>
<MoonIcon className="h-4 w-4 flex-shrink-0 text-xl rounded-full" />
</button>
<button
className={`h-8 w-8 rounded-full flex flex-row items-center justify-center hover:opacity-100 transition-all ${
!isDark ? "bg-black/10 dark:bg-white/10" : "opacity-60"
}`}
onClick={() => setTheme("light")}
>
<SunIcon className="h-6 w-6 flex-shrink-0 text-xl rounded-full" />
</button>
</div>
);
}
export default Theme;

View File

@@ -1,10 +1,11 @@
"use client";
import { setTheme, LabelPolicyColors } from "#/utils/colors";
import { BrandingSettings } from "@zitadel/server";
import { setTheme } from "#/utils/colors";
import { useEffect } from "react";
type Props = {
branding: LabelPolicyColors | undefined;
branding: Partial<BrandingSettings> | undefined;
children: React.ReactNode;
};

View File

@@ -1,16 +1,37 @@
import { Avatar, AvatarSize } from "#/ui/Avatar";
import { Avatar } from "#/ui/Avatar";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
type Props = {
name: string;
loginName: string;
displayName?: string;
showDropdown: boolean;
};
export default function UserAvatar({ name }: Props) {
export default function UserAvatar({
loginName,
displayName,
showDropdown,
}: Props) {
return (
<div className="flex h-full w-full flex-row items-center rounded-full border p-[1px] dark:border-white/20">
<div>
<Avatar size={AvatarSize.SMALL} name={name} loginName={name} />
<Avatar
size="small"
name={displayName ?? loginName}
loginName={loginName}
/>
</div>
<span className="ml-4 text-14px">{name}</span>
<span className="ml-4 text-14px">{loginName}</span>
<span className="flex-grow"></span>
{showDropdown && (
<Link
href="/accounts"
className="flex items-center justify-center p-1 hover:bg-black/10 dark:hover:bg-white/10 rounded-full mr-1 transition-all"
>
<ChevronDownIcon className="h-4 w-4" />
</Link>
)}
</div>
);
}

View File

@@ -0,0 +1,83 @@
"use client";
import { useState } from "react";
import { Button, ButtonVariants } from "./Button";
import { TextInput } from "./Input";
import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
import { Spinner } from "./Spinner";
type Inputs = {
loginName: string;
};
export default function UsernameForm() {
const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur",
});
const [loading, setLoading] = useState<boolean>(false);
const router = useRouter();
async function submitUsername(values: Inputs) {
setLoading(true);
const res = await fetch("/session", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
loginName: values.loginName,
}),
});
setLoading(false);
if (!res.ok) {
throw new Error("Failed to set user");
}
return res.json();
}
function submitUsernameAndContinue(value: Inputs): Promise<boolean | void> {
return submitUsername(value).then(({ factors }) => {
return router.push(
`/password?` +
new URLSearchParams({ loginName: `${factors.user.loginName}` })
);
});
}
const { errors } = formState;
return (
<form className="w-full">
<div className="">
<TextInput
type="text"
autoComplete="username"
{...register("loginName", { required: "This field is required" })}
label="Loginname"
// error={errors.username?.message as string}
/>
</div>
<div className="mt-8 flex w-full flex-row items-center">
{/* <Button type="button" variant={ButtonVariants.Secondary}>
back
</Button> */}
<span className="flex-grow"></span>
<Button
type="submit"
className="self-end"
variant={ButtonVariants.Primary}
disabled={loading || !formState.isValid}
onClick={handleSubmit(submitUsernameAndContinue)}
>
{loading && <Spinner className="h-5 w-5 mr-2" />}
continue
</Button>
</div>
</form>
);
}

View File

@@ -0,0 +1,133 @@
"use client";
import { useEffect, useState } from "react";
import { Button, ButtonVariants } from "./Button";
import { TextInput } from "./Input";
import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
import { Spinner } from "./Spinner";
import Alert from "#/ui/Alert";
type Inputs = {
code: string;
};
type Props = {
userId: string;
code: string;
submit: boolean;
};
export default function VerifyEmailForm({ userId, code, submit }: Props) {
const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur",
defaultValues: {
code: code ?? "",
},
});
useEffect(() => {
if (submit && code && userId) {
submitCode({ code });
}
}, []);
const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const router = useRouter();
async function submitCode(values: Inputs) {
setLoading(true);
const res = await fetch("/verifyemail", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code: values.code,
userId,
}),
});
const response = await res.json();
if (!res.ok) {
setLoading(false);
setError(response.details);
return Promise.reject(response);
} else {
setLoading(false);
return response;
}
}
async function resendCode() {
setLoading(true);
const res = await fetch("/resendverifyemail", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const response = await res.json();
if (!res.ok) {
setLoading(false);
setError(response.details);
return Promise.reject(response);
} else {
setLoading(false);
return response;
}
}
function submitCodeAndContinue(value: Inputs): Promise<boolean | void> {
return submitCode(value).then((resp: any) => {
return router.push(`/username`);
});
}
return (
<form className="w-full">
<div className="">
<TextInput
type="text"
autoComplete="one-time-code"
{...register("code", { required: "This field is required" })}
label="Code"
// error={errors.username?.message as string}
/>
</div>
{error && (
<div className="py-4">
<Alert>{error}</Alert>
</div>
)}
<div className="mt-8 flex w-full flex-row items-center">
<Button
type="button"
onClick={() => resendCode()}
variant={ButtonVariants.Secondary}
>
resend code
</Button>
<span className="flex-grow"></span>
<Button
type="submit"
className="self-end"
variant={ButtonVariants.Primary}
disabled={loading || !formState.isValid}
onClick={handleSubmit(submitCodeAndContinue)}
>
{loading && <Spinner className="h-5 w-5 mr-2" />}
continue
</Button>
</div>
</form>
);
}

View File

@@ -1,5 +1,7 @@
import tinycolor from "tinycolor2";
import { BrandingSettings } from "@zitadel/server";
export interface Color {
name: string;
hex: string;
@@ -52,32 +54,36 @@ export type LabelPolicyColors = {
primaryColorDark: string;
};
export function setTheme(document: any, policy?: LabelPolicyColors) {
const lP = {
backgroundColor: BACKGROUND,
backgroundColorDark: DARK_BACKGROUND,
primaryColor: PRIMARY,
primaryColorDark: DARK_PRIMARY,
warnColor: WARN,
warnColorDark: DARK_WARN,
fontColor: TEXT,
fontColorDark: DARK_TEXT,
linkColor: TEXT,
linkColorDark: DARK_TEXT,
type BrandingColors = {
lightTheme: {
backgroundColor: string;
fontColor: string;
primaryColor: string;
warnColor: string;
};
darkTheme: {
backgroundColor: string;
fontColor: string;
primaryColor: string;
warnColor: string;
};
};
if (policy) {
lP.backgroundColor = policy.backgroundColor;
lP.backgroundColorDark = policy.backgroundColorDark;
lP.primaryColor = policy.primaryColor;
lP.primaryColorDark = policy.primaryColorDark;
lP.warnColor = policy.warnColor;
lP.warnColorDark = policy.warnColorDark;
lP.fontColor = policy.fontColor;
lP.fontColorDark = policy.fontColorDark;
lP.linkColor = policy.fontColor;
lP.linkColorDark = policy.fontColorDark;
}
export function setTheme(document: any, policy?: Partial<BrandingSettings>) {
const lP: BrandingColors = {
lightTheme: {
backgroundColor: policy?.lightTheme?.backgroundColor ?? BACKGROUND,
fontColor: policy?.lightTheme?.fontColor ?? TEXT,
primaryColor: policy?.lightTheme?.primaryColor ?? PRIMARY,
warnColor: policy?.lightTheme?.warnColor ?? WARN,
},
darkTheme: {
backgroundColor: policy?.darkTheme?.backgroundColor ?? DARK_BACKGROUND,
fontColor: policy?.darkTheme?.fontColor ?? DARK_TEXT,
primaryColor: policy?.darkTheme?.primaryColor ?? DARK_PRIMARY,
warnColor: policy?.darkTheme?.warnColor ?? DARK_WARN,
},
};
const dark = computeMap(lP, true);
const light = computeMap(lP, false);
@@ -177,25 +183,24 @@ function getContrast(color: string): string {
}
}
export function computeMap(
labelpolicy: LabelPolicyColors,
dark: boolean
): ColorMap {
export function computeMap(branding: BrandingColors, dark: boolean): ColorMap {
return {
background: computeColors(
dark ? labelpolicy.backgroundColorDark : labelpolicy.backgroundColor
dark
? branding.darkTheme.backgroundColor
: branding.lightTheme.backgroundColor
),
primary: computeColors(
dark ? labelpolicy.primaryColorDark : labelpolicy.primaryColor
dark ? branding.darkTheme.primaryColor : branding.lightTheme.primaryColor
),
warn: computeColors(
dark ? labelpolicy.warnColorDark : labelpolicy.warnColor
dark ? branding.darkTheme.warnColor : branding.lightTheme.warnColor
),
text: computeColors(
dark ? labelpolicy.fontColorDark : labelpolicy.fontColor
dark ? branding.darkTheme.fontColor : branding.lightTheme.fontColor
),
link: computeColors(
dark ? labelpolicy.fontColorDark : labelpolicy.fontColor
dark ? branding.darkTheme.fontColor : branding.lightTheme.fontColor
),
};
}

145
apps/login/utils/cookies.ts Normal file
View File

@@ -0,0 +1,145 @@
"use server";
import { cookies } from "next/headers";
export type SessionCookie = {
id: string;
token: string;
loginName: string;
changeDate: string;
};
function setSessionHttpOnlyCookie(sessions: SessionCookie[]) {
const cookiesList = cookies();
// @ts-ignore
return cookiesList.set({
name: "sessions",
value: JSON.stringify(sessions),
httpOnly: true,
path: "/",
});
}
export async function addSessionToCookie(session: SessionCookie): Promise<any> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
let currentSessions: SessionCookie[] = 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];
}
setSessionHttpOnlyCookie(currentSessions);
}
export async function updateSessionCookie(
id: string,
session: SessionCookie
): Promise<any> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
const sessions: SessionCookie[] = stringifiedCookie?.value
? JSON.parse(stringifiedCookie?.value)
: [session];
const foundIndex = sessions.findIndex((session) => session.id === id);
sessions[foundIndex] = session;
return setSessionHttpOnlyCookie(sessions);
}
export async function removeSessionFromCookie(
session: SessionCookie
): Promise<any> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
const sessions: SessionCookie[] = stringifiedCookie?.value
? JSON.parse(stringifiedCookie?.value)
: [session];
const filteredSessions = sessions.filter((s) => s.id !== session.id);
return setSessionHttpOnlyCookie(filteredSessions);
}
export async function getMostRecentSessionCookie(): Promise<any> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
const sessions: SessionCookie[] = 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();
}
}
export async function getAllSessionIds(): Promise<any> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value);
return sessions.map((session) => session.id);
} else {
return Promise.reject();
}
}
/**
* Returns most recent session filtered by optinal loginName
* @param loginName
* @returns most recent session
*/
export async function getMostRecentCookieWithLoginname(
loginName?: string
): Promise<any> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value);
const filtered = sessions.filter((cookie) => {
return !!loginName ? cookie.loginName === loginName : true;
});
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 {
return Promise.reject();
}
} else {
return Promise.reject();
}
}
export async function clearSessions() {}

View File

@@ -17,7 +17,7 @@
"eslint": "^7.32.0",
"eslint-config-zitadel": "workspace:*",
"prettier": "^2.5.1",
"turbo": "latest"
"turbo": "^1.9.8"
},
"packageManager": "pnpm@7.15.0"
}

View File

@@ -22,14 +22,15 @@
"@types/react": "^17.0.13",
"@types/react-dom": "^17.0.8",
"@zitadel/tsconfig": "workspace:*",
"zitadel-tailwind-config": "workspace:*",
"autoprefixer": "10.4.13",
"eslint": "^7.32.0",
"eslint-config-zitadel": "workspace:*",
"postcss": "8.4.21",
"sass": "^1.62.0",
"tailwindcss": "3.2.4",
"tsup": "^5.10.1",
"typescript": "^4.5.3"
"typescript": "^4.5.3",
"zitadel-tailwind-config": "workspace:*"
},
"publishConfig": {
"access": "public"

View File

@@ -4,14 +4,15 @@
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"type": "commonjs",
"sideEffects": false,
"license": "MIT",
"files": [
"dist/**"
],
"scripts": {
"build": "tsup src/index.ts src/*/index.ts --format esm,cjs --dts",
"dev": "tsup src/index.ts src/*/index.ts --format esm,cjs --watch --dts",
"build": "tsup --dts",
"dev": "tsup --dts --watch",
"lint": "eslint \"src/**/*.ts*\"",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
"prebuild": "pnpm run generate",

View File

@@ -1,12 +1,54 @@
export * from "./server";
import * as settings from "./v2/settings";
import * as session from "./v2/session";
import * as user from "./v2/user";
import * as login from "./proto/server/zitadel/settings/v2alpha/login_settings";
import * as password from "./proto/server/zitadel/settings/v2alpha/password_settings";
import * as legal from "./proto/server/zitadel/settings/v2alpha/legal_settings";
export {
BrandingSettings,
Theme,
} from "./proto/server/zitadel/settings/v2alpha/branding_settings";
export { Session } from "./proto/server/zitadel/session/v2alpha/session";
export {
ListSessionsResponse,
GetSessionResponse,
CreateSessionResponse,
SetSessionResponse,
} from "./proto/server/zitadel/session/v2alpha/session_service";
export {
GetPasswordComplexitySettingsResponse,
GetBrandingSettingsResponse,
GetLegalAndSupportSettingsResponse,
GetGeneralSettingsResponse,
} from "./proto/server/zitadel/settings/v2alpha/settings_service";
export {
AddHumanUserResponse,
VerifyEmailResponse,
} from "./proto/server/zitadel/user/v2alpha/user_service";
export { type LegalAndSupportSettings } from "./proto/server/zitadel/settings/v2alpha/legal_settings";
export { type PasswordComplexitySettings } from "./proto/server/zitadel/settings/v2alpha/password_settings";
import {
getServers,
initializeServer,
ZitadelServer,
ZitadelServerOptions,
} from "./server";
export * from "./middleware";
export * from "./management";
// export * as auth from "./auth";
// export * as management from "./management";
// export * as admin from "./admin";
// export * as system from "./system";
// export * from "./proto/server/zitadel/management";
// export * from "./proto/server/zitadel/system";
// export * from "./proto/server/zitadel/admin";
export {
getServers,
ZitadelServer,
type ZitadelServerOptions,
initializeServer,
user,
session,
settings,
login,
password,
legal,
};

View File

@@ -1,3 +1,3 @@
export * from "./management";
export * as management from "../proto/server/zitadel/management";
export * from "../proto/server/zitadel/policy";
export * as policy from "../proto/server/zitadel/policy";

View File

@@ -1,3 +1,11 @@
import { createChannel, createClientFactory } from "nice-grpc";
import {
SettingsServiceClient,
SettingsServiceDefinition,
} from "./proto/server/zitadel/settings/v2alpha/settings_service";
import { authMiddleware } from "./middleware";
import { CompatServiceDefinition } from "nice-grpc/lib/service-definitions";
let apps: ZitadelServer[] = [];
export interface ZitadelServerProps {
@@ -49,3 +57,18 @@ export function getServer(name?: string): ZitadelServer {
}
}
}
export const createClient = <Client>(
definition: CompatServiceDefinition,
apiUrl: string,
token: string
) => {
if (!apiUrl) {
throw Error("ZITADEL_API_URL not set");
}
const channel = createChannel(process.env.ZITADEL_API_URL ?? "");
return createClientFactory()
.use(authMiddleware(token))
.create(definition, channel) as Client;
};

View File

@@ -0,0 +1,2 @@
export * from "./session";
export * from "../../proto/server/zitadel/session/v2alpha/session";

View File

@@ -0,0 +1,28 @@
import { CompatServiceDefinition } from "nice-grpc/lib/service-definitions";
import {
SessionServiceClient,
SessionServiceDefinition,
} from "../../proto/server/zitadel/session/v2alpha/session_service";
import { ZitadelServer, createClient, getServers } from "../../server";
export const getSession = (server?: string | ZitadelServer) => {
let config;
if (server && typeof server === "string") {
const apps = getServers();
config = apps.find((a) => a.name === server)?.config;
} else if (server && typeof server === "object") {
config = server.config;
}
if (!config) {
throw Error("No ZITADEL server found");
}
return createClient<SessionServiceClient>(
SessionServiceDefinition as CompatServiceDefinition,
config.apiUrl,
config.token
);
};

View File

@@ -0,0 +1,2 @@
export * from "./settings";
export * from "../../proto/server/zitadel/settings/v2alpha/settings";

View File

@@ -0,0 +1,28 @@
import { CompatServiceDefinition } from "nice-grpc/lib/service-definitions";
import {
SettingsServiceClient,
SettingsServiceDefinition,
} from "../../proto/server/zitadel/settings/v2alpha/settings_service";
import { ZitadelServer, createClient, getServers } from "../../server";
export const getSettings = (server?: string | ZitadelServer) => {
let config;
if (server && typeof server === "string") {
const apps = getServers();
config = apps.find((a) => a.name === server)?.config;
} else if (server && typeof server === "object") {
config = server.config;
}
if (!config) {
throw Error("No ZITADEL server found");
}
return createClient<SettingsServiceClient>(
SettingsServiceDefinition as CompatServiceDefinition,
config.apiUrl,
config.token
);
};

View File

@@ -0,0 +1,2 @@
export * from "./user";
export * from "../../proto/server/zitadel/user/v2alpha/user";

View File

@@ -0,0 +1,28 @@
import { CompatServiceDefinition } from "nice-grpc/lib/service-definitions";
import {
UserServiceClient,
UserServiceDefinition,
} from "../../proto/server/zitadel/user/v2alpha/user_service";
import { ZitadelServer, createClient, getServers } from "../../server";
export const getUser = (server?: string | ZitadelServer) => {
let config;
if (server && typeof server === "string") {
const apps = getServers();
config = apps.find((a) => a.name === server)?.config;
} else if (server && typeof server === "object") {
config = server.config;
}
if (!config) {
throw Error("No ZITADEL server found");
}
return createClient<UserServiceClient>(
UserServiceDefinition as CompatServiceDefinition,
config.apiUrl,
config.token
);
};

View File

@@ -3,12 +3,7 @@
"include": ["src/**/*.ts"],
"compilerOptions": {
"baseUrl": ".",
"rootDir": "src",
"paths": {
"#": ["."],
"*": ["./*"],
"#/*": ["./*"]
}
"rootDir": "src"
},
"exclude": ["dist", "build", "node_modules"]
}

View File

@@ -0,0 +1,13 @@
import { defineConfig, Options } from "tsup";
export default defineConfig((options: Options) => ({
treeshake: true,
splitting: true,
publicDir: true,
entry: ["src/index.ts", "src/**/index.ts"],
format: ["esm", "cjs"],
dts: true,
minify: true,
clean: true,
...options,
}));

142
pnpm-lock.yaml generated
View File

@@ -8,13 +8,13 @@ importers:
eslint: ^7.32.0
eslint-config-zitadel: workspace:*
prettier: ^2.5.1
turbo: latest
turbo: ^1.9.8
devDependencies:
'@changesets/cli': 2.25.2
eslint: 7.32.0
eslint-config-zitadel: link:packages/eslint-config-zitadel
prettier: 2.8.0
turbo: 1.9.3
turbo: 1.9.8
apps/login:
specifiers:
@@ -41,7 +41,8 @@ importers:
grpc-tools: 1.11.3
lint-staged: 13.0.3
make-dir-cli: 3.0.0
next: 13.3.2-canary.2
moment: ^2.29.4
next: 13.4.2
next-themes: ^0.2.1
nice-grpc: 2.0.1
postcss: 8.4.21
@@ -65,8 +66,9 @@ importers:
'@zitadel/server': link:../../packages/zitadel-server
clsx: 1.2.1
date-fns: 2.29.3
next: 13.3.2-canary.2_krg7tz6h6n3fx3eq7tclunioeu
next-themes: 0.2.1_fbdiuaz6irj67j3n36ky3t2nbi
moment: 2.29.4
next: 13.4.2_krg7tz6h6n3fx3eq7tclunioeu
next-themes: 0.2.1_cmp7sjki5xcmfyvhcokzzink7a
nice-grpc: 2.0.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
@@ -154,6 +156,7 @@ importers:
'@types/react': ^17.0.13
'@types/react-dom': ^17.0.8
'@zitadel/tsconfig': workspace:*
autoprefixer: 10.4.13
eslint: ^7.32.0
eslint-config-zitadel: workspace:*
postcss: 8.4.21
@@ -169,6 +172,7 @@ importers:
'@types/react': 17.0.52
'@types/react-dom': 17.0.18
'@zitadel/tsconfig': link:../zitadel-tsconfig
autoprefixer: 10.4.13_postcss@8.4.21
eslint: 7.32.0
eslint-config-zitadel: link:../eslint-config-zitadel
postcss: 8.4.21
@@ -667,8 +671,8 @@ packages:
resolution: {integrity: sha512-FN50r/E+b8wuqyRjmGaqvqNDuWBWYWQiigfZ50KnSFH0f+AMQQyaZl+Zm2+CIpKk0fL9QxhLxOpTVA3xFHgFow==}
dev: false
/@next/env/13.3.2-canary.2:
resolution: {integrity: sha512-/NqWjXLGlNpGkxPAXR8TDWT6ZYsYGwWNfwhpPhtyMtUOU78wwWiT5p/smGd/+h/PFaIeLjrjtqiA7hHqrw0u0A==}
/@next/env/13.4.2:
resolution: {integrity: sha512-Wqvo7lDeS0KGwtwg9TT9wKQ8raelmUxt+TQKWvG/xKfcmDXNOtCuaszcfCF8JzlBG1q0VhpI6CKaRMbVPMDWgw==}
dev: false
/@next/eslint-plugin-next/13.3.1:
@@ -704,8 +708,8 @@ packages:
dev: false
optional: true
/@next/swc-darwin-arm64/13.3.2-canary.2:
resolution: {integrity: sha512-HdqGogdJAF88hzmVLhSXu/msxlkv2MP395natN1MmGxjqfTNGLSJewWmPf4vdOBIP54lDc6Nap/b2joYWOrCDw==}
/@next/swc-darwin-arm64/13.4.2:
resolution: {integrity: sha512-6BBlqGu3ewgJflv9iLCwO1v1hqlecaIH2AotpKfVUEzUxuuDNJQZ2a4KLb4MBl8T9/vca1YuWhSqtbF6ZuUJJw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
@@ -722,8 +726,8 @@ packages:
dev: false
optional: true
/@next/swc-darwin-x64/13.3.2-canary.2:
resolution: {integrity: sha512-u9LPNpaRXjKi6WPDqhrXEYW3UJxyf3J2mva8fmb3CGZHR8BrkItRDcn7VDgSZ0jTHRHpCGqYXlPE+z6+bVYdeg==}
/@next/swc-darwin-x64/13.4.2:
resolution: {integrity: sha512-iZuYr7ZvGLPjPmfhhMl0ISm+z8EiyLBC1bLyFwGBxkWmPXqdJ60mzuTaDSr5WezDwv0fz32HB7JHmRC6JVHSZg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
@@ -758,8 +762,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-gnu/13.3.2-canary.2:
resolution: {integrity: sha512-e/aUm7RZoDcvLHrK7sTiRMX3cS+1LVlN2gUKV9PYrrXGftuQGkIwJyZPUm4nsJUX7ozNWXPU50YeHPvt9K0c2Q==}
/@next/swc-linux-arm64-gnu/13.4.2:
resolution: {integrity: sha512-2xVabFtIge6BJTcJrW8YuUnYTuQjh4jEuRuS2mscyNVOj6zUZkom3CQg+egKOoS+zh2rrro66ffSKIS+ztFJTg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -776,8 +780,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-musl/13.3.2-canary.2:
resolution: {integrity: sha512-wDvtL9LcN0pSao+M/A3qSYVHvPcyH1H9d0v7aIbwd6F/JuTIlTeXgKuxVCYY5OBNC6dXbzOyGSREZ8hLCx9Wjw==}
/@next/swc-linux-arm64-musl/13.4.2:
resolution: {integrity: sha512-wKRCQ27xCUJx5d6IivfjYGq8oVngqIhlhSAJntgXLt7Uo9sRT/3EppMHqUZRfyuNBTbykEre1s5166z+pvRB5A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -794,8 +798,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-gnu/13.3.2-canary.2:
resolution: {integrity: sha512-Z/GTeCcD6YK92rBdrAa5GVLC9TzXkXpGKnlDLJLm/2oY1eBRTVpQT5/vp0vrRcPYjdHXubizquk1Q3eyAtlKTg==}
/@next/swc-linux-x64-gnu/13.4.2:
resolution: {integrity: sha512-NpCa+UVhhuNeaFVUP1Bftm0uqtvLWq2JTm7+Ta48+2Uqj2mNXrDIvyn1DY/ZEfmW/1yvGBRaUAv9zkMkMRixQA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -812,8 +816,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-musl/13.3.2-canary.2:
resolution: {integrity: sha512-P0KCzP17aoxfq3k+rtgDhOl8BILdgw3pw8w88/qD5WA2xK2R9Rg4lRI6pAQSro0++ToNDgnrXpRuJov7n1OfeQ==}
/@next/swc-linux-x64-musl/13.4.2:
resolution: {integrity: sha512-ZWVC72x0lW4aj44e3khvBrj2oSYj1bD0jESmyah3zG/3DplEy/FOtYkMzbMjHTdDSheso7zH8GIlW6CDQnKhmQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -830,8 +834,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-arm64-msvc/13.3.2-canary.2:
resolution: {integrity: sha512-yGpQpU0To4gp/bjhwKHqu3zVJ/Jco+g4Okv95IWnbYUX7sd14kophZGwHiZN4dLErB9Pdd4vvmz8ccJP5h+Ubg==}
/@next/swc-win32-arm64-msvc/13.4.2:
resolution: {integrity: sha512-pLT+OWYpzJig5K4VKhLttlIfBcVZfr2+Xbjra0Tjs83NQSkFS+y7xx+YhCwvpEmXYLIvaggj2ONPyjbiigOvHQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@@ -848,8 +852,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-ia32-msvc/13.3.2-canary.2:
resolution: {integrity: sha512-iHtddC48Xdl7RxCdhBWZ6+1hq/eC0duTR4y3yYPELpXpZnIwGjOT5W5N+3nVRXUVLsj6teRf8fEfWBp3WbJ0RQ==}
/@next/swc-win32-ia32-msvc/13.4.2:
resolution: {integrity: sha512-dhpiksQCyGca4WY0fJyzK3FxMDFoqMb0Cn+uDB+9GYjpU2K5//UGPQlCwiK4JHxuhg8oLMag5Nf3/IPSJNG8jw==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
@@ -866,8 +870,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-x64-msvc/13.3.2-canary.2:
resolution: {integrity: sha512-Ctw3gL8cBMvREpJM09xvC+pPKsG8TVSWxsQPTLvD33qFED0gtU9HSIacJ09eXd8mqtRGebcXaNjY9fVFfGHZ3A==}
/@next/swc-win32-x64-msvc/13.4.2:
resolution: {integrity: sha512-O7bort1Vld00cu8g0jHZq3cbSTUNMohOEvYqsqE10+yfohhdPHzvzO+ziJRz4Dyyr/fYKREwS7gR4JC0soSOMw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -948,8 +952,8 @@ packages:
tslib: 2.4.1
dev: false
/@swc/helpers/0.5.0:
resolution: {integrity: sha512-SjY/p4MmECVVEWspzSRpQEM3sjR17sP8PbGxELWrT+YZMBfiUyt1MRUNjMV23zohwlG2HYtCQOsCwsTHguXkyg==}
/@swc/helpers/0.5.1:
resolution: {integrity: sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==}
dependencies:
tslib: 2.4.1
dev: false
@@ -1354,7 +1358,7 @@ packages:
postcss: ^8.1.0
dependencies:
browserslist: 4.21.5
caniuse-lite: 1.0.30001434
caniuse-lite: 1.0.30001473
fraction.js: 4.2.0
normalize-range: 0.1.2
picocolors: 1.0.0
@@ -1481,6 +1485,7 @@ packages:
/caniuse-lite/1.0.30001434:
resolution: {integrity: sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==}
dev: false
/caniuse-lite/1.0.30001473:
resolution: {integrity: sha512-ewDad7+D2vlyy+E4UJuVfiBsU69IL+8oVmTuZnH5Q6CIUbxNfI50uVpRHbUPDD6SUaN2o0Lh4DhTrvLG/Tn1yg==}
@@ -3581,6 +3586,10 @@ packages:
hasBin: true
dev: true
/moment/2.29.4:
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
dev: false
/ms/2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
dev: false
@@ -3608,14 +3617,14 @@ packages:
/natural-compare/1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
/next-themes/0.2.1_fbdiuaz6irj67j3n36ky3t2nbi:
/next-themes/0.2.1_cmp7sjki5xcmfyvhcokzzink7a:
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
peerDependencies:
next: '*'
react: '*'
react-dom: '*'
dependencies:
next: 13.3.2-canary.2_krg7tz6h6n3fx3eq7tclunioeu
next: 13.4.2_krg7tz6h6n3fx3eq7tclunioeu
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
@@ -3667,9 +3676,9 @@ packages:
- babel-plugin-macros
dev: false
/next/13.3.2-canary.2_krg7tz6h6n3fx3eq7tclunioeu:
resolution: {integrity: sha512-tAJBdhzzQxzomn2Ge3lR3zCVPBnPSfXy6+fTQTDtZHDQe/pH9xJgnMpwvA8kBYEr5yrCcJn0U3kxeo32LRJUjw==}
engines: {node: '>=14.18.0'}
/next/13.4.2_krg7tz6h6n3fx3eq7tclunioeu:
resolution: {integrity: sha512-aNFqLs3a3nTGvLWlO9SUhCuMUHVPSFQC0+tDNGAsDXqx+WJDFSbvc233gOJ5H19SBc7nw36A9LwQepOJ2u/8Kg==}
engines: {node: '>=16.8.0'}
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
@@ -3688,8 +3697,8 @@ packages:
sass:
optional: true
dependencies:
'@next/env': 13.3.2-canary.2
'@swc/helpers': 0.5.0
'@next/env': 13.4.2
'@swc/helpers': 0.5.1
busboy: 1.6.0
caniuse-lite: 1.0.30001473
postcss: 8.4.14
@@ -3697,16 +3706,17 @@ packages:
react-dom: 18.2.0_react@18.2.0
sass: 1.62.0
styled-jsx: 5.1.1_react@18.2.0
zod: 3.21.4
optionalDependencies:
'@next/swc-darwin-arm64': 13.3.2-canary.2
'@next/swc-darwin-x64': 13.3.2-canary.2
'@next/swc-linux-arm64-gnu': 13.3.2-canary.2
'@next/swc-linux-arm64-musl': 13.3.2-canary.2
'@next/swc-linux-x64-gnu': 13.3.2-canary.2
'@next/swc-linux-x64-musl': 13.3.2-canary.2
'@next/swc-win32-arm64-msvc': 13.3.2-canary.2
'@next/swc-win32-ia32-msvc': 13.3.2-canary.2
'@next/swc-win32-x64-msvc': 13.3.2-canary.2
'@next/swc-darwin-arm64': 13.4.2
'@next/swc-darwin-x64': 13.4.2
'@next/swc-linux-arm64-gnu': 13.4.2
'@next/swc-linux-arm64-musl': 13.4.2
'@next/swc-linux-x64-gnu': 13.4.2
'@next/swc-linux-x64-musl': 13.4.2
'@next/swc-win32-arm64-msvc': 13.4.2
'@next/swc-win32-ia32-msvc': 13.4.2
'@next/swc-win32-x64-msvc': 13.4.2
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
@@ -5072,65 +5082,65 @@ packages:
yargs: 17.6.2
dev: true
/turbo-darwin-64/1.9.3:
resolution: {integrity: sha512-0dFc2cWXl82kRE4Z+QqPHhbEFEpUZho1msHXHWbz5+PqLxn8FY0lEVOHkq5tgKNNEd5KnGyj33gC/bHhpZOk5g==}
/turbo-darwin-64/1.9.8:
resolution: {integrity: sha512-PkTdBjPfgpj/Dob/6SjkzP0BBP80/KmFjLEocXVEECCLJE6tHKbWLRdvc79B0N6SufdYdZ1uvvoU3KPtBokSPw==}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/turbo-darwin-arm64/1.9.3:
resolution: {integrity: sha512-1cYbjqLBA2zYE1nbf/qVnEkrHa4PkJJbLo7hnuMuGM0bPzh4+AnTNe98gELhqI1mkTWBu/XAEeF5u6dgz0jLNA==}
/turbo-darwin-arm64/1.9.8:
resolution: {integrity: sha512-sLwqOx3XV57QCEoJM9GnDDnnqidG8wf29ytxssBaWHBdeJTjupyrmzTUrX+tyKo3Q+CjWvbPLyqVqxT4g5NuXQ==}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/turbo-linux-64/1.9.3:
resolution: {integrity: sha512-UuBPFefawEwpuxh5pM9Jqq3q4C8M0vYxVYlB3qea/nHQ80pxYq7ZcaLGEpb10SGnr3oMUUs1zZvkXWDNKCJb8Q==}
/turbo-linux-64/1.9.8:
resolution: {integrity: sha512-AMg6VT6sW7aOD1uOs5suxglXfTYz9T0uVyKGKokDweGOYTWmuTMGU5afUT1tYRUwQ+kVPJI+83Atl5Ob0oBsgw==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/turbo-linux-arm64/1.9.3:
resolution: {integrity: sha512-vUrNGa3hyDtRh9W0MkO+l1dzP8Co2gKnOVmlJQW0hdpOlWlIh22nHNGGlICg+xFa2f9j4PbQlWTsc22c019s8Q==}
/turbo-linux-arm64/1.9.8:
resolution: {integrity: sha512-tLnxFv+OIklwTjiOZ8XMeEeRDAf150Ry4BCivNwgTVFAqQGEqkFP6KGBy56hb5RRF1frPQpoPGipJNVm7c8m1w==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/turbo-windows-64/1.9.3:
resolution: {integrity: sha512-0BZ7YaHs6r+K4ksqWus1GKK3W45DuDqlmfjm/yuUbTEVc8szmMCs12vugU2Zi5GdrdJSYfoKfEJ/PeegSLIQGQ==}
/turbo-windows-64/1.9.8:
resolution: {integrity: sha512-r3pCjvXTMR7kq2E3iqwFlN1R7pFO/TOsuUjMhOSPP7HwuuUIinAckU4I9foM3q7ZCQd1XXScBUt3niDyHijAqQ==}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/turbo-windows-arm64/1.9.3:
resolution: {integrity: sha512-QJUYLSsxdXOsR1TquiOmLdAgtYcQ/RuSRpScGvnZb1hY0oLc7JWU0llkYB81wVtWs469y8H9O0cxbKwCZGR4RQ==}
/turbo-windows-arm64/1.9.8:
resolution: {integrity: sha512-CWzRbX2TM5IfHBC6uWM659qUOEDC4h0nn16ocG8yIq1IF3uZMzKRBHgGOT5m1BHom+R08V0NcjTmPRoqpiI0dg==}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/turbo/1.9.3:
resolution: {integrity: sha512-ID7mxmaLUPKG/hVkp+h0VuucB1U99RPCJD9cEuSEOdIPoSIuomcIClEJtKamUsdPLhLCud+BvapBNnhgh58Nzw==}
/turbo/1.9.8:
resolution: {integrity: sha512-dTouGZBm4a2fE0OPafcTQERCp4i3ZOow0Pr0JlOyxKmzJy0JRwXypH013kbZoK6k1ET5tS/g9rwUXIM/AmWXXQ==}
hasBin: true
requiresBuild: true
optionalDependencies:
turbo-darwin-64: 1.9.3
turbo-darwin-arm64: 1.9.3
turbo-linux-64: 1.9.3
turbo-linux-arm64: 1.9.3
turbo-windows-64: 1.9.3
turbo-windows-arm64: 1.9.3
turbo-darwin-64: 1.9.8
turbo-darwin-arm64: 1.9.8
turbo-linux-64: 1.9.8
turbo-linux-arm64: 1.9.8
turbo-windows-64: 1.9.8
turbo-windows-arm64: 1.9.8
dev: true
/type-check/0.4.0:
@@ -5420,3 +5430,7 @@ packages:
/yocto-queue/0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
/zod/3.21.4:
resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==}
dev: false

View File

@@ -23,11 +23,5 @@
}
},
"globalDependencies": ["**/.env.*local"],
"globalEnv": [
"ZITADEL_API_URL",
"ZITADEL_PROJECT_ID",
"ZITADEL_APP_ID",
"ZITADEL_SERVICE_USER_TOKEN",
"ZITADEL_ORG_ID"
]
"globalEnv": ["ZITADEL_API_URL", "ZITADEL_SERVICE_USER_TOKEN"]
}