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:
+37
-27
@@ -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
@@ -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",
|
||||||
|
|||||||
Generated
+535
-523
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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: {
|
||||||
|
|||||||
@@ -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
@@ -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};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user