mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
fix: link social, admin delete account,update
This commit is contained in:
+38
-28
@@ -1,32 +1,42 @@
|
||||
import React from "react";
|
||||
import {redirect} from "next/navigation";
|
||||
import {currentUser} from "@/lib/auth/current-user";
|
||||
import {AuthLogoSection} from "@/components/wrappers/auth/auth-logo-section";
|
||||
import {env} from "@/env.mjs";
|
||||
import {Heart} from "lucide-react";
|
||||
import React, { Suspense } from "react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
import { AuthLogoSection } from "@/components/wrappers/auth/auth-logo-section";
|
||||
import { env } from "@/env.mjs";
|
||||
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") {
|
||||
redirect("/dashboard/home");
|
||||
}
|
||||
|
||||
return (
|
||||
<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">
|
||||
<AuthLogoSection/>
|
||||
<div className="mt-4">{children}</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>
|
||||
return (
|
||||
<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">
|
||||
<AuthLogoSection />
|
||||
<div className="mt-4">
|
||||
<Suspense>
|
||||
<AuthLayout>{children}</AuthLayout>
|
||||
</Suspense>
|
||||
</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
@@ -14,8 +14,8 @@
|
||||
"auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@better-auth/passkey": "^1.4.18",
|
||||
"@better-auth/sso": "^1.4.18",
|
||||
"@better-auth/passkey": "^1.4.19",
|
||||
"@better-auth/sso": "^1.4.19",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
@@ -50,7 +50,7 @@
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/nodemailer": "^6.4.22",
|
||||
"@types/nodemailer": "^6.4.23",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@zenstackhq/runtime": "2.14.2",
|
||||
"argon2": "^0.43.1",
|
||||
@@ -70,7 +70,7 @@
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.553.0",
|
||||
"minio": "^8.0.6",
|
||||
"motion": "^12.34.0",
|
||||
"motion": "^12.34.3",
|
||||
"next": "16.1.5",
|
||||
"next-safe-action": "^7.10.8",
|
||||
"next-themes": "^0.4.6",
|
||||
@@ -85,7 +85,7 @@
|
||||
"react-dom": "^19.2.4",
|
||||
"react-dropzone": "^14.4.1",
|
||||
"react-email": "^4.3.2",
|
||||
"react-hook-form": "^7.71.1",
|
||||
"react-hook-form": "^7.71.2",
|
||||
"react-qr-code": "^2.0.18",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"react-twc": "^1.5.1",
|
||||
@@ -95,8 +95,8 @@
|
||||
"socket.io": "^4.8.3",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"sonner": "^2.0.7",
|
||||
"swiper": "^12.1.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"swiper": "^12.1.2",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vaul": "^1.1.2",
|
||||
"ws": "^8.19.0",
|
||||
@@ -106,7 +106,7 @@
|
||||
"@iconify/react": "^6.0.2",
|
||||
"@react-email/preview-server": "4.3.2",
|
||||
"@react-email/render": "^2.0.4",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@tailwindcss/postcss": "^4.2.1",
|
||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||
"@types/node": "^22.19.11",
|
||||
"@types/node-forge": "^1.3.14",
|
||||
@@ -115,15 +115,15 @@
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@zenstackhq/openapi": "^2.22.1",
|
||||
"@zenstackhq/tanstack-query": "^2.22.2",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"baseline-browser-mapping": "^2.10.0",
|
||||
"drizzle-kit": "^0.31.9",
|
||||
"esbuild": "^0.27.3",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint": "^9.39.3",
|
||||
"eslint-config-next": "^16.1.6",
|
||||
"eslint-plugin-tailwindcss": "^3.18.2",
|
||||
"framer-motion": "^12.34.0",
|
||||
"framer-motion": "^12.34.3",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tsx": "^4.21.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
Generated
+535
-523
File diff suppressed because it is too large
Load Diff
@@ -1,75 +1,81 @@
|
||||
import {NextRequest, NextResponse} from "next/server";
|
||||
import {loggingMiddleware} from "@/middleware/loggingMiddleware";
|
||||
import {errorHandler} from "@/middleware/errorHandler";
|
||||
import {auth} from "@/lib/auth/auth";
|
||||
import {headers} from "next/headers";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { loggingMiddleware } from "@/middleware/loggingMiddleware";
|
||||
import { errorHandler } from "@/middleware/errorHandler";
|
||||
import { auth } from "@/lib/auth/auth";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
export async function proxy(request: NextRequest) {
|
||||
const url = request.nextUrl.clone();
|
||||
const redirectUrl = encodeURIComponent(request.nextUrl.pathname)
|
||||
const url = request.nextUrl.clone();
|
||||
const redirectUrl = encodeURIComponent(request.nextUrl.pathname);
|
||||
|
||||
if (url.pathname.startsWith("/dashboard")) {
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers(),
|
||||
});
|
||||
if (!session) {
|
||||
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 (url.pathname.startsWith("/dashboard")) {
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers(),
|
||||
});
|
||||
if (!session) {
|
||||
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 (url.pathname.startsWith("/api/auth")) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
if (url.pathname.startsWith("/api/auth")) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/api")) {
|
||||
const routeExists = checkRouteExists(url.pathname);
|
||||
if (!routeExists) {
|
||||
return new NextResponse(JSON.stringify({message: "This API route does not exist.", status: 404}), {
|
||||
status: 404,
|
||||
headers: {"Content-Type": "application/json"},
|
||||
});
|
||||
}
|
||||
}
|
||||
try {
|
||||
loggingMiddleware(request);
|
||||
} catch (err) {
|
||||
errorHandler(err);
|
||||
if (url.pathname.startsWith("/api")) {
|
||||
const routeExists = checkRouteExists(url.pathname);
|
||||
if (!routeExists) {
|
||||
return new NextResponse(
|
||||
JSON.stringify({
|
||||
message: "This API route does not exist.",
|
||||
status: 404,
|
||||
}),
|
||||
{
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
try {
|
||||
loggingMiddleware(request);
|
||||
} catch (err) {
|
||||
errorHandler(err);
|
||||
}
|
||||
}
|
||||
|
||||
function checkRouteExists(pathname: string) {
|
||||
const routePatterns = [
|
||||
/^\/api\/agent\/[^/]+\/status\/?$/,
|
||||
/^\/api\/agent\/[^/]+\/backup\/?$/,
|
||||
/^\/api\/agent\/[^/]+\/backup\/upload\/init\/?$/,
|
||||
/^\/api\/agent\/[^/]+\/backup\/upload\/status\/?$/,
|
||||
/^\/api\/agent\/[^/]+\/restore\/?$/,
|
||||
/^\/api\/files\/images\/[^/]+\/?$/,
|
||||
/^\/api\/files\/backups\/?$/,
|
||||
/^\/api\/tus\/hooks\/?$/,
|
||||
/^\/api\/events\/?$/,
|
||||
/^\/api\/config\/?$/,
|
||||
/^\/api\/google\/drive\/callback\/?$/,
|
||||
];
|
||||
return routePatterns.some((pattern) => pattern.test(pathname));
|
||||
const routePatterns = [
|
||||
/^\/api\/agent\/[^/]+\/status\/?$/,
|
||||
/^\/api\/agent\/[^/]+\/backup\/?$/,
|
||||
/^\/api\/agent\/[^/]+\/backup\/upload\/init\/?$/,
|
||||
/^\/api\/agent\/[^/]+\/backup\/upload\/status\/?$/,
|
||||
/^\/api\/agent\/[^/]+\/restore\/?$/,
|
||||
/^\/api\/files\/images\/[^/]+\/?$/,
|
||||
/^\/api\/files\/backups\/?$/,
|
||||
/^\/api\/tus\/hooks\/?$/,
|
||||
/^\/api\/events\/?$/,
|
||||
/^\/api\/config\/?$/,
|
||||
/^\/api\/google\/drive\/callback\/?$/,
|
||||
];
|
||||
return routePatterns.some((pattern) => pattern.test(pathname));
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
"/api/:path*",
|
||||
"/dashboard/:path*",
|
||||
"/dashboard",
|
||||
]
|
||||
matcher: ["/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";
|
||||
|
||||
import {useEffect, useState} from "react";
|
||||
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 { useMutation } from "@tanstack/react-query";
|
||||
import Link from "next/link";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {PasswordInput} from "@/components/ui/password-input";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
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 = {
|
||||
defaultValues?: LoginType;
|
||||
isPasskeyEnabled?: boolean;
|
||||
defaultValues?: LoginType;
|
||||
isPasskeyEnabled?: boolean;
|
||||
};
|
||||
|
||||
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 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 router = useRouter();
|
||||
|
||||
const form = useZodForm({
|
||||
schema: LoginSchema,
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const mutation = useMutation({
|
||||
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);
|
||||
},
|
||||
}
|
||||
);
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: LoginType) => {
|
||||
await signIn.email(
|
||||
{
|
||||
password: values.password,
|
||||
email: values.email,
|
||||
callbackURL: url?.get("redirect") ?? "/dashboard",
|
||||
},
|
||||
});
|
||||
{
|
||||
onSuccess: (context) => {
|
||||
if (context.data.twoFactorRedirect) {
|
||||
router.push(
|
||||
"/guard?redirect=" +
|
||||
encodeURIComponent(context.data.callbackURL || "/dashboard"),
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4 mb-1"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email Address</FormLabel>
|
||||
<FormControl>
|
||||
<Input autoComplete="email" autoFocus placeholder="example@portabase.io" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>Password</FormLabel>
|
||||
<div className="text-center text-sm">
|
||||
<Link 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>
|
||||
);
|
||||
toast.success("Login success");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.error.message);
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4 mb-1"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email Address</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoComplete="email"
|
||||
autoFocus
|
||||
placeholder="example@portabase.io"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>Password</FormLabel>
|
||||
<div className="text-center text-sm">
|
||||
<Link
|
||||
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";
|
||||
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {User} from "@/db/schema/02_user";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {useRouter} from "next/navigation";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { User } from "@/db/schema/02_user";
|
||||
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {toast} from "sonner";
|
||||
import {setSuperAdminOwnerOfOrganizationsOwnedByUser} from "@/components/wrappers/dashboard/admin/users/user.action";
|
||||
import { authClient } from "@/lib/auth/auth-client";
|
||||
import { toast } from "sonner";
|
||||
import { setSuperAdminOwnerOfOrganizationsOwnedByUser } from "@/components/wrappers/dashboard/admin/users/user.action";
|
||||
|
||||
type AdminDeleteUserModalProps = {
|
||||
open: boolean;
|
||||
user: User;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
open: boolean;
|
||||
user: User;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export const AdminDeleteUserModal = ({user, open, onOpenChange}: AdminDeleteUserModalProps) => {
|
||||
const router = useRouter();
|
||||
export const AdminDeleteUserModal = ({
|
||||
user,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: AdminDeleteUserModalProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const res = await setSuperAdminOwnerOfOrganizationsOwnedByUser({userId: user.id});
|
||||
const result = res?.data;
|
||||
if (result?.success) {
|
||||
await authClient.admin.removeUser(
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
{
|
||||
onSuccess: async (response) => {
|
||||
toast.success(`User ${user.name} successfully deleted`);
|
||||
onOpenChange(false);
|
||||
router.refresh();
|
||||
},
|
||||
onError: async (error) => {
|
||||
toast.error("An error has occurred while deleting user");
|
||||
onOpenChange(false);
|
||||
},
|
||||
}
|
||||
);
|
||||
} else {
|
||||
toast.error("An error has occurred while deleting user");
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const res = await setSuperAdminOwnerOfOrganizationsOwnedByUser({
|
||||
userId: user.id,
|
||||
});
|
||||
const result = res?.data;
|
||||
if (result?.success) {
|
||||
await authClient.admin.removeUser(
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
toast.success(`User ${user.name} successfully deleted`);
|
||||
onOpenChange(false);
|
||||
router.refresh();
|
||||
},
|
||||
onError: async (error) => {
|
||||
toast.error("An error has occurred while deleting user");
|
||||
onOpenChange(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
toast.error("An error has occurred while deleting user");
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure you want to delete {user.name} ?</AlertDialogTitle>
|
||||
<AlertDialogDescription>This action is irreversible: it will lead to the deletion of the user's
|
||||
data.</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<ButtonWithLoading
|
||||
onClick={async () => await mutation.mutateAsync()}>Confirm</ButtonWithLoading>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you sure you want to delete {user.name} ?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action is irreversible: it will lead to the deletion of the
|
||||
user's data.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<ButtonWithLoading onClick={async () => await mutation.mutateAsync()}>
|
||||
Confirm
|
||||
</ButtonWithLoading>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -63,7 +63,7 @@ export function ProfileProviders({
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
} else {
|
||||
result = await authClient.signIn.social({
|
||||
result = await authClient.linkSocial({
|
||||
provider: provider.id as any,
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
|
||||
+1
-13
@@ -134,7 +134,7 @@ export const auth = betterAuth({
|
||||
trustedProviders: [
|
||||
"credential",
|
||||
...getOAuthProviders().map((p) => p.id),
|
||||
...oidcProviders.map((p) => p.id),
|
||||
...oidcProviders.map((p) => p.id)
|
||||
],
|
||||
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: {
|
||||
update: {
|
||||
|
||||
@@ -85,7 +85,8 @@ export function getOAuthProviders(): OAuthProvider[] {
|
||||
const client = process.env[`AUTH_SOCIAL_${prefix}_CLIENT`];
|
||||
if (!client) return;
|
||||
|
||||
const providerId = prefix.toLowerCase();
|
||||
const providerId =
|
||||
prefix.toLowerCase() === "x" ? "twitter" : prefix.toLowerCase();
|
||||
|
||||
const envTitle = process.env[`AUTH_SOCIAL_${prefix}_TITLE`];
|
||||
const envDesc = process.env[`AUTH_SOCIAL_${prefix}_DESC`];
|
||||
|
||||
+42
-46
@@ -1,76 +1,72 @@
|
||||
import {createAccessControl} from "better-auth/plugins/access";
|
||||
import {defaultStatements, adminAc} from "better-auth/plugins/admin/access";
|
||||
import { createAccessControl } from "better-auth/plugins/access";
|
||||
import { defaultStatements, adminAc } from "better-auth/plugins/admin/access";
|
||||
import {
|
||||
defaultStatements as orgDefaultStatements,
|
||||
adminAc as orgAdminAc,
|
||||
ownerAc as orgOwnerAc,
|
||||
memberAc as orgMemberAc
|
||||
defaultStatements as orgDefaultStatements,
|
||||
adminAc as orgAdminAc,
|
||||
ownerAc as orgOwnerAc,
|
||||
memberAc as orgMemberAc,
|
||||
} from "better-auth/plugins/organization/access";
|
||||
|
||||
const statement = {
|
||||
...defaultStatements,
|
||||
...orgDefaultStatements,
|
||||
project: ["create", "list", "update", "delete"],
|
||||
database: ["create", "list", "update", "delete", "backup"],
|
||||
agent: ["create", "list", "update", "delete"],
|
||||
|
||||
...defaultStatements,
|
||||
...orgDefaultStatements,
|
||||
project: ["create", "list", "update", "delete"],
|
||||
database: ["create", "list", "update", "delete", "backup"],
|
||||
agent: ["create", "list", "update", "delete"],
|
||||
} as const;
|
||||
|
||||
const ac = createAccessControl(statement);
|
||||
|
||||
|
||||
const superadmin = ac.newRole({
|
||||
project: ["create", "list", "update", "delete"],
|
||||
database: ["create", "list", "update", "delete"],
|
||||
agent: ["create", "list", "update", "delete"],
|
||||
...adminAc.statements,
|
||||
...orgMemberAc.statements,
|
||||
...orgAdminAc.statements,
|
||||
...orgOwnerAc.statements,
|
||||
project: ["create", "list", "update", "delete"],
|
||||
database: ["create", "list", "update", "delete"],
|
||||
agent: ["create", "list", "update", "delete"],
|
||||
...adminAc.statements,
|
||||
...orgMemberAc.statements,
|
||||
...orgAdminAc.statements,
|
||||
...orgOwnerAc.statements,
|
||||
});
|
||||
|
||||
const admin = ac.newRole({
|
||||
project: ["create", "list", "update", "delete"],
|
||||
database: ["create", "list", "update", "delete"],
|
||||
agent: ["create", "list", "update", "delete"],
|
||||
...adminAc.statements,
|
||||
...orgMemberAc.statements,
|
||||
...orgAdminAc.statements,
|
||||
project: ["create", "list", "update", "delete"],
|
||||
database: ["create", "list", "update", "delete"],
|
||||
agent: ["create", "list", "update", "delete"],
|
||||
...adminAc.statements,
|
||||
...orgMemberAc.statements,
|
||||
...orgAdminAc.statements,
|
||||
});
|
||||
|
||||
const user = ac.newRole({
|
||||
project: [],
|
||||
database: [],
|
||||
agent: [],
|
||||
project: [],
|
||||
database: [],
|
||||
agent: [],
|
||||
});
|
||||
|
||||
const pending = ac.newRole({
|
||||
project: [],
|
||||
database: [],
|
||||
agent: [],
|
||||
project: [],
|
||||
database: [],
|
||||
agent: [],
|
||||
});
|
||||
|
||||
//org
|
||||
const orgMember = ac.newRole({
|
||||
project: ["list"],
|
||||
database: ["list"],
|
||||
agent: ["list"],
|
||||
...orgMemberAc.statements,
|
||||
project: ["list"],
|
||||
database: ["list"],
|
||||
agent: ["list"],
|
||||
...orgMemberAc.statements,
|
||||
});
|
||||
|
||||
const orgAdmin = ac.newRole({
|
||||
project: ["create", "update"],
|
||||
...orgMemberAc.statements,
|
||||
...orgAdminAc.statements,
|
||||
project: ["create", "update"],
|
||||
...orgMemberAc.statements,
|
||||
...orgAdminAc.statements,
|
||||
});
|
||||
|
||||
const orgOwner = ac.newRole({
|
||||
project: ["create", "update", "delete"],
|
||||
...orgMemberAc.statements,
|
||||
...orgAdminAc.statements,
|
||||
...orgOwnerAc.statements,
|
||||
|
||||
project: ["create", "update", "delete"],
|
||||
...orgMemberAc.statements,
|
||||
...orgAdminAc.statements,
|
||||
...orgOwnerAc.statements,
|
||||
});
|
||||
|
||||
|
||||
export {ac, admin, superadmin, user, pending, orgAdmin, orgMember, orgOwner};
|
||||
export { ac, admin, superadmin, user, pending, orgAdmin, orgMember, orgOwner };
|
||||
|
||||
Reference in New Issue
Block a user