session service, username, password form

This commit is contained in:
Max Peintner
2023-05-16 17:34:52 +02:00
parent f149f1aebc
commit 8a190e28c6
20 changed files with 395 additions and 133 deletions

View File

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

View File

@@ -2,41 +2,15 @@
import { Button, ButtonVariants } from "#/ui/Button"; import { Button, ButtonVariants } from "#/ui/Button";
import IdentityProviders from "#/ui/IdentityProviders"; import IdentityProviders from "#/ui/IdentityProviders";
import { TextInput } from "#/ui/Input"; import UsernameForm from "#/ui/UsernameForm";
import { useRouter } from "next/navigation";
export default function Page() { export default function Page() {
const router = useRouter();
function submit() {
router.push("/password");
}
return ( return (
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>Welcome back!</h1> <h1>Welcome back!</h1>
<p className="ztdl-p">Enter your login data.</p> <p className="ztdl-p">Enter your login data.</p>
<form className="w-full" onSubmit={() => submit()}> <UsernameForm />
<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>
</div> </div>
); );
} }

View File

@@ -8,7 +8,7 @@ import { Analytics } from "@vercel/analytics/react";
import ThemeWrapper from "#/ui/ThemeWrapper"; import ThemeWrapper from "#/ui/ThemeWrapper";
import { getBrandingSettings } from "#/lib/zitadel"; import { getBrandingSettings } from "#/lib/zitadel";
import { server } from "../lib/zitadel"; import { server } from "../lib/zitadel";
import { LabelPolicyColors } from "#/utils/colors"; import { BrandingSettings } from "@zitadel/server";
const lato = Lato({ const lato = Lato({
weight: ["400", "700", "900"], weight: ["400", "700", "900"],
@@ -25,26 +25,20 @@ export default async function RootLayout({
// later only shown with dev mode enabled // later only shown with dev mode enabled
const showNav = true; const showNav = true;
const branding = await getBrandingSettings(server); // const general = await getGeneralSettings(server);
let partialPolicy: LabelPolicyColors | undefined; const branding: BrandingSettings = await getBrandingSettings(server);
console.log(branding); let partial: Partial<BrandingSettings> | undefined;
if (branding) { if (branding) {
partialPolicy = { partial = {
backgroundColor: branding?.backgroundColor, lightTheme: branding?.lightTheme,
backgroundColorDark: branding?.backgroundColorDark, darkTheme: branding?.darkTheme,
primaryColor: branding?.primaryColor,
primaryColorDark: branding?.primaryColorDark,
warnColor: branding?.warnColor,
warnColorDark: branding?.warnColorDark,
fontColor: branding?.fontColor,
fontColorDark: branding?.fontColorDark,
}; };
} }
return ( return (
<html lang="en" className={`${lato.className}`} suppressHydrationWarning> <html lang="en" className={`${lato.className}`} suppressHydrationWarning>
<head /> <head />
<body> <body>
<ThemeWrapper branding={partialPolicy}> <ThemeWrapper branding={partial}>
<LayoutProviders> <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')]"> <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 />} {showNav && <GlobalNav />}

View File

@@ -0,0 +1,26 @@
import { createSession, server, setSession } from "#/lib/zitadel";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
if (body) {
const { loginName } = body;
const session = await createSession(server, loginName);
return NextResponse.json(session);
} else {
return NextResponse.error();
}
}
export async function PUT(request: NextRequest) {
const body = await request.json();
if (body) {
const { loginName } = body;
const session = await setSession(server, loginName);
return NextResponse.json(session);
} else {
return NextResponse.error();
}
}

View File

@@ -1,14 +1,12 @@
import { import {
management,
ZitadelServer, ZitadelServer,
ZitadelServerOptions, ZitadelServerOptions,
orgMetadata, management,
getServer, settings,
getServers, getServers,
initializeServer, initializeServer,
settings, session,
} from "@zitadel/server"; } from "@zitadel/server";
// import { getAuth } from "@zitadel/server/auth";
export const zitadelConfig: ZitadelServerOptions = { export const zitadelConfig: ZitadelServerOptions = {
name: "zitadel login", name: "zitadel login",
@@ -38,6 +36,21 @@ export function getBrandingSettings(
.then((resp) => resp.settings); .then((resp) => resp.settings);
} }
export function getGeneralSettings(
server: ZitadelServer
): Promise<any | undefined> {
// settings.branding_settings.BrandingSettings
const settingsService = settings.getSettings(server);
return settingsService
.getGeneralSettings(
{},
{
// metadata: orgMetadata(process.env.ZITADEL_ORG_ID ?? "")
}
)
.then((resp) => resp.supportedLanguages);
}
export function getLegalAndSupportSettings( export function getLegalAndSupportSettings(
server: ZitadelServer server: ZitadelServer
): Promise<any | undefined> { ): Promise<any | undefined> {
@@ -56,6 +69,7 @@ export function getPasswordComplexitySettings(
server: ZitadelServer server: ZitadelServer
): Promise<any | undefined> { ): Promise<any | undefined> {
const settingsService = settings.getSettings(server); const settingsService = settings.getSettings(server);
return settingsService return settingsService
.getPasswordComplexitySettings( .getPasswordComplexitySettings(
{}, {},
@@ -66,6 +80,22 @@ export function getPasswordComplexitySettings(
.then((resp) => resp.settings); .then((resp) => resp.settings);
} }
export function createSession(
server: ZitadelServer,
loginName: string
): Promise<any | undefined> {
const sessionService = session.getSession(server);
return sessionService.createSession({ checks: { user: { loginName } } }, {});
}
export function setSession(
server: ZitadelServer,
loginName: string
): Promise<any | undefined> {
const sessionService = session.getSession(server);
return sessionService.setSession({ checks: { user: { loginName } } }, {});
}
export type AddHumanUserData = { export type AddHumanUserData = {
firstName: string; firstName: string;
lastName: string; lastName: string;

View File

@@ -0,0 +1,84 @@
"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 = {
password: string;
};
export default function UsernameForm() {
const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur",
});
const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const router = useRouter();
async function submitUsername(values: Inputs) {
setLoading(true);
const res = await fetch("/session", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
password: values.password,
}),
});
if (!res.ok) {
setLoading(false);
throw new Error("Failed to register user");
}
setLoading(false);
return res.json();
}
function submitAndLink(value: Inputs): Promise<boolean | void> {
return submitUsername(value).then((resp: any) => {
return router.push(`/password`);
});
}
const { errors } = formState;
return (
<form className="w-full">
<div className="">
<TextInput
type="password"
autoComplete="password"
{...register("password", { 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(submitAndLink)}
>
{loading && <Spinner className="h-5 w-5 mr-2" />}
continue
</Button>
</div>
</form>
);
}

View File

@@ -1,6 +1,9 @@
"use client"; "use client";
import { PasswordComplexityPolicy, PrivacyPolicy } from "@zitadel/server"; import {
LegalAndSupportSettings,
PasswordComplexitySettings,
} from "@zitadel/server";
import PasswordComplexity from "./PasswordComplexity"; import PasswordComplexity from "./PasswordComplexity";
import { useState } from "react"; import { useState } from "react";
import { Button, ButtonVariants } from "./Button"; import { Button, ButtonVariants } from "./Button";
@@ -27,8 +30,8 @@ type Inputs =
| FieldValues; | FieldValues;
type Props = { type Props = {
privacyPolicy: PrivacyPolicy; privacyPolicy: LegalAndSupportSettings;
passwordComplexityPolicy: PasswordComplexityPolicy; passwordComplexityPolicy: PasswordComplexitySettings;
}; };
export default function RegisterForm({ export default function RegisterForm({
@@ -90,10 +93,10 @@ export default function RegisterForm({
const policyIsValid = const policyIsValid =
passwordComplexityPolicy && passwordComplexityPolicy &&
(passwordComplexityPolicy.hasLowercase ? hasLowercase : true) && (passwordComplexityPolicy.requiresLowercase ? hasLowercase : true) &&
(passwordComplexityPolicy.hasNumber ? hasNumber : true) && (passwordComplexityPolicy.requiresNumber ? hasNumber : true) &&
(passwordComplexityPolicy.hasUppercase ? hasUppercase : true) && (passwordComplexityPolicy.requiresUppercase ? hasUppercase : true) &&
(passwordComplexityPolicy.hasSymbol ? hasSymbol : true) && (passwordComplexityPolicy.requiresSymbol ? hasSymbol : true) &&
hasMinLength; hasMinLength;
return ( return (

View File

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

View File

@@ -0,0 +1,82 @@
"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,
}),
});
if (!res.ok) {
setLoading(false);
throw new Error("Failed to register user");
}
setLoading(false);
return res.json();
}
function submitAndLink(value: Inputs): Promise<boolean | void> {
return submitUsername(value).then((resp: any) => {
return router.push(`/password`);
});
}
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(submitAndLink)}
>
{loading && <Spinner className="h-5 w-5 mr-2" />}
continue
</Button>
</div>
</form>
);
}

View File

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

View File

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

View File

@@ -1,13 +1,36 @@
export * from "./server"; import * as management from "./management";
import * as settings from "./v2/settings";
import * as session from "./v2/session";
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 { 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 "./middleware";
export * as management from "./management";
export * as settings from "./v2/settings";
// export * as auth from "./auth"; export {
// export * as management from "./management"; getServers,
// export * as admin from "./admin"; ZitadelServer,
// export * as system from "./system"; type ZitadelServerOptions,
initializeServer,
// export * from "./proto/server/zitadel/management"; management,
// export * from "./proto/server/zitadel/system"; session,
// export * from "./proto/server/zitadel/admin"; settings,
login,
password,
legal,
};

View File

@@ -1,3 +1,3 @@
export * from "./management"; export * from "./management";
export * as management from "../proto/server/zitadel/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[] = []; let apps: ZitadelServer[] = [];
export interface ZitadelServerProps { 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,29 @@
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) => {
console.log("init session");
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

@@ -1,6 +1,2 @@
export * from "./settings"; export * from "./settings";
export * from "../../proto/server/zitadel/settings/v2alpha/settings"; export * from "../../proto/server/zitadel/settings/v2alpha/settings";
export * as branding from "../../proto/server/zitadel/settings/v2alpha/branding_settings";
export * as login from "../../proto/server/zitadel/settings/v2alpha/login_settings";
export * as password from "../../proto/server/zitadel/settings/v2alpha/password_settings";
export * as legal from "../../proto/server/zitadel/settings/v2alpha/legal_settings";

View File

@@ -1,28 +1,11 @@
import { CompatServiceDefinition } from "nice-grpc/lib/service-definitions"; import { CompatServiceDefinition } from "nice-grpc/lib/service-definitions";
import { createChannel, createClientFactory } from "nice-grpc";
import { import {
SettingsServiceClient, SettingsServiceClient,
SettingsServiceDefinition, SettingsServiceDefinition,
} from "../../proto/server/zitadel/settings/v2alpha/settings_service"; } from "../../proto/server/zitadel/settings/v2alpha/settings_service";
import { authMiddleware } from "../../middleware"; import { ZitadelServer, createClient, getServers } from "../../server";
import { ZitadelServer, getServers } from "../../server";
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;
};
export const getSettings = (server?: string | ZitadelServer) => { export const getSettings = (server?: string | ZitadelServer) => {
console.log("init settings"); console.log("init settings");

View File

@@ -3,12 +3,7 @@
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"rootDir": "src", "rootDir": "src"
"paths": {
"#": ["."],
"*": ["./*"],
"#/*": ["./*"]
}
}, },
"exclude": ["dist", "build", "node_modules"] "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,
}));