Merge branch 'dev' into feat/oidc

This commit is contained in:
Théo LAGACHE
2026-02-24 16:49:16 +01:00
35 changed files with 1191 additions and 3598 deletions
+10 -4
View File
@@ -6,7 +6,7 @@ import {env} from "@/env.mjs";
interface EmailCreateUserProps {
email: string;
password: string;
password?: string;
}
export const EmailCreateUser = ({email, password}: EmailCreateUserProps) => {
@@ -23,9 +23,15 @@ export const EmailCreateUser = ({email, password}: EmailCreateUserProps) => {
<strong>Email: </strong>{email}
</Text>
<Text className="text-[14px] text-black leading-[24px]">
<strong>Default password: </strong>{password}
</Text>
{password ? (
<Text className="text-[14px] text-black leading-[24px]">
<strong>Default password: </strong>{password}
</Text>
) : (
<Text className="text-[14px] text-black leading-[24px]">
You can log in using one of the single sign-on (SSO) providers configured by your administrator.
</Text>
)}
<Section className="mt-[32px] mb-[32px] text-center">
<Button
@@ -39,7 +39,8 @@ export function SocialAuthButtons({ providers }: { providers: AuthProviderConfig
});
} else {
result = await authClient.signIn.social({
provider: provider.id as "google" | "github",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
provider: provider.id as any,
callbackURL: "/dashboard",
});
}
@@ -5,8 +5,9 @@ import {User} from "@/db/schema/02_user";
type AdminUserListProps = {
users: User[];
isPasswordAuthEnabled: boolean;
};
export const AdminUserList = ({ users }: AdminUserListProps) => {
return <DataTable columns={usersListColumns()} data={users} enablePagination={true} enableSelect={false} />;
export const AdminUserList = ({ users, isPasswordAuthEnabled }: AdminUserListProps) => {
return <DataTable columns={usersListColumns({ isPasswordAuthEnabled })} data={users} enablePagination={true} enableSelect={false} />;
};
@@ -9,7 +9,11 @@ import {Info} from "lucide-react";
import {Table, TableBody, TableCell, TableRow} from "@/components/ui/table";
import {UserActionsCell} from "@/components/wrappers/dashboard/admin/users/user-actions-cell";
export function usersListColumns(): ColumnDef<User>[] {
type UsersListColumnsProps = {
isPasswordAuthEnabled: boolean;
}
export function usersListColumns({ isPasswordAuthEnabled }: UsersListColumnsProps): ColumnDef<User>[] {
return [
{
@@ -85,7 +89,7 @@ export function usersListColumns(): ColumnDef<User>[] {
{
id: "actions",
header: "Actions",
cell: ({row}) => <UserActionsCell user={row.original}/>,
cell: ({row}) => <UserActionsCell user={row.original} isPasswordAuthEnabled={isPasswordAuthEnabled} />,
},
];
}
@@ -20,9 +20,10 @@ import {AdminDeleteUserModal} from "@/components/wrappers/dashboard/admin/users/
interface UserActionsCellProps {
user: User;
isPasswordAuthEnabled: boolean;
}
export function UserActionsCell({user}: UserActionsCellProps) {
export function UserActionsCell({user, isPasswordAuthEnabled}: UserActionsCellProps) {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isModalChangePasswordOpen, setIsModalChangePasswordOpen] = useState(false);
@@ -43,9 +44,11 @@ export function UserActionsCell({user}: UserActionsCellProps) {
onOpenChange={setIsModalChangePasswordOpen}/>
<AdminUserEdit user={user} open={isModalEditUserOpen} onOpenChange={setIsModalEditUserOpen}/>
<div className={cn("flex items-center space-x-2")}>
<Button variant="outline" size="icon" onClick={() => setIsModalChangePasswordOpen(true)}>
<RotateCcwKey className="w-4 h-4"/>
</Button>
{isPasswordAuthEnabled && (
<Button variant="outline" size="icon" onClick={() => setIsModalChangePasswordOpen(true)}>
<RotateCcwKey className="w-4 h-4"/>
</Button>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
@@ -1,191 +1,211 @@
"use server";
import * as drizzleDb from "@/db";
import {ServerActionResult} from "@/types/action-type";
import {render} from "@react-email/render";
import {UserSchema} from "@/components/wrappers/dashboard/admin/users/user.schema";
import {extractNameFromEmail} from "@/utils/name-from-email";
import {generateValidPassword} from "@/utils/password";
import {auth} from "@/lib/auth/auth";
import {z} from "zod";
import {Organization} from "@/db/schema/03_organization";
import {db} from "@/db";
import {and, eq} from "drizzle-orm";
import { ServerActionResult } from "@/types/action-type";
import { render } from "@react-email/render";
import { UserSchema } from "@/components/wrappers/dashboard/admin/users/user.schema";
import { extractNameFromEmail } from "@/utils/name-from-email";
import { generateValidPassword } from "@/utils/password";
import { auth } from "@/lib/auth/auth";
import { z } from "zod";
import { Organization } from "@/db/schema/03_organization";
import { db } from "@/db";
import { and, eq } from "drizzle-orm";
import {zEmail, zString} from "@/lib/zod";
import {withUpdatedAt} from "@/db/utils";
import {userAction} from "@/lib/safe-actions/actions";
import {
addMemberOrganizationAction
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/add-member.action";
import {sendEmail} from "@/lib/email";
import { zEmail, zString } from "@/lib/zod";
import { withUpdatedAt } from "@/db/utils";
import { userAction } from "@/lib/safe-actions/actions";
import { addMemberOrganizationAction } from "@/components/wrappers/dashboard/admin/organizations/organization/details/add-member.action";
import { sendEmail } from "@/lib/email";
import EmailCreateUser from "@/components/emails/email-create-user";
import {SignUpUser} from "@/types/auth";
import {createUserDb} from "@/db/services/user";
import {User} from "@/db/schema/02_user";
import { SignUpUser } from "@/types/auth";
import { createUserDb } from "@/db/services/user";
import { User } from "@/db/schema/02_user";
import { env } from "@/env.mjs";
export const createUserAction = userAction.schema(UserSchema).action(async ({parsedInput}): Promise<ServerActionResult<User>> => {
export const createUserAction = userAction
.schema(UserSchema)
.action(async ({ parsedInput }): Promise<ServerActionResult<User>> => {
try {
const password = generateValidPassword();
const isPasswordAuthEnabled = env.AUTH_EMAIL_PASSWORD_ENABLED === "true";
let password;
const userData: SignUpUser = {
name: parsedInput.name || extractNameFromEmail(parsedInput.email),
email: parsedInput.email,
password: password,
theme: "dark",
role: "user",
};
const userData: SignUpUser = {
name: parsedInput.name || extractNameFromEmail(parsedInput.email),
email: parsedInput.email,
theme: "dark",
role: "user",
};
const newUser = await createUserDb(userData);
if (isPasswordAuthEnabled) {
password = generateValidPassword();
userData.password = password;
}
if (newUser) {
const newUser = await createUserDb(userData);
await sendEmail({
to: parsedInput.email,
subject: "Your account is created",
html: await render(EmailCreateUser({
password: password,
email: parsedInput.email,
})),
});
if (newUser) {
await sendEmail({
to: parsedInput.email,
subject: "Your account is created",
html: await render(
EmailCreateUser({
password: password,
email: parsedInput.email,
}),
),
});
const defaultOrganization = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, "default"),
});
const defaultOrganization = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, "default"),
});
if (defaultOrganization) {
await auth.api.addMember({
body: {
userId: newUser.id,
organizationId: defaultOrganization.id,
role: "admin",
},
});
}
return {
success: true,
value: newUser,
actionSuccess: {
message: "user_created",
},
};
if (defaultOrganization) {
await auth.api.addMember({
body: {
userId: newUser.id,
organizationId: defaultOrganization.id,
role: "admin",
},
});
}
return {
success: false,
actionError: {
message: "user_created",
cause: "Unknown error",
},
success: true,
value: newUser,
actionSuccess: {
message: "user_created",
},
};
}
return {
success: false,
actionError: {
message: "user_created",
cause: "Unknown error",
},
};
} catch (error) {
return {
success: false,
actionError: {
message: "user_created",
cause: error instanceof Error ? error.message : "Unknown error",
},
};
return {
success: false,
actionError: {
message: "user_created",
cause: error instanceof Error ? error.message : "Unknown error",
},
};
}
});
});
export const updateUserAction = userAction
.schema(
z.object({
id: zString(),
name: zString().optional(),
email: zEmail(),
})
)
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<{}>> => {
try {
.schema(
z.object({
id: zString(),
name: zString().optional(),
email: zEmail(),
}),
)
.action(async ({ parsedInput, ctx }): Promise<ServerActionResult<{}>> => {
try {
const [updatedUser] = await db
.update(drizzleDb.schemas.user)
.set(
withUpdatedAt({
name: parsedInput.name
? parsedInput.name
: extractNameFromEmail(parsedInput.email),
email: parsedInput.email,
emailVerified: false,
}),
)
.where(eq(drizzleDb.schemas.user.id, parsedInput.id))
.returning();
if (updatedUser) {
return {
success: true,
actionSuccess: {
message: "user_updated",
},
};
}
const [updatedUser] = await db.update(drizzleDb.schemas.user).set(withUpdatedAt({
name: parsedInput.name ? parsedInput.name : extractNameFromEmail(parsedInput.email),
email: parsedInput.email,
emailVerified: false
})).where(eq(drizzleDb.schemas.user.id, parsedInput.id)).returning();
if (updatedUser) {
return {
success: true,
actionSuccess: {
message: "user_updated",
},
};
}
return {
success: false,
actionError: {
message: "user_updated",
cause: "Unknown error",
},
};
} catch (error) {
return {
success: false,
actionError: {
message: "user_updated",
cause: error instanceof Error ? error.message : "Unknown error",
},
};
}
});
return {
success: false,
actionError: {
message: "user_updated",
cause: "Unknown error",
},
};
} catch (error) {
return {
success: false,
actionError: {
message: "user_updated",
cause: error instanceof Error ? error.message : "Unknown error",
},
};
}
});
export const setSuperAdminOwnerOfOrganizationsOwnedByUser = userAction
.schema(
z.object({
userId: z.string(),
})
)
.action(async ({parsedInput}): Promise<ServerActionResult<Organization[]>> => {
try {
const organizationsWhereUserIsMemberAndOwner = await db.query.member.findMany({
where: and(eq(drizzleDb.schemas.member.role, "owner"), eq(drizzleDb.schemas.member.userId, parsedInput.userId)),
with: {
organization: true,
},
});
.schema(
z.object({
userId: z.string(),
}),
)
.action(
async ({ parsedInput }): Promise<ServerActionResult<Organization[]>> => {
try {
const organizationsWhereUserIsMemberAndOwner =
await db.query.member.findMany({
where: and(
eq(drizzleDb.schemas.member.role, "owner"),
eq(drizzleDb.schemas.member.userId, parsedInput.userId),
),
with: {
organization: true,
},
});
const superAdminUser = await db.query.user.findFirst();
if (!superAdminUser) {
return {
success: false,
actionError: {
message: "set_super_admin_owner_of_organizations_owned_by_user",
cause: "Unknown error",
},
};
}
for (let {organization} of organizationsWhereUserIsMemberAndOwner) {
await addMemberOrganizationAction({
userId: superAdminUser.id,
organizationId: organization.id,
role: "owner",
});
}
const organizations = organizationsWhereUserIsMemberAndOwner.map(
(organizationWhereUserIsMemberAndOwner) => organizationWhereUserIsMemberAndOwner.organization
);
return {
success: true,
value: organizations as unknown as Organization[],
actionSuccess: {
message: "set_super_admin_owner_of_organizations_owned_by_user",
},
};
} catch (error) {
return {
success: false,
actionError: {
message: "error_set_super_admin_owner_of_organizations_owned_by_user",
cause: error instanceof Error ? error.message : "Unknown error",
},
};
const superAdminUser = await db.query.user.findFirst();
if (!superAdminUser) {
return {
success: false,
actionError: {
message: "set_super_admin_owner_of_organizations_owned_by_user",
cause: "Unknown error",
},
};
}
});
for (let { organization } of organizationsWhereUserIsMemberAndOwner) {
await addMemberOrganizationAction({
userId: superAdminUser.id,
organizationId: organization.id,
role: "owner",
});
}
const organizations = organizationsWhereUserIsMemberAndOwner.map(
(organizationWhereUserIsMemberAndOwner) =>
organizationWhereUserIsMemberAndOwner.organization,
);
return {
success: true,
value: organizations as unknown as Organization[],
actionSuccess: {
message: "set_super_admin_owner_of_organizations_owned_by_user",
},
};
} catch (error) {
return {
success: false,
actionError: {
message:
"error_set_super_admin_owner_of_organizations_owned_by_user",
cause: error instanceof Error ? error.message : "Unknown error",
},
};
}
},
);
@@ -8,7 +8,11 @@ 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 {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { SetPasswordProfileProviderModal } from "./modal/set-password-modal";
import { Icon } from "@iconify/react";
@@ -17,164 +21,217 @@ import type { AuthProviderConfig } from "@/lib/auth/config";
import { Account } from "@/db/schema/02_user";
interface ProfileProviderProps {
accounts: Account[];
providers: AuthProviderConfig[];
accounts: Account[];
providers: AuthProviderConfig[];
}
export function ProfileProviders({ accounts, providers }: ProfileProviderProps) {
const router = useRouter();
const totalConnected = accounts.length;
const [loadingProvider, setLoadingProvider] = useState<string | null>(null);
export function ProfileProviders({
accounts,
providers,
}: ProfileProviderProps) {
const router = useRouter();
const totalConnected = accounts.length;
const [loadingProvider, setLoadingProvider] = useState<string | null>(null);
const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false);
const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false);
const { mutate: unlinkAccount } = useMutation({
mutationFn: async (providerId: string) => {
setLoadingProvider(providerId);
const { error } = await authClient.unlinkAccount({ providerId });
if (error) throw error;
},
onSuccess: () => {
toast.success("Provider successfully unlinked!");
setLoadingProvider(null);
router.refresh();
},
onError: () => {
toast.error("An error occurred while unlinking provider.");
setLoadingProvider(null);
},
});
const { mutate: unlinkAccount } = useMutation({
mutationFn: async (providerId: string) => {
setLoadingProvider(providerId);
const { error } = await authClient.unlinkAccount({ providerId });
if (error) throw error;
},
onSuccess: () => {
toast.success("Provider successfully unlinked!");
setLoadingProvider(null);
router.refresh();
},
onError: () => {
toast.error("An error occurred while unlinking provider.");
setLoadingProvider(null);
},
});
const { mutate: linkAccount } = useMutation({
mutationFn: async (provider: AuthProviderConfig) => {
setLoadingProvider(provider.id);
let result;
if (provider.type === "sso") {
result = await authClient.signIn.sso({
providerId: provider.id,
providerType: "oidc",
callbackURL: "/dashboard",
});
} else {
result = await authClient.signIn.social({
provider: provider.id as "google" | "github" | "credential",
callbackURL: "/dashboard",
});
const { mutate: linkAccount } = useMutation({
mutationFn: async (provider: AuthProviderConfig) => {
setLoadingProvider(provider.id);
let result;
if (provider.type === "sso") {
result = await authClient.signIn.sso({
providerId: provider.id,
providerType: "oidc",
callbackURL: "/dashboard",
});
} else {
result = await authClient.signIn.social({
provider: provider.id as any,
callbackURL: "/dashboard",
});
}
if (result.error) throw result.error;
},
onSuccess: () => {
setLoadingProvider(null);
router.refresh();
},
onError: () => {
toast.error("An error occurred while linking provider.");
setLoadingProvider(null);
},
});
const enterpriseProviders = providers.filter(
(p) => p.type === "sso" && p.id !== "passkey" && p.type !== "credential",
);
const otherProviders = providers.filter(
(p) => p.type !== "sso" && p.id !== "passkey" && p.type !== "credential",
);
const renderProvider = (provider: AuthProviderConfig) => {
const linkedAccount = accounts.find(
(acc) => acc.providerId === provider.id,
);
const isConnected = !!linkedAccount;
const canUnlink =
totalConnected > 1 || (totalConnected === 1 && !provider.isManual);
const isLoading = loadingProvider === provider.id;
const isUnlinkDisabled = !canUnlink || provider.allowUnlinking === false;
const unlinkButton = (
<Button
variant="outline"
size="sm"
onClick={() => unlinkAccount(provider.id)}
disabled={isUnlinkDisabled || isLoading || provider.isManual}
className={isUnlinkDisabled ? "opacity-50 cursor-not-allowed" : ""}
>
{isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Unlink"}
</Button>
);
let actionElement;
if (!isConnected) {
actionElement =
provider.id === "credential" ? (
<SetPasswordProfileProviderModal
open={isPasswordDialogOpen}
onOpenChange={setIsPasswordDialogOpen}
/>
) : (
<Button
variant="default"
size="sm"
onClick={() => linkAccount(provider)}
disabled={
isLoading || provider.isManual || provider.allowLinking === false
}
if (result.error) throw result.error;
},
onSuccess: () => {
setLoadingProvider(null);
router.refresh();
},
onError: () => {
toast.error("An error occurred while linking provider.");
setLoadingProvider(null);
},
});
const enterpriseProviders = providers.filter((p) => p.type === "sso" && p.id !== "passkey" && p.type !== "credential");
const otherProviders = providers.filter((p) => p.type !== "sso" && p.id !== "passkey" && p.type !== "credential");
const renderProvider = (provider: AuthProviderConfig) => {
const linkedAccount = accounts.find((acc) => acc.providerId === provider.id);
const isConnected = !!linkedAccount;
const canUnlink = totalConnected > 1 || (totalConnected === 1 && !provider.isManual);
const isLoading = loadingProvider === provider.id;
return (
<div key={provider.id} className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted/30 transition-colors">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center">
{provider.icon.startsWith("/") || provider.icon.startsWith("http") ? (
<Image
src={provider.icon}
alt={provider.id}
width={20}
height={20}
className="w-5 h-5"
unoptimized={provider.icon.startsWith("http")}
/>
) : (
<Icon icon={provider.icon} className="w-5 h-5" />
)}
</div>
<div className="space-y-0.5">
<div className="font-medium flex items-center gap-2">
{provider.title || provider.name}
{isConnected && (
<Badge variant="secondary" className="text-[10px] h-5 px-1.5 text-green-600 bg-green-500/10 border-0">
Active
</Badge>
)}
</div>
<div className="text-sm text-muted-foreground">{provider.description}</div>
</div>
</div>
<div className="flex items-center gap-2">
{isConnected ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span tabIndex={0}>
<Button
variant="outline"
size="sm"
onClick={() => unlinkAccount(provider.id)}
disabled={!canUnlink || isLoading || provider.isManual || provider.allowUnlinking === false}
className={!canUnlink || provider.allowUnlinking === false ? "opacity-50 cursor-not-allowed" : ""}
>
{isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Unlink"}
</Button>
</span>
</TooltipTrigger>
{(!canUnlink || provider.allowUnlinking === false) && (
<TooltipContent>
<p>{provider.allowUnlinking === false ? "Unlinking is disabled for this provider." : "You cannot unlink your last authentication provider."}</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
) : (
<>
{provider.id === "credential" ? (
<SetPasswordProfileProviderModal open={isPasswordDialogOpen} onOpenChange={setIsPasswordDialogOpen} />
) : (
<Button variant="default" size="sm" onClick={() => linkAccount(provider)} disabled={isLoading || provider.isManual || provider.allowLinking === false}>
{isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Link"}
</Button>
)}
</>
)}
</div>
</div>
>
{isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Link"}
</Button>
);
};
} else if (isUnlinkDisabled) {
actionElement = (
<Tooltip>
<TooltipTrigger asChild>
<div tabIndex={0} className="inline-block">
{unlinkButton}
</div>
</TooltipTrigger>
<TooltipContent>
<p>
{provider.allowUnlinking === false
? "Unlinking is disabled for this provider."
: "You cannot unlink your last authentication provider."}
</p>
</TooltipContent>
</Tooltip>
);
} else {
actionElement = unlinkButton;
}
return (
<div className="space-y-8 animate-in fade-in-50 duration-300">
<div className="mb-6 space-y-1">
<h2 className="text-2xl font-semibold tracking-tight">Authentication</h2>
<p className="text-sm text-muted-foreground">Manage how you access your account.</p>
</div>
{enterpriseProviders.length > 0 && (
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">Enterprise Connection</h3>
<div className="grid gap-4">{enterpriseProviders.map(renderProvider)}</div>
</div>
<div
key={provider.id}
className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted/30 transition-colors"
>
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center">
{provider.icon.startsWith("/") ||
provider.icon.startsWith("http") ? (
<Image
src={provider.icon}
alt={provider.id}
width={20}
height={20}
className="w-5 h-5"
unoptimized={provider.icon.startsWith("http")}
/>
) : (
<Icon icon={provider.icon} className="w-5 h-5" />
)}
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">Standard Connections</h3>
<div className="grid gap-4">{otherProviders.map(renderProvider)}</div>
</div>
<div className="space-y-0.5">
<div className="font-medium flex items-center gap-2">
{provider.title || provider.name}
{isConnected && (
<Badge
variant="secondary"
className="text-[10px] h-5 px-1.5 text-green-600 bg-green-500/10 border-0"
>
Active
</Badge>
)}
</div>
<Alert variant={"default"}>
<AlertTriangle className="w-5 h-5 shrink-0 mt-0.5" />
<AlertDescription>Linked providers allow you to log in to your account using any of these methods.</AlertDescription>
</Alert>
<div className="text-sm text-muted-foreground">
{provider.description}
</div>
</div>
</div>
<div className="flex items-center gap-2">{actionElement}</div>
</div>
);
};
return (
<div className="space-y-8 animate-in fade-in-50 duration-300 pb-10">
<div className="mb-6 space-y-1">
<h2 className="text-2xl font-semibold tracking-tight">
Authentication
</h2>
<p className="text-sm text-muted-foreground">
Manage how you access your account.
</p>
</div>
{enterpriseProviders.length > 0 && (
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
Enterprise Connection
</h3>
<div className="grid gap-4">
{enterpriseProviders.map(renderProvider)}
</div>
</div>
)}
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
Standard Connections
</h3>
<div className="grid gap-4">{otherProviders.map(renderProvider)}</div>
</div>
<Alert variant={"default"}>
<AlertTriangle className="w-5 h-5 shrink-0 mt-0.5" />
<AlertDescription>
Linked providers allow you to log in to your account using any of
these methods.
</AlertDescription>
</Alert>
</div>
);
}
@@ -162,77 +162,81 @@ export function ProfileSecurity({
</p>
</div>
<div className="space-y-6">
<h3 className="text-lg font-medium">Authentication</h3>
<div className="border rounded-lg p-4 space-y-4">
{isPasswordEnabled && (
<>
{isPasskeyEnabled && (
<div className="space-y-6">
<h3 className="text-lg font-medium">Authentication</h3>
<div className="border rounded-lg p-4 space-y-4">
{isPasswordEnabled && (
<>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="space-y-1">
<div className="font-medium">Password</div>
<div className="text-sm text-muted-foreground">
{user.lastChangedPasswordAt
? `Last changed ${timeAgo(new Date(user.lastChangedPasswordAt))}`
: "Never changed"}
</div>
</div>
{credentialAccount ? (
<ResetPasswordProfileProviderModal
open={isPasswordDialogOpen}
onOpenChange={setIsPasswordDialogOpen}
/>
) : (
<SetPasswordProfileProviderModal
open={isPasswordDialogOpen}
onOpenChange={setIsPasswordDialogOpen}
/>
)}
</div>
<Separator />
</>
)}
{isPasswordEnabled && (
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="space-y-1">
<div className="font-medium">Password</div>
<div className="flex items-center gap-2">
<div className="font-medium">Two-Factor Authentication</div>
{user.twoFactorEnabled && (
<Badge
variant="secondary"
className="text-[10px] h-5 px-1.5 text-green-600 bg-green-500/10 border-0"
>
Active
</Badge>
)}
</div>
<div className="text-sm text-muted-foreground">
{user.lastChangedPasswordAt
? `Last changed ${timeAgo(new Date(user.lastChangedPasswordAt))}`
: "Never changed"}
Enhance the security of your account by requiring a second
form of verification during login.
</div>
</div>
{credentialAccount ? (
<ResetPasswordProfileProviderModal
open={isPasswordDialogOpen}
onOpenChange={setIsPasswordDialogOpen}
/>
{user.twoFactorEnabled ? (
<div className="flex flex-col items-center gap-2">
<ViewBackupCodesModal
open={isBackupCodesDialogOpen}
onOpenChange={setIsBackupCodesDialogOpen}
/>
<Disable2FAProfileProviderModal
open={isDisable2FADialogOpen}
onOpenChange={setIsDisable2FADialogOpen}
/>
</div>
) : (
<SetPasswordProfileProviderModal
open={isPasswordDialogOpen}
onOpenChange={setIsPasswordDialogOpen}
<Setup2FAProfileProviderModal
disabled={!credentialAccount}
open={isSetup2FADialogOpen}
onOpenChange={setIsSetup2FADialogOpen}
/>
)}
</div>
<Separator />
</>
)}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="space-y-1">
<div className="flex items-center gap-2">
<div className="font-medium">Two-Factor Authentication</div>
{user.twoFactorEnabled && (
<Badge
variant="secondary"
className="text-[10px] h-5 px-1.5 text-green-600 bg-green-500/10 border-0"
>
Active
</Badge>
)}
</div>
<div className="text-sm text-muted-foreground">
Enhance the security of your account by requiring a second form
of verification during login.
</div>
</div>
{user.twoFactorEnabled ? (
<div className="flex flex-col items-center gap-2">
<ViewBackupCodesModal
open={isBackupCodesDialogOpen}
onOpenChange={setIsBackupCodesDialogOpen}
/>
<Disable2FAProfileProviderModal
open={isDisable2FADialogOpen}
onOpenChange={setIsDisable2FADialogOpen}
/>
</div>
) : (
<Setup2FAProfileProviderModal
disabled={!credentialAccount}
open={isSetup2FADialogOpen}
onOpenChange={setIsSetup2FADialogOpen}
/>
)}
</div>
</div>
</div>
)}
{isPasskeyEnabled && (
<div className="space-y-6">
@@ -377,20 +381,21 @@ function SessionRow({
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center text-muted-foreground relative">
<deviceInfo.Icon className="w-5 h-5" />
{provider && (
<div className="absolute -bottom-1 -right-1 w-5 h-5 rounded-full bg-background border flex items-center justify-center overflow-hidden">
{provider.icon.startsWith("/") || provider.icon.startsWith("http") ? (
<Image
src={provider.icon}
alt={provider.id}
width={12}
height={12}
className="w-3 h-3"
unoptimized={provider.icon.startsWith("http")}
/>
) : (
<Icon icon={provider.icon} className="w-3 h-3" />
)}
</div>
<div className="absolute -bottom-1 -right-1 w-5 h-5 rounded-full bg-background border flex items-center justify-center overflow-hidden">
{provider.icon.startsWith("/") ||
provider.icon.startsWith("http") ? (
<Image
src={provider.icon}
alt={provider.id}
width={12}
height={12}
className="w-3 h-3"
unoptimized={provider.icon.startsWith("http")}
/>
) : (
<Icon icon={provider.icon} className="w-3 h-3" />
)}
</div>
)}
</div>
<div className="space-y-0.5">
@@ -400,9 +405,9 @@ function SessionRow({
{deviceInfo.browser}
</span>
{provider && (
<span className="text-muted-foreground font-normal">
{provider.title || provider.name}
</span>
<span className="text-muted-foreground font-normal">
{provider.title || provider.name}
</span>
)}
{session.id === currentSession.id && (
<Badge