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="* * * * *"
+11 -7
View File
@@ -1,10 +1,15 @@
import {CardContent, CardHeader} from "@/components/ui/card"; 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 {CardAuth} from "@/features/layout/card-auth"; import { env } from "@/env.mjs";
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,12 +17,11 @@ 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>
<ForgotPasswordForm/> <ForgotPasswordForm />
</CardContent> </CardContent>
</CardAuth> </CardAuth>
</TooltipProvider> </TooltipProvider>
+27 -28
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}/>
</>
)} )}
<div className="mt-4 text-center text-sm"> {SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive).length > 0 && <SocialAuthButtons providers={SUPPORTED_PROVIDERS} />}
Don&apos;t have an account ?{" "}
<Link href="/register" className="underline"> {env.AUTH_SIGNUP_ENABLED === "true" && (
Sign up <div className="mt-4 text-center text-sm">
</Link> Don&apos;t have an account ?{" "}
</div> <Link href="/register" className="underline">
Sign up
</Link>
</div>
)}
</CardContent> </CardContent>
</CardAuth> </CardAuth>
</TooltipProvider> </TooltipProvider>
);
)
} }
+12 -6
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>
) );
} }
+5 -1
View File
@@ -4,9 +4,13 @@ import { ResetPasswordForm } from "@/components/wrappers/auth/login/reset-passwo
import { redirect } from "next/navigation"; 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;
+41 -29
View File
@@ -1,35 +1,47 @@
name: portabase-dev name: portabase-dev
services: services:
db: db:
image: postgres:17-alpine image: postgres:17-alpine
ports: ports:
- "5433:5432" - "5433:5432"
volumes: volumes:
- postgres-data:/var/lib/postgresql/data - postgres-data:/var/lib/postgresql/data
environment: environment:
- POSTGRES_DB=devdb - POSTGRES_DB=devdb
- POSTGRES_USER=devuser - POSTGRES_USER=devuser
- POSTGRES_PASSWORD=changeme - POSTGRES_PASSWORD=changeme
healthcheck: healthcheck:
test: [ "CMD-SHELL", "pg_isready -U devuser -d devdb" ] test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
tusd: tusd:
image: tusproject/tusd:v2.8.0 image: tusproject/tusd:v2.8.0
ports: ports:
- "1080:8080" - "1080:8080"
command: > command: >
-upload-dir /data/uploads/tmp -upload-dir /data/uploads/tmp
-hooks-http http://localhost:8887/api/tus/hooks -hooks-http http://localhost:8887/api/tus/hooks
-max-size 21474836480 -max-size 21474836480
-base-path /tus/files/ -base-path /tus/files/
extra_hosts: extra_hosts:
- "localhost:host-gateway" - "localhost:host-gateway"
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/>
+53 -35
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") {
callbackURL: "/dashboard", result = await authClient.signIn.passkey({
}); fetchOptions: {
onSuccess() {
if (error) { router.push("/dashboard");
toast.error("An error occurred while signing in with the provider. Please try again."); },
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",
});
} 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 ? (
<Icon icon={provider.icon} className="h-4 w-4"/> <Loader2 className="h-4 w-4 animate-spin" />
} ) : provider.icon.startsWith("/") || provider.icon.startsWith("http") ? (
<span>{PROVIDERS_TEXT[provider.id].title}</span> <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" />
)}
<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 (
@@ -1,29 +1,21 @@
"use client"; "use client";
import {ChevronsUpDown} from "lucide-react"; import { ChevronsUpDown } from "lucide-react";
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar"; 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
@@ -40,20 +32,16 @@ export const LoggedInButtonClient = ({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Avatar className="size-6"> <Avatar className="size-6">
<AvatarFallback>{user.name[0].toUpperCase()}</AvatarFallback> <AvatarFallback>{user.name[0].toUpperCase()}</AvatarFallback>
{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 {user.email}
className="text-xs text-muted-foreground max-w-[170px] truncate" </span>
title={user.email}
>
{user.email}
</span>
</div> </div>
</div> </div>
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50"/> <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
</SidebarMenuButton> </SidebarMenuButton>
</LoggedInDropdown> </LoggedInDropdown>
); );
@@ -1,6 +1,6 @@
"use client"; "use client";
import {PropsWithChildren, ReactNode, useState} from "react"; import { PropsWithChildren, ReactNode, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -10,11 +10,11 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSepara
import { signOut } from "@/lib/auth/auth-client"; import { signOut } from "@/lib/auth/auth-client";
import {ProfileModal} from "@/components/wrappers/dashboard/common/profile/profile-modal"; import { ProfileModal } from "@/components/wrappers/dashboard/common/profile/profile-modal";
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);
@@ -44,50 +41,46 @@ export const LoggedInDropdown = ({ user, sessions, currentSession, accounts, chi
onOpenChange={setIsModalOpen} onOpenChange={setIsModalOpen}
providers={providers} providers={providers}
/> />
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger> <DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
className="w-[var(--radix-popper-anchor-width)] rounded-xl border-2 border-border bg-popover shadow-none p-1" className="w-[var(--radix-popper-anchor-width)] rounded-xl border-2 border-border bg-popover shadow-none p-1"
align="start" align="start"
side="top" side="top"
sideOffset={8} sideOffset={8}
> >
<DropdownMenuItem <DropdownMenuItem
onClick={() => setIsModalOpen(!isModalOpen)} onClick={() => setIsModalOpen(!isModalOpen)}
className="group gap-2 p-1 cursor-pointer rounded-lg mb-1 transition-colors focus:bg-accent hover:bg-accent/50 border border-transparent" className="group gap-2 p-1 cursor-pointer rounded-lg mb-1 transition-colors focus:bg-accent hover:bg-accent/50 border border-transparent"
> >
<div className="flex size-9 items-center justify-center rounded-md border border-border bg-muted/50 shadow-sm transition-all group-hover:shadow-md group-hover:bg-background"> <div className="flex size-9 items-center justify-center rounded-md border border-border bg-muted/50 shadow-sm transition-all group-hover:shadow-md group-hover:bg-background">
<User size={18} className="text-muted-foreground group-hover:text-foreground transition-colors" /> <User size={18} className="text-muted-foreground group-hover:text-foreground transition-colors" />
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-sm font-medium leading-none">Account Settings</span> <span className="text-sm font-medium leading-none">Account Settings</span>
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
className="group gap-2 p-1 cursor-pointer rounded-lg transition-colors focus:bg-red-50 dark:focus:bg-red-950/20 border border-transparent text-red-600 focus:text-red-600" className="group gap-2 p-1 cursor-pointer rounded-lg transition-colors focus:bg-red-50 dark:focus:bg-red-950/20 border border-transparent text-red-600 focus:text-red-600"
onClick={async () => { onClick={async () => {
await signOut({ await signOut({
fetchOptions: { fetchOptions: {
onSuccess: () => { onSuccess: () => {
router.push("/login"); router.push("/login");
}, },
}, },
}); });
}} }}
> >
<div className="flex size-9 items-center justify-center rounded-md border border-red-100 bg-red-50/50 dark:border-red-900/30 dark:bg-red-950/20 shadow-sm transition-all group-hover:shadow-md"> <div className="flex size-9 items-center justify-center rounded-md border border-red-100 bg-red-50/50 dark:border-red-900/30 dark:bg-red-950/20 shadow-sm transition-all group-hover:shadow-md">
<LogOut size={18} className="text-red-500" /> <LogOut size={18} className="text-red-500" />
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-sm font-medium leading-none">Logout</span> <span className="text-sm font-medium leading-none">Logout</span>
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
</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,37 +1,37 @@
"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"; import { useRouter } from "next/navigation";
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 { Icon } from "@iconify/react";
import {AuthProviderConfig} from "../../../../../portabase.config"; import Image from "next/image";
import {Icon} from "@iconify/react"; 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) {
const router = useRouter(); const router = useRouter();
const totalConnected = accounts.length; const totalConnected = accounts.length;
const [loadingProvider, setLoadingProvider] = useState<string | null>(null); const [loadingProvider, setLoadingProvider] = useState<string | null>(null);
const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false); const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false);
const {mutate: unlinkAccount} = useMutation({ const { mutate: unlinkAccount } = useMutation({
mutationFn: async (providerId: string) => { mutationFn: async (providerId: string) => {
setLoadingProvider(providerId); setLoadingProvider(providerId);
const {error} = await authClient.unlinkAccount({ providerId }); const { error } = await authClient.unlinkAccount({ providerId });
if (error) throw error; if (error) throw error;
}, },
onSuccess: () => { onSuccess: () => {
@@ -45,17 +45,25 @@ 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") {
callbackURL: "/dashboard", result = await authClient.signIn.sso({
}); providerId: provider.id,
if (error) throw error; providerType: "oidc",
callbackURL: "/dashboard",
});
} 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,120 +73,108 @@ export function ProfileProviders({accounts, providers}: ProfileProviderProps) {
}, },
}); });
const enterpriseProviders = providers.filter((p) => p.type === "sso" && p.id !== "passkey" && p.type !== "credential");
const otherProviders = providers.filter((p) => p.type !== "sso" && p.id !== "passkey" && p.type !== "credential");
const renderProvider = (provider: AuthProviderConfig) => {
const linkedAccount = accounts.find((acc) => acc.providerId === provider.id);
const isConnected = !!linkedAccount;
const canUnlink = totalConnected > 1 || (totalConnected === 1 && !provider.isManual);
const isLoading = loadingProvider === provider.id;
return (
<div key={provider.id} 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="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" />
)}
</div>
<div className="space-y-0.5">
<div className="font-medium flex items-center gap-2">
{provider.title || provider.name}
{isConnected && (
<Badge variant="secondary" className="text-[10px] h-5 px-1.5 text-green-600 bg-green-500/10 border-0">
Active
</Badge>
)}
</div>
<div className="text-sm text-muted-foreground">{provider.description}</div>
</div>
</div>
<div className="flex items-center gap-2">
{isConnected ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span tabIndex={0}>
<Button
variant="outline"
size="sm"
onClick={() => unlinkAccount(provider.id)}
disabled={!canUnlink || isLoading || provider.isManual}
className={!canUnlink ? "opacity-50 cursor-not-allowed" : ""}
>
{isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Unlink"}
</Button>
</span>
</TooltipTrigger>
{!canUnlink && (
<TooltipContent>
<p>You cannot unlink your last authentication provider.</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
) : (
<>
{provider.id === "credential" ? (
<SetPasswordProfileProviderModal open={isPasswordDialogOpen} onOpenChange={setIsPasswordDialogOpen} />
) : (
<Button variant="default" size="sm" onClick={() => linkAccount(provider)} disabled={isLoading || provider.isManual}>
{isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Link"}
</Button>
)}
</>
)}
</div>
</div>
);
};
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">Connected Accounts</h2> <h2 className="text-2xl font-semibold tracking-tight">Authentication</h2>
<p className="text-sm text-muted-foreground">Manage the providers used to sign in to your account.</p> <p className="text-sm text-muted-foreground">Manage how you access your account.</p>
</div> </div>
<div className="grid gap-4"> {enterpriseProviders.length > 0 && (
{providers.map((provider) => { <div className="space-y-4">
const linkedAccount = accounts.find((acc) => acc.providerId === provider.id); <h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">Enterprise Connection</h3>
const isConnected = !!linkedAccount; <div className="grid gap-4">{enterpriseProviders.map(renderProvider)}</div>
// const canUnlink = totalConnected > 1 || (totalConnected === 1 && !provider.isManual); </div>
const canUnlink = totalConnected > 1 ; )}
// const isLoading = isUnlinking || isLinking;
const isLoading = loadingProvider === provider.id;
return ( <div className="space-y-4">
<div key={provider.id} <h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">Standard Connections</h3>
className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted/30 transition-colors"> <div className="grid gap-4">{otherProviders.map(renderProvider)}</div>
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center">
<Icon icon={provider.icon} className="w-5 h-5"/>
</div>
<div className="space-y-0.5">
<div className="font-medium flex items-center gap-2">
{PROVIDERS_TEXT[provider.id].title}
{isConnected && (
<Badge variant="secondary"
className="text-[10px] h-5 px-1.5 text-green-600 bg-green-500/10 border-0">
Active
</Badge>
)}
</div>
<div className="text-sm text-muted-foreground">
{isConnected ? "Connected" : "Not Connected"}
</div>
</div>
</div>
<div className="flex items-center gap-2">
{isConnected ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span tabIndex={0}>
<Button
variant="outline"
size="sm"
onClick={() => unlinkAccount(provider.id)}
disabled={!canUnlink || isLoading || provider.isManual}
className={!canUnlink ? "opacity-50 cursor-not-allowed" : ""}
>
{isLoading ? <Loader2
className="w-4 h-4 animate-spin"/> : "Unlink"}
</Button>
</span>
</TooltipTrigger>
{!canUnlink && (
<TooltipContent>
<p>
You cannot unlink your last authentication provider or if you
don't have a password set.
</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
) : (
<>
{provider.id === "credential" ? (
<SetPasswordProfileProviderModal open={isPasswordDialogOpen}
onOpenChange={setIsPasswordDialogOpen}/>
) : (
<Button
variant="default"
size="sm"
onClick={() => linkAccount(provider.id)}
disabled={isLoading || provider.isManual}
>
{isLoading ? <Loader2 className="w-4 h-4 animate-spin"/> : "Link"}
</Button>
)}
</>
)}
</div>
</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.",
},
};
@@ -1,41 +1,49 @@
"use client"; "use client";
import {useState} from "react"; 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, getPasskeysAction, revokePasskeyAction } from "./actions/security.action";
import {revokeAllSessionsAction, revokeSessionAction} 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"; import { Setup2FAProfileProviderModal } from "./modal/setup-2fa-modal";
import {Setup2FAProfileProviderModal} from "./modal/setup-2fa-modal"; import { Disable2FAProfileProviderModal } from "./modal/disable-2fa-modal";
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) => {
const result = await revokeSessionAction({token}); const result = await revokeSessionAction({ token });
const inner = result?.data; const inner = result?.data;
if (inner?.success) { if (inner?.success) {
toast.success("Session successfully revoked"); toast.success("Session successfully revoked");
@@ -46,7 +54,7 @@ export function ProfileSecurity({user, sessions, credentialAccount, currentSessi
}, },
}); });
const {mutate: revokeOthers, isPending: isRevokingOthers} = useMutation({ const { mutate: revokeOthers, isPending: isRevokingOthers } = useMutation({
mutationFn: async () => { mutationFn: async () => {
const result = await revokeAllSessionsAction(); const result = await revokeAllSessionsAction();
const inner = result?.data; const inner = result?.data;
@@ -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">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> {isPasswordEnabled && (
<div className="space-y-1"> <>
<div className="font-medium">Password</div> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="text-sm text-muted-foreground"> <div className="space-y-1">
{user.lastChangedPasswordAt <div className="font-medium">Password</div>
? `Last changed ${timeAgo(new Date(user.lastChangedPasswordAt))}` <div className="text-sm text-muted-foreground">
: "Never changed"} {user.lastChangedPasswordAt ? `Last changed ${timeAgo(new Date(user.lastChangedPasswordAt))}` : "Never changed"}
</div>
</div>
{credentialAccount ? (
<ResetPasswordProfileProviderModal open={isPasswordDialogOpen} onOpenChange={setIsPasswordDialogOpen} />
) : (
<SetPasswordProfileProviderModal open={isPasswordDialogOpen} onOpenChange={setIsPasswordDialogOpen} />
)}
</div> </div>
</div>
{credentialAccount ? (
<ResetPasswordProfileProviderModal open={isPasswordDialogOpen}
onOpenChange={setIsPasswordDialogOpen}/>
) : (
<SetPasswordProfileProviderModal open={isPasswordDialogOpen}
onOpenChange={setIsPasswordDialogOpen}/>
)}
</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>
@@ -135,7 +245,7 @@ export function ProfileSecurity({user, sessions, credentialAccount, currentSessi
onClick={() => revokeOthers()} onClick={() => revokeOthers()}
disabled={isRevokingOthers || (sessions?.length || 0) <= 1} disabled={isRevokingOthers || (sessions?.length || 0) <= 1}
> >
{isRevokingOthers && <Loader2 className="mr-2 h-4 w-4 animate-spin"/>} {isRevokingOthers && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Revoke All Revoke All
</Button> </Button>
)} )}
@@ -161,11 +271,11 @@ export function ProfileSecurity({user, sessions, credentialAccount, currentSessi
} }
function SessionRow({ function SessionRow({
session, session,
onRevoke, onRevoke,
isRevoking, isRevoking,
currentSession, currentSession,
}: { }: {
session: Session; session: Session;
onRevoke: (token: string) => void; onRevoke: (token: string) => void;
isRevoking: boolean; isRevoking: boolean;
@@ -177,12 +287,11 @@ function SessionRow({
<div className="flex items-center justify-between p-4"> <div className="flex items-center justify-between p-4">
<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 text-muted-foreground"> <div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center text-muted-foreground">
<deviceInfo.Icon className="w-5 h-5"/> <deviceInfo.Icon className="w-5 h-5" />
</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"
@@ -193,12 +302,8 @@ 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>
@@ -211,10 +316,36 @@ function SessionRow({
onClick={() => onRevoke(session.token)} onClick={() => onRevoke(session.token)}
disabled={isRevoking} disabled={isRevoking}
> >
{isRevoking ? <Loader2 className="h-4 w-4 animate-spin"/> : <LogOut className="w-4 h-4"/>} {isRevoking ? <Loader2 className="h-4 w-4 animate-spin" /> : <LogOut className="w-4 h-4" />}
<span className="sr-only">Revoke</span> <span className="sr-only">Revoke</span>
</Button> </Button>
)} )}
</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
} }
] ]
} }
+41 -13
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>;
+45 -7
View File
@@ -1,8 +1,8 @@
import {createEnv} from "@t3-oss/env-nextjs"; import { createEnv } from "@t3-oss/env-nextjs";
import {z} from "zod"; import { z } from "zod";
import packageJson from "../package.json" with {type: "json"}; import packageJson from "../package.json" with { type: "json" };
const {version} = packageJson; const { version } = packageJson;
export const env = createEnv({ export const env = createEnv({
server: { server: {
@@ -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"
}
];