verify email

This commit is contained in:
Max Peintner
2023-05-22 11:48:18 +02:00
parent db7ba251ed
commit b169faad0e
12 changed files with 159 additions and 33 deletions

View File

@@ -1,4 +1,5 @@
import { listSessions, server } from "#/lib/zitadel"; import { listSessions, server } from "#/lib/zitadel";
import Alert from "#/ui/Alert";
import { Avatar } from "#/ui/Avatar"; import { Avatar } from "#/ui/Avatar";
import { getAllSessionIds } from "#/utils/cookies"; import { getAllSessionIds } from "#/utils/cookies";
import { import {
@@ -35,11 +36,12 @@ export default async function Page() {
<div className="flex flex-col w-full space-y-1"> <div className="flex flex-col w-full space-y-1">
{sessions ? ( {sessions ? (
sessions.map((session: any) => { sessions.map((session: any, index: number) => {
const validPassword = session.factors.password?.verifiedAt; const validPassword = session.factors.password?.verifiedAt;
console.log(session); console.log(session);
return ( return (
<Link <Link
key={"session-" + index}
href={ href={
validPassword validPassword
? `/signedin?` + ? `/signedin?` +
@@ -87,10 +89,7 @@ export default async function Page() {
); );
}) })
) : ( ) : (
<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"> <Alert>No Sessions available!</Alert>
<ExclamationTriangleIcon className="h-5 w-5 mr-2" />
<span className="text-center text-sm">No Sessions available!</span>
</div>
)} )}
</div> </div>
</div> </div>

View File

@@ -2,7 +2,7 @@ import VerifyEmailForm from "#/ui/VerifyEmailForm";
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
export default async function Page({ searchParams }: { searchParams: any }) { export default async function Page({ searchParams }: { searchParams: any }) {
const { userID, code, orgID, loginname, passwordset } = searchParams; const { userID, code, submit, orgID, loginname, passwordset } = searchParams;
return ( return (
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
@@ -12,7 +12,11 @@ export default async function Page({ searchParams }: { searchParams: any }) {
</p> </p>
{userID ? ( {userID ? (
<VerifyEmailForm userId={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"> <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" /> <ExclamationTriangleIcon className="h-5 w-5 mr-2" />

View File

@@ -34,6 +34,10 @@ export default async function RootLayout({
darkTheme: branding?.darkTheme, darkTheme: branding?.darkTheme,
}; };
} }
let domain = process.env.ZITADEL_API_URL;
domain = domain ? domain.replace("https://", "") : "acme.com";
return ( return (
<html lang="en" className={`${lato.className}`} suppressHydrationWarning> <html lang="en" className={`${lato.className}`} suppressHydrationWarning>
<head /> <head />
@@ -48,7 +52,7 @@ export default async function RootLayout({
{showNav && ( {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-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"> <div className="rounded-lg bg-background-light-400 dark:bg-background-dark-500">
<AddressBar /> <AddressBar domain={domain} />
</div> </div>
</div> </div>
)} )}

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

@@ -6,9 +6,13 @@ export async function POST(request: NextRequest) {
if (body) { if (body) {
const { userId, code } = body; const { userId, code } = body;
return verifyEmail(server, userId, code).then((resp) => { return verifyEmail(server, userId, code)
return NextResponse.json(resp); .then((resp) => {
}); return NextResponse.json(resp);
})
.catch((error) => {
return NextResponse.json(error, { status: 500 });
});
} else { } else {
return NextResponse.error(); return NextResponse.error();
} }

View File

@@ -6,6 +6,11 @@ import {
getServers, getServers,
initializeServer, initializeServer,
session, session,
GetGeneralSettingsResponse,
GetBrandingSettingsResponse,
GetPasswordComplexitySettingsResponse,
GetLegalAndSupportSettingsResponse,
AddHumanUserResponse,
} from "@zitadel/server"; } from "@zitadel/server";
export const zitadelConfig: ZitadelServerOptions = { export const zitadelConfig: ZitadelServerOptions = {
@@ -33,7 +38,7 @@ export function getBrandingSettings(
// metadata: orgMetadata(process.env.ZITADEL_ORG_ID ?? "") // metadata: orgMetadata(process.env.ZITADEL_ORG_ID ?? "")
} }
) )
.then((resp) => resp.settings); .then((resp: GetBrandingSettingsResponse) => resp.settings);
} }
export function getGeneralSettings( export function getGeneralSettings(
@@ -48,7 +53,7 @@ export function getGeneralSettings(
// metadata: orgMetadata(process.env.ZITADEL_ORG_ID ?? "") // metadata: orgMetadata(process.env.ZITADEL_ORG_ID ?? "")
} }
) )
.then((resp) => resp.supportedLanguages); .then((resp: GetGeneralSettingsResponse) => resp.supportedLanguages);
} }
export function getLegalAndSupportSettings( export function getLegalAndSupportSettings(
@@ -62,7 +67,7 @@ export function getLegalAndSupportSettings(
// metadata: orgMetadata(process.env.ZITADEL_ORG_ID ?? "") // metadata: orgMetadata(process.env.ZITADEL_ORG_ID ?? "")
} }
) )
.then((resp) => resp.settings); .then((resp: GetLegalAndSupportSettingsResponse) => resp.settings);
} }
export function getPasswordComplexitySettings( export function getPasswordComplexitySettings(
@@ -77,7 +82,7 @@ export function getPasswordComplexitySettings(
// metadata: orgMetadata(process.env.ZITADEL_ORG_ID ?? "") // metadata: orgMetadata(process.env.ZITADEL_ORG_ID ?? "")
} }
) )
.then((resp) => resp.settings); .then((resp: GetPasswordComplexitySettingsResponse) => resp.settings);
} }
export function createSession( export function createSession(
@@ -145,7 +150,7 @@ export function addHumanUser(
// metadata: orgMetadata(process.env.ZITADEL_ORG_ID ?? "") // metadata: orgMetadata(process.env.ZITADEL_ORG_ID ?? "")
} }
) )
.then((resp) => { .then((resp: AddHumanUserResponse) => {
console.log("added user", resp.userId); console.log("added user", resp.userId);
return resp.userId; return resp.userId;
}); });
@@ -156,8 +161,8 @@ export function verifyEmail(
userId: string, userId: string,
verificationCode: string verificationCode: string
): Promise<any> { ): Promise<any> {
const mgmt = user.getUser(server); const userservice = user.getUser(server);
return mgmt.verifyEmail( return userservice.verifyEmail(
{ {
userId, userId,
verificationCode, verificationCode,
@@ -166,4 +171,20 @@ export function verifyEmail(
); );
} }
/**
*
* @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 }; export { server };

View File

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

View File

@@ -1,11 +1,12 @@
"use client"; "use client";
import { useState } from "react"; import { useEffect, useState } from "react";
import { Button, ButtonVariants } from "./Button"; import { Button, ButtonVariants } from "./Button";
import { TextInput } from "./Input"; import { TextInput } from "./Input";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Spinner } from "./Spinner"; import { Spinner } from "./Spinner";
import Alert from "#/ui/Alert";
type Inputs = { type Inputs = {
code: string; code: string;
@@ -13,13 +14,24 @@ type Inputs = {
type Props = { type Props = {
userId: string; userId: string;
code: string;
submit: boolean;
}; };
export default function VerifyEmailForm({ userId }: Props) { export default function VerifyEmailForm({ userId, code, submit }: Props) {
const { register, handleSubmit, formState } = useForm<Inputs>({ const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur", mode: "onBlur",
defaultValues: {
code: code ?? "",
},
}); });
useEffect(() => {
if (submit && code && userId) {
submitCode({ code });
}
}, []);
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
@@ -39,13 +51,37 @@ export default function VerifyEmailForm({ userId }: Props) {
}), }),
}); });
const response = await res.json();
if (!res.ok) { if (!res.ok) {
setLoading(false); setLoading(false);
throw new Error("Failed to verify email"); setError(response.details);
return Promise.reject(response);
} else {
setLoading(false);
return response;
} }
}
setLoading(false); async function resendCode() {
return res.json(); 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> { function submitCodeAndContinue(value: Inputs): Promise<boolean | void> {
@@ -55,8 +91,6 @@ export default function VerifyEmailForm({ userId }: Props) {
}); });
} }
const { errors } = formState;
return ( return (
<form className="w-full"> <form className="w-full">
<div className=""> <div className="">
@@ -69,10 +103,20 @@ export default function VerifyEmailForm({ userId }: Props) {
/> />
</div> </div>
{error && (
<div className="py-4">
<Alert>{error}</Alert>
</div>
)}
<div className="mt-8 flex w-full flex-row items-center"> <div className="mt-8 flex w-full flex-row items-center">
{/* <Button type="button" variant={ButtonVariants.Secondary}> <Button
back type="button"
</Button> */} onClick={() => resendCode()}
variant={ButtonVariants.Secondary}
>
resend code
</Button>
<span className="flex-grow"></span> <span className="flex-grow"></span>
<Button <Button
type="submit" type="submit"

View File

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

View File

@@ -11,6 +11,14 @@ export {
Theme, Theme,
} from "./proto/server/zitadel/settings/v2alpha/branding_settings"; } from "./proto/server/zitadel/settings/v2alpha/branding_settings";
export {
GetPasswordComplexitySettingsResponse,
GetBrandingSettingsResponse,
GetLegalAndSupportSettingsResponse,
GetGeneralSettingsResponse,
} from "./proto/server/zitadel/settings/v2alpha/settings_service";
export { AddHumanUserResponse } from "./proto/server/zitadel/user/v2alpha/user_service";
export { type LegalAndSupportSettings } from "./proto/server/zitadel/settings/v2alpha/legal_settings"; export { type LegalAndSupportSettings } from "./proto/server/zitadel/settings/v2alpha/legal_settings";
export { type PasswordComplexitySettings } from "./proto/server/zitadel/settings/v2alpha/password_settings"; export { type PasswordComplexitySettings } from "./proto/server/zitadel/settings/v2alpha/password_settings";

5
pnpm-lock.yaml generated
View File

@@ -156,6 +156,7 @@ importers:
'@types/react': ^17.0.13 '@types/react': ^17.0.13
'@types/react-dom': ^17.0.8 '@types/react-dom': ^17.0.8
'@zitadel/tsconfig': workspace:* '@zitadel/tsconfig': workspace:*
autoprefixer: 10.4.13
eslint: ^7.32.0 eslint: ^7.32.0
eslint-config-zitadel: workspace:* eslint-config-zitadel: workspace:*
postcss: 8.4.21 postcss: 8.4.21
@@ -171,6 +172,7 @@ importers:
'@types/react': 17.0.52 '@types/react': 17.0.52
'@types/react-dom': 17.0.18 '@types/react-dom': 17.0.18
'@zitadel/tsconfig': link:../zitadel-tsconfig '@zitadel/tsconfig': link:../zitadel-tsconfig
autoprefixer: 10.4.13_postcss@8.4.21
eslint: 7.32.0 eslint: 7.32.0
eslint-config-zitadel: link:../eslint-config-zitadel eslint-config-zitadel: link:../eslint-config-zitadel
postcss: 8.4.21 postcss: 8.4.21
@@ -1356,7 +1358,7 @@ packages:
postcss: ^8.1.0 postcss: ^8.1.0
dependencies: dependencies:
browserslist: 4.21.5 browserslist: 4.21.5
caniuse-lite: 1.0.30001434 caniuse-lite: 1.0.30001473
fraction.js: 4.2.0 fraction.js: 4.2.0
normalize-range: 0.1.2 normalize-range: 0.1.2
picocolors: 1.0.0 picocolors: 1.0.0
@@ -1483,6 +1485,7 @@ packages:
/caniuse-lite/1.0.30001434: /caniuse-lite/1.0.30001434:
resolution: {integrity: sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==} resolution: {integrity: sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==}
dev: false
/caniuse-lite/1.0.30001473: /caniuse-lite/1.0.30001473:
resolution: {integrity: sha512-ewDad7+D2vlyy+E4UJuVfiBsU69IL+8oVmTuZnH5Q6CIUbxNfI50uVpRHbUPDD6SUaN2o0Lh4DhTrvLG/Tn1yg==} resolution: {integrity: sha512-ewDad7+D2vlyy+E4UJuVfiBsU69IL+8oVmTuZnH5Q6CIUbxNfI50uVpRHbUPDD6SUaN2o0Lh4DhTrvLG/Tn1yg==}