mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-12 10:15:04 +00:00
verify email
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
20
apps/login/app/resendverifyemail/route.ts
Normal file
20
apps/login/app/resendverifyemail/route.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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
14
apps/login/ui/Alert.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
5
pnpm-lock.yaml
generated
@@ -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==}
|
||||||
|
|||||||
Reference in New Issue
Block a user