add: oidc,passkey,disable email/password,sign-up

This commit is contained in:
Théo LAGACHE
2026-02-13 16:48:13 +01:00
parent 7ab213e5e4
commit 3bf731055f
29 changed files with 6199 additions and 525 deletions
@@ -1,37 +1,37 @@
"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 {AuthProviderConfig} from "../../../../../portabase.config";
import {Icon} from "@iconify/react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Loader2, AlertTriangle } from "lucide-react";
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 { Icon } from "@iconify/react";
import Image from "next/image";
import { AuthProviderConfig } from "@/lib/auth/config";
import { Account } from "@/db/schema/02_user";
interface ProfileProviderProps {
accounts: Account[];
providers: AuthProviderConfig[]
providers: AuthProviderConfig[];
}
export function ProfileProviders({accounts, providers}: ProfileProviderProps) {
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 {mutate: unlinkAccount} = useMutation({
const { mutate: unlinkAccount } = useMutation({
mutationFn: async (providerId: string) => {
setLoadingProvider(providerId);
const {error} = await authClient.unlinkAccount({ providerId });
const { error } = await authClient.unlinkAccount({ providerId });
if (error) throw error;
},
onSuccess: () => {
@@ -45,17 +45,25 @@ export function ProfileProviders({accounts, providers}: ProfileProviderProps) {
},
});
const {mutate: linkAccount} = useMutation({
mutationFn: async (providerId: string) => {
setLoadingProvider(providerId);
const {error} = await authClient.signIn.social({
provider: providerId as "google" | "github" | "credential",
callbackURL: "/dashboard",
});
if (error) throw error;
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",
});
}
if (result.error) throw result.error;
},
onSuccess: () => {
toast.success("Provider successfully Linked!");
setLoadingProvider(null);
router.refresh();
},
@@ -65,120 +73,108 @@ export function ProfileProviders({accounts, providers}: ProfileProviderProps) {
},
});
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}
className={!canUnlink ? "opacity-50 cursor-not-allowed" : ""}
>
{isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Unlink"}
</Button>
</span>
</TooltipTrigger>
{!canUnlink && (
<TooltipContent>
<p>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}>
{isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Link"}
</Button>
)}
</>
)}
</div>
</div>
);
};
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">Connected Accounts</h2>
<p className="text-sm text-muted-foreground">Manage the providers used to sign in to your account.</p>
<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>
<div className="grid gap-4">
{providers.map((provider) => {
const linkedAccount = accounts.find((acc) => acc.providerId === provider.id);
const isConnected = !!linkedAccount;
// const canUnlink = totalConnected > 1 || (totalConnected === 1 && !provider.isManual);
const canUnlink = totalConnected > 1 ;
// const isLoading = isUnlinking || isLinking;
const isLoading = loadingProvider === provider.id;
{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>
)}
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">
<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">
{PROVIDERS_TEXT[provider.id].title}
{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">
{isConnected ? "Connected" : "Not Connected"}
</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}
className={!canUnlink ? "opacity-50 cursor-not-allowed" : ""}
>
{isLoading ? <Loader2
className="w-4 h-4 animate-spin"/> : "Unlink"}
</Button>
</span>
</TooltipTrigger>
{!canUnlink && (
<TooltipContent>
<p>
You cannot unlink your last authentication provider or if you
don't have a password set.
</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
) : (
<>
{provider.id === "credential" ? (
<SetPasswordProfileProviderModal open={isPasswordDialogOpen}
onOpenChange={setIsPasswordDialogOpen}/>
) : (
<Button
variant="default"
size="sm"
onClick={() => linkAccount(provider.id)}
disabled={isLoading || provider.isManual}
>
{isLoading ? <Loader2 className="w-4 h-4 animate-spin"/> : "Link"}
</Button>
)}
</>
)}
</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. If you use the same
email address with another provider, it will be automatically linked when you log in.
</AlertDescription>
<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>
);
}
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.",
},
};