-
-
OR
-
+
);
};
diff --git a/src/components/wrappers/auth/login/reset-password-form/reset-password-form.action.ts b/src/components/wrappers/auth/login/reset-password-form/reset-password-form.action.ts
new file mode 100644
index 00000000..f059fde6
--- /dev/null
+++ b/src/components/wrappers/auth/login/reset-password-form/reset-password-form.action.ts
@@ -0,0 +1,85 @@
+"use server";
+import { ServerActionResult } from "@/types/action-type";
+import { auth } from "@/lib/auth/auth";
+import { zPassword, zString } from "@/lib/zod";
+import z from "zod";
+import {action} from "@/lib/safe-actions/actions";
+
+export const resetPasswordAction = action
+ .schema(
+ z.object({
+ schema: z.object({
+ password: zPassword(),
+ }),
+ token: zString(),
+ })
+ )
+ .action(async ({ parsedInput }): Promise
> => {
+ try {
+ const verification = await (await auth.$context).internalAdapter.findVerificationValue(`reset-password:${parsedInput.token}`);
+ if (!verification || verification.expiresAt < new Date()) {
+ return {
+ success: false,
+ actionError: {
+ message: "password_reset",
+ cause: "invalid_or_expired_token",
+ },
+ };
+ }
+
+ const user = await (await auth.$context).internalAdapter.findUserById(verification.value);
+ console.log(user)
+ if (!user) {
+ return {
+ success: false,
+ actionError: {
+ message: "password_reset",
+ cause: "user_not_found",
+ },
+ };
+ }
+
+ const hashedPassword = await (await auth.$context).password.hash(parsedInput.schema.password);
+ console.log(hashedPassword)
+ console.log("ok")
+ // await (await auth.$context).internalAdapter.updatePassword(user.id, hashedPassword);
+ console.log("ici")
+ // await (await auth.$context).internalAdapter.deleteSessions(user.id);
+ // console.log("ici2")
+ // await (await auth.$context).internalAdapter.deleteVerificationValue(verification.id);
+ // console.log("ici3");
+ //
+ // (await auth.$context).internalAdapter.updateUser(user.id, {
+ // lastChangedPasswordAt: new Date(),
+ // });
+
+ // await auth.api.resetPassword({
+ // headers: await headers(),
+ // body: {
+ // newPassword: parsedInput.schema.password,
+ // token: parsedInput.token,
+ // },
+ // });
+
+ await (
+ await auth.$context
+ ).internalAdapter.updateUser(user.id, {
+ isDefaultPassword: false,
+ });
+
+ return {
+ success: true,
+ actionSuccess: {
+ message: "password_reset",
+ },
+ };
+ } catch (error) {
+ return {
+ success: false,
+ actionError: {
+ message: "password_reset",
+ cause: error instanceof Error ? error.message : "Unknown error",
+ },
+ };
+ }
+ });
diff --git a/src/components/wrappers/auth/login/reset-password-form/reset-password-form.schema.ts b/src/components/wrappers/auth/login/reset-password-form/reset-password-form.schema.ts
new file mode 100644
index 00000000..58191391
--- /dev/null
+++ b/src/components/wrappers/auth/login/reset-password-form/reset-password-form.schema.ts
@@ -0,0 +1,21 @@
+"use client";
+
+import {z} from "zod";
+import {zPassword} from "@/lib/zod";
+
+export const ResetPasswordSchema = z
+ .object({
+ password: zPassword(),
+ confirmPassword: zPassword(),
+ })
+ .superRefine(({confirmPassword, password}, ctx) => {
+ if (confirmPassword !== password) {
+ ctx.addIssue({
+ code: "custom",
+ message: "Confirmation password does not match",
+ path: ["confirmPassword"],
+ });
+ }
+ });
+
+export type ResetPasswordType = z.infer;
diff --git a/src/components/wrappers/auth/login/reset-password-form/reset-password-form.tsx b/src/components/wrappers/auth/login/reset-password-form/reset-password-form.tsx
new file mode 100644
index 00000000..5402e578
--- /dev/null
+++ b/src/components/wrappers/auth/login/reset-password-form/reset-password-form.tsx
@@ -0,0 +1,97 @@
+"use client";
+
+import {useMutation} from "@tanstack/react-query";
+import {toast} from "sonner";
+
+import {FormControl, FormField, FormItem, FormLabel, useZodForm} from "@/components/ui/form";
+import {Form} from "@/components/ui/form";
+import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
+import Link from "next/link";
+import {ResetPasswordSchema, ResetPasswordType} from "./reset-password-form.schema";
+import {PasswordStrengthInput} from "@/components/ui/password-input-indicator";
+import {useRouter, useSearchParams} from "next/navigation";
+import {ArrowLeft} from "lucide-react";
+import {authClient} from "@/lib/auth/auth-client";
+import {BetterAuthError} from "@/types/auth";
+import {PasswordInput} from "@/components/ui/password-input";
+
+export type ResetPasswordFormProps = {
+ defaultValues?: ResetPasswordType;
+};
+
+export const ResetPasswordForm = (props: ResetPasswordFormProps) => {
+ const searchParams = useSearchParams();
+
+ const form = useZodForm({
+ schema: ResetPasswordSchema,
+ });
+
+ const router = useRouter();
+
+
+ const mutation = useMutation({
+ mutationFn: async (values: ResetPasswordType) => {
+
+ const {data, error} = await authClient.resetPassword({
+ newPassword: values.password,
+ token: searchParams.get("token") || "",
+ });
+
+ if (error) throw error;
+ },
+ onSuccess: () => {
+ toast.success("Password successfully reset!");
+ setTimeout(() => router.push("/"), 1400);
+ },
+ onError: (error: BetterAuthError) => {
+ console.log(error)
+ toast.error("An error occurred while resetting password");
+ },
+ });
+
+
+ return (
+
+ );
+};
diff --git a/src/components/wrappers/auth/social-buttons.tsx b/src/components/wrappers/auth/social-buttons.tsx
new file mode 100644
index 00000000..b3c69fa1
--- /dev/null
+++ b/src/components/wrappers/auth/social-buttons.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import React, { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { authClient } from "@/lib/auth/auth-client";
+import { Loader2 } from "lucide-react";
+import { toast } from "sonner";
+import {SUPPORTED_PROVIDERS} from "../../../../portabase.config";
+
+export function SocialAuthButtons() {
+ const [isLoading, setIsLoading] = useState(null);
+
+ const handleSocialSignIn = async (providerId: string) => {
+ setIsLoading(providerId);
+ try {
+ const { error } = await authClient.signIn.social({
+ provider: providerId as "google" | "github",
+ callbackURL: "/dashboard",
+ });
+
+ if (error) {
+ toast.error("An error occurred while signing in with the provider. Please try again.");
+ } else {
+ toast.success("Redirecting to provider...");
+ }
+ } catch (err) {
+ toast.error("An error occurred while signing in with the provider. Please try again.");
+ } finally {
+ setIsLoading(null);
+ }
+ };
+
+ const socialProviders = SUPPORTED_PROVIDERS.filter((p) => !p.isManual);
+
+ if (socialProviders.length === 0) return null;
+
+
+ return (
+
+ {socialProviders.map((provider) => (
+
handleSocialSignIn(provider.id)} disabled={!!isLoading}>
+ {isLoading === provider.id ? : }
+ {PROVIDERS_TEXT[provider.id].title}
+
+ ))}
+
+ );
+}
+
+
+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."
+ }
+}
\ No newline at end of file
diff --git a/src/components/wrappers/dashboard/admin/organizations/columns-organizations.tsx b/src/components/wrappers/dashboard/admin/organizations/columns-organizations.tsx
index 37bad487..ad5fe75e 100644
--- a/src/components/wrappers/dashboard/admin/organizations/columns-organizations.tsx
+++ b/src/components/wrappers/dashboard/admin/organizations/columns-organizations.tsx
@@ -1,17 +1,5 @@
"use client"
import {ColumnDef} from "@tanstack/react-table";
-import {Badge} from "@/components/ui/badge";
-import {updateUserAction} from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
-import {useMutation} from "@tanstack/react-query";
-import {toast} from "sonner";
-import {useRouter} from "next/navigation";
-import {useState} from "react";
-import {Trash2} from "lucide-react";
-import {deleteUserAction} from "@/components/wrappers/dashboard/profile/button-delete-account/delete-account.action";
-import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
-import {UserWithAccounts} from "@/db/schema/02_user";
-import {authClient, useSession} from "@/lib/auth/auth-client";
-import {providerSwitch} from "@/components/wrappers/common/provider-switch";
import {Organization} from "@/db/schema/03_organization";
export const organizationsColumnsAdmin: ColumnDef[] = [
diff --git a/src/components/wrappers/dashboard/admin/users/accounts/table-columns.tsx b/src/components/wrappers/dashboard/admin/users/accounts/table-columns.tsx
index e4c195fe..774b7a37 100644
--- a/src/components/wrappers/dashboard/admin/users/accounts/table-columns.tsx
+++ b/src/components/wrappers/dashboard/admin/users/accounts/table-columns.tsx
@@ -4,7 +4,7 @@ import {ColumnDef} from "@tanstack/react-table";
import {Unlink} from "lucide-react";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
-import {unlinkUserProviderAction} from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
+import {unlinkUserProviderAction} from "@/components/wrappers/dashboard/profile2/user-form/user-form.action";
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
import {Account} from "better-auth";
diff --git a/src/components/wrappers/dashboard/admin/users/button-delete-use.tsx b/src/components/wrappers/dashboard/admin/users/button-delete-use.tsx
index c126bab1..80886fa4 100644
--- a/src/components/wrappers/dashboard/admin/users/button-delete-use.tsx
+++ b/src/components/wrappers/dashboard/admin/users/button-delete-use.tsx
@@ -5,7 +5,7 @@ import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with
import {useMutation} from "@tanstack/react-query";
import {useRouter} from "next/navigation";
import {toast} from "sonner";
-import {deleteUserAction} from "@/components/wrappers/dashboard/profile/button-delete-account/delete-account.action";
+import {deleteUserAction} from "@/components/wrappers/dashboard/profile2/button-delete-account/delete-account.action";
export type ButtonDeleteUserProps = {
userId: string;
diff --git a/src/components/wrappers/dashboard/admin/users/columns-users.tsx b/src/components/wrappers/dashboard/admin/users/columns-users.tsx
index df3c9352..669e6fa5 100644
--- a/src/components/wrappers/dashboard/admin/users/columns-users.tsx
+++ b/src/components/wrappers/dashboard/admin/users/columns-users.tsx
@@ -1,7 +1,7 @@
"use client"
import {ColumnDef} from "@tanstack/react-table";
import {Badge} from "@/components/ui/badge";
-import {updateUserAction} from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
+import {updateUserAction} from "@/components/wrappers/dashboard/profile2/user-form/user-form.action";
import {useMutation} from "@tanstack/react-query";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
diff --git a/src/components/wrappers/dashboard/admin/users/sessions/table-columns.tsx b/src/components/wrappers/dashboard/admin/users/sessions/table-columns.tsx
index 44c4f0e0..8c6e4760 100644
--- a/src/components/wrappers/dashboard/admin/users/sessions/table-columns.tsx
+++ b/src/components/wrappers/dashboard/admin/users/sessions/table-columns.tsx
@@ -9,7 +9,7 @@ import detectOSWithUA from "@/utils/os-parser";
import {Icon} from "@iconify/react";
import {authClient} from "@/lib/auth/auth-client";
import {timeAgo} from "@/utils/date-formatting";
-import {deleteUserSessionAction} from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
+import {deleteUserSessionAction} from "@/components/wrappers/dashboard/profile2/user-form/user-form.action";
export const sessionsColumns: ColumnDef[] = [
{
diff --git a/src/components/wrappers/dashboard/common/logged-in/logged-in-button.tsx b/src/components/wrappers/dashboard/common/logged-in/logged-in-button.tsx
index 49edcb68..0822a96d 100644
--- a/src/components/wrappers/dashboard/common/logged-in/logged-in-button.tsx
+++ b/src/components/wrappers/dashboard/common/logged-in/logged-in-button.tsx
@@ -1,34 +1,42 @@
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
-import { SidebarMenuButton } from "@/components/ui/sidebar";
import { ChevronUp } from "lucide-react";
+
+import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
import { currentUser } from "@/lib/auth/current-user";
+import { SidebarMenuButton } from "@/components/ui/sidebar";
+import { getAccounts, getSession, getSessions } from "@/lib/auth/auth";
import {LoggedInDropdown} from "@/components/wrappers/dashboard/common/logged-in/logged-in-dropdown";
export const LoggedInButton = async () => {
const user = await currentUser();
+ const sessions = await getSessions();
+ const currentSession = await getSession();
+ const accounts = await getAccounts();
if (!user) return null;
return (
-
-
-
- {user.name[0].toUpperCase()}
- {user.image ? : null}
-
- {user.name}
-
-
-
+ <>
+
+
+
+
+ {user.name[0].toUpperCase()}
+ {user.image ? : null}
+
+
+ {user.name}
+
+
+
+ >
);
};
diff --git a/src/components/wrappers/dashboard/common/logged-in/logged-in-dropdown.tsx b/src/components/wrappers/dashboard/common/logged-in/logged-in-dropdown.tsx
index ba4f438b..7fd0afa3 100644
--- a/src/components/wrappers/dashboard/common/logged-in/logged-in-dropdown.tsx
+++ b/src/components/wrappers/dashboard/common/logged-in/logged-in-dropdown.tsx
@@ -1,63 +1,63 @@
"use client";
-import { PropsWithChildren } from "react";
-import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
-import { redirect } from "next/navigation";
-import { CircleUser, LogOut, ShieldHalf } from "lucide-react";
-import { signOut } from "@/lib/auth/auth-client";
+import {PropsWithChildren, ReactNode, useState} from "react";
import { useRouter } from "next/navigation";
-import { User } from "@/db/schema/02_user";
+import { CircleUser, LogOut } from "lucide-react";
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
+import { signOut } from "@/lib/auth/auth-client";
+import {ProfileModal} from "@/components/wrappers/dashboard/common/profile/profile-modal";
+import {Account, Session, User} from "@/db/schema/02_user";
export type LoggedInDropdownProps = PropsWithChildren<{
user: User;
+ sessions: Session[];
+ currentSession: Session;
+ accounts: Account[];
+ children: ReactNode;
}>;
-export const LoggedInDropdown = (props: LoggedInDropdownProps) => {
+export const LoggedInDropdown = ({ user, sessions, currentSession, accounts, children }: LoggedInDropdownProps) => {
const router = useRouter();
+ const [isModalOpen, setIsModalOpen] = useState(false);
return (
-
- {props.children}
-
- {
- redirect("/dashboard/profile");
- }}
- >
-
-
- Account
-
-
- {/*{(props.user.role === "superadmin" || props.user.role === "admin") && (*/}
- {/* {*/}
- {/* redirect("/dashboard/admin");*/}
- {/* }}*/}
- {/* >*/}
- {/* */}
- {/* */}
- {/* Administration Panel */}
- {/*
*/}
- {/* */}
- {/*)}*/}
- {
- await signOut({
- fetchOptions: {
- onSuccess: () => {
- router.push("/login");
+ <>
+
+
+
+ {children}
+
+ setIsModalOpen(!isModalOpen)}>
+
+
+ Account
+
+
+ {
+ await signOut({
+ fetchOptions: {
+ onSuccess: () => {
+ router.push("/login");
+ },
},
- },
- });
- }}
- >
-
-
- Log out
-
-
-
-
+ });
+ }}
+ >
+
+
+ Logout
+
+
+
+
+ >
);
};
diff --git a/src/components/wrappers/dashboard/common/profile/profile-modal.tsx b/src/components/wrappers/dashboard/common/profile/profile-modal.tsx
new file mode 100644
index 00000000..b38a77a7
--- /dev/null
+++ b/src/components/wrappers/dashboard/common/profile/profile-modal.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import React from "react";
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { Tabs, TabsContent } from "@/components/ui/tabs";
+import { Account, Session, User } from "@/db/schema/02_user";
+import { ProfileSidebar } from "./profile-sidebar";
+import {ProfileProviders} from "@/components/wrappers/dashboard/profile/profile-providers";
+import {ProfileAccount} from "@/components/wrappers/dashboard/profile/profile-account";
+import {ProfileAppearance} from "@/components/wrappers/dashboard/profile/profile-apperance";
+import {ProfileSecurity} from "@/components/wrappers/dashboard/profile/profile-security";
+import {ProfileGeneral} from "@/components/wrappers/dashboard/profile/profile-general";
+
+type ProfileModalProps = {
+ open: boolean;
+ user: User;
+ sessions: Session[];
+ currentSession: Session;
+ accounts: Account[];
+ onOpenChange: (open: boolean) => void;
+};
+
+export const ProfileModal = ({ user, sessions, currentSession, accounts, open, onOpenChange }: ProfileModalProps) => {
+ return (
+
+
+
+ Settings
+ Manage your account settings
+
+
+
+
+
+
+
+
+
+
+ acc.providerId === "credential")!}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/wrappers/dashboard/common/profile/profile-sidebar.tsx b/src/components/wrappers/dashboard/common/profile/profile-sidebar.tsx
new file mode 100644
index 00000000..df579496
--- /dev/null
+++ b/src/components/wrappers/dashboard/common/profile/profile-sidebar.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+import React, { use } from "react";
+import { TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { UserIcon, Settings, Palette, ShieldHalf, Workflow } from "lucide-react";
+import { User } from "@/db/schema/02_user";
+
+interface ProfileSidebarProps {
+ user: User;
+}
+
+export function ProfileSidebar({ user }: ProfileSidebarProps) {
+
+ return (
+
+
+ Settings
+
+
+
+ }>
+ Profile
+
+ }>
+ Security & Access
+
+ }>
+ Connected Accounts
+
+ }>
+ Account
+
+ }>
+ Appearance
+
+
+
+ );
+}
+
+function SettingsTabTrigger({ value, icon, children }: { value: string; icon: React.ReactNode; children: React.ReactNode }) {
+ return (
+
+ {icon}
+ {children}
+
+ );
+}
diff --git a/src/components/wrappers/dashboard/common/sidebar/app-sidebar.tsx b/src/components/wrappers/dashboard/common/sidebar/app-sidebar.tsx
index 4559989f..84a202a8 100644
--- a/src/components/wrappers/dashboard/common/sidebar/app-sidebar.tsx
+++ b/src/components/wrappers/dashboard/common/sidebar/app-sidebar.tsx
@@ -34,6 +34,7 @@ export function AppSidebar() {
+
diff --git a/src/components/wrappers/dashboard/profile/avatar/avatar.action.ts b/src/components/wrappers/dashboard/profile/actions/avatar.action.ts
similarity index 100%
rename from src/components/wrappers/dashboard/profile/avatar/avatar.action.ts
rename to src/components/wrappers/dashboard/profile/actions/avatar.action.ts
diff --git a/src/components/wrappers/dashboard/profile/actions/profile.action.ts b/src/components/wrappers/dashboard/profile/actions/profile.action.ts
new file mode 100644
index 00000000..c098bd57
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/actions/profile.action.ts
@@ -0,0 +1,55 @@
+"use server";
+
+import { db } from "@/db";
+import { eq } from "drizzle-orm";
+import { ServerActionResult } from "@/types/action-type";
+import { z } from "zod";
+import { headers } from "next/headers";
+import { auth } from "@/lib/auth/auth";
+import { user } from "@/db/schema/02_user";
+import {userAction} from "@/lib/safe-actions/actions";
+
+const UpdateProfileSchema = z.object({
+ name: z.string().optional(),
+});
+
+export const updateProfileSettingsAction = userAction.schema(UpdateProfileSchema).action(async ({ parsedInput }): Promise> => {
+ try {
+ const session = await auth.api.getSession({
+ headers: await headers(),
+ });
+
+ if (!session) {
+ return {
+ success: false,
+ actionError: {
+ message: "unauthorized",
+ cause: "User not authenticated",
+ },
+ };
+ }
+
+ await db
+ .update(user)
+ .set({
+ ...(parsedInput.name ? { name: parsedInput.name } : {}),
+ })
+ .where(eq(user.id, session.user.id));
+
+ return {
+ success: true,
+ value: {},
+ actionSuccess: {
+ message: "profile_updated",
+ },
+ };
+ } catch (error) {
+ return {
+ success: false,
+ actionError: {
+ message: "error_updating_profile",
+ cause: error instanceof Error ? error.message : "Unknown error",
+ },
+ };
+ }
+});
diff --git a/src/components/wrappers/dashboard/profile/actions/provider.action.ts b/src/components/wrappers/dashboard/profile/actions/provider.action.ts
new file mode 100644
index 00000000..16b0db9b
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/actions/provider.action.ts
@@ -0,0 +1,53 @@
+"use server";
+
+import { ServerActionResult } from "@/types/action-type";
+import { headers } from "next/headers";
+import { auth } from "@/lib/auth/auth";
+import z from "zod";
+import { zPassword } from "@/lib/zod";
+import {userAction} from "@/lib/safe-actions/actions";
+
+export const linkPasswordProfileProviderAction = userAction
+ .schema(
+ z.object({
+ password: zPassword(),
+ })
+ )
+ .action(async ({ parsedInput }): Promise> => {
+ try {
+ const session = await auth.api.getSession({
+ headers: await headers(),
+ });
+
+ if (!session) {
+ return {
+ success: false,
+ actionError: {
+ message: "unauthorized",
+ cause: "User not authenticated",
+ },
+ };
+ }
+
+ await auth.api.setPassword({
+ headers: await headers(),
+ body: {
+ newPassword: parsedInput.password,
+ },
+ });
+ return {
+ success: true,
+ actionSuccess: {
+ message: "profile_updated",
+ },
+ };
+ } catch (error) {
+ return {
+ success: false,
+ actionError: {
+ message: "error_updating_profile",
+ cause: error instanceof Error ? error.message : "Unknown error",
+ },
+ };
+ }
+ });
diff --git a/src/components/wrappers/dashboard/profile/actions/security.action.ts b/src/components/wrappers/dashboard/profile/actions/security.action.ts
new file mode 100644
index 00000000..b7dce2ca
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/actions/security.action.ts
@@ -0,0 +1,101 @@
+"use server";
+
+import { ServerActionResult } from "@/types/action-type";
+import { z } from "zod";
+import { headers } from "next/headers";
+import { auth } from "@/lib/auth/auth";
+import {userAction} from "@/lib/safe-actions/actions";
+
+const RevokeSessionSchema = z.object({
+ token: z.string(),
+});
+
+export const revokeSessionAction = userAction.schema(RevokeSessionSchema).action(async ({ parsedInput }): Promise> => {
+ try {
+ const session = await auth.api.getSession({
+ headers: await headers(),
+ });
+
+ if (!session) {
+ return {
+ success: false,
+ actionError: {
+ message: "unauthorized",
+ cause: "User not authenticated",
+ },
+ };
+ }
+
+ await auth.api.revokeSession({
+ body: {
+ token: parsedInput.token,
+ },
+ headers: await headers(),
+ });
+
+ return {
+ success: true,
+ value: {},
+ actionSuccess: {
+ message: "session_revoked",
+ },
+ };
+ } catch (error) {
+ return {
+ success: false,
+ actionError: {
+ message: "error_revoking_session",
+ cause: error instanceof Error ? error.message : "Unknown error",
+ },
+ };
+ }
+});
+
+export const revokeAllSessionsAction = userAction.action(async (): Promise> => {
+ try {
+ const session = await auth.api.getSession({
+ headers: await headers(),
+ });
+
+ if (!session) {
+ return {
+ success: false,
+ actionError: {
+ message: "unauthorized",
+ cause: "User not authenticated",
+ },
+ };
+ }
+
+ const sessions = await auth.api.listSessions({
+ headers: await headers(),
+ });
+
+ const otherSessions = sessions.filter((s) => s.token !== session.session.token);
+
+ for (const s of otherSessions) {
+ await auth.api.revokeSession({
+ body: {
+ token: s.token,
+ },
+ headers: await headers(),
+ });
+ }
+
+ return {
+ success: true,
+ value: {},
+ actionSuccess: {
+ message: "other_sessions_revoked",
+ },
+ };
+ } catch (error) {
+ return {
+ success: false,
+ actionError: {
+ message: "error_revoking_other_sessions",
+ cause: error instanceof Error ? error.message : "Unknown error",
+ },
+ };
+ }
+});
diff --git a/src/components/wrappers/dashboard/profile/components/avatar-with-upload.tsx b/src/components/wrappers/dashboard/profile/components/avatar-with-upload.tsx
new file mode 100644
index 00000000..8cc64eef
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/components/avatar-with-upload.tsx
@@ -0,0 +1,82 @@
+"use client";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { UploadIcon } from "lucide-react";
+import { toast } from "sonner";
+import { uploadImageAction } from "@/features/upload/public/upload.action";
+import { useMutation } from "@tanstack/react-query";
+import { updateImageUserAction } from "@/components/wrappers/dashboard/profile2/avatar/avatar.action";
+import { useRouter } from "next/navigation";
+import { User } from "@/db/schema/02_user";
+import React, {ChangeEvent} from "react";
+
+export type AvatarWithUploadProps = {
+ user: User;
+};
+
+export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
+ const user = props.user;
+ const router = useRouter();
+
+ const submitImage = useMutation({
+ mutationFn: async (file: File) => {
+ const formData = new FormData();
+ formData.set("file", file);
+ const uploadImage = await uploadImageAction(formData);
+ const data = uploadImage?.data?.data;
+
+ if (uploadImage?.serverError || !data) {
+ console.log(uploadImage?.serverError);
+ toast.error(uploadImage?.serverError);
+ return;
+ }
+
+ const updateUser = await updateImageUserAction(data.url);
+ const dataUser = updateUser?.data?.data;
+
+ if (updateUser?.serverError || !dataUser) {
+ console.log(updateUser?.serverError);
+ toast.error(updateUser?.serverError);
+ return;
+ }
+
+ toast.success("Successfully uploaded user image!");
+ router.refresh();
+ },
+ });
+
+ const handleImageUpload = async (event: ChangeEvent) => {
+ const file = event.target.files?.[0];
+ if (!file) return;
+ if (!file.type.includes("image")) {
+ toast.error("File not an image");
+ return;
+ }
+ submitImage.mutate(file);
+ };
+
+
+
+ return (
+
+
+
+
+ {user.name.charAt(0)}
+
+
+
{
+ const fileInput = document.createElement("input");
+ fileInput.type = "file";
+ fileInput.accept = "image/*";
+ // @ts-ignore
+ fileInput.onchange = handleImageUpload;
+ fileInput.click();
+ }}
+ className="cursor-pointer absolute inset-0 flex justify-center items-center opacity-0 transition-opacity hover:opacity-100 hover:bg-gray-500 hover:bg-opacity-50 rounded-full w-24 h-24 lg:w-32 lg:h-32"
+ >
+
+
+
+ );
+};
diff --git a/src/components/wrappers/dashboard/profile/components/backup-codes-list.tsx b/src/components/wrappers/dashboard/profile/components/backup-codes-list.tsx
new file mode 100644
index 00000000..d28ab241
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/components/backup-codes-list.tsx
@@ -0,0 +1,72 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { AlertTriangle, Copy, Download } from "lucide-react";
+import { toast } from "sonner";
+import { humanReadableDate } from "@/utils/date-formatting";
+
+type BackupCodesListProps = {
+ codes: string[];
+ className?: string;
+};
+
+export function BackupCodesList({ codes, className }: BackupCodesListProps) {
+
+ const handleCopyBackupCodes = () => {
+ navigator.clipboard.writeText(codes.join("\n"));
+ toast.success("Backup codes copied to clipboard");
+ };
+
+ if (!codes.length) return null;
+
+ const handleDownload = () => {
+ const header ="Your Backup Codes" + "\n\n";
+ const content = `Backup Code: ${codes.join("\n")}`;
+ const footer = "\n\n" + `Keep these codes in a safe place. They can be used to access your account if you lose access to your authentication device. Generated on ${humanReadableDate(new Date())}.`;
+ const text = header + content + footer;
+
+ const blob = new Blob([text], { type: "text/plain" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `backup-codes.txt`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ toast.success("Backup codes downloaded");
+ };
+
+
+
+ return (
+
+
+
+
Your Backup Codes
+
+
+ Copy All Codes
+
+
+
+ {codes.map((code, i) => (
+
+ {code}
+
+ ))}
+
+
+
+
+ These codes can only be used once. After using a code, make sure to generate new backup codes to maintain account security.
+
+
+
+
+ Download Backup Codes
+
+
+
+ );
+}
diff --git a/src/components/wrappers/dashboard/profile/form/2fa-form.tsx b/src/components/wrappers/dashboard/profile/form/2fa-form.tsx
new file mode 100644
index 00000000..2876e6b5
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/form/2fa-form.tsx
@@ -0,0 +1,168 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { Form, FormControl, FormField, FormItem, FormMessage, useZodForm } from "@/components/ui/form";
+import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
+import { Input } from "@/components/ui/input";
+import { useMutation } from "@tanstack/react-query";
+import { Smartphone, Loader2, FileKey2 } from "lucide-react";
+import { toast } from "sonner";
+import { BackupCodeSchema, OtpSchema, OtpSchemaType } from "./2fa.schema";
+import { authClient } from "@/lib/auth/auth-client";
+import { useState } from "react";
+
+type TwoFactorFormProps = {
+ onSuccess?: (success: boolean) => void;
+ onSuccessData?: (data: any) => void;
+};
+
+export default function TwoFactorForm({ onSuccess, onSuccessData }: TwoFactorFormProps) {
+
+ const [isBackupCodeMode, setIsBackupCodeMode] = useState(false);
+
+ const otpForm = useZodForm({
+ schema: isBackupCodeMode ? BackupCodeSchema : OtpSchema,
+ defaultValues: {
+ code: "",
+ },
+ });
+
+ const { mutate: verifyOtp, isPending: isVerifyingOtp } = useMutation({
+ mutationFn: async (values: OtpSchemaType) => {
+ const { data, error } = await authClient.twoFactor.verifyTotp({
+ code: values.code,
+ });
+ if (error) throw error;
+ return data;
+ },
+ onSuccess: (data) => {
+ toast.success("Authentication successful.");
+ onSuccess?.(true);
+ onSuccessData?.(data);
+ },
+ onError: (e) => {
+ console.error("totp", e);
+ toast.error("Incorrect or expired code.");
+ otpForm.reset();
+ onSuccess?.(false);
+ },
+ });
+
+ const { mutate: verifyBackupCode, isPending: isVerifyingBackupCode } = useMutation({
+ mutationFn: async (values: OtpSchemaType) => {
+ const { data, error } = await authClient.twoFactor.verifyBackupCode({
+ code: values.code,
+ });
+ if (error) throw error;
+ return data;
+ },
+ onSuccess: (data) => {
+ toast.success("Backup code accepted successfully.");
+ onSuccess?.(true);
+ onSuccessData?.(data);
+ },
+ onError: (e) => {
+ console.error("bak", e);
+ toast.error("Invalid backup code.");
+ otpForm.reset();
+ onSuccess?.(false);
+ },
+ });
+
+ const handleSubmit = async (values: OtpSchemaType) => {
+ if (isBackupCodeMode) {
+ verifyBackupCode(values);
+ } else {
+ verifyOtp(values);
+ }
+ };
+
+ const isPending = isVerifyingOtp || isVerifyingBackupCode;
+
+
+
+ return (
+
+ );
+}
diff --git a/src/components/wrappers/dashboard/profile/form/2fa.schema.ts b/src/components/wrappers/dashboard/profile/form/2fa.schema.ts
new file mode 100644
index 00000000..eb708eee
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/form/2fa.schema.ts
@@ -0,0 +1,14 @@
+import { zString } from "@/lib/zod";
+import { z } from "zod";
+
+export const OtpSchema = z.object({
+ code: zString().min(6, { message: "Le code doit contenir 6 chiffres" }),
+});
+
+export type OtpSchemaType = z.infer;
+
+export const BackupCodeSchema = z.object({
+ code: zString().min(1, "Le code est requis"),
+});
+
+export type BackupCodeSchemaType = z.infer;
diff --git a/src/components/wrappers/dashboard/profile/form/reset-password-form.tsx b/src/components/wrappers/dashboard/profile/form/reset-password-form.tsx
new file mode 100644
index 00000000..2628f5eb
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/form/reset-password-form.tsx
@@ -0,0 +1,127 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Form, FormControl, FormField, FormItem, FormLabel, useZodForm } from "@/components/ui/form";
+import { Loader2 } from "lucide-react";
+import { PasswordStrengthInput } from "@/components/ui/password-input-indicator";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { useMutation } from "@tanstack/react-query";
+import { useRouter } from "next/navigation";
+import { toast } from "sonner";
+import { authClient } from "@/lib/auth/auth-client";
+import { ResetPasswordSecuritySchema, ResetPasswordSecuritySchemaType } from "../schemas/security.schema";
+import {PasswordInput} from "@/components/ui/password-input";
+
+type ResetPasswordFormProps = {
+ onSuccess?: () => void;
+ isDefault?: boolean;
+};
+
+export default function ResetPasswordForm({ onSuccess, isDefault }: ResetPasswordFormProps) {
+ const router = useRouter();
+
+ const [allowConfirmPassword, setAllowConfirmPassword] = useState(false);
+
+ const form = useZodForm({
+ schema: ResetPasswordSecuritySchema,
+ });
+
+ const { mutate: changePassword, isPending: isChangingPassword } = useMutation({
+ mutationFn: async (values: ResetPasswordSecuritySchemaType) => {
+ const { error } = await authClient.changePassword({
+ currentPassword: values.currentPassword,
+ newPassword: values.newPassword,
+ revokeOtherSessions: true,
+ });
+ // await authClient.updateUser({
+ // isDefaultPassword: false,
+ // });
+ if (error) throw error;
+ },
+ onSuccess: () => {
+ toast.success("Password reset successfully.");
+ form.reset();
+ router.refresh();
+ setAllowConfirmPassword(false);
+
+ if (onSuccess) {
+ onSuccess();
+ }
+ },
+ onError: () => {
+ toast.error("Failed to reset password.");
+ },
+ });
+
+
+ return (
+
+ );
+}
+
+
diff --git a/src/components/wrappers/dashboard/profile/form/set-password-form.tsx b/src/components/wrappers/dashboard/profile/form/set-password-form.tsx
new file mode 100644
index 00000000..365a2b33
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/form/set-password-form.tsx
@@ -0,0 +1,92 @@
+"use client";
+
+import {Button} from "@/components/ui/button";
+import {Form, FormControl, FormField, FormItem, FormLabel, useZodForm} from "@/components/ui/form";
+import {Loader2} from "lucide-react";
+import {PasswordStrengthInput} from "@/components/ui/password-input-indicator";
+import {useMutation} from "@tanstack/react-query";
+import {useRouter} from "next/navigation";
+import {toast} from "sonner";
+import {PasswordProviderSchema, PasswordProviderSchemaType} from "../schemas/provider.schema";
+import {linkPasswordProfileProviderAction} from "../actions/provider.action";
+import {PasswordInput} from "@/components/ui/password-input";
+
+type SetPasswordFormProps = {
+ onSuccess?: () => void;
+};
+
+export default function SetPasswordForm({onSuccess}: SetPasswordFormProps) {
+ const router = useRouter();
+
+ const form = useZodForm({
+ schema: PasswordProviderSchema,
+ });
+
+ const {mutateAsync: setPasswordMutation, isPending: isSettingPassword} = useMutation({
+ mutationFn: async (values: PasswordProviderSchemaType) => {
+ const result = await linkPasswordProfileProviderAction({
+ password: values.password,
+ });
+ return result?.data;
+ },
+ onSuccess: (data) => {
+ if (data?.success) {
+ toast.success("Password set successfully.");
+ form.reset();
+ router.refresh();
+
+ if (onSuccess) {
+ onSuccess();
+ }
+ } else {
+ toast.error("Failed to set password.");
+ }
+ },
+ onError: () => {
+ toast.error("Failed to set password.");
+ },
+ });
+
+
+ return (
+
+ );
+}
diff --git a/src/components/wrappers/dashboard/profile/modal/disable-2fa-modal.tsx b/src/components/wrappers/dashboard/profile/modal/disable-2fa-modal.tsx
new file mode 100644
index 00000000..9a2d4f68
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/modal/disable-2fa-modal.tsx
@@ -0,0 +1,122 @@
+"use client";
+
+import { useState } from "react";
+import { useMutation } from "@tanstack/react-query";
+import { useRouter } from "next/navigation";
+import { toast } from "sonner";
+import { z } from "zod";
+import { Loader2, ShieldX } from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
+import { authClient } from "@/lib/auth/auth-client";
+import TwoFactorForm from "../form/2fa-form";
+import { zPassword } from "@/lib/zod";
+import {PasswordInput} from "@/components/ui/password-input";
+
+const PasswordSchema = z.object({
+ password: zPassword(),
+});
+
+type Password = z.infer;
+
+type Disable2FAModalProps = {
+ onOpenChange: (open: boolean) => void;
+ open: boolean;
+};
+
+export function Disable2FAProfileProviderModal({ onOpenChange, open }: Disable2FAModalProps) {
+
+ const router = useRouter();
+ const [step, setStep] = useState<"OTP" | "PASSWORD">("OTP");
+
+ const passwordForm = useZodForm({
+ schema: PasswordSchema,
+ defaultValues: {
+ password: "",
+ },
+ });
+
+ const { mutate: disable2FA, isPending: isDisabling } = useMutation({
+ mutationFn: async (values: Password) => {
+ const { data, error } = await authClient.twoFactor.disable({
+ password: values.password,
+ });
+ if (error) throw error;
+ return data;
+ },
+ onSuccess: () => {
+ router.refresh();
+ toast.success("Two-factor authentication disabled successfully.");
+ onOpenChange(false);
+ setStep("OTP");
+ passwordForm.reset();
+ },
+ onError: () => {
+ toast.error("Failed to disable two-factor authentication.");
+ },
+ });
+
+ const handleClose = () => {
+ passwordForm.reset();
+ setStep("OTP");
+ onOpenChange(false);
+ };
+
+
+ return (
+ (!v ? handleClose() : onOpenChange(v))}>
+
+
+
+ Disable Two-Factor
+
+
+
+
+ Disable Two-Factor Authentication
+ Are you sure you want to disable two-factor authentication? This will reduce the security of your account.
+
+
+ {step === "OTP" && (
+ {
+ if (success) {
+ setStep("PASSWORD");
+ }
+ }}
+ />
+ )}
+
+ {step === "PASSWORD" && (
+
+ )}
+
+
+ );
+}
diff --git a/src/components/wrappers/dashboard/profile/modal/reset-password-modal.tsx b/src/components/wrappers/dashboard/profile/modal/reset-password-modal.tsx
new file mode 100644
index 00000000..1943d096
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/modal/reset-password-modal.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
+import ResetPasswordForm from "../form/reset-password-form";
+
+type ResetPasswordModalProps = {
+ onOpenChange: (open: boolean) => void;
+ open: boolean;
+};
+
+export function ResetPasswordProfileProviderModal({ onOpenChange, open }: ResetPasswordModalProps) {
+
+ return (
+
+
+
+ Reset Password
+
+
+
+
+ Reset Password
+ Enter a new password for your account below.
+
+ onOpenChange(false)} />
+
+
+ );
+}
+
diff --git a/src/components/wrappers/dashboard/profile/modal/set-password-modal.tsx b/src/components/wrappers/dashboard/profile/modal/set-password-modal.tsx
new file mode 100644
index 00000000..13ff747b
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/modal/set-password-modal.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
+import SetPasswordForm from "../form/set-password-form";
+
+type SetPasswordModalProps = {
+ onOpenChange: (open: boolean) => void;
+ open: boolean;
+};
+
+export function SetPasswordProfileProviderModal({ onOpenChange, open }: SetPasswordModalProps) {
+
+ return (
+
+
+
+ Set Password
+
+
+
+
+ Set Password
+ Create a password for your account to enable password-based login.
+
+
+ onOpenChange(false)} />
+
+
+ );
+}
+
diff --git a/src/components/wrappers/dashboard/profile/modal/setup-2fa-modal.tsx b/src/components/wrappers/dashboard/profile/modal/setup-2fa-modal.tsx
new file mode 100644
index 00000000..60832d30
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/modal/setup-2fa-modal.tsx
@@ -0,0 +1,253 @@
+"use client";
+
+import {useState} from "react";
+import {Button} from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger
+} from "@/components/ui/dialog";
+import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
+import {Loader2, Copy, CheckCircle2, ShieldCheck} from "lucide-react";
+import {useMutation} from "@tanstack/react-query";
+import {useRouter} from "next/navigation";
+import {Setup2FASecuritySchema, Setup2FASecuritySchemaType} from "../schemas/security.schema";
+import {toast} from "sonner";
+import {authClient} from "@/lib/auth/auth-client";
+import {Alert, AlertDescription} from "@/components/ui/alert";
+import {InputOTP, InputOTPGroup, InputOTPSlot} from "@/components/ui/input-otp";
+import QRCode from "react-qr-code";
+import z from "zod";
+import {zPassword} from "@/lib/zod";
+import {BackupCodesList} from "../components/backup-codes-list";
+import {PasswordInput} from "@/components/ui/password-input";
+
+const PasswordSchema = z.object({
+ password: zPassword(),
+});
+
+type Password = z.infer;
+
+type Setup2FAModalProps = {
+ onOpenChange: (open: boolean) => void;
+ open: boolean;
+};
+
+export function Setup2FAProfileProviderModal({onOpenChange, open}: Setup2FAModalProps) {
+
+ const router = useRouter();
+ const [step, setStep] = useState<"PASSWORD" | "QR" | "BACKUP">("PASSWORD");
+
+ const [totpURI, setTotpURI] = useState("");
+ const [secret, setSecret] = useState("");
+ const [backupCodes, setBackupCodes] = useState([]);
+
+ const form = useZodForm({
+ schema: Setup2FASecuritySchema,
+ defaultValues: {
+ code: "",
+ },
+ });
+
+ const passwordForm = useZodForm({
+ schema: PasswordSchema,
+ defaultValues: {
+ password: "",
+ },
+ });
+
+ const {mutate: enable2FA, isPending: isEnabling} = useMutation({
+ mutationFn: async (values: Password) => {
+ const {data, error} = await authClient.twoFactor.enable({
+ password: values.password,
+ });
+
+ if (error) throw error;
+ return data;
+ },
+ onSuccess: (data) => {
+ setTotpURI(data.totpURI);
+ setSecret(data.totpURI.split("secret=")[1].split("&")[0]);
+ setBackupCodes(data.backupCodes || []);
+ setStep("QR");
+ },
+ onError: () => {
+ toast.error("Failed to enable two-factor authentication.");
+ },
+ });
+
+ const {mutate: verify2FA, isPending: isVerifying} = useMutation({
+ mutationFn: async (values: Setup2FASecuritySchemaType) => {
+ const {data, error} = await authClient.twoFactor.verifyTotp({
+ code: values.code,
+ trustDevice: true,
+ });
+
+ if (error) throw error;
+ return data;
+ },
+ onSuccess: () => {
+ toast.success("Two-factor authentication enabled successfully.");
+ setStep("BACKUP");
+ },
+ onError: () => {
+ toast.error("The provided code is invalid.");
+ form.reset();
+ },
+ });
+
+ const handleCopySecret = () => {
+ navigator.clipboard.writeText(secret);
+ toast.success("Secret copied to clipboard");
+ };
+
+ const handleClose = () => {
+ router.refresh();
+ onOpenChange(false);
+ setStep("PASSWORD");
+ form.reset();
+ passwordForm.reset();
+ };
+
+
+
+ return (
+ (!v ? handleClose() : onOpenChange(v))}>
+
+
+
+ Enable Two-Factor
+
+
+
+
+ Enable Two-Factor Authentication
+
+ {step === "PASSWORD" && ""}
+ {step === "QR" && "Scan the QR code below with your authentication app or enter the secret key manually."}
+ {step === "BACKUP" && "Save these backup codes in a secure location. They can be used to access your account if you lose access to your authentication device."}
+
+
+
+ {step === "PASSWORD" && (
+
+ )}
+
+ {step === "QR" && (
+
+ )}
+
+ {step === "BACKUP" && (
+
+
+
+ Two Factor Authentication is now enabled on your account.
+
+
+
+
+
+ Finish Setup
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/wrappers/dashboard/profile/modal/view-backup-codes-modal.tsx b/src/components/wrappers/dashboard/profile/modal/view-backup-codes-modal.tsx
new file mode 100644
index 00000000..26d873b0
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/modal/view-backup-codes-modal.tsx
@@ -0,0 +1,128 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
+import { Loader2, FileKey2, RefreshCw, AlertTriangle, Download } from "lucide-react";
+import { useMutation } from "@tanstack/react-query";
+import { authClient } from "@/lib/auth/auth-client";
+import { toast } from "sonner";
+import { z } from "zod";
+import { zPassword } from "@/lib/zod";
+import { BackupCodesList } from "../components/backup-codes-list";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import {PasswordInput} from "@/components/ui/password-input";
+
+const PasswordSchema = z.object({
+ password: zPassword(),
+});
+
+type Password = z.infer;
+
+type ViewBackupCodesModalProps = {
+ onOpenChange: (open: boolean) => void;
+ open: boolean;
+};
+
+export function ViewBackupCodesModal({ onOpenChange, open }: ViewBackupCodesModalProps) {
+
+
+ const [step, setStep] = useState<"PASSWORD" | "CODES">("PASSWORD");
+ const [codes, setCodes] = useState([]);
+
+ const form = useZodForm({
+ schema: PasswordSchema,
+ defaultValues: {
+ password: "",
+ },
+ });
+
+ const { mutate: generateCodes, isPending } = useMutation({
+ mutationFn: async (values: Password) => {
+ const { data, error } = await authClient.twoFactor.generateBackupCodes({
+ password: values.password,
+ });
+ if (error) throw error;
+ return data;
+ },
+ onSuccess: (data) => {
+ if (data?.backupCodes) {
+ setCodes(data.backupCodes);
+ setStep("CODES");
+ toast.success("New backup codes generated successfully.");
+ }
+ },
+ onError: () => {
+ toast.error("Failed to generate backup codes. Your password may be incorrect.");
+ },
+ });
+
+ const handleClose = () => {
+ form.reset();
+ setStep("PASSWORD");
+ onOpenChange(false);
+ };
+
+ return (
+ (!v ? handleClose() : onOpenChange(v))}>
+
+
+
+ Regenerate Backup Codes
+
+
+
+
+ Backup Codes
+ {step === "PASSWORD" ? "For security reasons, existing codes are hidden. You must generate a new set to view them." : "Save these codes securely. They will not be shown again once you close this window."}
+
+
+ {step === "PASSWORD" && (
+
+ )}
+
+ {step === "CODES" && (
+
+
+
+
+ handleClose()} className="w-full sm:w-auto">
+ I Have Saved My Codes
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/wrappers/dashboard/profile/profile-account.tsx b/src/components/wrappers/dashboard/profile/profile-account.tsx
new file mode 100644
index 00000000..de8b3f36
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/profile-account.tsx
@@ -0,0 +1,167 @@
+"use client";
+
+import {useEffect} from "react";
+import {Button} from "@/components/ui/button";
+import {Input} from "@/components/ui/input";
+import {AlertCircle, Loader2} from "lucide-react";
+import {useMutation} from "@tanstack/react-query";
+import {toast} from "sonner";
+import {useRouter} from "next/navigation";
+import {authClient} from "@/lib/auth/auth-client";
+import {User} from "@/db/schema/02_user";
+import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
+import {EmailSchema, EmailSchemaType} from "./schemas/account.schema";
+import {BetterAuthError} from "@/types/auth";
+
+
+interface ProfileAccountProps {
+ user: User;
+}
+
+export function ProfileAccount({user}: ProfileAccountProps) {
+
+ const router = useRouter();
+
+ useEffect(() => {
+ let interval: NodeJS.Timeout;
+
+ interval = setInterval(async () => {
+ router.refresh();
+ }, 5000);
+
+ return () => {
+ if (interval) clearInterval(interval);
+ };
+ });
+
+ const emailForm = useZodForm({
+ schema: EmailSchema,
+ defaultValues: {
+ email: user.email,
+ },
+ });
+
+ const {mutate: updateEmail, isPending: isUpdatingEmail} = useMutation({
+ mutationFn: async (values: EmailSchemaType) => {
+ const {error} = await authClient.changeEmail({
+ newEmail: values.email,
+ callbackURL: window.location.href,
+ });
+
+ if (error) throw error;
+ return values.email;
+ },
+ onSuccess: (newEmail) => {
+ toast.success("Email updated successfully.");
+ emailForm.reset({email: newEmail});
+
+ router.refresh();
+ },
+ onError: (error: BetterAuthError) => {
+ console.log(error)
+ if (error.code === "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL") {
+ toast.error("User already exists, use another email address!");
+ emailForm.reset({email: user.email});
+ router.refresh()
+ } else {
+ toast.error("An error occurred while trying to update your password!");
+ }
+ },
+ });
+
+ const {mutate: resendVerificationEmail, isPending: isResendingVerification} = useMutation({
+ mutationFn: async () => {
+ const currentEmailInput = emailForm.getValues("email");
+
+
+ let error: BetterAuthError | null = null;
+
+ if (currentEmailInput === user.email) {
+ const sendVerification = await authClient.sendVerificationEmail({
+ email: currentEmailInput,
+ callbackURL: window.location.href,
+ });
+ error = sendVerification.error;
+ } else {
+ const result = await authClient.changeEmail({
+ callbackURL: window.location.href,
+ newEmail: currentEmailInput,
+ });
+ error = result.error;
+ }
+
+ if (error) throw error;
+ },
+ onSuccess: () => {
+ toast.success("Verification email resent successfully.");
+ },
+ onError: (e: BetterAuthError) => {
+ console.error(e);
+ toast.error("Failed to resend verification email.");
+ },
+ });
+
+
+ return (
+
+
+
Account Settings
+
Update your email and preferences.
+
+
+
+
+ );
+}
diff --git a/src/components/wrappers/dashboard/profile/profile-apperance.tsx b/src/components/wrappers/dashboard/profile/profile-apperance.tsx
new file mode 100644
index 00000000..f4f90ddf
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/profile-apperance.tsx
@@ -0,0 +1,97 @@
+"use client";
+
+import React from "react";
+import {cn} from "@/lib/utils";
+import {useTheme} from "next-themes";
+import {authClient} from "@/lib/auth/auth-client";
+
+const themes = [{value: "light"}, {value: "dark"}, {value: "system"}];
+
+export function ProfileAppearance() {
+
+ return (
+
+
+
Appearance Settings
+
Customize the look and feel of your dashboard.
+
+
+
+ );
+}
+
+function ThemeSelector() {
+ const {theme, setTheme} = useTheme();
+
+
+ return (
+
+ {themes.map((item) => {
+ const isDark = item.value === "dark";
+ const isSystem = item.value === "system";
+ const isActive = theme === item.value;
+
+ return (
+
{
+ // setTheme(item.value)
+ // await authClient.updateUser({theme: item.value});
+ }}
+ >
+
+
+
+
{THEME_TEXT[item.value as ThemeKey]}
+
+
+
+ );
+ })}
+
+ );
+}
+
+type ThemeKey = "dark" | "light" | "system";
+
+
+const THEME_TEXT: Record = {
+ dark: "Dark",
+ light: "Light",
+ system: "System",
+};
\ No newline at end of file
diff --git a/src/components/wrappers/dashboard/profile/profile-general.tsx b/src/components/wrappers/dashboard/profile/profile-general.tsx
new file mode 100644
index 00000000..cb1174ba
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/profile-general.tsx
@@ -0,0 +1,130 @@
+"use client";
+
+import React from "react";
+import {Button} from "@/components/ui/button";
+import {Input} from "@/components/ui/input";
+import {Badge} from "@/components/ui/badge";
+import {Loader2} from "lucide-react";
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ useZodForm
+} from "@/components/ui/form";
+import {useMutation} from "@tanstack/react-query";
+import {toast} from "sonner";
+import {useRouter} from "next/navigation";
+import {updateProfileSettingsAction} from "./actions/profile.action";
+import {User} from "@/db/schema/02_user";
+import {ProfileSchema, ProfileSchemaType} from "./schemas/general.schema";
+import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/components/avatar-with-upload";
+
+interface ProfileGeneralProps {
+ user: User;
+}
+
+export function ProfileGeneral({user}: ProfileGeneralProps) {
+ const router = useRouter();
+
+ const profileForm = useZodForm({
+ schema: ProfileSchema,
+ defaultValues: {
+ name: user.name || "",
+ role: user.role || "",
+ },
+ });
+
+ const {mutate: updateProfile, isPending: isUpdatingProfile} = useMutation({
+ mutationFn: async (values: ProfileSchemaType) => {
+ const result = await updateProfileSettingsAction({name: values.name});
+ const inner = result?.data;
+ if (inner?.success) {
+ toast.success("Profile updated successfully.");
+ router.refresh();
+ profileForm.reset({name: values.name, role: values.role});
+ } else {
+ toast.error("Failed to update profile.");
+ }
+ },
+ });
+
+ return (
+
+
+
Profile Settings
+
Manage your personal information and preferences.
+
+
+
+
+ );
+}
+
+function RoleBadge({role}: { role: string }) {
+
+ const variant = role === "admin" ? "default" : "secondary";
+
+ return (
+
+ {role}
+
+ );
+}
diff --git a/src/components/wrappers/dashboard/profile/profile-providers.tsx b/src/components/wrappers/dashboard/profile/profile-providers.tsx
new file mode 100644
index 00000000..b3601c67
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/profile-providers.tsx
@@ -0,0 +1,175 @@
+"use client";
+
+import React, {useState} from "react";
+import {Button} from "@/components/ui/button";
+import {Badge} from "@/components/ui/badge";
+import {Loader2, AlertTriangle} from "lucide-react";
+import {Account} from "@/db/schema/02_user";
+import {authClient} from "@/lib/auth/auth-client";
+import {useMutation} from "@tanstack/react-query";
+import {toast} from "sonner";
+import {useRouter} from "next/navigation";
+import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
+import {Alert, AlertDescription} from "@/components/ui/alert";
+import {SetPasswordProfileProviderModal} from "./modal/set-password-modal";
+import {SUPPORTED_PROVIDERS} from "../../../../../portabase.config";
+
+interface ProfileProviderProps {
+ accounts: Account[];
+}
+
+export function ProfileProviders({accounts}: ProfileProviderProps) {
+ const router = useRouter();
+ const totalConnected = accounts.length;
+
+ const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false);
+
+ const {mutate: unlinkAccount, isPending: isUnlinking} = useMutation({
+ mutationFn: async (providerId: string) => {
+ const {error} = await authClient.unlinkAccount({
+ providerId,
+ });
+ if (error) throw error;
+ },
+ onSuccess: () => {
+ toast.success("Provider successfully unlinked!");
+ router.refresh();
+ },
+ onError: () => {
+ toast.error("An error occurred while unlinking provider.");
+ },
+ });
+
+ const {mutate: linkAccount, isPending: isLinking} = useMutation({
+ mutationFn: async (providerId: string) => {
+ const {error} = await authClient.signIn.social({
+ provider: providerId as "google" | "github",
+ callbackURL: "/dashboard",
+ });
+ if (error) throw error;
+ },
+ onSuccess: () => {
+ toast.success("Provider successfully Linked!");
+ router.refresh();
+ },
+ onError: () => {
+ toast.error("An error occurred while linked provider.");
+ },
+ });
+
+ return (
+
+
+
Connected Accounts
+
Manage the providers used to sign in to your account.
+
+
+
+ {SUPPORTED_PROVIDERS.map((provider) => {
+ const linkedAccount = accounts.find((acc) => acc.providerId === provider.id);
+ const isConnected = !!linkedAccount;
+
+ const canUnlink = totalConnected > 1 || (totalConnected === 1 && !provider.isManual);
+ const isLoading = isUnlinking || isLinking;
+
+ return (
+
+
+
+
+
+ {PROVIDERS_TEXT[provider.id].title}
+ {isConnected && (
+
+ Active
+
+ )}
+
+
+ {isConnected ? "Connected" : "Not Connected"}
+
+
+
+
+
+ {isConnected ? (
+
+
+
+
+ unlinkAccount(provider.id)}
+ disabled={!canUnlink || isLoading || provider.isManual}
+ className={!canUnlink ? "opacity-50 cursor-not-allowed" : ""}
+ >
+ {isLoading ? : "Unlink"}
+
+
+
+ {!canUnlink && (
+
+
+ You cannot unlink your last authentication provider or if you
+ don't have a password set.
+
+
+ )}
+
+
+ ) : (
+ <>
+ {provider.id === "credential" ? (
+
+ ) : (
+ linkAccount(provider.id)}
+ disabled={isLoading || provider.isManual}
+ >
+ {isLoading ? : "Link"}
+
+ )}
+ >
+ )}
+
+
+ );
+ })}
+
+
+
+
+
+ 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.
+
+
+
+ );
+}
+
+
+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.",
+ },
+};
+
diff --git a/src/components/wrappers/dashboard/profile/profile-security.tsx b/src/components/wrappers/dashboard/profile/profile-security.tsx
new file mode 100644
index 00000000..68557d13
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/profile-security.tsx
@@ -0,0 +1,219 @@
+"use client";
+
+import {useState} from "react";
+import {Button} from "@/components/ui/button";
+import {Separator} from "@/components/ui/separator";
+import {Badge} from "@/components/ui/badge";
+import {Globe, LogOut, Loader2} from "lucide-react";
+import {Account, Session, User} from "@/db/schema/02_user";
+import {useMutation} from "@tanstack/react-query";
+import {toast} from "sonner";
+import {revokeAllSessionsAction, revokeSessionAction} from "./actions/security.action";
+import {useRouter} from "next/navigation";
+import {ResetPasswordProfileProviderModal} from "./modal/reset-password-modal";
+import {SetPasswordProfileProviderModal} from "./modal/set-password-modal";
+import {Setup2FAProfileProviderModal} from "./modal/setup-2fa-modal";
+import {Disable2FAProfileProviderModal} from "./modal/disable-2fa-modal";
+import {ViewBackupCodesModal} from "./modal/view-backup-codes-modal";
+import {getDeviceDetails} from "@/utils/detection";
+import {timeAgo} from "@/utils/date-formatting";
+
+interface ProfileSecurityProps {
+ user: User;
+ sessions: Session[];
+ credentialAccount: Account;
+ currentSession: Session;
+}
+
+export function ProfileSecurity({user, sessions, credentialAccount, currentSession}: ProfileSecurityProps) {
+ const router = useRouter();
+
+ const [isBackupCodesDialogOpen, setIsBackupCodesDialogOpen] = useState(false);
+ const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false);
+ const [isSetup2FADialogOpen, setIsSetup2FADialogOpen] = useState(false);
+ const [isDisable2FADialogOpen, setIsDisable2FADialogOpen] = useState(false);
+
+ const {mutate: revokeSession, isPending: isRevoking} = useMutation({
+ mutationFn: async (token: string) => {
+ const result = await revokeSessionAction({token});
+ const inner = result?.data;
+ if (inner?.success) {
+ toast.success("Session successfully revoked");
+ router.refresh();
+ } else {
+ toast.error("An error occurred while revoking session");
+ }
+ },
+ });
+
+ const {mutate: revokeOthers, isPending: isRevokingOthers} = useMutation({
+ mutationFn: async () => {
+ const result = await revokeAllSessionsAction();
+ const inner = result?.data;
+ if (inner?.success) {
+ toast.success("Revoking all sessions successfully done.");
+ router.refresh();
+ } else {
+ toast.error("An error occurred while revoking all sessions");
+ }
+ },
+ });
+
+ return (
+
+
+
Security Settings
+
Manage your password, two-factor authentication and
+ sessions.
+
+
+
+
Authentication
+
+
+
+
Password
+
+ {/*{user.lastChangedPasswordAt*/}
+ {/* ? t("sections.authentication.password.description.last_changed", {*/}
+ {/* date: timeAgo(new Date(user.lastChangedPasswordAt), locale),*/}
+ {/* })*/}
+ {/* : "Never changed"}*/}
+
+
+ {credentialAccount ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
Two-Factor Authentication
+ {user.twoFactorEnabled && (
+
+ Active
+
+ )}
+
+
Enhance the security of your account by
+ requiring a second form of verification during login.
+
+
+
+ {user.twoFactorEnabled ? (
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
Active Sessions
+ {sessions && sessions.length > 1 && (
+ revokeOthers()}
+ disabled={isRevokingOthers || (sessions?.length || 0) <= 1}
+ >
+ {isRevokingOthers && }
+ Revoke All
+
+ )}
+
+
+ {sessions && sessions.length > 0 ? (
+ sessions?.map((session) => (
+
revokeSession(token)}
+ isRevoking={isRevoking}
+ currentSession={currentSession}
+ />
+ ))
+ ) : (
+ No active sessions found.
+ )}
+
+
+
+ );
+}
+
+function SessionRow({
+ session,
+ onRevoke,
+ isRevoking,
+ currentSession,
+ }: {
+ session: Session;
+ onRevoke: (token: string) => void;
+ isRevoking: boolean;
+ currentSession: Session;
+}) {
+ const deviceInfo = getDeviceDetails(session.userAgent);
+
+ return (
+
+
+
+
+
+
+
+ {deviceInfo.os} • {deviceInfo.browser}
+ {session.id === currentSession.id && (
+
+ This device
+
+ )}
+
+
+ {session.ipAddress} •
+
+ {session.id === currentSession.id
+ ? "Active now"
+ : `Last active ${timeAgo(new Date(session.createdAt))}`}
+
+
+
+
+
+ {session.id !== currentSession.id && (
+
onRevoke(session.token)}
+ disabled={isRevoking}
+ >
+ {isRevoking ? : }
+ Revoke
+
+ )}
+
+ );
+}
diff --git a/src/components/wrappers/dashboard/profile/schemas/account.schema.ts b/src/components/wrappers/dashboard/profile/schemas/account.schema.ts
new file mode 100644
index 00000000..7c147e6a
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/schemas/account.schema.ts
@@ -0,0 +1,8 @@
+import z from "zod";
+import {zEmail} from "@/lib/zod";
+
+export const EmailSchema = z.object({
+ email: zEmail(),
+});
+
+export type EmailSchemaType = z.infer;
diff --git a/src/components/wrappers/dashboard/profile/schemas/general.schema.ts b/src/components/wrappers/dashboard/profile/schemas/general.schema.ts
new file mode 100644
index 00000000..0689e229
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/schemas/general.schema.ts
@@ -0,0 +1,9 @@
+import z from "zod";
+import {zString} from "@/lib/zod";
+
+export const ProfileSchema = z.object({
+ name: zString().nonempty(),
+ role: zString().nonempty(),
+});
+
+export type ProfileSchemaType = z.infer;
diff --git a/src/components/wrappers/dashboard/profile/schemas/provider.schema.ts b/src/components/wrappers/dashboard/profile/schemas/provider.schema.ts
new file mode 100644
index 00000000..4501a3fe
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/schemas/provider.schema.ts
@@ -0,0 +1,11 @@
+"use client";
+
+import { zPassword } from "@/lib/zod";
+import z from "zod";
+
+export const PasswordProviderSchema = z.object({
+ password: zPassword(),
+ confirmPassword: zPassword(),
+});
+
+export type PasswordProviderSchemaType = z.infer;
diff --git a/src/components/wrappers/dashboard/profile/schemas/security.schema.ts b/src/components/wrappers/dashboard/profile/schemas/security.schema.ts
new file mode 100644
index 00000000..d5950853
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile/schemas/security.schema.ts
@@ -0,0 +1,28 @@
+"use client";
+
+import z from "zod";
+import {zPassword} from "@/lib/zod";
+
+export const ResetPasswordSecuritySchema = z
+ .object({
+ currentPassword: zPassword(),
+ newPassword: zPassword(),
+ confirmPassword: zPassword(),
+ })
+ .superRefine(({ confirmPassword, newPassword }, ctx) => {
+ if (confirmPassword !== newPassword) {
+ ctx.addIssue({
+ code: "custom",
+ message: "New password does not match",
+ path: ["confirmPassword"],
+ });
+ }
+ });
+
+export type ResetPasswordSecuritySchemaType = z.infer;
+
+export const Setup2FASecuritySchema = z.object({
+ code: z.string().min(6, "Code need to contain at least 6 characters"),
+});
+
+export type Setup2FASecuritySchemaType = z.infer;
diff --git a/src/components/wrappers/dashboard/profile/avatar/avatar-with-upload.tsx b/src/components/wrappers/dashboard/profile2/avatar/avatar-with-upload.tsx
similarity index 98%
rename from src/components/wrappers/dashboard/profile/avatar/avatar-with-upload.tsx
rename to src/components/wrappers/dashboard/profile2/avatar/avatar-with-upload.tsx
index 6ed9b638..2acf76b6 100644
--- a/src/components/wrappers/dashboard/profile/avatar/avatar-with-upload.tsx
+++ b/src/components/wrappers/dashboard/profile2/avatar/avatar-with-upload.tsx
@@ -4,7 +4,7 @@ import { UploadIcon } from "lucide-react";
import { toast } from "sonner";
import { uploadImageAction } from "@/features/upload/public/upload.action";
import { useMutation } from "@tanstack/react-query";
-import { updateImageUserAction } from "@/components/wrappers/dashboard/profile/avatar/avatar.action";
+import { updateImageUserAction } from "@/components/wrappers/dashboard/profile2/avatar/avatar.action";
import { useRouter } from "next/navigation";
import { User } from "@/db/schema/02_user";
import {ChangeEvent} from "react";
diff --git a/src/components/wrappers/dashboard/profile2/avatar/avatar.action.ts b/src/components/wrappers/dashboard/profile2/avatar/avatar.action.ts
new file mode 100644
index 00000000..d44cc2a2
--- /dev/null
+++ b/src/components/wrappers/dashboard/profile2/avatar/avatar.action.ts
@@ -0,0 +1,15 @@
+"use server";
+import { db } from "@/db";
+import {userAction} from "@/lib/safe-actions/actions";
+import { eq } from "drizzle-orm";
+import { z } from "zod";
+import * as drizzleDb from "@/db";
+
+
+export const updateImageUserAction = userAction.schema(z.string()).action(async ({ parsedInput, ctx }) => {
+ const [updatedUser] = await db.update(drizzleDb.schemas.user).set({ image: parsedInput }).where(eq(drizzleDb.schemas.user.id, ctx.user.id)).returning();
+
+ return {
+ data: updatedUser,
+ };
+});
diff --git a/src/components/wrappers/dashboard/profile/button-delete-account/button-delete-account.tsx b/src/components/wrappers/dashboard/profile2/button-delete-account/button-delete-account.tsx
similarity index 100%
rename from src/components/wrappers/dashboard/profile/button-delete-account/button-delete-account.tsx
rename to src/components/wrappers/dashboard/profile2/button-delete-account/button-delete-account.tsx
diff --git a/src/components/wrappers/dashboard/profile/button-delete-account/delete-account.action.ts b/src/components/wrappers/dashboard/profile2/button-delete-account/delete-account.action.ts
similarity index 100%
rename from src/components/wrappers/dashboard/profile/button-delete-account/delete-account.action.ts
rename to src/components/wrappers/dashboard/profile2/button-delete-account/delete-account.action.ts
diff --git a/src/components/wrappers/dashboard/profile/user-avatar/user-avatar.tsx b/src/components/wrappers/dashboard/profile2/user-avatar/user-avatar.tsx
similarity index 100%
rename from src/components/wrappers/dashboard/profile/user-avatar/user-avatar.tsx
rename to src/components/wrappers/dashboard/profile2/user-avatar/user-avatar.tsx
diff --git a/src/components/wrappers/dashboard/profile/user-form/user-form.action.ts b/src/components/wrappers/dashboard/profile2/user-form/user-form.action.ts
similarity index 97%
rename from src/components/wrappers/dashboard/profile/user-form/user-form.action.ts
rename to src/components/wrappers/dashboard/profile2/user-form/user-form.action.ts
index 1efaad26..ee1da954 100644
--- a/src/components/wrappers/dashboard/profile/user-form/user-form.action.ts
+++ b/src/components/wrappers/dashboard/profile2/user-form/user-form.action.ts
@@ -1,7 +1,7 @@
"use server";
import {userAction} from "@/lib/safe-actions/actions";
import { z } from "zod";
-import { UserSchema } from "@/components/wrappers/dashboard/profile/user-form/user-form.schema";
+import { UserSchema } from "@/components/wrappers/dashboard/profile2/user-form/user-form.schema";
import { db } from "@/db";
import { eq } from "drizzle-orm";
import * as drizzleDb from "@/db";
diff --git a/src/components/wrappers/dashboard/profile/user-form/user-form.schema.ts b/src/components/wrappers/dashboard/profile2/user-form/user-form.schema.ts
similarity index 100%
rename from src/components/wrappers/dashboard/profile/user-form/user-form.schema.ts
rename to src/components/wrappers/dashboard/profile2/user-form/user-form.schema.ts
diff --git a/src/components/wrappers/dashboard/profile/user-form/user-form.tsx b/src/components/wrappers/dashboard/profile2/user-form/user-form.tsx
similarity index 98%
rename from src/components/wrappers/dashboard/profile/user-form/user-form.tsx
rename to src/components/wrappers/dashboard/profile2/user-form/user-form.tsx
index c860e3c5..9b09a038 100644
--- a/src/components/wrappers/dashboard/profile/user-form/user-form.tsx
+++ b/src/components/wrappers/dashboard/profile2/user-form/user-form.tsx
@@ -8,9 +8,9 @@ import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
import { useMutation } from "@tanstack/react-query";
import { TooltipProvider } from "@/components/ui/tooltip";
-import { UserSchema, UserType } from "@/components/wrappers/dashboard/profile/user-form/user-form.schema";
+import { UserSchema, UserType } from "@/components/wrappers/dashboard/profile2/user-form/user-form.schema";
import { toast } from "sonner";
-import { updateUserAction } from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
+import { updateUserAction } from "@/components/wrappers/dashboard/profile2/user-form/user-form.action";
import {DataTable} from "@/components/wrappers/common/table/data-table";
import {sessionsColumns} from "@/components/wrappers/dashboard/admin/users/sessions/table-columns";
import {accountsColumns} from "@/components/wrappers/dashboard/admin/users/accounts/table-columns";
diff --git a/src/db/migrations/0013_past_logan.sql b/src/db/migrations/0013_past_logan.sql
new file mode 100644
index 00000000..656bb0b8
--- /dev/null
+++ b/src/db/migrations/0013_past_logan.sql
@@ -0,0 +1,2 @@
+ALTER TABLE "user" ADD COLUMN "lastChangedPasswordAt" timestamp;--> statement-breakpoint
+ALTER TABLE "user" ADD COLUMN "two_factor_enabled" boolean DEFAULT false;
\ No newline at end of file
diff --git a/src/db/migrations/meta/0013_snapshot.json b/src/db/migrations/meta/0013_snapshot.json
new file mode 100644
index 00000000..f6b7015b
--- /dev/null
+++ b/src/db/migrations/meta/0013_snapshot.json
@@ -0,0 +1,1745 @@
+{
+ "id": "111a7060-826c-4df7-bc2a-320b6e477ac1",
+ "prevId": "05a7de9f-f36d-47d1-b66c-0ef01d476b44",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.settings": {
+ "name": "settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "storage": {
+ "name": "storage",
+ "type": "type_storage",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'local'"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "s3_endpoint_url": {
+ "name": "s3_endpoint_url",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "s3_access_key_id": {
+ "name": "s3_access_key_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "s3_secret_access_key": {
+ "name": "s3_secret_access_key",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "s3_bucket_name": {
+ "name": "s3_bucket_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "smtp_password": {
+ "name": "smtp_password",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "smtp_from": {
+ "name": "smtp_from",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "smtp_host": {
+ "name": "smtp_host",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "smtp_port": {
+ "name": "smtp_port",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "smtp_user": {
+ "name": "smtp_user",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "settings_name_unique": {
+ "name": "settings_name_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "name"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.account": {
+ "name": "account",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.session": {
+ "name": "session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "impersonated_by": {
+ "name": "impersonated_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "active_organization_id": {
+ "name": "active_organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "session_token_unique": {
+ "name": "session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user": {
+ "name": "user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "banned": {
+ "name": "banned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ban_reason": {
+ "name": "ban_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ban_expires": {
+ "name": "ban_expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lastChangedPasswordAt": {
+ "name": "lastChangedPasswordAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "two_factor_enabled": {
+ "name": "two_factor_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.verification": {
+ "name": "verification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organization": {
+ "name": "organization",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "logo": {
+ "name": "logo",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "organization_slug_unique": {
+ "name": "organization_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.member": {
+ "name": "member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "member_organization_id_organization_id_fk": {
+ "name": "member_organization_id_organization_id_fk",
+ "tableFrom": "member",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "member_user_id_user_id_fk": {
+ "name": "member_user_id_user_id_fk",
+ "tableFrom": "member",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.invitation": {
+ "name": "invitation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "inviter_id": {
+ "name": "inviter_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "invitation_organization_id_organization_id_fk": {
+ "name": "invitation_organization_id_organization_id_fk",
+ "tableFrom": "invitation",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "invitation_inviter_id_user_id_fk": {
+ "name": "invitation_inviter_id_user_id_fk",
+ "tableFrom": "invitation",
+ "tableTo": "user",
+ "columnsFrom": [
+ "inviter_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.projects": {
+ "name": "projects",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_archived": {
+ "name": "is_archived",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "projects_organization_id_organization_id_fk": {
+ "name": "projects_organization_id_organization_id_fk",
+ "tableFrom": "projects",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "projects_slug_unique": {
+ "name": "projects_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.backups": {
+ "name": "backups",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "status": {
+ "name": "status",
+ "type": "status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'waiting'"
+ },
+ "file": {
+ "name": "file",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "database_id": {
+ "name": "database_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "backups_database_id_databases_id_fk": {
+ "name": "backups_database_id_databases_id_fk",
+ "tableFrom": "backups",
+ "tableTo": "databases",
+ "columnsFrom": [
+ "database_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.databases": {
+ "name": "databases",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "agent_database_id": {
+ "name": "agent_database_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dbms": {
+ "name": "dbms",
+ "type": "dbms_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "backup_policy": {
+ "name": "backup_policy",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_waiting_for_backup": {
+ "name": "is_waiting_for_backup",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "backup_to_restore": {
+ "name": "backup_to_restore",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "agent_id": {
+ "name": "agent_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_contact": {
+ "name": "last_contact",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "databases_agent_id_agents_id_fk": {
+ "name": "databases_agent_id_agents_id_fk",
+ "tableFrom": "databases",
+ "tableTo": "agents",
+ "columnsFrom": [
+ "agent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "databases_project_id_projects_id_fk": {
+ "name": "databases_project_id_projects_id_fk",
+ "tableFrom": "databases",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.restorations": {
+ "name": "restorations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "status": {
+ "name": "status",
+ "type": "status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'waiting'"
+ },
+ "backup_id": {
+ "name": "backup_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "database_id": {
+ "name": "database_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "restorations_backup_id_backups_id_fk": {
+ "name": "restorations_backup_id_backups_id_fk",
+ "tableFrom": "restorations",
+ "tableTo": "backups",
+ "columnsFrom": [
+ "backup_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "restorations_database_id_databases_id_fk": {
+ "name": "restorations_database_id_databases_id_fk",
+ "tableFrom": "restorations",
+ "tableTo": "databases",
+ "columnsFrom": [
+ "database_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.retention_policies": {
+ "name": "retention_policies",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "database_id": {
+ "name": "database_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "retention_policy_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "count": {
+ "name": "count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 7
+ },
+ "days": {
+ "name": "days",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 30
+ },
+ "gfs_daily": {
+ "name": "gfs_daily",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 7
+ },
+ "gfs_weekly": {
+ "name": "gfs_weekly",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 4
+ },
+ "gfs_monthly": {
+ "name": "gfs_monthly",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 12
+ },
+ "gfs_yearly": {
+ "name": "gfs_yearly",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 3
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "retention_policies_database_id_databases_id_fk": {
+ "name": "retention_policies_database_id_databases_id_fk",
+ "tableFrom": "retention_policies",
+ "tableTo": "databases",
+ "columnsFrom": [
+ "database_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.agents": {
+ "name": "agents",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_archived": {
+ "name": "is_archived",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "last_contact": {
+ "name": "last_contact",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "agents_slug_unique": {
+ "name": "agents_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.notification_channel": {
+ "name": "notification_channel",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "provider": {
+ "name": "provider",
+ "type": "provider_kind",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organization_notification_channels": {
+ "name": "organization_notification_channels",
+ "schema": "",
+ "columns": {
+ "organization_id": {
+ "name": "organization_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "notification_channel_id": {
+ "name": "notification_channel_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "organization_notification_channels_organization_id_organization_id_fk": {
+ "name": "organization_notification_channels_organization_id_organization_id_fk",
+ "tableFrom": "organization_notification_channels",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "organization_notification_channels_notification_channel_id_notification_channel_id_fk": {
+ "name": "organization_notification_channels_notification_channel_id_notification_channel_id_fk",
+ "tableFrom": "organization_notification_channels",
+ "tableTo": "notification_channel",
+ "columnsFrom": [
+ "notification_channel_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "organization_notification_channels_organization_id_notification_channel_id_unique": {
+ "name": "organization_notification_channels_organization_id_notification_channel_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "organization_id",
+ "notification_channel_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.alert_policy": {
+ "name": "alert_policy",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "notification_channel_id": {
+ "name": "notification_channel_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_kind": {
+ "name": "event_kind",
+ "type": "event_kind[]",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "database_id": {
+ "name": "database_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "alert_policy_notification_channel_id_notification_channel_id_fk": {
+ "name": "alert_policy_notification_channel_id_notification_channel_id_fk",
+ "tableFrom": "alert_policy",
+ "tableTo": "notification_channel",
+ "columnsFrom": [
+ "notification_channel_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "alert_policy_database_id_databases_id_fk": {
+ "name": "alert_policy_database_id_databases_id_fk",
+ "tableFrom": "alert_policy",
+ "tableTo": "databases",
+ "columnsFrom": [
+ "database_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.notification_log": {
+ "name": "notification_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "channel_id": {
+ "name": "channel_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "policy_id": {
+ "name": "policy_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "event": {
+ "name": "event",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_name": {
+ "name": "provider_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "level": {
+ "name": "level",
+ "type": "level",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "success": {
+ "name": "success",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_response": {
+ "name": "provider_response",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sent_at": {
+ "name": "sent_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.retention_policy_type": {
+ "name": "retention_policy_type",
+ "schema": "public",
+ "values": [
+ "count",
+ "days",
+ "gfs"
+ ]
+ },
+ "public.provider_kind": {
+ "name": "provider_kind",
+ "schema": "public",
+ "values": [
+ "slack",
+ "smtp"
+ ]
+ },
+ "public.event_kind": {
+ "name": "event_kind",
+ "schema": "public",
+ "values": [
+ "error_backup",
+ "error_restore",
+ "success_restore",
+ "success_backup",
+ "weekly_report"
+ ]
+ },
+ "public.level": {
+ "name": "level",
+ "schema": "public",
+ "values": [
+ "critical",
+ "warning",
+ "info"
+ ]
+ },
+ "public.dbms_status": {
+ "name": "dbms_status",
+ "schema": "public",
+ "values": [
+ "postgresql",
+ "mysql"
+ ]
+ },
+ "public.status": {
+ "name": "status",
+ "schema": "public",
+ "values": [
+ "waiting",
+ "ongoing",
+ "failed",
+ "success"
+ ]
+ },
+ "public.type_storage": {
+ "name": "type_storage",
+ "schema": "public",
+ "values": [
+ "local",
+ "s3"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json
index 6511a8b6..8ecc167f 100644
--- a/src/db/migrations/meta/_journal.json
+++ b/src/db/migrations/meta/_journal.json
@@ -92,6 +92,13 @@
"when": 1766308683196,
"tag": "0012_peaceful_leopardon",
"breakpoints": true
+ },
+ {
+ "idx": 13,
+ "version": "7",
+ "when": 1766327760039,
+ "tag": "0013_past_logan",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/src/db/schema/02_user.ts b/src/db/schema/02_user.ts
index 23dedec1..66ff84ef 100644
--- a/src/db/schema/02_user.ts
+++ b/src/db/schema/02_user.ts
@@ -6,7 +6,7 @@ import {project} from "./06_project";
import {member, OrganizationMember} from "@/db/schema/04_member";
import {invitation} from "@/db/schema/05_invitation";
import {organization} from "@/db/schema/03_organization";
-import {Account} from "better-auth";
+import {Account as BetterAuthAccount} from "better-auth";
import {timestamps} from "@/db/schema/00_common";
export const user = pgTable("user", {
@@ -19,6 +19,8 @@ export const user = pgTable("user", {
banned: boolean("banned"),
banReason: text("ban_reason"),
banExpires: timestamp("ban_expires"),
+ lastChangedPasswordAt: timestamp(),
+ twoFactorEnabled: boolean("two_factor_enabled").default(false),
...timestamps
});
@@ -95,7 +97,14 @@ export const projectRelations = relations(project, ({one}) => ({
export const userSchema = createSelectSchema(user);
export type User = z.infer;
-type FixedAccount = Omit & {
+export const sessionSchema = createSelectSchema(session);
+export type Session = z.infer;
+
+export const accountSchema = createSelectSchema(account);
+export type Account = z.infer;
+
+
+type FixedAccount = Omit & {
updatedAt: Date | null;
};
diff --git a/src/features/layout/Header.tsx b/src/features/layout/Header.tsx
index dca1a487..deebb3f4 100644
--- a/src/features/layout/Header.tsx
+++ b/src/features/layout/Header.tsx
@@ -3,10 +3,9 @@ import {notFound} from "next/navigation";
import {SidebarTrigger} from "@/components/ui/sidebar";
import {ModeToggle} from "@/features/theme/ModeToggle";
import {currentUser} from "@/lib/auth/current-user";
-import {LoggedInButton} from "@/components/wrappers/dashboard/common/logged-in/logged-in-button";
import {BreadCrumbsWrapper} from "@/components/wrappers/common/bread-crumbs/bread-crumbs";
import GitHubStarsButtonCustom from "@/components/wrappers/common/github/github-button";
-// import GitHubStarsButtonCustom from "@/components/wrappers/common/github-button";
+import {LoggedInButton} from "@/components/wrappers/dashboard/common/logged-in/logged-in-button";
export const Header = async () => {
const user = await currentUser();
diff --git a/src/lib/auth/auth-client.ts b/src/lib/auth/auth-client.ts
index 7390f80c..2fe12af2 100644
--- a/src/lib/auth/auth-client.ts
+++ b/src/lib/auth/auth-client.ts
@@ -1,7 +1,7 @@
"use client"
import {createAuthClient} from "better-auth/react";
-import {adminClient, inferAdditionalFields, organizationClient} 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 {auth} from "@/lib/auth/auth";
import {getServerUrl} from "@/utils/get-server-url";
@@ -12,6 +12,7 @@ const {PROJECT_URL} = await res.json();
export const authClient = createAuthClient({
baseURL: PROJECT_URL,
plugins: [
+ twoFactorClient(),
organizationClient({
ac,
roles: {
@@ -34,4 +35,4 @@ export const authClient = createAuthClient({
});
-export const {signIn, signOut, signUp, useSession, listAccounts, admin} = authClient;
+export const {signIn, signOut, signUp, useSession, listAccounts, admin, requestPasswordReset} = authClient;
diff --git a/src/lib/auth/auth.ts b/src/lib/auth/auth.ts
index 82e56550..4198ba10 100644
--- a/src/lib/auth/auth.ts
+++ b/src/lib/auth/auth.ts
@@ -4,7 +4,7 @@ import * as drizzleDb from "@/db";
import {db} from "@/db";
import {env} from "@/env.mjs";
import {nextCookies} from "better-auth/next-js";
-import {admin as adminPlugin, openAPI, Organization, organization} from "better-auth/plugins";
+import {admin as adminPlugin, openAPI, Organization, organization, twoFactor} from "better-auth/plugins";
import {ac, admin, orgAdmin, orgMember, orgOwner, pending, superadmin, user} from "@/lib/auth/permissions";
import {headers} from "next/headers";
import {count, eq} from "drizzle-orm";
@@ -12,6 +12,9 @@ import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_or
import {sendEmail} from "@/lib/email/email-helper";
import {render} from "@react-email/render";
import EmailResetPassword from "@/components/emails/email-reset-password";
+import {SUPPORTED_PROVIDERS} from "../../../portabase.config";
+import {withUpdatedAt} from "@/db/utils";
+import EmailVerification from "@/components/emails/email-verification";
export const auth = betterAuth({
database: drizzleAdapter(db, {
@@ -23,34 +26,73 @@ export const auth = betterAuth({
enabled: true,
requireEmailVerification: false,
sendResetPassword: async ({user, url, token}, request) => {
+
+ const [updatedUser] = await db.update(drizzleDb.schemas.user).set(withUpdatedAt({
+ emailVerified: true,
+ })).where(eq(drizzleDb.schemas.user.id, user.id)).returning();
+
await sendEmail({
to: user.email,
subject: "Reset your password",
html: await render(EmailResetPassword({url: url})),
});
+
},
onPasswordReset: async ({user}, request) => {
console.log(`Password for user ${user.email} has been reset.`);
},
-
- /*
- async sendVerificationEmail(data, request) {
- // Send an email to the user with a link to verify their email
- },
- async verifyEmail(data, request) {
- // Verify the email address
- },*/
-
},
- socialProviders: {
- google: {
- clientId: env.AUTH_GOOGLE_ID!,
- clientSecret: env.AUTH_GOOGLE_SECRET!,
+ emailVerification: {
+ async sendVerificationEmail({user, token, url}) {
+
+
+
+ await sendEmail({
+ to: user.email,
+ subject: "Portabase Email Verification",
+ html: await render(EmailVerification({
+ firstname: user.name,
+ url: url
+ })),
+ });
+
+ await (
+ await auth.$context
+ ).internalAdapter.updateUser(user.id, {
+ emailVerified: false,
+ });
+ },
+ async afterEmailVerification(user) {
+ await (
+ await auth.$context
+ ).internalAdapter.updateUser(user.id, {
+ emailVerified: true,
+ });
},
},
+
+
+ socialProviders: SUPPORTED_PROVIDERS.reduce((acc: any, provider: any) => {
+ if (provider.id === "credential") return acc;
+ if (provider.id === "google") {
+ acc.google = {
+ clientId: env.AUTH_GOOGLE_ID! as string,
+ clientSecret: env.AUTH_GOOGLE_SECRET! as string,
+ };
+ }
+ return acc;
+ }, {}),
+ account: {
+ accountLinking: {
+ enabled: true,
+ },
+ },
+
+
plugins: [
openAPI(),
nextCookies(),
+ twoFactor(),
organization({
ac,
roles: {
@@ -80,6 +122,9 @@ export const auth = betterAuth({
deleteUser: {
enabled: true,
},
+ changeEmail: {
+ enabled: true,
+ },
additionalFields: {
deletedAt: {
type: "number",
diff --git a/src/lib/zod.ts b/src/lib/zod.ts
new file mode 100644
index 00000000..151903f0
--- /dev/null
+++ b/src/lib/zod.ts
@@ -0,0 +1,24 @@
+import { z } from "zod";
+
+export const zString = () =>
+ z.string();
+
+export const zEnum = (values: T) => z.enum(values, { message: "Field required" });
+
+export const zEmail = () => z.string().email({ message: "Invalid email" });
+
+const passwordRegex = /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-])/;
+
+export const zPassword = () => zString().min(8, { message: "New Password too short" }).regex(passwordRegex, { message: "New password too weak" });
+
+export const zDate = () =>
+ z.preprocess(
+ (arg) => {
+ if (typeof arg === "string" || arg instanceof Date) {
+ const date = new Date(arg);
+ return isNaN(date.getTime()) ? undefined : date;
+ }
+ return undefined;
+ },
+ z.date()
+ );
diff --git a/src/types/auth.ts b/src/types/auth.ts
new file mode 100644
index 00000000..bc6ea81c
--- /dev/null
+++ b/src/types/auth.ts
@@ -0,0 +1,17 @@
+
+export type SignUpUser = {
+ name: string
+ email: string
+ password: string
+ callbackURL?: string
+ theme: string
+ isDefaultPassword: boolean
+}
+
+
+export type BetterAuthError = {
+ code?: string;
+ message?: string;
+ status: number;
+ statusText: string;
+};
\ No newline at end of file
diff --git a/src/utils/date-formatting.ts b/src/utils/date-formatting.ts
index 536d30ab..ca8922c1 100644
--- a/src/utils/date-formatting.ts
+++ b/src/utils/date-formatting.ts
@@ -1,4 +1,5 @@
import {formatDistanceToNow} from "date-fns";
+import {format} from "date-fns";
/**
* Get user's locale and timezone from the browser
@@ -36,3 +37,4 @@ export function formatDateLastContact(lastContact: string | number | Date | null
? formatLocalizedDate(lastContact)
: "Never connected.";
}
+
diff --git a/src/utils/detection.ts b/src/utils/detection.ts
new file mode 100644
index 00000000..8db1f96a
--- /dev/null
+++ b/src/utils/detection.ts
@@ -0,0 +1,37 @@
+import { Smartphone, Laptop, type LucideIcon } from "lucide-react";
+
+interface DeviceDetails {
+ os: string;
+ browser: string;
+ isMobile: boolean;
+ Icon: LucideIcon;
+ deviceType: "Mobile" | "Desktop";
+}
+
+export const getDeviceDetails = (userAgent: string | undefined | null): DeviceDetails => {
+ const ua = (userAgent || "").toLowerCase();
+
+ const isMobile = /mobile|iphone|android/.test(ua);
+
+ const os = /mac os/.test(ua)
+ ? "macOS"
+ : /windows/.test(ua)
+ ? "Windows"
+ : /linux/.test(ua)
+ ? "Linux"
+ : /android/.test(ua)
+ ? "Android"
+ : /ios/.test(ua)
+ ? "iOS"
+ : "Unknown OS";
+
+ const browser = /chrome/.test(ua) ? "Chrome" : /firefox/.test(ua) ? "Firefox" : /safari/.test(ua) ? "Safari" : /edg/.test(ua) ? "Edge" : "Browser";
+
+ return {
+ os,
+ browser,
+ isMobile,
+ deviceType: isMobile ? "Mobile" : "Desktop",
+ Icon: isMobile ? Smartphone : Laptop,
+ };
+};
diff --git a/yarn.lock b/yarn.lock
index ba373d47..c381fe7f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11869,6 +11869,7 @@ __metadata:
react-dropzone: "npm:^14.3.8"
react-email: "npm:^4.0.13"
react-hook-form: "npm:^7.56.3"
+ react-qr-code: "npm:^2.0.18"
react-resizable-panels: "npm:^3.0.2"
react-twc: "npm:^1.4.2"
react-use-measure: "npm:^2.1.7"
@@ -12147,6 +12148,13 @@ __metadata:
languageName: node
linkType: hard
+"qr.js@npm:0.0.0":
+ version: 0.0.0
+ resolution: "qr.js@npm:0.0.0"
+ checksum: 10c0/1c6a4c7a58d04e52ec2fee99e39b680fdc5b2a510a981df42c36b716a8eac6634d130fc4d65af8f030f2a07dbf5fa046b97cdfa7456c250ebb50a73916efdcb5
+ languageName: node
+ linkType: hard
+
"query-string@npm:^7.1.3":
version: 7.1.3
resolution: "query-string@npm:7.1.3"
@@ -12291,6 +12299,18 @@ __metadata:
languageName: node
linkType: hard
+"react-qr-code@npm:^2.0.18":
+ version: 2.0.18
+ resolution: "react-qr-code@npm:2.0.18"
+ dependencies:
+ prop-types: "npm:^15.8.1"
+ qr.js: "npm:0.0.0"
+ peerDependencies:
+ react: "*"
+ checksum: 10c0/4e13b795cbb10f1dcf0e39d682bb59851e4c84010ba2be7225b2ad9d5c1ffea52d2d38f884ee26235b7002b8ca99e83b805f55e877663c39d67496764d975cf1
+ languageName: node
+ linkType: hard
+
"react-remove-scroll-bar@npm:^2.3.7":
version: 2.3.8
resolution: "react-remove-scroll-bar@npm:2.3.8"