mirror of
https://github.com/michivonah/nextjs.git
synced 2025-12-22 22:16:28 +01:00
add authentication with next-auth
This commit is contained in:
parent
5487bccd8d
commit
759acde045
11 changed files with 318 additions and 98 deletions
125
README.md
125
README.md
|
|
@ -50,6 +50,14 @@ npm run dev
|
||||||
Afterwards the site can be accessed in the browser at `http://localhost:3000`.
|
Afterwards the site can be accessed in the browser at `http://localhost:3000`.
|
||||||

|

|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
`pnpm` is a faster and more efficient packet manager for `Node.js` than `npm`. It is recommended by `Next.js`.
|
||||||
|
|
||||||
|
Install `pnpm`
|
||||||
|
```zsh
|
||||||
|
npm install -g pnpm
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## General structure
|
## General structure
|
||||||
- The whole app is organzied in multiple folders. THe typescript files for the app are stored in `src/app`.
|
- The whole app is organzied in multiple folders. THe typescript files for the app are stored in `src/app`.
|
||||||
|
|
@ -662,6 +670,123 @@ if(!invoice){
|
||||||
Therefor the file `not-found.tsx` should be created with the UI you want to show, when something is not found.
|
Therefor the file `not-found.tsx` should be created with the UI you want to show, when something is not found.
|
||||||
Thoose specific error pages will be shown before the general error page.
|
Thoose specific error pages will be shown before the general error page.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
`NextAuth.js` or `Auth.js` is a popular library for implementing authentication into a `Next.js` application. This library takes away the most complexitiy and effort in building a robust authentication system from scratch.
|
||||||
|
|
||||||
|
Install `Auth.js`:
|
||||||
|
```tsx
|
||||||
|
pnpm i next-auth@beta
|
||||||
|
```
|
||||||
|
|
||||||
|
Before using `Auth.js` it's required to generate a secret. This can be done with following command:
|
||||||
|
```zsh
|
||||||
|
openssl rand -base64 32
|
||||||
|
```
|
||||||
|
|
||||||
|
Than this values has to be added to the `.env`-File as `AUTH_SECRET=<SECRET>`.
|
||||||
|
|
||||||
|
The secret will be used to encrypt cookies for the user sessions.
|
||||||
|
|
||||||
|
To configure the authentication a file `auth.config.ts` has to be created in the root directory of the app.
|
||||||
|
This file has to export a object with the config.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```ts
|
||||||
|
import type { NextAuthConfig } from 'next-auth';
|
||||||
|
|
||||||
|
export const authConfig = {
|
||||||
|
pages: {
|
||||||
|
signIn: '/login',
|
||||||
|
},
|
||||||
|
} satisfies NextAuthConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
But now the app routes aren't protect and everyone can access them. This can be changed by adding callbacks to `auth.config.ts` which control the pages that need authorization.
|
||||||
|
```ts
|
||||||
|
import type { NextAuthConfig } from 'next-auth';
|
||||||
|
|
||||||
|
export const authConfig = {
|
||||||
|
pages: {
|
||||||
|
signIn: '/login',
|
||||||
|
},
|
||||||
|
callbacks: {
|
||||||
|
authorized({ auth, request: { nextUrl } }) {
|
||||||
|
const isLoggedIn = !!auth?.user;
|
||||||
|
const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
|
||||||
|
if (isOnDashboard) {
|
||||||
|
if (isLoggedIn) return true;
|
||||||
|
return false;
|
||||||
|
} else if (isLoggedIn) {
|
||||||
|
return Response.redirect(new URL('/dashboard', nextUrl));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
providers: [],
|
||||||
|
} satisfies NextAuthConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
Next a middleware has to be created. This is defined in a file `middleware.ts` in the project's root.
|
||||||
|
```ts
|
||||||
|
import NextAuth from 'next-auth';
|
||||||
|
import { authConfig } from './auth.config';
|
||||||
|
|
||||||
|
export default NextAuth(authConfig).auth;
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
|
||||||
|
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Additonal a `auth.ts` file is needed. This could look like this:
|
||||||
|
```ts
|
||||||
|
import NextAuth from 'next-auth';
|
||||||
|
import { authConfig } from './auth.config';
|
||||||
|
import Credentials from 'next-auth/providers/credentials';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { User } from '@/app/lib/definitions';
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
import postgres from 'postgres';
|
||||||
|
|
||||||
|
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
|
||||||
|
|
||||||
|
async function getUser(email: string): Promise<User | undefined> {
|
||||||
|
try{
|
||||||
|
const user = await sql<User[]>`SELECT * FROM users WHERE email=${email}`;
|
||||||
|
return user[0];
|
||||||
|
} catch(error){
|
||||||
|
console.error('Failed to fetch user:', error);
|
||||||
|
throw new Error('Failed to fetch user.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { auth, signIn, signOut } = NextAuth({
|
||||||
|
...authConfig,
|
||||||
|
providers: [Credentials({
|
||||||
|
async authorize(credentials){
|
||||||
|
const parsedCredentials = z
|
||||||
|
.object({ email: z.string().email(), password: z.string().min(6)})
|
||||||
|
.safeParse(credentials);
|
||||||
|
|
||||||
|
if(parsedCredentials.success){
|
||||||
|
const { email, password } = parsedCredentials.data;
|
||||||
|
const user = await getUser(email);
|
||||||
|
if(!user) return null;
|
||||||
|
const passwordsMatch = await bcrypt.compare(password, user.password);
|
||||||
|
|
||||||
|
if(passwordsMatch) return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Invalid credentials');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
});
|
||||||
|
````
|
||||||
|
|
||||||
|
More details about the authentication can be found in the [Next.js course](https://nextjs.org/learn/dashboard-app/adding-authentication).
|
||||||
|
|
||||||
|
|
||||||
## Ressources
|
## Ressources
|
||||||
- [Next.js Installation](https://nextjs.org/docs/app/getting-started/installation)
|
- [Next.js Installation](https://nextjs.org/docs/app/getting-started/installation)
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,30 @@ import { z } from 'zod';
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import postgres from 'postgres';
|
import postgres from 'postgres';
|
||||||
|
import { signIn } from '@/auth';
|
||||||
|
import { AuthError } from 'next-auth';
|
||||||
|
|
||||||
const sql = postgres(process.env.POSTGRES_URL!, {ssl: 'require'});
|
const sql = postgres(process.env.POSTGRES_URL!, {ssl: 'require'});
|
||||||
|
|
||||||
|
export async function authenticate(
|
||||||
|
prevState: string | undefined,
|
||||||
|
formData: FormData,
|
||||||
|
){
|
||||||
|
try{
|
||||||
|
await signIn('credentials', formData)
|
||||||
|
} catch(error){
|
||||||
|
if(error instanceof AuthError){
|
||||||
|
switch(error.type){
|
||||||
|
case 'CredentialsSignIn':
|
||||||
|
return 'Invalid credentials.';
|
||||||
|
default:
|
||||||
|
return 'Something went wrong.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const FormSchema = z.object({
|
const FormSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
customerId: z.string({
|
customerId: z.string({
|
||||||
|
|
|
||||||
20
dashboard-app-course/app/login/page.tsx
Normal file
20
dashboard-app-course/app/login/page.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import AcmeLogo from "../ui/acme-logo";
|
||||||
|
import LoginForm from "../ui/login-form";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
export default function LoginPage(){
|
||||||
|
return (
|
||||||
|
<main className="flex items-center justify-center md:h-screen">
|
||||||
|
<div className="relative mx-auto flex w-full max-w-[400px] flex-col space-y-2.5 p-4 md:-mt-32">
|
||||||
|
<div className="flex h-20 w-full items-end rounded-lg bg-blue-500 p-3 md:h-36">
|
||||||
|
<div className="w-32 text-white md:w-36">
|
||||||
|
<AcmeLogo />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Suspense>
|
||||||
|
<LoginForm />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import Link from 'next/link';
|
||||||
import NavLinks from '@/app/ui/dashboard/nav-links';
|
import NavLinks from '@/app/ui/dashboard/nav-links';
|
||||||
import AcmeLogo from '@/app/ui/acme-logo';
|
import AcmeLogo from '@/app/ui/acme-logo';
|
||||||
import { PowerIcon } from '@heroicons/react/24/outline';
|
import { PowerIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { signOut } from '@/auth';
|
||||||
|
|
||||||
export default function SideNav() {
|
export default function SideNav() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -17,7 +18,12 @@ export default function SideNav() {
|
||||||
<div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
|
<div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
|
||||||
<NavLinks />
|
<NavLinks />
|
||||||
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
|
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
|
||||||
<form>
|
<form
|
||||||
|
action={async () => {
|
||||||
|
'use server';
|
||||||
|
await signOut({redirectTo: '/'});
|
||||||
|
}}
|
||||||
|
>
|
||||||
<button className="flex h-[48px] w-full grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
|
<button className="flex h-[48px] w-full grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
|
||||||
<PowerIcon className="w-6" />
|
<PowerIcon className="w-6" />
|
||||||
<div className="hidden md:block">Sign Out</div>
|
<div className="hidden md:block">Sign Out</div>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { lusitana } from '@/app/ui/fonts';
|
import { lusitana } from '@/app/ui/fonts';
|
||||||
import {
|
import {
|
||||||
AtSymbolIcon,
|
AtSymbolIcon,
|
||||||
|
|
@ -6,10 +8,19 @@ import {
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { ArrowRightIcon } from '@heroicons/react/20/solid';
|
import { ArrowRightIcon } from '@heroicons/react/20/solid';
|
||||||
import { Button } from './button';
|
import { Button } from './button';
|
||||||
|
import { useActionState } from 'react';
|
||||||
|
import { authenticate } from '../lib/actions';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
export default function LoginForm() {
|
export default function LoginForm() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const callbackUrl = searchParams.get('callbackUrl') || '/dashboard';
|
||||||
|
const [errorMessage, formAction, isPending] = useActionState(
|
||||||
|
authenticate,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<form className="space-y-3">
|
<form action={formAction} className="space-y-3">
|
||||||
<div className="flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8">
|
<div className="flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8">
|
||||||
<h1 className={`${lusitana.className} mb-3 text-2xl`}>
|
<h1 className={`${lusitana.className} mb-3 text-2xl`}>
|
||||||
Please log in to continue.
|
Please log in to continue.
|
||||||
|
|
@ -55,11 +66,21 @@ export default function LoginForm() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button className="mt-4 w-full">
|
<input type="hidden" name="redirectTo" value={callbackUrl} />
|
||||||
|
<Button className="mt-4 w-full" aria-disabled={isPending}>
|
||||||
Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
|
Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex h-8 items-end space-x-1">
|
<div
|
||||||
{/* Add form errors here */}
|
className="flex h-8 items-end space-x-1"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-atomic="true"
|
||||||
|
>
|
||||||
|
{errorMessage && (
|
||||||
|
<>
|
||||||
|
<ExclamationCircleIcon className="h-5 w-5 text-red-500" />
|
||||||
|
<p className="text-sm text-red-500">{errorMessage}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
21
dashboard-app-course/auth.config.ts
Normal file
21
dashboard-app-course/auth.config.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type { NextAuthConfig } from 'next-auth';
|
||||||
|
|
||||||
|
export const authConfig = {
|
||||||
|
pages: {
|
||||||
|
signIn: '/login',
|
||||||
|
},
|
||||||
|
callbacks: {
|
||||||
|
authorized({ auth, request: { nextUrl } }) {
|
||||||
|
const isLoggedIn = !!auth?.user;
|
||||||
|
const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
|
||||||
|
if (isOnDashboard) {
|
||||||
|
if (isLoggedIn) return true;
|
||||||
|
return false;
|
||||||
|
} else if (isLoggedIn) {
|
||||||
|
return Response.redirect(new URL('/dashboard', nextUrl));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
providers: [],
|
||||||
|
} satisfies NextAuthConfig;
|
||||||
42
dashboard-app-course/auth.ts
Normal file
42
dashboard-app-course/auth.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import NextAuth from 'next-auth';
|
||||||
|
import { authConfig } from './auth.config';
|
||||||
|
import Credentials from 'next-auth/providers/credentials';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { User } from '@/app/lib/definitions';
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
import postgres from 'postgres';
|
||||||
|
|
||||||
|
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
|
||||||
|
|
||||||
|
async function getUser(email: string): Promise<User | undefined> {
|
||||||
|
try{
|
||||||
|
const user = await sql<User[]>`SELECT * FROM users WHERE email=${email}`;
|
||||||
|
return user[0];
|
||||||
|
} catch(error){
|
||||||
|
console.error('Failed to fetch user:', error);
|
||||||
|
throw new Error('Failed to fetch user.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { auth, signIn, signOut } = NextAuth({
|
||||||
|
...authConfig,
|
||||||
|
providers: [Credentials({
|
||||||
|
async authorize(credentials){
|
||||||
|
const parsedCredentials = z
|
||||||
|
.object({ email: z.string().email(), password: z.string().min(6)})
|
||||||
|
.safeParse(credentials);
|
||||||
|
|
||||||
|
if(parsedCredentials.success){
|
||||||
|
const { email, password } = parsedCredentials.data;
|
||||||
|
const user = await getUser(email);
|
||||||
|
if(!user) return null;
|
||||||
|
const passwordsMatch = await bcrypt.compare(password, user.password);
|
||||||
|
|
||||||
|
if(passwordsMatch) return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Invalid credentials');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
});
|
||||||
9
dashboard-app-course/middleware.ts
Normal file
9
dashboard-app-course/middleware.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import NextAuth from 'next-auth';
|
||||||
|
import { authConfig } from './auth.config';
|
||||||
|
|
||||||
|
export default NextAuth(authConfig).auth;
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
|
||||||
|
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
|
||||||
|
};
|
||||||
68
dashboard-app-course/package-lock.json
generated
68
dashboard-app-course/package-lock.json
generated
|
|
@ -12,7 +12,7 @@
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"next": "latest",
|
"next": "latest",
|
||||||
"next-auth": "5.0.0-beta.25",
|
"next-auth": "5.0.0-beta.26",
|
||||||
"postcss": "8.5.1",
|
"postcss": "8.5.1",
|
||||||
"postgres": "^3.4.5",
|
"postgres": "^3.4.5",
|
||||||
"react": "latest",
|
"react": "latest",
|
||||||
|
|
@ -42,18 +42,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@auth/core": {
|
"node_modules/@auth/core": {
|
||||||
"version": "0.37.2",
|
"version": "0.39.0",
|
||||||
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.37.2.tgz",
|
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.39.0.tgz",
|
||||||
"integrity": "sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==",
|
"integrity": "sha512-jusviw/sUSfAh6S/wjY5tRmJOq0Itd3ImF+c/b4HB9DfmfChtcfVJTNJeqCeExeCG8oh4PBKRsMQJsn2W6NhFQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@panva/hkdf": "^1.2.1",
|
"@panva/hkdf": "^1.2.1",
|
||||||
"@types/cookie": "0.6.0",
|
"jose": "^6.0.6",
|
||||||
"cookie": "0.7.1",
|
"oauth4webapi": "^3.3.0",
|
||||||
"jose": "^5.9.3",
|
"preact": "10.24.3",
|
||||||
"oauth4webapi": "^3.0.0",
|
"preact-render-to-string": "6.5.11"
|
||||||
"preact": "10.11.3",
|
|
||||||
"preact-render-to-string": "5.2.3"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@simplewebauthn/browser": "^9.0.1",
|
"@simplewebauthn/browser": "^9.0.1",
|
||||||
|
|
@ -841,12 +839,6 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/cookie": {
|
|
||||||
"version": "0.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
|
||||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.10.7",
|
"version": "22.10.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
|
||||||
|
|
@ -1267,15 +1259,6 @@
|
||||||
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
|
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
|
||||||
"version": "0.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
|
||||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
|
|
@ -1729,9 +1712,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jose": {
|
"node_modules/jose": {
|
||||||
"version": "5.10.0",
|
"version": "6.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/jose/-/jose-6.0.10.tgz",
|
||||||
"integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
|
"integrity": "sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/panva"
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
|
@ -1964,12 +1947,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/next-auth": {
|
"node_modules/next-auth": {
|
||||||
"version": "5.0.0-beta.25",
|
"version": "5.0.0-beta.26",
|
||||||
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.25.tgz",
|
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.26.tgz",
|
||||||
"integrity": "sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog==",
|
"integrity": "sha512-yAQLIP2x6FAM+GX6FTlQjoPph6msO/9HI3pjI1z1yws3VnvS77atetcxQOmCpxSLTO4jzvpQqPaBZMgRxDgsYg==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/core": "0.37.2"
|
"@auth/core": "0.39.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@simplewebauthn/browser": "^9.0.1",
|
"@simplewebauthn/browser": "^9.0.1",
|
||||||
|
|
@ -2429,9 +2412,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/preact": {
|
"node_modules/preact": {
|
||||||
"version": "10.11.3",
|
"version": "10.24.3",
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
|
||||||
"integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==",
|
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|
@ -2439,23 +2422,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/preact-render-to-string": {
|
"node_modules/preact-render-to-string": {
|
||||||
"version": "5.2.3",
|
"version": "6.5.11",
|
||||||
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz",
|
||||||
"integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==",
|
"integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
|
||||||
"pretty-format": "^3.8.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"preact": ">=10"
|
"preact": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pretty-format": {
|
|
||||||
"version": "3.8.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
|
|
||||||
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"next": "latest",
|
"next": "latest",
|
||||||
"next-auth": "5.0.0-beta.25",
|
"next-auth": "5.0.0-beta.26",
|
||||||
"postcss": "8.5.1",
|
"postcss": "8.5.1",
|
||||||
"postgres": "^3.4.5",
|
"postgres": "^3.4.5",
|
||||||
"react": "latest",
|
"react": "latest",
|
||||||
|
|
|
||||||
71
dashboard-app-course/pnpm-lock.yaml
generated
71
dashboard-app-course/pnpm-lock.yaml
generated
|
|
@ -30,8 +30,8 @@ importers:
|
||||||
specifier: latest
|
specifier: latest
|
||||||
version: 15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
next-auth:
|
next-auth:
|
||||||
specifier: 5.0.0-beta.25
|
specifier: 5.0.0-beta.26
|
||||||
version: 5.0.0-beta.25(next@15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
|
version: 5.0.0-beta.26(next@15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
|
||||||
postcss:
|
postcss:
|
||||||
specifier: 8.5.1
|
specifier: 8.5.1
|
||||||
version: 8.5.1
|
version: 8.5.1
|
||||||
|
|
@ -76,8 +76,8 @@ packages:
|
||||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
'@auth/core@0.37.2':
|
'@auth/core@0.39.0':
|
||||||
resolution: {integrity: sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==}
|
resolution: {integrity: sha512-jusviw/sUSfAh6S/wjY5tRmJOq0Itd3ImF+c/b4HB9DfmfChtcfVJTNJeqCeExeCG8oh4PBKRsMQJsn2W6NhFQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@simplewebauthn/browser': ^9.0.1
|
'@simplewebauthn/browser': ^9.0.1
|
||||||
'@simplewebauthn/server': ^9.0.2
|
'@simplewebauthn/server': ^9.0.2
|
||||||
|
|
@ -317,9 +317,6 @@ packages:
|
||||||
'@types/bcrypt@5.0.2':
|
'@types/bcrypt@5.0.2':
|
||||||
resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==}
|
resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==}
|
||||||
|
|
||||||
'@types/cookie@0.6.0':
|
|
||||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
|
||||||
|
|
||||||
'@types/node@22.10.7':
|
'@types/node@22.10.7':
|
||||||
resolution: {integrity: sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==}
|
resolution: {integrity: sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==}
|
||||||
|
|
||||||
|
|
@ -462,10 +459,6 @@ packages:
|
||||||
console-control-strings@1.1.0:
|
console-control-strings@1.1.0:
|
||||||
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
|
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
|
||||||
|
|
||||||
cookie@0.7.1:
|
|
||||||
resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
|
|
||||||
engines: {node: '>= 0.6'}
|
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
@ -625,8 +618,8 @@ packages:
|
||||||
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
|
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
jose@5.9.6:
|
jose@6.0.10:
|
||||||
resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==}
|
resolution: {integrity: sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==}
|
||||||
|
|
||||||
lilconfig@3.1.3:
|
lilconfig@3.1.3:
|
||||||
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||||
|
|
@ -693,8 +686,8 @@ packages:
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
next-auth@5.0.0-beta.25:
|
next-auth@5.0.0-beta.26:
|
||||||
resolution: {integrity: sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog==}
|
resolution: {integrity: sha512-yAQLIP2x6FAM+GX6FTlQjoPph6msO/9HI3pjI1z1yws3VnvS77atetcxQOmCpxSLTO4jzvpQqPaBZMgRxDgsYg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@simplewebauthn/browser': ^9.0.1
|
'@simplewebauthn/browser': ^9.0.1
|
||||||
'@simplewebauthn/server': ^9.0.2
|
'@simplewebauthn/server': ^9.0.2
|
||||||
|
|
@ -762,8 +755,8 @@ packages:
|
||||||
resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
|
resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
|
||||||
deprecated: This package is no longer supported.
|
deprecated: This package is no longer supported.
|
||||||
|
|
||||||
oauth4webapi@3.1.4:
|
oauth4webapi@3.5.0:
|
||||||
resolution: {integrity: sha512-eVfN3nZNbok2s/ROifO0UAc5G8nRoLSbrcKJ09OqmucgnhXEfdIQOR4gq1eJH1rN3gV7rNw62bDEgftsgFtBEg==}
|
resolution: {integrity: sha512-DF3mLWNuxPkxJkHmWxbSFz4aE5CjWOsm465VBfBdWzmzX4Mg3vF8icxK+iKqfdWrIumBJ2TaoNQWx+SQc2bsPQ==}
|
||||||
|
|
||||||
object-assign@4.1.1:
|
object-assign@4.1.1:
|
||||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||||
|
|
@ -895,16 +888,13 @@ packages:
|
||||||
resolution: {integrity: sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==}
|
resolution: {integrity: sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
preact-render-to-string@5.2.3:
|
preact-render-to-string@6.5.11:
|
||||||
resolution: {integrity: sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==}
|
resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
preact: '>=10'
|
preact: '>=10'
|
||||||
|
|
||||||
preact@10.11.3:
|
preact@10.24.3:
|
||||||
resolution: {integrity: sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==}
|
resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==}
|
||||||
|
|
||||||
pretty-format@3.8.0:
|
|
||||||
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
|
|
||||||
|
|
||||||
queue-microtask@1.2.3:
|
queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
@ -1127,15 +1117,13 @@ snapshots:
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
|
|
||||||
'@auth/core@0.37.2':
|
'@auth/core@0.39.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@panva/hkdf': 1.2.1
|
'@panva/hkdf': 1.2.1
|
||||||
'@types/cookie': 0.6.0
|
jose: 6.0.10
|
||||||
cookie: 0.7.1
|
oauth4webapi: 3.5.0
|
||||||
jose: 5.9.6
|
preact: 10.24.3
|
||||||
oauth4webapi: 3.1.4
|
preact-render-to-string: 6.5.11(preact@10.24.3)
|
||||||
preact: 10.11.3
|
|
||||||
preact-render-to-string: 5.2.3(preact@10.11.3)
|
|
||||||
|
|
||||||
'@emnapi/runtime@1.3.1':
|
'@emnapi/runtime@1.3.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -1325,8 +1313,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.10.7
|
'@types/node': 22.10.7
|
||||||
|
|
||||||
'@types/cookie@0.6.0': {}
|
|
||||||
|
|
||||||
'@types/node@22.10.7':
|
'@types/node@22.10.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.20.0
|
undici-types: 6.20.0
|
||||||
|
|
@ -1473,8 +1459,6 @@ snapshots:
|
||||||
|
|
||||||
console-control-strings@1.1.0: {}
|
console-control-strings@1.1.0: {}
|
||||||
|
|
||||||
cookie@0.7.1: {}
|
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
|
|
@ -1630,7 +1614,7 @@ snapshots:
|
||||||
|
|
||||||
jiti@1.21.7: {}
|
jiti@1.21.7: {}
|
||||||
|
|
||||||
jose@5.9.6: {}
|
jose@6.0.10: {}
|
||||||
|
|
||||||
lilconfig@3.1.3: {}
|
lilconfig@3.1.3: {}
|
||||||
|
|
||||||
|
|
@ -1684,9 +1668,9 @@ snapshots:
|
||||||
|
|
||||||
nanoid@3.3.8: {}
|
nanoid@3.3.8: {}
|
||||||
|
|
||||||
next-auth@5.0.0-beta.25(next@15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0):
|
next-auth@5.0.0-beta.26(next@15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@auth/core': 0.37.2
|
'@auth/core': 0.39.0
|
||||||
next: 15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
next: 15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
react: 19.0.0
|
react: 19.0.0
|
||||||
|
|
||||||
|
|
@ -1738,7 +1722,7 @@ snapshots:
|
||||||
gauge: 3.0.2
|
gauge: 3.0.2
|
||||||
set-blocking: 2.0.0
|
set-blocking: 2.0.0
|
||||||
|
|
||||||
oauth4webapi@3.1.4: {}
|
oauth4webapi@3.5.0: {}
|
||||||
|
|
||||||
object-assign@4.1.1: {}
|
object-assign@4.1.1: {}
|
||||||
|
|
||||||
|
|
@ -1844,14 +1828,11 @@ snapshots:
|
||||||
|
|
||||||
postgres@3.4.5: {}
|
postgres@3.4.5: {}
|
||||||
|
|
||||||
preact-render-to-string@5.2.3(preact@10.11.3):
|
preact-render-to-string@6.5.11(preact@10.24.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
preact: 10.11.3
|
preact: 10.24.3
|
||||||
pretty-format: 3.8.0
|
|
||||||
|
|
||||||
preact@10.11.3: {}
|
preact@10.24.3: {}
|
||||||
|
|
||||||
pretty-format@3.8.0: {}
|
|
||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue