fix: link social, admin delete account,update

This commit is contained in:
Théo LAGACHE
2026-02-25 09:58:15 +01:00
parent b9ef58044e
commit 31285dd0d6
11 changed files with 928 additions and 857 deletions
+37 -27
View File
@@ -1,32 +1,42 @@
import React from "react"; import React, { Suspense } from "react";
import {redirect} from "next/navigation"; import { redirect } from "next/navigation";
import {currentUser} from "@/lib/auth/current-user"; import { currentUser } from "@/lib/auth/current-user";
import {AuthLogoSection} from "@/components/wrappers/auth/auth-logo-section"; import { AuthLogoSection } from "@/components/wrappers/auth/auth-logo-section";
import {env} from "@/env.mjs"; import { env } from "@/env.mjs";
import {Heart} from "lucide-react"; import { Heart } from "lucide-react";
import { AuthLayout } from "@/components/wrappers/auth/auth-layout";
export default async function Layout({children}: { children: React.ReactNode }) { export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const user = await currentUser();
const user = await currentUser(); if (user && !user.banned && user.role !== "pending") {
redirect("/dashboard/home");
}
if (user && !user.banned && user.role !== "pending") { return (
redirect("/dashboard/home"); <div className="flex min-h-screen flex-col justify-between py-10 sm:px-6 lg:px-8 ">
} <div className="flex flex-col items-center justify-center flex-1">
<div className="mx-auto w-full max-w-md">
return ( <AuthLogoSection />
<div className="flex min-h-screen flex-col justify-between py-10 sm:px-6 lg:px-8 "> <div className="mt-4">
<div className="flex flex-col items-center justify-center flex-1"> <Suspense>
<div className="mx-auto w-full max-w-md"> <AuthLayout>{children}</AuthLayout>
<AuthLogoSection/> </Suspense>
<div className="mt-4">{children}</div> </div>
</div>
</div>
<footer className="mt-8 text-center text-xs text-muted-foreground flex flex-col gap-1">
<p className="flex items-center justify-center gap-1">
Made with <Heart className="size-3 fill-red-500 text-red-500" /> by <span className="font-medium text-foreground">Portabase</span>
</p>
<p>v{env.NEXT_PUBLIC_PROJECT_VERSION}</p>
</footer>
</div> </div>
) </div>
<footer className="mt-8 text-center text-xs text-muted-foreground flex flex-col gap-1">
<p className="flex items-center justify-center gap-1">
Made with{" "}
<Heart className="size-3 fill-red-500 text-red-500 animate-pulse" />{" "}
by <span className="font-medium text-foreground">Portabase</span>
</p>
<p>v{env.NEXT_PUBLIC_PROJECT_VERSION}</p>
</footer>
</div>
);
} }
+12 -12
View File
@@ -14,8 +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/passkey": "^1.4.19",
"@better-auth/sso": "^1.4.18", "@better-auth/sso": "^1.4.19",
"@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",
@@ -50,7 +50,7 @@
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.90.21",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/nodemailer": "^6.4.22", "@types/nodemailer": "^6.4.23",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"@zenstackhq/runtime": "2.14.2", "@zenstackhq/runtime": "2.14.2",
"argon2": "^0.43.1", "argon2": "^0.43.1",
@@ -70,7 +70,7 @@
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.553.0", "lucide-react": "^0.553.0",
"minio": "^8.0.6", "minio": "^8.0.6",
"motion": "^12.34.0", "motion": "^12.34.3",
"next": "16.1.5", "next": "16.1.5",
"next-safe-action": "^7.10.8", "next-safe-action": "^7.10.8",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
@@ -85,7 +85,7 @@
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-dropzone": "^14.4.1", "react-dropzone": "^14.4.1",
"react-email": "^4.3.2", "react-email": "^4.3.2",
"react-hook-form": "^7.71.1", "react-hook-form": "^7.71.2",
"react-qr-code": "^2.0.18", "react-qr-code": "^2.0.18",
"react-resizable-panels": "^3.0.6", "react-resizable-panels": "^3.0.6",
"react-twc": "^1.5.1", "react-twc": "^1.5.1",
@@ -95,8 +95,8 @@
"socket.io": "^4.8.3", "socket.io": "^4.8.3",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"swiper": "^12.1.0", "swiper": "^12.1.2",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.5.0",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"ws": "^8.19.0", "ws": "^8.19.0",
@@ -106,7 +106,7 @@
"@iconify/react": "^6.0.2", "@iconify/react": "^6.0.2",
"@react-email/preview-server": "4.3.2", "@react-email/preview-server": "4.3.2",
"@react-email/render": "^2.0.4", "@react-email/render": "^2.0.4",
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.2.1",
"@types/eslint-plugin-tailwindcss": "^3.17.0", "@types/eslint-plugin-tailwindcss": "^3.17.0",
"@types/node": "^22.19.11", "@types/node": "^22.19.11",
"@types/node-forge": "^1.3.14", "@types/node-forge": "^1.3.14",
@@ -115,15 +115,15 @@
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@zenstackhq/openapi": "^2.22.1", "@zenstackhq/openapi": "^2.22.1",
"@zenstackhq/tanstack-query": "^2.22.2", "@zenstackhq/tanstack-query": "^2.22.2",
"baseline-browser-mapping": "^2.9.19", "baseline-browser-mapping": "^2.10.0",
"drizzle-kit": "^0.31.9", "drizzle-kit": "^0.31.9",
"esbuild": "^0.27.3", "esbuild": "^0.27.3",
"eslint": "^9.39.2", "eslint": "^9.39.3",
"eslint-config-next": "^16.1.6", "eslint-config-next": "^16.1.6",
"eslint-plugin-tailwindcss": "^3.18.2", "eslint-plugin-tailwindcss": "^3.18.2",
"framer-motion": "^12.34.0", "framer-motion": "^12.34.3",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.2.1",
"tsx": "^4.21.0", "tsx": "^4.21.0",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
+535 -523
View File
File diff suppressed because it is too large Load Diff
+67 -61
View File
@@ -1,75 +1,81 @@
import {NextRequest, NextResponse} from "next/server"; import { NextRequest, NextResponse } from "next/server";
import {loggingMiddleware} from "@/middleware/loggingMiddleware"; import { loggingMiddleware } from "@/middleware/loggingMiddleware";
import {errorHandler} from "@/middleware/errorHandler"; import { errorHandler } from "@/middleware/errorHandler";
import {auth} from "@/lib/auth/auth"; import { auth } from "@/lib/auth/auth";
import {headers} from "next/headers"; import { headers } from "next/headers";
export async function proxy(request: NextRequest) { export async function proxy(request: NextRequest) {
const url = request.nextUrl.clone(); const url = request.nextUrl.clone();
const redirectUrl = encodeURIComponent(request.nextUrl.pathname) const redirectUrl = encodeURIComponent(request.nextUrl.pathname);
if (url.pathname.startsWith("/dashboard")) { if (url.pathname.startsWith("/dashboard")) {
const session = await auth.api.getSession({ const session = await auth.api.getSession({
headers: await headers(), headers: await headers(),
}); });
if (!session) { if (!session) {
return NextResponse.redirect(new URL(`/login?redirect=${redirectUrl}`, request.url)); return NextResponse.redirect(
} new URL(`/login?redirect=${redirectUrl}`, request.url),
if (session.user.banned) { );
await auth.api.signOut({headers: await headers()});
return NextResponse.redirect(new URL("/login?error=banned", request.url));
}
if (session.user.role === "pending") {
await auth.api.signOut({headers: await headers()});
return NextResponse.redirect(new URL(`/login?error=pending?redirect=${redirectUrl}`, request.url));
}
if (url.pathname === "/dashboard") {
return NextResponse.redirect(new URL(`/dashboard/home`, request.url));
}
return NextResponse.next();
} }
if (session.user.banned) {
await auth.api.signOut({ headers: await headers() });
return NextResponse.redirect(new URL("/login?error=banned", request.url));
}
if (session.user.role === "pending") {
await auth.api.signOut({ headers: await headers() });
return NextResponse.redirect(
new URL(`/login?error=pending?redirect=${redirectUrl}`, request.url),
);
}
if (url.pathname === "/dashboard") {
return NextResponse.redirect(new URL(`/dashboard/home`, request.url));
}
return NextResponse.next();
}
if (url.pathname.startsWith("/api/auth")) { if (url.pathname.startsWith("/api/auth")) {
return NextResponse.next(); return NextResponse.next();
} }
if (url.pathname.startsWith("/api")) { if (url.pathname.startsWith("/api")) {
const routeExists = checkRouteExists(url.pathname); const routeExists = checkRouteExists(url.pathname);
if (!routeExists) { if (!routeExists) {
return new NextResponse(JSON.stringify({message: "This API route does not exist.", status: 404}), { return new NextResponse(
status: 404, JSON.stringify({
headers: {"Content-Type": "application/json"}, message: "This API route does not exist.",
}); status: 404,
} }),
} {
try { status: 404,
loggingMiddleware(request); headers: { "Content-Type": "application/json" },
} catch (err) { },
errorHandler(err); );
} }
}
try {
loggingMiddleware(request);
} catch (err) {
errorHandler(err);
}
} }
function checkRouteExists(pathname: string) { function checkRouteExists(pathname: string) {
const routePatterns = [ const routePatterns = [
/^\/api\/agent\/[^/]+\/status\/?$/, /^\/api\/agent\/[^/]+\/status\/?$/,
/^\/api\/agent\/[^/]+\/backup\/?$/, /^\/api\/agent\/[^/]+\/backup\/?$/,
/^\/api\/agent\/[^/]+\/backup\/upload\/init\/?$/, /^\/api\/agent\/[^/]+\/backup\/upload\/init\/?$/,
/^\/api\/agent\/[^/]+\/backup\/upload\/status\/?$/, /^\/api\/agent\/[^/]+\/backup\/upload\/status\/?$/,
/^\/api\/agent\/[^/]+\/restore\/?$/, /^\/api\/agent\/[^/]+\/restore\/?$/,
/^\/api\/files\/images\/[^/]+\/?$/, /^\/api\/files\/images\/[^/]+\/?$/,
/^\/api\/files\/backups\/?$/, /^\/api\/files\/backups\/?$/,
/^\/api\/tus\/hooks\/?$/, /^\/api\/tus\/hooks\/?$/,
/^\/api\/events\/?$/, /^\/api\/events\/?$/,
/^\/api\/config\/?$/, /^\/api\/config\/?$/,
/^\/api\/google\/drive\/callback\/?$/, /^\/api\/google\/drive\/callback\/?$/,
]; ];
return routePatterns.some((pattern) => pattern.test(pathname)); return routePatterns.some((pattern) => pattern.test(pathname));
} }
export const config = { export const config = {
matcher: [ matcher: ["/api/:path*", "/dashboard/:path*", "/dashboard"],
"/api/:path*",
"/dashboard/:path*",
"/dashboard",
]
}; };
@@ -0,0 +1,37 @@
"use client";
import React, { useEffect } from "react";
import { useSearchParams } from "next/navigation";
import { toast } from "sonner";
export const AuthLayout = ({ children }: { children: React.ReactNode }) => {
const url = useSearchParams();
useEffect(() => {
const error = url.get("error");
if (!error) return;
const errorMessages: Record<string, string> = {
pending: "Your account is not active.",
invalid_or_expired_token: "Password reset invalid token.",
};
const match = Object.entries(errorMessages).find(([key]) =>
error.includes(key),
);
if (!match) return;
toast.error(match[1]);
const params = new URLSearchParams(url.toString());
params.delete("error");
const newUrl =
window.location.pathname + (params.toString() ? `?${params.toString()}` : "");
window.history.replaceState({}, document.title, newUrl);
}, [url]);
return <>{children}</>;
};
@@ -1,123 +1,133 @@
"use client"; "use client";
import {useEffect, useState} from "react"; import { useMutation } from "@tanstack/react-query";
import {useMutation} from "@tanstack/react-query";
import {toast} from "sonner";
import {FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Form} from "@/components/ui/form";
import {LoginSchema, LoginType} from "@/components/wrappers/auth/login/login-form/login-form.schema";
import {signIn} from "@/lib/auth/auth-client";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import Link from "next/link"; import Link from "next/link";
import {useRouter} from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import {PasswordInput} from "@/components/ui/password-input"; import { useEffect } from "react";
import { toast } from "sonner";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
useZodForm,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { PasswordInput } from "@/components/ui/password-input";
import {
LoginSchema,
type LoginType,
} from "@/components/wrappers/auth/login/login-form/login-form.schema";
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
import { signIn } from "@/lib/auth/auth-client";
export type loginFormProps = { export type loginFormProps = {
defaultValues?: LoginType; defaultValues?: LoginType;
isPasskeyEnabled?: boolean; isPasskeyEnabled?: boolean;
}; };
export const LoginForm = (props: loginFormProps) => { export const LoginForm = (props: loginFormProps) => {
const {isPasskeyEnabled = false} = props; const { isPasskeyEnabled = false } = props;
const url = useSearchParams();
const [urlParams, setUrlParams] = useState<URLSearchParams>(); const form = useZodForm({
schema: LoginSchema,
});
useEffect(() => { const router = useRouter();
const urlParams = new URLSearchParams(window.location.search);
setUrlParams(urlParams);
const error = urlParams.get("error");
if (error?.includes("pending")) {
toast.error("Your account is not active.");
urlParams.delete("error");
window.history.replaceState({}, document.title, window.location.pathname + "?" + urlParams.toString());
}
if (error?.includes("invalid_or_expired_token")) {
toast.error("Password reset invalid token.");
urlParams.delete("error");
window.history.replaceState({}, document.title, window.location.pathname + "?" + urlParams.toString());
}
}, []);
const form = useZodForm({ const mutation = useMutation({
schema: LoginSchema, mutationFn: async (values: LoginType) => {
}); await signIn.email(
{
const router = useRouter(); password: values.password,
email: values.email,
const mutation = useMutation({ callbackURL: url?.get("redirect") ?? "/dashboard",
mutationFn: async (values: LoginType) => {
await signIn.email(
{
password: values.password,
email: values.email,
callbackURL: urlParams?.get("redirect") ?? "/dashboard",
},
{
onSuccess: (context) => {
if (context.data.twoFactorRedirect) {
//@ts-ignore
router.push("/guard?redirect=" + encodeURIComponent(context.data.callbackURL || "/dashboard"));
}
toast.success("Login success");
},
onError: (error) => {
toast.error(error.error.message);
},
}
);
}, },
}); {
onSuccess: (context) => {
if (context.data.twoFactorRedirect) {
router.push(
"/guard?redirect=" +
encodeURIComponent(context.data.callbackURL || "/dashboard"),
);
}
return ( toast.success("Login success");
<Form },
form={form} onError: (error) => {
className="flex flex-col gap-4 mb-1" toast.error(error.error.message);
onSubmit={async (values) => { },
await mutation.mutateAsync(values); },
}} );
> },
<FormField });
control={form.control}
name="email" return (
defaultValue="" <Form
render={({field}) => ( form={form}
<FormItem> className="flex flex-col gap-4 mb-1"
<FormLabel>Email Address</FormLabel> onSubmit={async (values) => {
<FormControl> await mutation.mutateAsync(values);
<Input autoComplete="email" autoFocus placeholder="example@portabase.io" {...field} /> }}
</FormControl> >
<FormMessage/> <FormField
</FormItem> control={form.control}
)} name="email"
/> defaultValue=""
<FormField render={({ field }) => (
control={form.control} <FormItem>
name="password" <FormLabel>Email Address</FormLabel>
defaultValue="" <FormControl>
render={({field}) => ( <Input
<FormItem> autoComplete="email"
<div className="flex items-center justify-between"> autoFocus
<FormLabel>Password</FormLabel> placeholder="example@portabase.io"
<div className="text-center text-sm"> {...field}
<Link href={"/forgot-password"} className="hover:underline ml-1"> />
Forgot your password ? </FormControl>
</Link> <FormMessage />
</div> </FormItem>
</div> )}
<FormControl> />
<PasswordInput autoComplete={isPasskeyEnabled ? "current-password webauthn" : "current-password"} <FormField
placeholder={"Enter your password"} {...field} /> control={form.control}
</FormControl> name="password"
<FormMessage/> defaultValue=""
</FormItem> render={({ field }) => (
)} <FormItem>
/> <div className="flex items-center justify-between">
<ButtonWithLoading className="mt-2 h-11" isPending={mutation.isPending}> <FormLabel>Password</FormLabel>
Login <div className="text-center text-sm">
</ButtonWithLoading> <Link
</Form> href={"/forgot-password"}
); className="hover:underline ml-1"
>
Forgot your password ?
</Link>
</div>
</div>
<FormControl>
<PasswordInput
autoComplete={
isPasskeyEnabled
? "current-password webauthn"
: "current-password"
}
placeholder={"Enter your password"}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<ButtonWithLoading className="mt-2 h-11" isPending={mutation.isPending}>
Login
</ButtonWithLoading>
</Form>
);
}; };
@@ -1,75 +1,86 @@
"use client"; "use client";
import {useMutation} from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import {User} from "@/db/schema/02_user"; import { User } from "@/db/schema/02_user";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading"; import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
import {useRouter} from "next/navigation"; import { useRouter } from "next/navigation";
import { import {
AlertDialog, AlertDialog,
AlertDialogCancel, AlertDialogCancel,
AlertDialogContent, AlertDialogContent,
AlertDialogDescription, AlertDialogDescription,
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import {authClient} from "@/lib/auth/auth-client"; import { authClient } from "@/lib/auth/auth-client";
import {toast} from "sonner"; import { toast } from "sonner";
import {setSuperAdminOwnerOfOrganizationsOwnedByUser} from "@/components/wrappers/dashboard/admin/users/user.action"; import { setSuperAdminOwnerOfOrganizationsOwnedByUser } from "@/components/wrappers/dashboard/admin/users/user.action";
type AdminDeleteUserModalProps = { type AdminDeleteUserModalProps = {
open: boolean; open: boolean;
user: User; user: User;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
}; };
export const AdminDeleteUserModal = ({user, open, onOpenChange}: AdminDeleteUserModalProps) => { export const AdminDeleteUserModal = ({
const router = useRouter(); user,
open,
onOpenChange,
}: AdminDeleteUserModalProps) => {
const router = useRouter();
const mutation = useMutation({ const mutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
const res = await setSuperAdminOwnerOfOrganizationsOwnedByUser({userId: user.id}); const res = await setSuperAdminOwnerOfOrganizationsOwnedByUser({
const result = res?.data; userId: user.id,
if (result?.success) { });
await authClient.admin.removeUser( const result = res?.data;
{ if (result?.success) {
userId: user.id, await authClient.admin.removeUser(
}, {
{ userId: user.id,
onSuccess: async (response) => { },
toast.success(`User ${user.name} successfully deleted`); {
onOpenChange(false); onSuccess: async () => {
router.refresh(); toast.success(`User ${user.name} successfully deleted`);
}, onOpenChange(false);
onError: async (error) => { router.refresh();
toast.error("An error has occurred while deleting user"); },
onOpenChange(false); onError: async (error) => {
}, toast.error("An error has occurred while deleting user");
} onOpenChange(false);
); },
} else { },
toast.error("An error has occurred while deleting user"); );
} } else {
}, toast.error("An error has occurred while deleting user");
onError: (error) => { }
toast.error(error.message); },
}, onError: (error) => {
}); toast.error(error.message);
},
});
return ( return (
<AlertDialog open={open} onOpenChange={onOpenChange}> <AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Are you sure you want to delete {user.name} ?</AlertDialogTitle> <AlertDialogTitle>
<AlertDialogDescription>This action is irreversible: it will lead to the deletion of the user's Are you sure you want to delete {user.name} ?
data.</AlertDialogDescription> </AlertDialogTitle>
</AlertDialogHeader> <AlertDialogDescription>
<AlertDialogFooter> This action is irreversible: it will lead to the deletion of the
<AlertDialogCancel>Cancel</AlertDialogCancel> user's data.
<ButtonWithLoading </AlertDialogDescription>
onClick={async () => await mutation.mutateAsync()}>Confirm</ButtonWithLoading> </AlertDialogHeader>
</AlertDialogFooter> <AlertDialogFooter>
</AlertDialogContent> <AlertDialogCancel>Cancel</AlertDialogCancel>
</AlertDialog> <ButtonWithLoading onClick={async () => await mutation.mutateAsync()}>
); Confirm
</ButtonWithLoading>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}; };
@@ -63,7 +63,7 @@ export function ProfileProviders({
callbackURL: "/dashboard", callbackURL: "/dashboard",
}); });
} else { } else {
result = await authClient.signIn.social({ result = await authClient.linkSocial({
provider: provider.id as any, provider: provider.id as any,
callbackURL: "/dashboard", callbackURL: "/dashboard",
}); });
+1 -13
View File
@@ -134,7 +134,7 @@ export const auth = betterAuth({
trustedProviders: [ trustedProviders: [
"credential", "credential",
...getOAuthProviders().map((p) => p.id), ...getOAuthProviders().map((p) => p.id),
...oidcProviders.map((p) => p.id), ...oidcProviders.map((p) => p.id)
], ],
allowDifferentEmails: true, allowDifferentEmails: true,
}, },
@@ -338,18 +338,6 @@ export const auth = betterAuth({
} }
}, },
}, },
delete: {
before: async (account) => {
const provider = SUPPORTED_PROVIDERS.find(
(p) => p.id === account.providerId,
);
if (provider && provider.allowUnlinking === false) {
throw new APIError("FORBIDDEN", {
message: "Unlinking is disabled for this provider.",
});
}
},
},
}, },
user: { user: {
update: { update: {
+2 -1
View File
@@ -85,7 +85,8 @@ export function getOAuthProviders(): OAuthProvider[] {
const client = process.env[`AUTH_SOCIAL_${prefix}_CLIENT`]; const client = process.env[`AUTH_SOCIAL_${prefix}_CLIENT`];
if (!client) return; if (!client) return;
const providerId = prefix.toLowerCase(); const providerId =
prefix.toLowerCase() === "x" ? "twitter" : prefix.toLowerCase();
const envTitle = process.env[`AUTH_SOCIAL_${prefix}_TITLE`]; const envTitle = process.env[`AUTH_SOCIAL_${prefix}_TITLE`];
const envDesc = process.env[`AUTH_SOCIAL_${prefix}_DESC`]; const envDesc = process.env[`AUTH_SOCIAL_${prefix}_DESC`];
+42 -46
View File
@@ -1,76 +1,72 @@
import {createAccessControl} from "better-auth/plugins/access"; import { createAccessControl } from "better-auth/plugins/access";
import {defaultStatements, adminAc} from "better-auth/plugins/admin/access"; import { defaultStatements, adminAc } from "better-auth/plugins/admin/access";
import { import {
defaultStatements as orgDefaultStatements, defaultStatements as orgDefaultStatements,
adminAc as orgAdminAc, adminAc as orgAdminAc,
ownerAc as orgOwnerAc, ownerAc as orgOwnerAc,
memberAc as orgMemberAc memberAc as orgMemberAc,
} from "better-auth/plugins/organization/access"; } from "better-auth/plugins/organization/access";
const statement = { const statement = {
...defaultStatements, ...defaultStatements,
...orgDefaultStatements, ...orgDefaultStatements,
project: ["create", "list", "update", "delete"], project: ["create", "list", "update", "delete"],
database: ["create", "list", "update", "delete", "backup"], database: ["create", "list", "update", "delete", "backup"],
agent: ["create", "list", "update", "delete"], agent: ["create", "list", "update", "delete"],
} as const; } as const;
const ac = createAccessControl(statement); const ac = createAccessControl(statement);
const superadmin = ac.newRole({ const superadmin = ac.newRole({
project: ["create", "list", "update", "delete"], project: ["create", "list", "update", "delete"],
database: ["create", "list", "update", "delete"], database: ["create", "list", "update", "delete"],
agent: ["create", "list", "update", "delete"], agent: ["create", "list", "update", "delete"],
...adminAc.statements, ...adminAc.statements,
...orgMemberAc.statements, ...orgMemberAc.statements,
...orgAdminAc.statements, ...orgAdminAc.statements,
...orgOwnerAc.statements, ...orgOwnerAc.statements,
}); });
const admin = ac.newRole({ const admin = ac.newRole({
project: ["create", "list", "update", "delete"], project: ["create", "list", "update", "delete"],
database: ["create", "list", "update", "delete"], database: ["create", "list", "update", "delete"],
agent: ["create", "list", "update", "delete"], agent: ["create", "list", "update", "delete"],
...adminAc.statements, ...adminAc.statements,
...orgMemberAc.statements, ...orgMemberAc.statements,
...orgAdminAc.statements, ...orgAdminAc.statements,
}); });
const user = ac.newRole({ const user = ac.newRole({
project: [], project: [],
database: [], database: [],
agent: [], agent: [],
}); });
const pending = ac.newRole({ const pending = ac.newRole({
project: [], project: [],
database: [], database: [],
agent: [], agent: [],
}); });
//org //org
const orgMember = ac.newRole({ const orgMember = ac.newRole({
project: ["list"], project: ["list"],
database: ["list"], database: ["list"],
agent: ["list"], agent: ["list"],
...orgMemberAc.statements, ...orgMemberAc.statements,
}); });
const orgAdmin = ac.newRole({ const orgAdmin = ac.newRole({
project: ["create", "update"], project: ["create", "update"],
...orgMemberAc.statements, ...orgMemberAc.statements,
...orgAdminAc.statements, ...orgAdminAc.statements,
}); });
const orgOwner = ac.newRole({ const orgOwner = ac.newRole({
project: ["create", "update", "delete"], project: ["create", "update", "delete"],
...orgMemberAc.statements, ...orgMemberAc.statements,
...orgAdminAc.statements, ...orgAdminAc.statements,
...orgOwnerAc.statements, ...orgOwnerAc.statements,
}); });
export { ac, admin, superadmin, user, pending, orgAdmin, orgMember, orgOwner };
export {ac, admin, superadmin, user, pending, orgAdmin, orgMember, orgOwner};