add: oidc,passkey,disable email/password,sign-up

This commit is contained in:
Théo LAGACHE
2026-02-13 16:48:13 +01:00
parent 7ab213e5e4
commit 3bf731055f
29 changed files with 6199 additions and 525 deletions
+18
View File
@@ -26,6 +26,24 @@ AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET= AUTH_GOOGLE_SECRET=
AUTH_GOOGLE_METHOD= AUTH_GOOGLE_METHOD=
AUTH_OIDC_ID=""
AUTH_OIDC_TITLE=""
AUTH_OIDC_DESC=""
AUTH_OIDC_ICON=""
AUTH_OIDC_CLIENT=""
AUTH_OIDC_SECRET=""
AUTH_OIDC_ISSUER_URL=""
AUTH_OIDC_HOST=""
AUTH_OIDC_SCOPES=""
AUTH_OIDC_DISCOVERY_ENDPOINT=""
AUTH_OIDC_JWKS_ENDPOINT=""
AUTH_OIDC_PKCE=true
ALLOWED_GROUP="admin"
AUTH_EMAIL_PASSWORD_ENABLED=true
AUTH_SIGNUP_ENABLED=true
AUTH_PASSKEY_ENABLED=true
# Retention # Retention
RETENTION_CRON="* * * * *" RETENTION_CRON="* * * * *"
+6 -2
View File
@@ -2,9 +2,14 @@ import {CardContent, CardHeader} from "@/components/ui/card";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { ForgotPasswordForm } from "@/components/wrappers/auth/login/forgot-password-form/forgot-password-form"; import { ForgotPasswordForm } from "@/components/wrappers/auth/login/forgot-password-form/forgot-password-form";
import { env } from "@/env.mjs";
import { CardAuth } from "@/features/layout/card-auth"; import { CardAuth } from "@/features/layout/card-auth";
import { redirect } from "next/navigation";
export default async function RoutePage(props: { searchParams: Promise<{ callbackUrl: string | undefined }> }) { export default async function RoutePage(props: { searchParams: Promise<{ callbackUrl: string | undefined }> }) {
if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") {
redirect("/login");
}
return ( return (
<TooltipProvider> <TooltipProvider>
@@ -12,8 +17,7 @@ export default async function RoutePage(props: { searchParams: Promise<{ callbac
<CardHeader> <CardHeader>
<div className="grid gap-2 text-center mb-2"> <div className="grid gap-2 text-center mb-2">
<h1 className="text-3xl font-bold">Reset password</h1> <h1 className="text-3xl font-bold">Reset password</h1>
<p className="text-balance text-muted-foreground">Enter your email address and we'll send you a <p className="text-balance text-muted-foreground">Enter your email address and we'll send you a link to reset your password.</p>
link to reset your password.</p>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
+9 -10
View File
@@ -1,19 +1,19 @@
import { LoginForm } from "@/components/wrappers/auth/login/login-form/login-form"; import { LoginForm } from "@/components/wrappers/auth/login/login-form/login-form";
import { Metadata } from "next"; import { Metadata } from "next";
import {SUPPORTED_PROVIDERS} from "../../../portabase.config"; import { SUPPORTED_PROVIDERS } from "@/lib/auth/config";
import { SocialAuthButtons } from "@/components/wrappers/auth/social-buttons"; import { SocialAuthButtons } from "@/components/wrappers/auth/social-buttons";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { CardContent, CardHeader } from "@/components/ui/card"; import { CardContent, CardHeader } from "@/components/ui/card";
import Link from "next/link"; import Link from "next/link";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { CardAuth } from "@/features/layout/card-auth"; import { CardAuth } from "@/features/layout/card-auth";
import { env } from "@/env.mjs";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Login", title: "Login",
}; };
export default async function SignInPage() { export default async function SignInPage() {
return ( return (
<TooltipProvider> <TooltipProvider>
<CardAuth className="w-full"> <CardAuth className="w-full">
@@ -24,29 +24,28 @@ export default async function SignInPage() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<LoginForm/> {env.AUTH_EMAIL_PASSWORD_ENABLED === "true" && <LoginForm isPasskeyEnabled={env.AUTH_PASSKEY_ENABLED === "true"} />}
{SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive).length > 0 && ( {env.AUTH_EMAIL_PASSWORD_ENABLED === "true" && SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive).length > 0 && (
<>
<div className="relative my-4 flex items-center justify-center overflow-hidden"> <div className="relative my-4 flex items-center justify-center overflow-hidden">
<Separator /> <Separator />
<div className="px-2 text-center text-sm">OR</div> <div className="px-2 text-center text-sm">OR</div>
<Separator /> <Separator />
</div> </div>
<SocialAuthButtons providers={SUPPORTED_PROVIDERS}/>
</>
)} )}
{SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive).length > 0 && <SocialAuthButtons providers={SUPPORTED_PROVIDERS} />}
{env.AUTH_SIGNUP_ENABLED === "true" && (
<div className="mt-4 text-center text-sm"> <div className="mt-4 text-center text-sm">
Don&apos;t have an account ?{" "} Don&apos;t have an account ?{" "}
<Link href="/register" className="underline"> <Link href="/register" className="underline">
Sign up Sign up
</Link> </Link>
</div> </div>
)}
</CardContent> </CardContent>
</CardAuth> </CardAuth>
</TooltipProvider> </TooltipProvider>
);
)
} }
+7 -1
View File
@@ -1,15 +1,21 @@
import { PageParams } from "@/types/next"; import { PageParams } from "@/types/next";
import { RegisterForm } from "@/components/wrappers/auth/register/register-form/register-form"; import { RegisterForm } from "@/components/wrappers/auth/register/register-form/register-form";
import { Metadata } from "next"; import { Metadata } from "next";
import { redirect } from "next/navigation";
import { env } from "@/env.mjs";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Register", title: "Register",
}; };
export default async function RoutePage(props: PageParams<{}>) { export default async function RoutePage(props: PageParams<{}>) {
if (env.AUTH_SIGNUP_ENABLED !== "true") {
redirect("/login");
}
return ( return (
<div className="mx-auto grid w-full gap-6"> <div className="mx-auto grid w-full gap-6">
<RegisterForm /> <RegisterForm />
</div> </div>
) );
} }
+4
View File
@@ -5,8 +5,12 @@ import { redirect } from "next/navigation";
import { auth } from "@/lib/auth/auth"; import { auth } from "@/lib/auth/auth";
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar"; import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
import { CardAuth } from "@/features/layout/card-auth"; import { CardAuth } from "@/features/layout/card-auth";
import { env } from "@/env.mjs";
export default async function RoutePage(props: { searchParams: Promise<{ token: string | undefined }> }) { export default async function RoutePage(props: { searchParams: Promise<{ token: string | undefined }> }) {
if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") {
return redirect("/login");
}
const { token } = await props.searchParams; const { token } = await props.searchParams;
+12
View File
@@ -31,5 +31,17 @@ services:
volumes: volumes:
- ./private/uploads/tmp:/data/uploads/tmp - ./private/uploads/tmp:/data/uploads/tmp
keycloak:
image: quay.io/keycloak/keycloak:latest
command: start-dev
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
ports:
- "8080:8080"
volumes:
- keycloak-data:/opt/keycloak/data
volumes: volumes:
postgres-data: postgres-data:
keycloak-data:
+3 -1
View File
@@ -14,6 +14,8 @@
"auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts" "auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts"
}, },
"dependencies": { "dependencies": {
"@better-auth/passkey": "^1.4.18",
"@better-auth/sso": "^1.4.18",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-alert-dialog": "^1.1.15",
@@ -53,7 +55,7 @@
"@zenstackhq/runtime": "2.14.2", "@zenstackhq/runtime": "2.14.2",
"argon2": "^0.43.1", "argon2": "^0.43.1",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"better-auth": "1.4.5", "better-auth": "1.4.18",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
+429 -36
View File
@@ -8,6 +8,12 @@ importers:
.: .:
dependencies: dependencies:
'@better-auth/passkey':
specifier: ^1.4.18
version: 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-auth@1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(better-call@1.1.8(zod@3.25.76))(nanostores@1.1.0)
'@better-auth/sso':
specifier: ^1.4.18
version: 1.4.18(@better-auth/utils@0.3.0)(better-auth@1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
'@hookform/resolvers': '@hookform/resolvers':
specifier: ^5.2.2 specifier: ^5.2.2
version: 5.2.2(react-hook-form@7.71.1(react@19.2.4)) version: 5.2.2(react-hook-form@7.71.1(react@19.2.4))
@@ -126,8 +132,8 @@ importers:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0 version: 6.0.0
better-auth: better-auth:
specifier: 1.4.5 specifier: 1.4.18
version: 1.4.5(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
class-variance-authority: class-variance-authority:
specifier: ^0.7.1 specifier: ^0.7.1
version: 0.7.1 version: 0.7.1
@@ -350,6 +356,10 @@ packages:
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
'@authenio/xml-encryption@2.0.2':
resolution: {integrity: sha512-cTlrKttbrRHEw3W+0/I609A2Matj5JQaRvfLtEIGZvlN0RaPi+3ANsMeqAyCAVlH/lUIW2tmtBlSMni74lcXeg==}
engines: {node: '>=12'}
'@babel/code-frame@7.29.0': '@babel/code-frame@7.29.0':
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -437,26 +447,42 @@ packages:
'@balena/dockerignore@1.0.2': '@balena/dockerignore@1.0.2':
resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
'@better-auth/core@1.4.5': '@better-auth/core@1.4.18':
resolution: {integrity: sha512-dQ3hZOkUJzeBXfVEPTm2LVbzmWwka1nqd9KyWmB2OMlMfjr7IdUeBX4T7qJctF67d7QDhlX95jMoxu6JG0Eucw==} resolution: {integrity: sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg==}
peerDependencies: peerDependencies:
'@better-auth/utils': 0.3.0 '@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.18 '@better-fetch/fetch': 1.1.21
better-call: 1.1.4 better-call: 1.1.8
jose: ^6.1.0 jose: ^6.1.0
kysely: ^0.28.5 kysely: ^0.28.5
nanostores: ^1.0.1 nanostores: ^1.0.1
'@better-auth/telemetry@1.4.5': '@better-auth/passkey@1.4.18':
resolution: {integrity: sha512-r3NyksbaBYA10SC86JA6QwmZfHwFutkUGcphgWGfu6MVx1zutYmZehIeC8LxTjOWZqqF9FI8vLjglWBHvPQeTg==} resolution: {integrity: sha512-27YfrHCc95fm9e6V3RSN44JZunGHbQd0Lc26ErndPl2bCQ0VMJQi70WNLu6iqEiZYG8YSnxbMOg4/ivk8D7+lA==}
peerDependencies: peerDependencies:
'@better-auth/core': 1.4.5 '@better-auth/core': 1.4.18
'@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.21
better-auth: 1.4.18
better-call: 1.1.8
nanostores: ^1.0.1
'@better-auth/sso@1.4.18':
resolution: {integrity: sha512-jwTxZUBp71W6YVOavy50DBZ4OFi0a9MGvTTAi+mxMuINFRcz2RyGZv0gb3jy8AJT97TwKU60qRYIWYYyJj1uhA==}
peerDependencies:
'@better-auth/utils': 0.3.0
better-auth: 1.4.18
'@better-auth/telemetry@1.4.18':
resolution: {integrity: sha512-e5rDF8S4j3Um/0LIVATL2in9dL4lfO2fr2v1Wio4qTMRbfxqnUDTa+6SZtwdeJrbc4O+a3c+IyIpjG9Q/6GpfQ==}
peerDependencies:
'@better-auth/core': 1.4.18
'@better-auth/utils@0.3.0': '@better-auth/utils@0.3.0':
resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==} resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==}
'@better-fetch/fetch@1.1.18': '@better-fetch/fetch@1.1.21':
resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==}
'@chevrotain/cst-dts-gen@10.4.2': '@chevrotain/cst-dts-gen@10.4.2':
resolution: {integrity: sha512-0+4bNjlndNWMoVLH/+y4uHnf6GrTipsC+YTppJxelVJo+xeRVQ0s2PpkdDCVTsu7efyj+8r1gFiwVXsp6JZ0iQ==} resolution: {integrity: sha512-0+4bNjlndNWMoVLH/+y4uHnf6GrTipsC+YTppJxelVJo+xeRVQ0s2PpkdDCVTsu7efyj+8r1gFiwVXsp6JZ0iQ==}
@@ -1160,6 +1186,9 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
hasBin: true hasBin: true
'@hexagon/base64@1.1.28':
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
'@hookform/resolvers@5.2.2': '@hookform/resolvers@5.2.2':
resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==}
peerDependencies: peerDependencies:
@@ -1516,6 +1545,9 @@ packages:
'@js-sdsl/ordered-map@4.4.2': '@js-sdsl/ordered-map@4.4.2':
resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
'@levischuck/tiny-cbor@0.2.11':
resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==}
'@lottiefiles/dotlottie-react@0.13.3': '@lottiefiles/dotlottie-react@0.13.3':
resolution: {integrity: sha512-V4FfdYlqzjBUX7f0KV6vfQOOI0Cp+3XeG/ZqSDFSEVg5P7fpROpDv5/I9aTM8sOCESK1SWT96Fem+QVUnBV1wQ==} resolution: {integrity: sha512-V4FfdYlqzjBUX7f0KV6vfQOOI0Cp+3XeG/ZqSDFSEVg5P7fpROpDv5/I9aTM8sOCESK1SWT96Fem+QVUnBV1wQ==}
peerDependencies: peerDependencies:
@@ -1671,6 +1703,43 @@ packages:
'@paralleldrive/cuid2@2.3.1': '@paralleldrive/cuid2@2.3.1':
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
'@peculiar/asn1-android@2.6.0':
resolution: {integrity: sha512-cBRCKtYPF7vJGN76/yG8VbxRcHLPF3HnkoHhKOZeHpoVtbMYfY9ROKtH3DtYUY9m8uI1Mh47PRhHf2hSK3xcSQ==}
'@peculiar/asn1-cms@2.6.1':
resolution: {integrity: sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==}
'@peculiar/asn1-csr@2.6.1':
resolution: {integrity: sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==}
'@peculiar/asn1-ecc@2.6.1':
resolution: {integrity: sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==}
'@peculiar/asn1-pfx@2.6.1':
resolution: {integrity: sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==}
'@peculiar/asn1-pkcs8@2.6.1':
resolution: {integrity: sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==}
'@peculiar/asn1-pkcs9@2.6.1':
resolution: {integrity: sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==}
'@peculiar/asn1-rsa@2.6.1':
resolution: {integrity: sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==}
'@peculiar/asn1-schema@2.6.0':
resolution: {integrity: sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==}
'@peculiar/asn1-x509-attr@2.6.1':
resolution: {integrity: sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==}
'@peculiar/asn1-x509@2.6.1':
resolution: {integrity: sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==}
'@peculiar/x509@1.14.3':
resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==}
engines: {node: '>=20.0.0'}
'@phc/format@1.0.0': '@phc/format@1.0.0':
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -2667,6 +2736,13 @@ packages:
'@selderee/plugin-htmlparser2@0.11.0': '@selderee/plugin-htmlparser2@0.11.0':
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
'@simplewebauthn/browser@13.2.2':
resolution: {integrity: sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==}
'@simplewebauthn/server@13.2.2':
resolution: {integrity: sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==}
engines: {node: '>=20.0.0'}
'@socket.io/component-emitter@3.1.2': '@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
@@ -3161,6 +3237,14 @@ packages:
'@webassemblyjs/wast-printer@1.14.1': '@webassemblyjs/wast-printer@1.14.1':
resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==}
'@xmldom/is-dom-node@1.0.1':
resolution: {integrity: sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q==}
engines: {node: '>= 16'}
'@xmldom/xmldom@0.8.11':
resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}
engines: {node: '>=10.0.0'}
'@xtuc/ieee754@1.2.0': '@xtuc/ieee754@1.2.0':
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
@@ -3344,6 +3428,10 @@ packages:
asn1@0.2.6: asn1@0.2.6:
resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
asn1js@3.0.7:
resolution: {integrity: sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==}
engines: {node: '>=12.0.0'}
ast-types-flow@0.0.8: ast-types-flow@0.0.8:
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
@@ -3405,27 +3493,55 @@ packages:
bcryptjs@2.4.3: bcryptjs@2.4.3:
resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==}
better-auth@1.4.5: better-auth@1.4.18:
resolution: {integrity: sha512-pHV2YE0OogRHvoA6pndHXCei4pcep/mjY7psSaHVrRgjBtumVI68SV1g9U9XPRZ4KkoGca9jfwuv+bB2UILiFw==} resolution: {integrity: sha512-bnyifLWBPcYVltH3RhS7CM62MoelEqC6Q+GnZwfiDWNfepXoQZBjEvn4urcERC7NTKgKq5zNBM8rvPvRBa6xcg==}
peerDependencies: peerDependencies:
'@lynx-js/react': '*' '@lynx-js/react': '*'
'@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0
'@sveltejs/kit': ^2.0.0 '@sveltejs/kit': ^2.0.0
'@tanstack/react-start': ^1.0.0 '@tanstack/react-start': ^1.0.0
'@tanstack/solid-start': ^1.0.0
better-sqlite3: ^12.0.0
drizzle-kit: '>=0.31.4'
drizzle-orm: '>=0.41.0'
mongodb: ^6.0.0 || ^7.0.0
mysql2: ^3.0.0
next: ^14.0.0 || ^15.0.0 || ^16.0.0 next: ^14.0.0 || ^15.0.0 || ^16.0.0
pg: ^8.0.0
prisma: ^5.0.0 || ^6.0.0 || ^7.0.0
react: ^18.0.0 || ^19.0.0 react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0
solid-js: ^1.0.0 solid-js: ^1.0.0
svelte: ^4.0.0 || ^5.0.0 svelte: ^4.0.0 || ^5.0.0
vitest: ^2.0.0 || ^3.0.0 || ^4.0.0
vue: ^3.0.0 vue: ^3.0.0
peerDependenciesMeta: peerDependenciesMeta:
'@lynx-js/react': '@lynx-js/react':
optional: true optional: true
'@prisma/client':
optional: true
'@sveltejs/kit': '@sveltejs/kit':
optional: true optional: true
'@tanstack/react-start': '@tanstack/react-start':
optional: true optional: true
'@tanstack/solid-start':
optional: true
better-sqlite3:
optional: true
drizzle-kit:
optional: true
drizzle-orm:
optional: true
mongodb:
optional: true
mysql2:
optional: true
next: next:
optional: true optional: true
pg:
optional: true
prisma:
optional: true
react: react:
optional: true optional: true
react-dom: react-dom:
@@ -3434,11 +3550,13 @@ packages:
optional: true optional: true
svelte: svelte:
optional: true optional: true
vitest:
optional: true
vue: vue:
optional: true optional: true
better-call@1.1.4: better-call@1.1.8:
resolution: {integrity: sha512-NJouLY6IVKv0nDuFoc6FcbKDFzEnmgMNofC9F60Mwx1Ecm7X6/Ecyoe5b+JSVZ42F/0n46/M89gbYP1ZCVv8xQ==} resolution: {integrity: sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw==}
peerDependencies: peerDependencies:
zod: ^4.0.0 zod: ^4.0.0
peerDependenciesMeta: peerDependenciesMeta:
@@ -3538,6 +3656,10 @@ packages:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
camelcase@6.3.0:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
caniuse-lite@1.0.30001769: caniuse-lite@1.0.30001769:
resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==}
@@ -4337,6 +4459,10 @@ packages:
resolution: {integrity: sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==} resolution: {integrity: sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==}
hasBin: true hasBin: true
fast-xml-parser@5.3.5:
resolution: {integrity: sha512-JeaA2Vm9ffQKp9VjvfzObuMCjUYAp5WDYhRYL5LrBPY/jUDlUtOvDfot0vKSkB9tuX885BDHjtw4fZadD95wnA==}
hasBin: true
fastq@1.20.1: fastq@1.20.1:
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
@@ -5221,10 +5347,6 @@ packages:
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
ms@4.0.0-nightly.202508271359:
resolution: {integrity: sha512-WC/Eo7NzFrOV/RRrTaI0fxKVbNCzEy76j2VqNV8SxDf9D69gSE2Lh0QwYvDlhiYmheBYExAvEAxVf5NoN0cj2A==}
engines: {node: '>=20'}
mz@2.7.0: mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
@@ -5375,6 +5497,9 @@ packages:
node-releases@2.0.27: node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
node-rsa@1.1.1:
resolution: {integrity: sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==}
nodemailer@7.0.13: nodemailer@7.0.13:
resolution: {integrity: sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==} resolution: {integrity: sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
@@ -5489,6 +5614,9 @@ packages:
package-json-from-dist@1.0.1: package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
param-case@3.0.4: param-case@3.0.4:
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
@@ -5729,6 +5857,13 @@ packages:
pure-rand@6.1.0: pure-rand@6.1.0:
resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==}
pvtsutils@1.3.6:
resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==}
pvutils@1.1.5:
resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==}
engines: {node: '>=16.0.0'}
qr.js@0.0.0: qr.js@0.0.0:
resolution: {integrity: sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==} resolution: {integrity: sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==}
@@ -5897,6 +6032,9 @@ packages:
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
reflect-metadata@0.2.2:
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
reflect.getprototypeof@1.0.10: reflect.getprototypeof@1.0.10:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -5979,6 +6117,9 @@ packages:
safer-buffer@2.1.2: safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
samlify@2.10.2:
resolution: {integrity: sha512-y5s1cHwclqwP8h7K2Wj9SfP1q+1S9+jrs5OAegYTLAiuFi7nDvuKqbiXLmUTvYPMpzHcX94wTY2+D604jgTKvA==}
sax@1.4.4: sax@1.4.4:
resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==} resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
engines: {node: '>=11.0.0'} engines: {node: '>=11.0.0'}
@@ -6235,6 +6376,9 @@ packages:
strnum@1.1.2: strnum@1.1.2:
resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==}
strnum@2.1.2:
resolution: {integrity: sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==}
styled-jsx@5.1.6: styled-jsx@5.1.6:
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
@@ -6391,6 +6535,9 @@ packages:
resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
engines: {node: '>=6'} engines: {node: '>=6'}
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
tslib@2.8.1: tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -6399,6 +6546,10 @@ packages:
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
hasBin: true hasBin: true
tsyringe@4.10.0:
resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==}
engines: {node: '>= 6.0.0'}
tw-animate-css@1.4.0: tw-animate-css@1.4.0:
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
@@ -6537,6 +6688,10 @@ packages:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true hasBin: true
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
uuid@9.0.1: uuid@9.0.1:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true hasBin: true
@@ -6693,10 +6848,20 @@ packages:
utf-8-validate: utf-8-validate:
optional: true optional: true
xml-crypto@6.1.2:
resolution: {integrity: sha512-leBOVQdVi8FvPJrMYoum7Ici9qyxfE4kVi+AkpUoYCSXaQF4IlBm1cneTK9oAxR61LpYxTx7lNcsnBIeRpGW2w==}
engines: {node: '>=16'}
xml-escape@1.1.0:
resolution: {integrity: sha512-B/T4sDK8Z6aUh/qNr7mjKAwwncIljFuUP+DO/D5hloYFj+90O88z8Wf7oSucZTHxBAsC1/CTP4rtx/x1Uf72Mg==}
xml2js@0.6.2: xml2js@0.6.2:
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
engines: {node: '>=4.0.0'} engines: {node: '>=4.0.0'}
xml@1.0.1:
resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==}
xmlbuilder@11.0.1: xmlbuilder@11.0.1:
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
@@ -6705,6 +6870,14 @@ packages:
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
xpath@0.0.32:
resolution: {integrity: sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==}
engines: {node: '>=0.6.0'}
xpath@0.0.33:
resolution: {integrity: sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA==}
engines: {node: '>=0.6.0'}
xtend@4.0.2: xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'} engines: {node: '>=0.4'}
@@ -6774,6 +6947,12 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.13 '@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31 '@jridgewell/trace-mapping': 0.3.31
'@authenio/xml-encryption@2.0.2':
dependencies:
'@xmldom/xmldom': 0.8.11
escape-html: 1.0.3
xpath: 0.0.32
'@babel/code-frame@7.29.0': '@babel/code-frame@7.29.0':
dependencies: dependencies:
'@babel/helper-validator-identifier': 7.28.5 '@babel/helper-validator-identifier': 7.28.5
@@ -6923,26 +7102,48 @@ snapshots:
'@balena/dockerignore@1.0.2': {} '@balena/dockerignore@1.0.2': {}
'@better-auth/core@1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)': '@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)':
dependencies: dependencies:
'@better-auth/utils': 0.3.0 '@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.18 '@better-fetch/fetch': 1.1.21
'@standard-schema/spec': 1.1.0 '@standard-schema/spec': 1.1.0
better-call: 1.1.4(zod@4.3.6) better-call: 1.1.8(zod@3.25.76)
jose: 6.1.3 jose: 6.1.3
kysely: 0.28.11 kysely: 0.28.11
nanostores: 1.1.0 nanostores: 1.1.0
zod: 4.3.6 zod: 4.3.6
'@better-auth/telemetry@1.4.5(@better-auth/core@1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))': '@better-auth/passkey@1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-auth@1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(better-call@1.1.8(zod@3.25.76))(nanostores@1.1.0)':
dependencies: dependencies:
'@better-auth/core': 1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)
'@better-auth/utils': 0.3.0 '@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.18 '@better-fetch/fetch': 1.1.21
'@simplewebauthn/browser': 13.2.2
'@simplewebauthn/server': 13.2.2
better-auth: 1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
better-call: 1.1.8(zod@3.25.76)
nanostores: 1.1.0
zod: 4.3.6
'@better-auth/sso@1.4.18(@better-auth/utils@0.3.0)(better-auth@1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))':
dependencies:
'@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.21
better-auth: 1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
fast-xml-parser: 5.3.5
jose: 6.1.3
samlify: 2.10.2
zod: 4.3.6
'@better-auth/telemetry@1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))':
dependencies:
'@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)
'@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.21
'@better-auth/utils@0.3.0': {} '@better-auth/utils@0.3.0': {}
'@better-fetch/fetch@1.1.18': {} '@better-fetch/fetch@1.1.21': {}
'@chevrotain/cst-dts-gen@10.4.2': '@chevrotain/cst-dts-gen@10.4.2':
dependencies: dependencies:
@@ -7377,6 +7578,8 @@ snapshots:
protobufjs: 7.5.4 protobufjs: 7.5.4
yargs: 17.7.2 yargs: 17.7.2
'@hexagon/base64@1.1.28': {}
'@hookform/resolvers@5.2.2(react-hook-form@7.71.1(react@19.2.4))': '@hookform/resolvers@5.2.2(react-hook-form@7.71.1(react@19.2.4))':
dependencies: dependencies:
'@standard-schema/utils': 0.3.0 '@standard-schema/utils': 0.3.0
@@ -7625,6 +7828,8 @@ snapshots:
'@js-sdsl/ordered-map@4.4.2': {} '@js-sdsl/ordered-map@4.4.2': {}
'@levischuck/tiny-cbor@0.2.11': {}
'@lottiefiles/dotlottie-react@0.13.3(react@19.0.0)': '@lottiefiles/dotlottie-react@0.13.3(react@19.0.0)':
dependencies: dependencies:
'@lottiefiles/dotlottie-web': 0.42.0 '@lottiefiles/dotlottie-web': 0.42.0
@@ -7719,6 +7924,102 @@ snapshots:
dependencies: dependencies:
'@noble/hashes': 1.8.0 '@noble/hashes': 1.8.0
'@peculiar/asn1-android@2.6.0':
dependencies:
'@peculiar/asn1-schema': 2.6.0
asn1js: 3.0.7
tslib: 2.8.1
'@peculiar/asn1-cms@2.6.1':
dependencies:
'@peculiar/asn1-schema': 2.6.0
'@peculiar/asn1-x509': 2.6.1
'@peculiar/asn1-x509-attr': 2.6.1
asn1js: 3.0.7
tslib: 2.8.1
'@peculiar/asn1-csr@2.6.1':
dependencies:
'@peculiar/asn1-schema': 2.6.0
'@peculiar/asn1-x509': 2.6.1
asn1js: 3.0.7
tslib: 2.8.1
'@peculiar/asn1-ecc@2.6.1':
dependencies:
'@peculiar/asn1-schema': 2.6.0
'@peculiar/asn1-x509': 2.6.1
asn1js: 3.0.7
tslib: 2.8.1
'@peculiar/asn1-pfx@2.6.1':
dependencies:
'@peculiar/asn1-cms': 2.6.1
'@peculiar/asn1-pkcs8': 2.6.1
'@peculiar/asn1-rsa': 2.6.1
'@peculiar/asn1-schema': 2.6.0
asn1js: 3.0.7
tslib: 2.8.1
'@peculiar/asn1-pkcs8@2.6.1':
dependencies:
'@peculiar/asn1-schema': 2.6.0
'@peculiar/asn1-x509': 2.6.1
asn1js: 3.0.7
tslib: 2.8.1
'@peculiar/asn1-pkcs9@2.6.1':
dependencies:
'@peculiar/asn1-cms': 2.6.1
'@peculiar/asn1-pfx': 2.6.1
'@peculiar/asn1-pkcs8': 2.6.1
'@peculiar/asn1-schema': 2.6.0
'@peculiar/asn1-x509': 2.6.1
'@peculiar/asn1-x509-attr': 2.6.1
asn1js: 3.0.7
tslib: 2.8.1
'@peculiar/asn1-rsa@2.6.1':
dependencies:
'@peculiar/asn1-schema': 2.6.0
'@peculiar/asn1-x509': 2.6.1
asn1js: 3.0.7
tslib: 2.8.1
'@peculiar/asn1-schema@2.6.0':
dependencies:
asn1js: 3.0.7
pvtsutils: 1.3.6
tslib: 2.8.1
'@peculiar/asn1-x509-attr@2.6.1':
dependencies:
'@peculiar/asn1-schema': 2.6.0
'@peculiar/asn1-x509': 2.6.1
asn1js: 3.0.7
tslib: 2.8.1
'@peculiar/asn1-x509@2.6.1':
dependencies:
'@peculiar/asn1-schema': 2.6.0
asn1js: 3.0.7
pvtsutils: 1.3.6
tslib: 2.8.1
'@peculiar/x509@1.14.3':
dependencies:
'@peculiar/asn1-cms': 2.6.1
'@peculiar/asn1-csr': 2.6.1
'@peculiar/asn1-ecc': 2.6.1
'@peculiar/asn1-pkcs9': 2.6.1
'@peculiar/asn1-rsa': 2.6.1
'@peculiar/asn1-schema': 2.6.0
'@peculiar/asn1-x509': 2.6.1
pvtsutils: 1.3.6
reflect-metadata: 0.2.2
tslib: 2.8.1
tsyringe: 4.10.0
'@phc/format@1.0.0': {} '@phc/format@1.0.0': {}
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
@@ -9204,6 +9505,19 @@ snapshots:
domhandler: 5.0.3 domhandler: 5.0.3
selderee: 0.11.0 selderee: 0.11.0
'@simplewebauthn/browser@13.2.2': {}
'@simplewebauthn/server@13.2.2':
dependencies:
'@hexagon/base64': 1.1.28
'@levischuck/tiny-cbor': 0.2.11
'@peculiar/asn1-android': 2.6.0
'@peculiar/asn1-ecc': 2.6.1
'@peculiar/asn1-rsa': 2.6.1
'@peculiar/asn1-schema': 2.6.0
'@peculiar/asn1-x509': 2.6.1
'@peculiar/x509': 1.14.3
'@socket.io/component-emitter@3.1.2': {} '@socket.io/component-emitter@3.1.2': {}
'@standard-schema/spec@1.1.0': {} '@standard-schema/spec@1.1.0': {}
@@ -9700,6 +10014,10 @@ snapshots:
'@webassemblyjs/ast': 1.14.1 '@webassemblyjs/ast': 1.14.1
'@xtuc/long': 4.2.2 '@xtuc/long': 4.2.2
'@xmldom/is-dom-node@1.0.1': {}
'@xmldom/xmldom@0.8.11': {}
'@xtuc/ieee754@1.2.0': {} '@xtuc/ieee754@1.2.0': {}
'@xtuc/long@4.2.2': {} '@xtuc/long@4.2.2': {}
@@ -10018,6 +10336,12 @@ snapshots:
dependencies: dependencies:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
asn1js@3.0.7:
dependencies:
pvtsutils: 1.3.6
pvutils: 1.1.5
tslib: 2.8.1
ast-types-flow@0.0.8: {} ast-types-flow@0.0.8: {}
async-exit-hook@2.0.1: {} async-exit-hook@2.0.1: {}
@@ -10065,30 +10389,43 @@ snapshots:
bcryptjs@2.4.3: {} bcryptjs@2.4.3: {}
better-auth@1.4.5(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): better-auth@1.4.18(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies: dependencies:
'@better-auth/core': 1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)
'@better-auth/telemetry': 1.4.5(@better-auth/core@1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) '@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))
'@better-auth/utils': 0.3.0 '@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.18 '@better-fetch/fetch': 1.1.21
'@noble/ciphers': 2.1.1 '@noble/ciphers': 2.1.1
'@noble/hashes': 2.0.1 '@noble/hashes': 2.0.1
better-call: 1.1.4(zod@4.3.6) better-call: 1.1.8(zod@4.3.6)
defu: 6.1.4 defu: 6.1.4
jose: 6.1.3 jose: 6.1.3
kysely: 0.28.11 kysely: 0.28.11
ms: 4.0.0-nightly.202508271359
nanostores: 1.1.0 nanostores: 1.1.0
zod: 4.3.6 zod: 4.3.6
optionalDependencies: optionalDependencies:
'@prisma/client': 6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3)
drizzle-kit: 0.31.9
drizzle-orm: 0.43.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0)(prisma@6.7.0(typescript@5.9.3))
next: 16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next: 16.1.5(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
pg: 8.18.0
prisma: 6.7.0(typescript@5.9.3)
react: 19.2.4 react: 19.2.4
react-dom: 19.2.4(react@19.2.4) react-dom: 19.2.4(react@19.2.4)
better-call@1.1.4(zod@4.3.6): better-call@1.1.8(zod@3.25.76):
dependencies: dependencies:
'@better-auth/utils': 0.3.0 '@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.18 '@better-fetch/fetch': 1.1.21
rou3: 0.7.12
set-cookie-parser: 2.7.2
optionalDependencies:
zod: 3.25.76
better-call@1.1.8(zod@4.3.6):
dependencies:
'@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.21
rou3: 0.7.12 rou3: 0.7.12
set-cookie-parser: 2.7.2 set-cookie-parser: 2.7.2
optionalDependencies: optionalDependencies:
@@ -10209,6 +10546,8 @@ snapshots:
camelcase-css@2.0.1: {} camelcase-css@2.0.1: {}
camelcase@6.3.0: {}
caniuse-lite@1.0.30001769: {} caniuse-lite@1.0.30001769: {}
capital-case@1.0.4: capital-case@1.0.4:
@@ -11217,6 +11556,10 @@ snapshots:
dependencies: dependencies:
strnum: 1.1.2 strnum: 1.1.2
fast-xml-parser@5.3.5:
dependencies:
strnum: 2.1.2
fastq@1.20.1: fastq@1.20.1:
dependencies: dependencies:
reusify: 1.1.0 reusify: 1.1.0
@@ -12070,8 +12413,6 @@ snapshots:
ms@2.1.3: {} ms@2.1.3: {}
ms@4.0.0-nightly.202508271359: {}
mz@2.7.0: mz@2.7.0:
dependencies: dependencies:
any-promise: 1.3.0 any-promise: 1.3.0
@@ -12189,6 +12530,10 @@ snapshots:
node-releases@2.0.27: {} node-releases@2.0.27: {}
node-rsa@1.1.1:
dependencies:
asn1: 0.2.6
nodemailer@7.0.13: {} nodemailer@7.0.13: {}
normalize-path@3.0.0: {} normalize-path@3.0.0: {}
@@ -12328,6 +12673,8 @@ snapshots:
package-json-from-dist@1.0.1: {} package-json-from-dist@1.0.1: {}
pako@1.0.11: {}
param-case@3.0.4: param-case@3.0.4:
dependencies: dependencies:
dot-case: 3.0.4 dot-case: 3.0.4
@@ -12559,6 +12906,12 @@ snapshots:
pure-rand@6.1.0: {} pure-rand@6.1.0: {}
pvtsutils@1.3.6:
dependencies:
tslib: 2.8.1
pvutils@1.1.5: {}
qr.js@0.0.0: {} qr.js@0.0.0: {}
qs@6.14.1: qs@6.14.1:
@@ -12784,6 +13137,8 @@ snapshots:
tiny-invariant: 1.3.3 tiny-invariant: 1.3.3
victory-vendor: 36.9.2 victory-vendor: 36.9.2
reflect-metadata@0.2.2: {}
reflect.getprototypeof@1.0.10: reflect.getprototypeof@1.0.10:
dependencies: dependencies:
call-bind: 1.0.8 call-bind: 1.0.8
@@ -12883,6 +13238,20 @@ snapshots:
safer-buffer@2.1.2: {} safer-buffer@2.1.2: {}
samlify@2.10.2:
dependencies:
'@authenio/xml-encryption': 2.0.2
'@xmldom/xmldom': 0.8.11
camelcase: 6.3.0
node-forge: 1.3.3
node-rsa: 1.1.1
pako: 1.0.11
uuid: 8.3.2
xml: 1.0.1
xml-crypto: 6.1.2
xml-escape: 1.1.0
xpath: 0.0.32
sax@1.4.4: {} sax@1.4.4: {}
scheduler@0.25.0: {} scheduler@0.25.0: {}
@@ -13270,6 +13639,8 @@ snapshots:
strnum@1.1.2: {} strnum@1.1.2: {}
strnum@2.1.2: {}
styled-jsx@5.1.6(@babel/core@7.26.10)(react@19.0.0): styled-jsx@5.1.6(@babel/core@7.26.10)(react@19.0.0):
dependencies: dependencies:
client-only: 0.0.1 client-only: 0.0.1
@@ -13456,6 +13827,8 @@ snapshots:
minimist: 1.2.8 minimist: 1.2.8
strip-bom: 3.0.0 strip-bom: 3.0.0
tslib@1.14.1: {}
tslib@2.8.1: {} tslib@2.8.1: {}
tsx@4.21.0: tsx@4.21.0:
@@ -13465,6 +13838,10 @@ snapshots:
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
tsyringe@4.10.0:
dependencies:
tslib: 1.14.1
tw-animate-css@1.4.0: {} tw-animate-css@1.4.0: {}
tweetnacl@0.14.5: {} tweetnacl@0.14.5: {}
@@ -13651,6 +14028,8 @@ snapshots:
uuid@11.1.0: {} uuid@11.1.0: {}
uuid@8.3.2: {}
uuid@9.0.1: {} uuid@9.0.1: {}
vary@1.1.2: {} vary@1.1.2: {}
@@ -13844,15 +14223,29 @@ snapshots:
ws@8.19.0: {} ws@8.19.0: {}
xml-crypto@6.1.2:
dependencies:
'@xmldom/is-dom-node': 1.0.1
'@xmldom/xmldom': 0.8.11
xpath: 0.0.33
xml-escape@1.1.0: {}
xml2js@0.6.2: xml2js@0.6.2:
dependencies: dependencies:
sax: 1.4.4 sax: 1.4.4
xmlbuilder: 11.0.1 xmlbuilder: 11.0.1
xml@1.0.1: {}
xmlbuilder@11.0.1: {} xmlbuilder@11.0.1: {}
xmlhttprequest-ssl@2.1.2: {} xmlhttprequest-ssl@2.1.2: {}
xpath@0.0.32: {}
xpath@0.0.33: {}
xtend@4.0.2: {} xtend@4.0.2: {}
y18n@5.0.8: {} y18n@5.0.8: {}
-47
View File
@@ -1,17 +1,3 @@
export interface AuthProviderConfig {
id: "google" | "github" | "credential";
isActive: boolean;
icon: string;
isManual?: boolean;
credentials?: {
clientId: string;
clientSecret: string;
};
}
export const PORTABASE_DEFAULT_SETTINGS = { export const PORTABASE_DEFAULT_SETTINGS = {
SECURITY: { SECURITY: {
CSP: { CSP: {
@@ -75,36 +61,3 @@ export const PORTABASE_DEFAULT_SETTINGS = {
}, },
}, },
}; };
export const SUPPORTED_PROVIDERS: AuthProviderConfig[] = [
{
id: "google",
icon: "hugeicons:chrome",
isActive: Boolean(process.env.AUTH_GOOGLE_METHOD),
// isManual: true,
credentials: {
clientId: process.env.AUTH_GOOGLE_ID || "",
clientSecret: process.env.AUTH_GOOGLE_SECRET || "",
},
},
{
id: "github",
icon: "iconoir:github",
isActive: Boolean(process.env.AUTH_GITHUB_METHOD),
credentials: {
clientId: process.env.AUTH_GITHUB_ID || "",
clientSecret: process.env.AUTH_GITHUB_SECRET || "",
},
},
{
id: "credential",
isActive: true,
icon: "proicons:key",
isManual: true,
credentials: {
clientId: "",
clientSecret: "",
},
},
];
@@ -15,9 +15,11 @@ import {PasswordInput} from "@/components/ui/password-input";
export type loginFormProps = { export type loginFormProps = {
defaultValues?: LoginType; defaultValues?: LoginType;
isPasskeyEnabled?: boolean;
}; };
export const LoginForm = (props: loginFormProps) => { export const LoginForm = (props: loginFormProps) => {
const {isPasskeyEnabled = false} = props;
const [urlParams, setUrlParams] = useState<URLSearchParams>(); const [urlParams, setUrlParams] = useState<URLSearchParams>();
@@ -106,7 +108,7 @@ export const LoginForm = (props: loginFormProps) => {
</div> </div>
</div> </div>
<FormControl> <FormControl>
<PasswordInput autoComplete="current-password webauthn" <PasswordInput autoComplete={isPasskeyEnabled ? "current-password webauthn" : "current-password"}
placeholder={"Enter your password"} {...field} /> placeholder={"Enter your password"} {...field} />
</FormControl> </FormControl>
<FormMessage/> <FormMessage/>
+49 -31
View File
@@ -1,29 +1,52 @@
"use client"; "use client";
import React, { useState } from "react"; import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { authClient } from "@/lib/auth/auth-client"; import { authClient, passkey } from "@/lib/auth/auth-client";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import {AuthProviderConfig} from "../../../../portabase.config";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import { AuthProviderConfig } from "@/lib/auth/config";
import Image from "next/image";
import { useRouter } from "next/navigation";
export function SocialAuthButtons({ providers }: { providers: AuthProviderConfig[] }) { export function SocialAuthButtons({ providers }: { providers: AuthProviderConfig[] }) {
const socialProviders = providers.filter(p => p.isActive && !p.isManual); const socialProviders = providers.filter((p) => p.isActive && p.type !== "credential");
const router = useRouter();
const [isLoading, setIsLoading] = useState<string | null>(null); const [isLoading, setIsLoading] = useState<string | null>(null);
const handleSocialSignIn = async (providerId: string) => { const handleSocialSignIn = async (provider: AuthProviderConfig) => {
setIsLoading(providerId); setIsLoading(provider.id);
try { try {
const { error } = await authClient.signIn.social({ let result;
provider: providerId as "google" | "github", if (provider.id === "passkey") {
result = await authClient.signIn.passkey({
fetchOptions: {
onSuccess() {
router.push("/dashboard");
},
onError() {
toast.error("An error occurred during passkey authentication. Please try again.");
},
},
});
} else if (provider.type === "sso") {
result = await authClient.signIn.sso({
providerId: provider.id,
providerType: "oidc",
callbackURL: "/dashboard", callbackURL: "/dashboard",
}); });
if (error) {
toast.error("An error occurred while signing in with the provider. Please try again.");
} else { } else {
result = await authClient.signIn.social({
provider: provider.id as "google" | "github",
callbackURL: "/dashboard",
});
}
if (result?.error) {
toast.error("An error occurred while signing in with the provider. Please try again.");
} else if (provider.id !== "passkey") {
toast.success("Redirecting to provider..."); toast.success("Redirecting to provider...");
} }
} catch (err) { } catch (err) {
@@ -38,29 +61,24 @@ export function SocialAuthButtons({ providers }: { providers: AuthProviderConfig
return ( return (
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
{socialProviders.map((provider) => ( {socialProviders.map((provider) => (
<Button key={provider.id} variant="outline" className="w-full gap-2" onClick={() => handleSocialSignIn(provider.id)} disabled={!!isLoading}> <Button key={provider.id} variant="outline" className="w-full gap-2" onClick={() => handleSocialSignIn(provider)} disabled={!!isLoading}>
{isLoading === provider.id ? <Loader2 className="h-4 w-4 animate-spin" /> : {isLoading === provider.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : provider.icon.startsWith("/") || provider.icon.startsWith("http") ? (
<Image
src={provider.icon}
alt={provider.id || "icon"}
width={16}
height={16}
className="h-4 w-4"
unoptimized={provider.icon.startsWith("http")}
/>
) : (
<Icon icon={provider.icon} className="h-4 w-4" /> <Icon icon={provider.icon} className="h-4 w-4" />
} )}
<span>{PROVIDERS_TEXT[provider.id].title}</span> <span>{provider.title || provider.name}</span>
</Button> </Button>
))} ))}
</div> </div>
); );
} }
const PROVIDERS_TEXT = {
credential: {
title: "Password",
description: "Use your email address and password to sign in."
},
google: {
title: "Google",
description: "Sign in with your Google account."
},
github: {
title: "GitHub",
description: "Sign in with your GitHub account."
}
}
@@ -1,8 +1,7 @@
import { currentUser } from "@/lib/auth/current-user"; import { currentUser } from "@/lib/auth/current-user";
import { getAccounts, getSession, getSessions } from "@/lib/auth/auth"; import { getAccounts, getSession, getSessions } from "@/lib/auth/auth";
import { LoggedInButtonClient } from "./logged-in-button"; import { LoggedInButtonClient } from "./logged-in-button";
import {SUPPORTED_PROVIDERS} from "../../../../../../portabase.config"; import { SUPPORTED_PROVIDERS } from "@/lib/auth/config";
export const LoggedInButton = async () => { export const LoggedInButton = async () => {
const user = await currentUser(); const user = await currentUser();
@@ -10,7 +9,6 @@ export const LoggedInButton = async () => {
const currentSession = await getSession(); const currentSession = await getSession();
const accounts = await getAccounts(); const accounts = await getAccounts();
if (!user) return null; if (!user) return null;
return ( return (
@@ -5,25 +5,17 @@ import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
import { SidebarMenuButton } from "@/components/ui/sidebar"; import { SidebarMenuButton } from "@/components/ui/sidebar";
import { LoggedInDropdown } from "./logged-in-dropdown"; import { LoggedInDropdown } from "./logged-in-dropdown";
import { Account, Session, User } from "better-auth"; import { Account, Session, User } from "better-auth";
import {AuthProviderConfig} from "../../../../../../portabase.config"; import { AuthProviderConfig } from "@/lib/auth/config";
type LoggedInButtonClientProps = { type LoggedInButtonClientProps = {
user: User, user: User;
sessions: Session[], sessions: Session[];
currentSession: Session, currentSession: Session;
accounts: Account[], accounts: Account[];
providers: AuthProviderConfig[] providers: AuthProviderConfig[];
} };
export const LoggedInButtonClient = ({
user,
sessions,
currentSession,
accounts,
providers
}: LoggedInButtonClientProps) => {
export const LoggedInButtonClient = ({ user, sessions, currentSession, accounts, providers }: LoggedInButtonClientProps) => {
return ( return (
<LoggedInDropdown <LoggedInDropdown
// @ts-ignore // @ts-ignore
@@ -43,12 +35,8 @@ export const LoggedInButtonClient = ({
{user.image && <AvatarImage src={user.image} />} {user.image && <AvatarImage src={user.image} />}
</Avatar> </Avatar>
<div className="flex flex-col items-start"> <div className="flex flex-col items-start">
<span <span className="text-sm font-medium first-letter:capitalize max-w-[170px] truncate">{user.name}</span>
className="text-sm font-medium first-letter:capitalize max-w-[170px] truncate">{user.name}</span> <span className="text-xs text-muted-foreground max-w-[170px] truncate" title={user.email}>
<span
className="text-xs text-muted-foreground max-w-[170px] truncate"
title={user.email}
>
{user.email} {user.email}
</span> </span>
</div> </div>
@@ -14,7 +14,7 @@ import {ProfileModal} from "@/components/wrappers/dashboard/common/profile/profi
import { Account, Session, User as UserType } from "@/db/schema/02_user"; import { Account, Session, User as UserType } from "@/db/schema/02_user";
import {AuthProviderConfig} from "../../../../../../portabase.config"; import { AuthProviderConfig } from "@/lib/auth/config";
export type LoggedInDropdownProps = PropsWithChildren<{ export type LoggedInDropdownProps = PropsWithChildren<{
user: UserType; user: UserType;
@@ -22,13 +22,10 @@ export type LoggedInDropdownProps = PropsWithChildren<{
currentSession: Session; currentSession: Session;
accounts: Account[]; accounts: Account[];
children: ReactNode; children: ReactNode;
providers: AuthProviderConfig[] providers: AuthProviderConfig[];
}>; }>;
export const LoggedInDropdown = ({ user, sessions, currentSession, accounts, children, providers }: LoggedInDropdownProps) => { export const LoggedInDropdown = ({ user, sessions, currentSession, accounts, children, providers }: LoggedInDropdownProps) => {
const router = useRouter(); const router = useRouter();
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
@@ -85,9 +82,5 @@ export const LoggedInDropdown = ({ user, sessions, currentSession, accounts, chi
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</> </>
); );
}; };
@@ -1,16 +1,15 @@
"use client"; "use client";
import React from "react";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Tabs, TabsContent } from "@/components/ui/tabs"; import { Tabs, TabsContent } from "@/components/ui/tabs";
import { Account, Session, User } from "@/db/schema/02_user";
import { ProfileSidebar } from "./profile-sidebar"; import { ProfileSidebar } from "./profile-sidebar";
import {ProfileProviders} from "@/components/wrappers/dashboard/profile/profile-providers"; import { AuthProviderConfig } from "@/lib/auth/config";
import {ProfileAccount} from "@/components/wrappers/dashboard/profile/profile-account"; import { User, Session, Account } from "@/db/schema/02_user";
import {ProfileAppearance} from "@/components/wrappers/dashboard/profile/profile-apperance"; import { ProfileGeneral } from "../../profile/profile-general";
import {ProfileSecurity} from "@/components/wrappers/dashboard/profile/profile-security"; import { ProfileSecurity } from "../../profile/profile-security";
import {ProfileGeneral} from "@/components/wrappers/dashboard/profile/profile-general"; import { ProfileProviders } from "../../profile/profile-providers";
import {AuthProviderConfig} from "../../../../../../portabase.config"; import { ProfileAccount } from "../../profile/profile-account";
import { ProfileAppearance } from "../../profile/profile-apperance";
type ProfileModalProps = { type ProfileModalProps = {
open: boolean; open: boolean;
@@ -19,14 +18,13 @@ type ProfileModalProps = {
currentSession: Session; currentSession: Session;
accounts: Account[]; accounts: Account[];
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
providers: AuthProviderConfig[] providers: AuthProviderConfig[];
}; };
export const ProfileModal = ({ user, sessions, currentSession, accounts, open, onOpenChange, providers }: ProfileModalProps) => { export const ProfileModal = ({ user, sessions, currentSession, accounts, open, onOpenChange, providers }: ProfileModalProps) => {
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-[95vw] h-[90vh] max-w-md lg:max-w-[1000px] lg:h-[800px] pb-6 overflow-hidden flex flex-col outline-none gap-0 rounded-xl bg-background"> <DialogContent className="w-[95vw] h-[90vh] max-w-md lg:max-w-[1000px] lg:h-[800px] pb-6 p-0 overflow-hidden flex flex-col outline-none gap-0 rounded-xl bg-background">
<DialogHeader className="sr-only"> <DialogHeader className="sr-only">
<DialogTitle>Settings</DialogTitle> <DialogTitle>Settings</DialogTitle>
<DialogDescription>Manage your account settings</DialogDescription> <DialogDescription>Manage your account settings</DialogDescription>
@@ -45,6 +43,8 @@ export const ProfileModal = ({ user, sessions, currentSession, accounts, open, o
sessions={sessions} sessions={sessions}
currentSession={currentSession} currentSession={currentSession}
credentialAccount={accounts.find((acc) => acc.providerId === "credential")!} credentialAccount={accounts.find((acc) => acc.providerId === "credential")!}
isPasswordEnabled={providers.some((p) => p.id === "credential")}
isPasskeyEnabled={providers.some((p) => p.id === "passkey")}
/> />
</TabsContent> </TabsContent>
@@ -1,6 +1,6 @@
"use client"; "use client";
import React, { use } from "react"; import React from "react";
import { TabsList, TabsTrigger } from "@/components/ui/tabs"; import { TabsList, TabsTrigger } from "@/components/ui/tabs";
import { UserIcon, Settings, Palette, ShieldHalf, Workflow } from "lucide-react"; import { UserIcon, Settings, Palette, ShieldHalf, Workflow } from "lucide-react";
import { User } from "@/db/schema/02_user"; import { User } from "@/db/schema/02_user";
@@ -10,7 +10,6 @@ interface ProfileSidebarProps {
} }
export function ProfileSidebar({ user }: ProfileSidebarProps) { export function ProfileSidebar({ user }: ProfileSidebarProps) {
return ( return (
<div className="w-full lg:w-[260px] flex-shrink-0 lg:border-r bg-muted/10 p-4 lg:p-6 flex flex-col gap-4 border-b lg:border-b-0"> <div className="w-full lg:w-[260px] flex-shrink-0 lg:border-r bg-muted/10 p-4 lg:p-6 flex flex-col gap-4 border-b lg:border-b-0">
<div className="flex items-center px-2 mb-2"> <div className="flex items-center px-2 mb-2">
@@ -3,13 +3,18 @@
import { ServerActionResult } from "@/types/action-type"; import { ServerActionResult } from "@/types/action-type";
import { z } from "zod"; import { z } from "zod";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { auth } from "@/lib/auth/auth"; import { auth, getPasskeys, revokePasskey } from "@/lib/auth/auth";
import { userAction } from "@/lib/safe-actions/actions"; import { userAction } from "@/lib/safe-actions/actions";
const RevokeSessionSchema = z.object({ const RevokeSessionSchema = z.object({
token: z.string(), token: z.string(),
}); });
const RevokePasskeySchema = z.object({
id: z.string(),
});
export const revokeSessionAction = userAction.schema(RevokeSessionSchema).action(async ({ parsedInput }): Promise<ServerActionResult<{}>> => { export const revokeSessionAction = userAction.schema(RevokeSessionSchema).action(async ({ parsedInput }): Promise<ServerActionResult<{}>> => {
try { try {
const session = await auth.api.getSession({ const session = await auth.api.getSession({
@@ -99,3 +104,46 @@ export const revokeAllSessionsAction = userAction.action(async (): Promise<Serve
}; };
} }
}); });
export const getPasskeysAction = userAction.action(async (): Promise<ServerActionResult<any[]>> => {
try {
const passkeys = await getPasskeys();
return {
success: true,
value: passkeys || [],
actionSuccess: {
message: "passkeys_fetched",
},
};
} catch (error) {
return {
success: false,
actionError: {
message: "error_fetching_passkeys",
cause: error instanceof Error ? error.message : "Unknown error",
},
};
}
});
export const revokePasskeyAction = userAction.schema(RevokePasskeySchema).action(async ({ parsedInput }): Promise<ServerActionResult<{}>> => {
try {
await revokePasskey(parsedInput.id);
return {
success: true,
value: {},
actionSuccess: {
message: "passkey_revoked",
},
};
} catch (error) {
return {
success: false,
actionError: {
message: "error_revoking_passkey",
cause: error instanceof Error ? error.message : "Unknown error",
},
};
}
});
@@ -1,10 +1,9 @@
"use client"; "use client";
import React, {useState} from "react"; import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Loader2, AlertTriangle } from "lucide-react"; import { Loader2, AlertTriangle } from "lucide-react";
import {Account} from "@/db/schema/02_user";
import { authClient } from "@/lib/auth/auth-client"; import { authClient } from "@/lib/auth/auth-client";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -12,13 +11,14 @@ import {useRouter} from "next/navigation";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { SetPasswordProfileProviderModal } from "./modal/set-password-modal"; import { SetPasswordProfileProviderModal } from "./modal/set-password-modal";
import {AuthProviderConfig} from "../../../../../portabase.config";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import Image from "next/image";
import { AuthProviderConfig } from "@/lib/auth/config";
import { Account } from "@/db/schema/02_user";
interface ProfileProviderProps { interface ProfileProviderProps {
accounts: Account[]; accounts: Account[];
providers: AuthProviderConfig[] providers: AuthProviderConfig[];
} }
export function ProfileProviders({ accounts, providers }: ProfileProviderProps) { export function ProfileProviders({ accounts, providers }: ProfileProviderProps) {
@@ -46,16 +46,24 @@ export function ProfileProviders({accounts, providers}: ProfileProviderProps) {
}); });
const { mutate: linkAccount } = useMutation({ const { mutate: linkAccount } = useMutation({
mutationFn: async (providerId: string) => { mutationFn: async (provider: AuthProviderConfig) => {
setLoadingProvider(providerId); setLoadingProvider(provider.id);
const {error} = await authClient.signIn.social({ let result;
provider: providerId as "google" | "github" | "credential", if (provider.type === "sso") {
result = await authClient.signIn.sso({
providerId: provider.id,
providerType: "oidc",
callbackURL: "/dashboard", callbackURL: "/dashboard",
}); });
if (error) throw error; } else {
result = await authClient.signIn.social({
provider: provider.id as "google" | "github" | "credential",
callbackURL: "/dashboard",
});
}
if (result.error) throw result.error;
}, },
onSuccess: () => { onSuccess: () => {
toast.success("Provider successfully Linked!");
setLoadingProvider(null); setLoadingProvider(null);
router.refresh(); router.refresh();
}, },
@@ -65,42 +73,42 @@ export function ProfileProviders({accounts, providers}: ProfileProviderProps) {
}, },
}); });
return ( const enterpriseProviders = providers.filter((p) => p.type === "sso" && p.id !== "passkey" && p.type !== "credential");
<div className="space-y-8 animate-in fade-in-50 duration-300"> const otherProviders = providers.filter((p) => p.type !== "sso" && p.id !== "passkey" && p.type !== "credential");
<div className="mb-6 space-y-1">
<h2 className="text-2xl font-semibold tracking-tight">Connected Accounts</h2>
<p className="text-sm text-muted-foreground">Manage the providers used to sign in to your account.</p>
</div>
<div className="grid gap-4"> const renderProvider = (provider: AuthProviderConfig) => {
{providers.map((provider) => {
const linkedAccount = accounts.find((acc) => acc.providerId === provider.id); const linkedAccount = accounts.find((acc) => acc.providerId === provider.id);
const isConnected = !!linkedAccount; const isConnected = !!linkedAccount;
// const canUnlink = totalConnected > 1 || (totalConnected === 1 && !provider.isManual); const canUnlink = totalConnected > 1 || (totalConnected === 1 && !provider.isManual);
const canUnlink = totalConnected > 1 ;
// const isLoading = isUnlinking || isLinking;
const isLoading = loadingProvider === provider.id; const isLoading = loadingProvider === provider.id;
return ( return (
<div key={provider.id} <div key={provider.id} className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted/30 transition-colors">
className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted/30 transition-colors">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center"> <div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center">
{provider.icon.startsWith("/") || provider.icon.startsWith("http") ? (
<Image
src={provider.icon}
alt={provider.id}
width={20}
height={20}
className="w-5 h-5"
unoptimized={provider.icon.startsWith("http")}
/>
) : (
<Icon icon={provider.icon} className="w-5 h-5" /> <Icon icon={provider.icon} className="w-5 h-5" />
)}
</div> </div>
<div className="space-y-0.5"> <div className="space-y-0.5">
<div className="font-medium flex items-center gap-2"> <div className="font-medium flex items-center gap-2">
{PROVIDERS_TEXT[provider.id].title} {provider.title || provider.name}
{isConnected && ( {isConnected && (
<Badge variant="secondary" <Badge variant="secondary" className="text-[10px] h-5 px-1.5 text-green-600 bg-green-500/10 border-0">
className="text-[10px] h-5 px-1.5 text-green-600 bg-green-500/10 border-0">
Active Active
</Badge> </Badge>
)} )}
</div> </div>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">{provider.description}</div>
{isConnected ? "Connected" : "Not Connected"}
</div>
</div> </div>
</div> </div>
@@ -117,17 +125,13 @@ export function ProfileProviders({accounts, providers}: ProfileProviderProps) {
disabled={!canUnlink || isLoading || provider.isManual} disabled={!canUnlink || isLoading || provider.isManual}
className={!canUnlink ? "opacity-50 cursor-not-allowed" : ""} className={!canUnlink ? "opacity-50 cursor-not-allowed" : ""}
> >
{isLoading ? <Loader2 {isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Unlink"}
className="w-4 h-4 animate-spin"/> : "Unlink"}
</Button> </Button>
</span> </span>
</TooltipTrigger> </TooltipTrigger>
{!canUnlink && ( {!canUnlink && (
<TooltipContent> <TooltipContent>
<p> <p>You cannot unlink your last authentication provider.</p>
You cannot unlink your last authentication provider or if you
don't have a password set.
</p>
</TooltipContent> </TooltipContent>
)} )}
</Tooltip> </Tooltip>
@@ -135,15 +139,9 @@ export function ProfileProviders({accounts, providers}: ProfileProviderProps) {
) : ( ) : (
<> <>
{provider.id === "credential" ? ( {provider.id === "credential" ? (
<SetPasswordProfileProviderModal open={isPasswordDialogOpen} <SetPasswordProfileProviderModal open={isPasswordDialogOpen} onOpenChange={setIsPasswordDialogOpen} />
onOpenChange={setIsPasswordDialogOpen}/>
) : ( ) : (
<Button <Button variant="default" size="sm" onClick={() => linkAccount(provider)} disabled={isLoading || provider.isManual}>
variant="default"
size="sm"
onClick={() => linkAccount(provider.id)}
disabled={isLoading || provider.isManual}
>
{isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Link"} {isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Link"}
</Button> </Button>
)} )}
@@ -152,33 +150,31 @@ export function ProfileProviders({accounts, providers}: ProfileProviderProps) {
</div> </div>
</div> </div>
); );
})} };
return (
<div className="space-y-8 animate-in fade-in-50 duration-300">
<div className="mb-6 space-y-1">
<h2 className="text-2xl font-semibold tracking-tight">Authentication</h2>
<p className="text-sm text-muted-foreground">Manage how you access your account.</p>
</div>
{enterpriseProviders.length > 0 && (
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">Enterprise Connection</h3>
<div className="grid gap-4">{enterpriseProviders.map(renderProvider)}</div>
</div>
)}
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">Standard Connections</h3>
<div className="grid gap-4">{otherProviders.map(renderProvider)}</div>
</div> </div>
<Alert variant={"default"}> <Alert variant={"default"}>
<AlertTriangle className="w-5 h-5 shrink-0 mt-0.5" /> <AlertTriangle className="w-5 h-5 shrink-0 mt-0.5" />
<AlertDescription> <AlertDescription>Linked providers allow you to log in to your account using any of these methods.</AlertDescription>
Linked providers allow you to log in to your account using any of these methods. If you use the same
email address with another provider, it will be automatically linked when you log in.
</AlertDescription>
</Alert> </Alert>
</div> </div>
); );
} }
const PROVIDERS_TEXT = {
credential: {
title: "Password",
description: "Use your email address and password to sign in.",
},
google: {
title: "Google",
description: "Sign in with your Google account.",
},
github: {
title: "GitHub",
description: "Sign in with your GitHub account.",
},
};
@@ -4,11 +4,10 @@ import {useState} from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import {Globe, LogOut, Loader2} from "lucide-react"; import { Globe, LogOut, Loader2, Fingerprint, Trash2, Plus } from "lucide-react";
import {Account, Session, User} from "@/db/schema/02_user"; import { useMutation, useQuery } from "@tanstack/react-query";
import {useMutation} from "@tanstack/react-query";
import { toast } from "sonner"; import { toast } from "sonner";
import {revokeAllSessionsAction, revokeSessionAction} from "./actions/security.action"; import { revokeAllSessionsAction, revokeSessionAction, getPasskeysAction, revokePasskeyAction } from "./actions/security.action";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { ResetPasswordProfileProviderModal } from "./modal/reset-password-modal"; import { ResetPasswordProfileProviderModal } from "./modal/reset-password-modal";
import { SetPasswordProfileProviderModal } from "./modal/set-password-modal"; import { SetPasswordProfileProviderModal } from "./modal/set-password-modal";
@@ -17,21 +16,30 @@ import {Disable2FAProfileProviderModal} from "./modal/disable-2fa-modal";
import { ViewBackupCodesModal } from "./modal/view-backup-codes-modal"; import { ViewBackupCodesModal } from "./modal/view-backup-codes-modal";
import { getDeviceDetails } from "@/utils/detection"; import { getDeviceDetails } from "@/utils/detection";
import { timeAgo } from "@/utils/date-formatting"; import { timeAgo } from "@/utils/date-formatting";
import { authClient } from "@/lib/auth/auth-client";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Account, Session, User } from "@/db/schema/02_user";
interface ProfileSecurityProps { interface ProfileSecurityProps {
user: User; user: User;
sessions: Session[]; sessions: Session[];
credentialAccount: Account; credentialAccount: Account;
currentSession: Session; currentSession: Session;
isPasswordEnabled?: boolean;
isPasskeyEnabled?: boolean;
} }
export function ProfileSecurity({user, sessions, credentialAccount, currentSession}: ProfileSecurityProps) { export function ProfileSecurity({ user, sessions, credentialAccount, currentSession, isPasswordEnabled = false, isPasskeyEnabled = false }: ProfileSecurityProps) {
const router = useRouter(); const router = useRouter();
const [isBackupCodesDialogOpen, setIsBackupCodesDialogOpen] = useState(false); const [isBackupCodesDialogOpen, setIsBackupCodesDialogOpen] = useState(false);
const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false); const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false);
const [isSetup2FADialogOpen, setIsSetup2FADialogOpen] = useState(false); const [isSetup2FADialogOpen, setIsSetup2FADialogOpen] = useState(false);
const [isDisable2FADialogOpen, setIsDisable2FADialogOpen] = useState(false); const [isDisable2FADialogOpen, setIsDisable2FADialogOpen] = useState(false);
const [isAddPasskeyOpen, setIsAddPasskeyOpen] = useState(false);
const [passkeyName, setPasskeyName] = useState("");
const { mutate: revokeSession, isPending: isRevoking } = useMutation({ const { mutate: revokeSession, isPending: isRevoking } = useMutation({
mutationFn: async (token: string) => { mutationFn: async (token: string) => {
@@ -59,71 +67,173 @@ export function ProfileSecurity({user, sessions, credentialAccount, currentSessi
}, },
}); });
const {
data: passkeys,
isLoading: isLoadingPasskeys,
refetch: refetchPasskeys,
} = useQuery({
queryKey: ["passkeys"],
queryFn: async () => {
const result = await getPasskeysAction();
if (result?.data?.success) {
return result.data.value;
}
throw new Error("Failed to fetch passkeys");
},
});
const { mutate: revokePasskey, isPending: isRevokingPasskey } = useMutation({
mutationFn: async (id: string) => {
const result = await revokePasskeyAction({ id });
if (!result?.data?.success) {
throw new Error("Failed to revoke passkey");
}
},
onSuccess: () => {
toast.success("Passkey revoked successfully");
refetchPasskeys();
},
onError: () => {
toast.error("Failed to revoke passkey");
},
});
const { mutate: addPasskey, isPending: isAddingPasskey } = useMutation({
mutationFn: async () => {
const result = await authClient.passkey.addPasskey({
name: passkeyName || "My Passkey",
});
if (result?.error) {
throw result.error;
}
return result;
},
onSuccess: () => {
toast.success("Passkey added successfully");
setIsAddPasskeyOpen(false);
setPasskeyName("");
refetchPasskeys();
},
onError: (error: any) => {
toast.error(error.message || "Failed to add passkey");
},
});
return ( return (
<div className="space-y-8 animate-in fade-in-50 duration-300"> <div className="space-y-8 animate-in fade-in-50 duration-300">
<div className="mb-6 space-y-1"> <div className="mb-6 space-y-1">
<h2 className="text-2xl font-semibold tracking-tight">Security Settings</h2> <h2 className="text-2xl font-semibold tracking-tight">Security Settings</h2>
<p className="text-sm text-muted-foreground">Manage your password, two-factor authentication and <p className="text-sm text-muted-foreground">Manage your password, two-factor authentication and sessions.</p>
sessions.</p>
</div> </div>
<div className="space-y-6"> <div className="space-y-6">
<h3 className="text-lg font-medium">Authentication</h3> <h3 className="text-lg font-medium">Authentication</h3>
<div className="border rounded-lg p-4 space-y-4"> <div className="border rounded-lg p-4 space-y-4">
{isPasswordEnabled && (
<>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="space-y-1"> <div className="space-y-1">
<div className="font-medium">Password</div> <div className="font-medium">Password</div>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{user.lastChangedPasswordAt {user.lastChangedPasswordAt ? `Last changed ${timeAgo(new Date(user.lastChangedPasswordAt))}` : "Never changed"}
? `Last changed ${timeAgo(new Date(user.lastChangedPasswordAt))}`
: "Never changed"}
</div> </div>
</div> </div>
{credentialAccount ? ( {credentialAccount ? (
<ResetPasswordProfileProviderModal open={isPasswordDialogOpen} <ResetPasswordProfileProviderModal open={isPasswordDialogOpen} onOpenChange={setIsPasswordDialogOpen} />
onOpenChange={setIsPasswordDialogOpen}/>
) : ( ) : (
<SetPasswordProfileProviderModal open={isPasswordDialogOpen} <SetPasswordProfileProviderModal open={isPasswordDialogOpen} onOpenChange={setIsPasswordDialogOpen} />
onOpenChange={setIsPasswordDialogOpen}/>
)} )}
</div> </div>
<Separator /> <Separator />
</>
)}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="font-medium">Two-Factor Authentication</div> <div className="font-medium">Two-Factor Authentication</div>
{user.twoFactorEnabled && ( {user.twoFactorEnabled && (
<Badge variant="secondary" <Badge variant="secondary" className="text-[10px] h-5 px-1.5 text-green-600 bg-green-500/10 border-0">
className="text-[10px] h-5 px-1.5 text-green-600 bg-green-500/10 border-0">
Active Active
</Badge> </Badge>
)} )}
</div> </div>
<div className="text-sm text-muted-foreground">Enhance the security of your account by <div className="text-sm text-muted-foreground">
requiring a second form of verification during login. Enhance the security of your account by requiring a second form of verification during login.
</div> </div>
</div> </div>
{user.twoFactorEnabled ? ( {user.twoFactorEnabled ? (
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<ViewBackupCodesModal open={isBackupCodesDialogOpen} <ViewBackupCodesModal open={isBackupCodesDialogOpen} onOpenChange={setIsBackupCodesDialogOpen} />
onOpenChange={setIsBackupCodesDialogOpen}/> <Disable2FAProfileProviderModal open={isDisable2FADialogOpen} onOpenChange={setIsDisable2FADialogOpen} />
<Disable2FAProfileProviderModal open={isDisable2FADialogOpen}
onOpenChange={setIsDisable2FADialogOpen}/>
</div> </div>
) : ( ) : (
<Setup2FAProfileProviderModal <Setup2FAProfileProviderModal disabled={!credentialAccount} open={isSetup2FADialogOpen} onOpenChange={setIsSetup2FADialogOpen} />
disabled={!credentialAccount}
open={isSetup2FADialogOpen}
onOpenChange={setIsSetup2FADialogOpen}
/>
)} )}
</div> </div>
</div> </div>
</div> </div>
{isPasskeyEnabled && (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="space-y-1">
<h3 className="text-lg font-medium">Passkeys</h3>
<div className="text-sm text-muted-foreground">Login securely with your fingerprint, face recognition, or hardware key.</div>
</div>
<Dialog open={isAddPasskeyOpen} onOpenChange={setIsAddPasskeyOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="sm">
<Plus className="mr-2 h-4 w-4" />
Add Passkey
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add New Passkey</DialogTitle>
<DialogDescription>Create a name for your passkey to identify it later.</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Passkey Name</Label>
<Input
id="name"
placeholder="e.g. MacBook Pro, iPhone, YubiKey"
value={passkeyName}
onChange={(e) => setPasskeyName(e.target.value)}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddPasskeyOpen(false)}>
Cancel
</Button>
<Button onClick={() => addPasskey()} disabled={isAddingPasskey}>
{isAddingPasskey && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Create Passkey
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<div className="border rounded-lg divide-y">
{isLoadingPasskeys ? (
<div className="flex items-center justify-center p-4">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : passkeys && passkeys.length > 0 ? (
passkeys.map((pk: any) => <PasskeyRow key={pk.id} passkey={pk} onRevoke={(id) => revokePasskey(id)} isRevoking={isRevokingPasskey} />)
) : (
<div className="p-4 text-center text-muted-foreground">No passkeys found.</div>
)}
</div>
</div>
)}
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-lg font-medium">Active Sessions</h3> <h3 className="text-lg font-medium">Active Sessions</h3>
@@ -181,8 +291,7 @@ function SessionRow({
</div> </div>
<div className="space-y-0.5"> <div className="space-y-0.5">
<div className="text-sm font-medium flex items-center gap-2"> <div className="text-sm font-medium flex items-center gap-2">
{deviceInfo.os} <span {deviceInfo.os} <span className="text-muted-foreground font-normal"> {deviceInfo.browser}</span>
className="text-muted-foreground font-normal"> {deviceInfo.browser}</span>
{session.id === currentSession.id && ( {session.id === currentSession.id && (
<Badge <Badge
variant="outline" variant="outline"
@@ -194,11 +303,7 @@ function SessionRow({
</div> </div>
<div className="text-xs text-muted-foreground flex items-center gap-1"> <div className="text-xs text-muted-foreground flex items-center gap-1">
<Globe className="w-3 h-3" /> {session.ipAddress} <Globe className="w-3 h-3" /> {session.ipAddress}
<span className="ml-1"> <span className="ml-1">{session.id === currentSession.id ? "Active now" : `Last active ${timeAgo(new Date(session.createdAt))}`}</span>
{session.id === currentSession.id
? "Active now"
: `Last active ${timeAgo(new Date(session.createdAt))}`}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -218,3 +323,29 @@ function SessionRow({
</div> </div>
); );
} }
function PasskeyRow({ passkey, onRevoke, isRevoking }: { passkey: any; onRevoke: (id: string) => void; isRevoking: boolean }) {
return (
<div className="flex items-center justify-between p-4">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center text-muted-foreground">
<Fingerprint className="w-5 h-5" />
</div>
<div className="space-y-0.5">
<div className="font-medium text-sm">{passkey.name || "Unnamed Passkey"}</div>
<div className="text-xs text-muted-foreground">Created {timeAgo(new Date(passkey.createdAt))}</div>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-destructive"
onClick={() => onRevoke(passkey.id)}
disabled={isRevoking}
>
{isRevoking ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
<span className="sr-only">Revoke</span>
</Button>
</div>
);
}
@@ -0,0 +1,13 @@
CREATE TABLE "sso_provider" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"issuer" text NOT NULL,
"oidc_config" json,
"saml_config" json,
"user_id" uuid,
"provider_id" text NOT NULL,
"organization_id" text,
"domain" text NOT NULL,
CONSTRAINT "sso_provider_provider_id_unique" UNIQUE("provider_id")
);
--> statement-breakpoint
ALTER TABLE "sso_provider" ADD CONSTRAINT "sso_provider_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
@@ -0,0 +1,14 @@
ALTER TABLE "passkey" DROP CONSTRAINT "passkey_userId_user_id_fk";
--> statement-breakpoint
ALTER TABLE "passkey" ADD COLUMN "public_key" text NOT NULL;--> statement-breakpoint
ALTER TABLE "passkey" ADD COLUMN "user_id" uuid NOT NULL;--> statement-breakpoint
ALTER TABLE "passkey" ADD COLUMN "credential_i_d" text NOT NULL;--> statement-breakpoint
ALTER TABLE "passkey" ADD COLUMN "device_type" text NOT NULL;--> statement-breakpoint
ALTER TABLE "passkey" ADD COLUMN "backed_up" boolean NOT NULL;--> statement-breakpoint
ALTER TABLE "passkey" ADD COLUMN "aaguid" text;--> statement-breakpoint
ALTER TABLE "passkey" ADD CONSTRAINT "passkey_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "passkey" DROP COLUMN "publicKey";--> statement-breakpoint
ALTER TABLE "passkey" DROP COLUMN "userId";--> statement-breakpoint
ALTER TABLE "passkey" DROP COLUMN "credentialId";--> statement-breakpoint
ALTER TABLE "passkey" DROP COLUMN "deviceType";--> statement-breakpoint
ALTER TABLE "passkey" DROP COLUMN "backedUp";
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+14
View File
@@ -239,6 +239,20 @@
"when": 1770923665015, "when": 1770923665015,
"tag": "0033_handy_valeria_richards", "tag": "0033_handy_valeria_richards",
"breakpoints": true "breakpoints": true
},
{
"idx": 34,
"version": "7",
"when": 1770991368921,
"tag": "0034_vengeful_blacklash",
"breakpoints": true
},
{
"idx": 35,
"version": "7",
"when": 1770993283219,
"tag": "0035_windy_shockwave",
"breakpoints": true
} }
] ]
} }
+40 -12
View File
@@ -1,5 +1,5 @@
import {relations} from "drizzle-orm"; import {relations} from "drizzle-orm";
import {boolean, integer, pgEnum, pgTable, text, timestamp, uuid} from "drizzle-orm/pg-core"; import {boolean, integer, pgEnum, json, pgTable, text, timestamp, uuid} from "drizzle-orm/pg-core";
import {createSelectSchema} from "drizzle-zod"; import {createSelectSchema} from "drizzle-zod";
import {z} from "zod"; import {z} from "zod";
import {project} from "./06_project"; import {project} from "./06_project";
@@ -72,19 +72,19 @@ export const verification = pgTable("verification", {
}); });
export const passkey = pgTable("passkey", { export const passkey = pgTable("passkey", {
id: uuid().defaultRandom().primaryKey(), id: uuid("id").defaultRandom().primaryKey(),
name: text(), name: text("name"),
publicKey: text().notNull(), publicKey: text("public_key").notNull(),
userId: uuid() userId: uuid("user_id")
.notNull() .notNull()
.references(() => user.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
credentialId: text().notNull(), credentialID: text("credential_i_d").notNull(),
counter: integer().notNull(), counter: integer("counter").notNull(),
deviceType: text().notNull(), deviceType: text("device_type").notNull(),
backedUp: boolean().notNull(), backedUp: boolean("backed_up").notNull(),
transports: text(), transports: text("transports"),
aaguid: text("aaguid"),
...timestamps, ...timestamps
}); });
export const twoFactor = pgTable("two_factor", { export const twoFactor = pgTable("two_factor", {
@@ -96,11 +96,24 @@ export const twoFactor = pgTable("two_factor", {
.references(() => user.id, {onDelete: "cascade"}), .references(() => user.id, {onDelete: "cascade"}),
}); });
export const ssoProvider = pgTable("sso_provider", {
id: uuid("id").defaultRandom().primaryKey(),
issuer: text("issuer").notNull(),
oidcConfig: json("oidc_config"),
samlConfig: json("saml_config"),
userId: uuid("user_id").references(() => user.id, { onDelete: "cascade" }),
providerId: text("provider_id").notNull().unique(),
organizationId: text("organization_id"),
domain: text("domain").notNull(),
});
export const userRelations = relations(user, ({many}) => ({ export const userRelations = relations(user, ({many}) => ({
sessions: many(session), sessions: many(session),
accounts: many(account), accounts: many(account),
ssoProviders: many(ssoProvider),
memberships: many(member), memberships: many(member),
invitations: many(invitation), invitations: many(invitation),
passkeys: many(passkey),
})); }));
export const sessionRelations = relations(session, ({one}) => ({ export const sessionRelations = relations(session, ({one}) => ({
@@ -117,6 +130,13 @@ export const accountRelations = relations(account, ({one}) => ({
}), }),
})); }));
export const ssoProviderRelations = relations(ssoProvider, ({ one }) => ({
user: one(user, {
fields: [ssoProvider.userId],
references: [user.id],
}),
}));
export const projectRelations = relations(project, ({one}) => ({ export const projectRelations = relations(project, ({one}) => ({
organization: one(organization, { organization: one(organization, {
fields: [project.organizationId], fields: [project.organizationId],
@@ -124,6 +144,14 @@ export const projectRelations = relations(project, ({one}) => ({
}), }),
})); }));
export const passkeyRelations = relations(passkey, ({ one }) => ({
user: one(user, {
fields: [passkey.userId],
references: [user.id],
}),
}));
export const userSchema = createSelectSchema(user); export const userSchema = createSelectSchema(user);
export type User = z.infer<typeof userSchema>; export type User = z.infer<typeof userSchema>;
+41 -3
View File
@@ -28,6 +28,9 @@ export const env = createEnv({
AUTH_GOOGLE_SECRET: z.string().optional(), AUTH_GOOGLE_SECRET: z.string().optional(),
AUTH_GOOGLE_METHOD: z.boolean().default(false), AUTH_GOOGLE_METHOD: z.boolean().default(false),
AUTH_GITHUB_ID: z.string().optional(),
AUTH_GITHUB_SECRET: z.string().optional(),
S3_ENDPOINT: z.string().optional(), S3_ENDPOINT: z.string().optional(),
S3_ACCESS_KEY: z.string().optional(), S3_ACCESS_KEY: z.string().optional(),
S3_SECRET_KEY: z.string().optional(), S3_SECRET_KEY: z.string().optional(),
@@ -37,10 +40,25 @@ export const env = createEnv({
STORAGE_TYPE: z.enum(["local", "s3"]).optional(), STORAGE_TYPE: z.enum(["local", "s3"]).optional(),
RETENTION_CRON: z RETENTION_CRON: z.string().default(process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *"),
.string()
.default(process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *"),
AUTH_OIDC_ID: z.string().optional().default("oidc"),
AUTH_OIDC_TITLE: z.string().optional(),
AUTH_OIDC_DESC: z.string().optional(),
AUTH_OIDC_ICON: z.string().optional(),
AUTH_OIDC_CLIENT: z.string().optional(),
AUTH_OIDC_SECRET: z.string().optional(),
AUTH_OIDC_ISSUER_URL: z.string().optional(),
AUTH_OIDC_HOST: z.string().optional(),
AUTH_OIDC_SCOPES: z.string().optional(),
AUTH_OIDC_DISCOVERY_ENDPOINT: z.string().optional(),
AUTH_OIDC_JWKS_ENDPOINT: z.string().optional(),
AUTH_OIDC_PKCE: z.string().optional(),
ALLOWED_GROUP: z.string().optional(),
AUTH_EMAIL_PASSWORD_ENABLED: z.string().optional().default("true"),
AUTH_SIGNUP_ENABLED: z.string().optional().default("true"),
AUTH_PASSKEY_ENABLED: z.string().optional().default("true"),
}, },
client: { client: {
NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(), NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(),
@@ -66,6 +84,9 @@ export const env = createEnv({
AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET, AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
AUTH_GOOGLE_METHOD: process.env.AUTH_GOOGLE_METHOD === "true", AUTH_GOOGLE_METHOD: process.env.AUTH_GOOGLE_METHOD === "true",
AUTH_GITHUB_ID: process.env.AUTH_GITHUB_ID,
AUTH_GITHUB_SECRET: process.env.AUTH_GITHUB_SECRET,
S3_ENDPOINT: process.env.S3_ENDPOINT, S3_ENDPOINT: process.env.S3_ENDPOINT,
S3_ACCESS_KEY: process.env.S3_ACCESS_KEY, S3_ACCESS_KEY: process.env.S3_ACCESS_KEY,
S3_SECRET_KEY: process.env.S3_SECRET_KEY, S3_SECRET_KEY: process.env.S3_SECRET_KEY,
@@ -77,5 +98,22 @@ export const env = createEnv({
RETENTION_CRON: process.env.RETENTION_CRON, RETENTION_CRON: process.env.RETENTION_CRON,
AUTH_OIDC_ID: process.env.AUTH_OIDC_ID,
AUTH_OIDC_TITLE: process.env.AUTH_OIDC_TITLE,
AUTH_OIDC_DESC: process.env.AUTH_OIDC_DESC,
AUTH_OIDC_ICON: process.env.AUTH_OIDC_ICON,
AUTH_OIDC_CLIENT: process.env.AUTH_OIDC_CLIENT,
AUTH_OIDC_SECRET: process.env.AUTH_OIDC_SECRET,
AUTH_OIDC_ISSUER_URL: process.env.AUTH_OIDC_ISSUER_URL,
AUTH_OIDC_HOST: process.env.AUTH_OIDC_HOST,
AUTH_OIDC_SCOPES: process.env.AUTH_OIDC_SCOPES,
AUTH_OIDC_DISCOVERY_ENDPOINT: process.env.AUTH_OIDC_DISCOVERY_ENDPOINT,
AUTH_OIDC_JWKS_ENDPOINT: process.env.AUTH_OIDC_JWKS_ENDPOINT,
AUTH_OIDC_PKCE: process.env.AUTH_OIDC_PKCE,
ALLOWED_GROUP: process.env.ALLOWED_GROUP,
AUTH_EMAIL_PASSWORD_ENABLED: process.env.AUTH_EMAIL_PASSWORD_ENABLED,
AUTH_SIGNUP_ENABLED: process.env.AUTH_SIGNUP_ENABLED,
AUTH_PASSKEY_ENABLED: process.env.AUTH_PASSKEY_ENABLED,
}, },
}); });
+6 -2
View File
@@ -3,8 +3,10 @@ import {createAuthClient} from "better-auth/react";
import {adminClient, inferAdditionalFields, organizationClient, twoFactorClient} from "better-auth/client/plugins"; import {adminClient, inferAdditionalFields, organizationClient, twoFactorClient} from "better-auth/client/plugins";
import {ac, user, admin as adminRole, pending, superadmin, orgAdmin, orgMember, orgOwner} from "./permissions"; import {ac, user, admin as adminRole, pending, superadmin, orgAdmin, orgMember, orgOwner} from "./permissions";
import {auth} from "@/lib/auth/auth"; import type {auth} from "@/lib/auth/auth";
import {getServerUrl} from "@/utils/get-server-url"; import {getServerUrl} from "@/utils/get-server-url";
import { ssoClient } from "@better-auth/sso/client";
import { passkeyClient } from "@better-auth/passkey/client"
const res = await fetch(`${getServerUrl()}/api/config`); const res = await fetch(`${getServerUrl()}/api/config`);
const {PROJECT_URL} = await res.json(); const {PROJECT_URL} = await res.json();
@@ -12,7 +14,9 @@ const {PROJECT_URL} = await res.json();
export const authClient = createAuthClient({ export const authClient = createAuthClient({
baseURL: PROJECT_URL, baseURL: PROJECT_URL,
plugins: [ plugins: [
passkeyClient(),
twoFactorClient(), twoFactorClient(),
ssoClient(),
organizationClient({ organizationClient({
ac, ac,
roles: { roles: {
@@ -35,4 +39,4 @@ export const authClient = createAuthClient({
}); });
export const {signIn, signOut, signUp, useSession, listAccounts, admin, requestPasswordReset} = authClient; export const { signIn, signOut, signUp, deleteUser, useSession, listAccounts, passkey, admin, twoFactor, requestPasswordReset, sso } = authClient;
+103 -7
View File
@@ -11,12 +11,14 @@ import {count, eq} from "drizzle-orm";
import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization"; import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
import {sendEmail} from "@/lib/email"; import {sendEmail} from "@/lib/email";
import {render} from "@react-email/render"; import {render} from "@react-email/render";
import {AuthProviderConfig, SUPPORTED_PROVIDERS} from "../../../portabase.config";
import {withUpdatedAt} from "@/db/utils"; import {withUpdatedAt} from "@/db/utils";
import EmailVerification from "@/components/emails/auth/email-verification"; import EmailVerification from "@/components/emails/auth/email-verification";
import EmailForgotPassword from "@/components/emails/auth/email-forgot-password"; import EmailForgotPassword from "@/components/emails/auth/email-forgot-password";
import {getDeviceDetails} from "@/utils/detection"; import {getDeviceDetails} from "@/utils/detection";
import EmailNewLogin from "@/components/emails/auth/email-new-login"; import EmailNewLogin from "@/components/emails/auth/email-new-login";
import { sso } from "@better-auth/sso";
import { AuthProviderConfig, SUPPORTED_PROVIDERS } from "@/lib/auth/config";
import { passkey } from "@better-auth/passkey";
export const auth = betterAuth({ export const auth = betterAuth({
database: drizzleAdapter(db, { database: drizzleAdapter(db, {
@@ -27,11 +29,11 @@ export const auth = betterAuth({
baseURL: env.PROJECT_URL, baseURL: env.PROJECT_URL,
secret: env.PROJECT_SECRET, secret: env.PROJECT_SECRET,
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: env.AUTH_EMAIL_PASSWORD_ENABLED === "true",
requireEmailVerification: false, requireEmailVerification: false,
sendResetPassword: async ({user, token}, request) => { sendResetPassword: async ({user, token}, request) => {
const [updatedUser] = await db.update(drizzleDb.schemas.user).set(withUpdatedAt({ await db.update(drizzleDb.schemas.user).set(withUpdatedAt({
emailVerified: true, emailVerified: true,
})).where(eq(drizzleDb.schemas.user.id, user.id)).returning(); })).where(eq(drizzleDb.schemas.user.id, user.id)).returning();
@@ -78,6 +80,7 @@ export const auth = betterAuth({
socialProviders: SUPPORTED_PROVIDERS.reduce((acc: any, provider: AuthProviderConfig) => { socialProviders: SUPPORTED_PROVIDERS.reduce((acc: any, provider: AuthProviderConfig) => {
if (!provider.isActive) return acc; if (!provider.isActive) return acc;
if (provider.id === "credential") return acc; if (provider.id === "credential") return acc;
if (provider.id === env.AUTH_OIDC_ID!) return acc;
if (provider.id === "google") { if (provider.id === "google") {
acc.google = { acc.google = {
clientId: env.AUTH_GOOGLE_ID! as string, clientId: env.AUTH_GOOGLE_ID! as string,
@@ -86,8 +89,8 @@ export const auth = betterAuth({
} }
if (provider.id === "github") { if (provider.id === "github") {
acc.github = { acc.github = {
clientId: provider.credentials?.clientId, // clientId: provider.credentials?.clientId,
clientSecret: provider.credentials?.clientSecret, // clientSecret: provider.credentials?.clientSecret,
}; };
} }
return acc; return acc;
@@ -95,12 +98,69 @@ export const auth = betterAuth({
account: { account: {
accountLinking: { accountLinking: {
enabled: true, enabled: true,
trustedProviders: ["google", "github", "credential"], trustedProviders: ["google", "github", "credential",env.AUTH_OIDC_ID!],
allowDifferentEmails: false allowDifferentEmails: false
}, },
}, },
plugins: [ plugins: [
sso({
defaultSSO: [{
oidcConfig: {
issuer: env.AUTH_OIDC_ISSUER_URL!,
discoveryEndpoint: env.AUTH_OIDC_DISCOVERY_ENDPOINT!,
jwksEndpoint: env.AUTH_OIDC_JWKS_ENDPOINT!,
clientId: env.AUTH_OIDC_CLIENT!,
clientSecret: env.AUTH_OIDC_SECRET!,
scopes: env.AUTH_OIDC_SCOPES?.split(" ") ?? ["openid", "profile", "email"],
pkce: env.AUTH_OIDC_PKCE === "true",
mapping: {
extraFields: {
groups: "groups"
}
}
},
providerId: env.AUTH_OIDC_ID!,
domain: env.AUTH_OIDC_HOST!,
//@ts-ignore
issuer: env.AUTH_OIDC_ISSUER_URL!
}],
provisionUser: async ({ user: usr, userInfo }) => {
const allowedGroup = env.ALLOWED_GROUP;
if (!allowedGroup) return;
const rawGroups = (userInfo as any).groups || (userInfo as any).roles || [];
const userGroups: string[] = Array.isArray(rawGroups) ? rawGroups : [rawGroups];
const hasAccess = userGroups.includes(allowedGroup);
if (!hasAccess) {
throw new Error("Access Denied");
}
const userCount = (await db.select({ count: count() }).from(drizzleDb.schemas.user))[0].count;
const isSuperadmin = userCount === 0 ? "superadmin" : undefined;
const roleToAssign = allowedGroup.includes('admin') || allowedGroup.includes('superadmin') ?
isSuperadmin ? 'superadmin' : "admin" : 'pending';
const existingUser = await db.query.user.findFirst({
where: eq(drizzleDb.schemas.user.email, usr.email)
});
if (existingUser) {
await db.update(drizzleDb.schemas.user)
.set({ role: roleToAssign, emailVerified: true })
.where(eq(drizzleDb.schemas.user.id, existingUser.id));
}
},
}),
...(env.AUTH_PASSKEY_ENABLED === "true" ? [passkey({
rpName: env.PROJECT_NAME || "Portabase",
rpID: env.PROJECT_URL ? new URL(env.PROJECT_URL).hostname : "localhost"
})] : []),
openAPI(), openAPI(),
nextCookies(), nextCookies(),
twoFactor(), twoFactor(),
@@ -155,11 +215,28 @@ export const auth = betterAuth({
}, },
databaseHooks: { databaseHooks: {
user: { user: {
update: {
async before(user, context) {
if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") {
if (user.password || user.lastChangedPasswordAt) {
throw new Error("Password updates are disabled");
}
}
return {
data: user,
};
},
},
create: { create: {
async before(user, context) { async before(user, context) {
const userCount = (await db.select({count: count()}).from(drizzleDb.schemas.user))[0].count; const userCount = (await db.select({count: count()}).from(drizzleDb.schemas.user))[0].count;
if (env.AUTH_SIGNUP_ENABLED !== "true" && userCount > 0) {
throw new Error("Sign up is disabled");
}
const role = userCount === 0 ? "superadmin" : "pending"; const role = userCount === 0 ? "superadmin" : "pending";
// const role = "admin";
return { return {
data: { data: {
...user, ...user,
@@ -417,6 +494,25 @@ export const getOrganization = async ({
} }
}; };
export const getPasskeys = async () => {
if (env.AUTH_PASSKEY_ENABLED !== "true") return [];
const passkeys = await auth.api.listPasskeys({
headers: await headers(),
});
return passkeys;
};
export const revokePasskey = async (e: string) => {
if (env.AUTH_PASSKEY_ENABLED !== "true") return;
await auth.api.deletePasskey({
body: {
id: e,
},
headers: await headers(),
});
};
export const listOrganizations = async (): Promise<Organization[] | null> => { export const listOrganizations = async (): Promise<Organization[] | null> => {
try { try {
return await auth.api.listOrganizations({ return await auth.api.listOrganizations({
+63
View File
@@ -0,0 +1,63 @@
import { env } from "@/env.mjs";
export interface AuthProviderConfig {
id: string;
isActive: boolean;
name?: string;
icon: string;
isManual?: boolean;
title?: string;
description?: string;
type: "social" | "sso" | "credential" | "passkey";
}
export const SUPPORTED_PROVIDERS: AuthProviderConfig[] = [
{
id: "credential",
isActive: env.AUTH_EMAIL_PASSWORD_ENABLED === "true",
name: "Password",
icon: "lucide:lock",
title: "Password",
description: "Standard email and password login.",
isManual: true,
type: "credential"
},
{
id: "google",
isActive: !!env.AUTH_GOOGLE_ID,
name: "Google",
icon: "logos:google-icon",
title: "Google",
description: "Sign in with your Google account.",
type: "social"
},
{
id: "github",
isActive: !!env.AUTH_GITHUB_ID,
name: "GitHub",
icon: "logos:github-icon",
title: "GitHub",
description: "Sign in with your GitHub account.",
type: "social"
},
{
id: env.AUTH_OIDC_ID || "oidc",
isActive: !!env.AUTH_OIDC_CLIENT,
name: env.AUTH_OIDC_TITLE || "SSO",
icon: env.AUTH_OIDC_ICON || "lucide:building",
title: env.AUTH_OIDC_TITLE || "SSO",
description: env.AUTH_OIDC_DESC || "Sign in with your SSO account.",
isManual: true,
type: "sso"
},
{
id: "passkey",
isActive: env.AUTH_PASSKEY_ENABLED === "true",
name: "Passkey",
icon: "lucide:fingerprint",
title: "Passkey",
description: "Sign in with your passkey.",
isManual: false,
type: "passkey"
}
];