"use client"; import { useState } from "react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { authClient } from "@/src/lib/auth-client"; import { Camera, Check, Clock, Copy, Key, Link, LogIn, Lock, Plus, Trash2, Unlink, User, AlertTriangle } from "lucide-react"; import type { ApiToken } from "@/lib/models/api-tokens"; import { createApiTokenAction, deleteApiTokenAction } from "../api-tokens/actions"; interface UserData { id: number; email: string; name: string | null; provider: string | null; subject: string | null; passwordHash: string | null; role: string; avatarUrl: string | null; } interface ProfileClientProps { user: UserData; enabledProviders: Array<{ id: string; name: string }>; apiTokens: ApiToken[]; } export default function ProfileClient({ user, enabledProviders, apiTokens }: ProfileClientProps) { const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); const [unlinkDialogOpen, setUnlinkDialogOpen] = useState(false); const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const [loading, setLoading] = useState(false); const [avatarUrl, setAvatarUrl] = useState(user.avatarUrl); const [newToken, setNewToken] = useState(null); const [copied, setCopied] = useState(false); const hasPassword = !!user.passwordHash; const hasOAuth = !!user.provider && user.provider !== "credentials"; const handlePasswordChange = async () => { setError(null); setSuccess(null); if (newPassword !== confirmPassword) { setError("Passwords do not match"); return; } if (newPassword.length < 12) { setError("Password must be at least 12 characters long"); return; } setLoading(true); try { const response = await fetch("/api/user/change-password", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ currentPassword, newPassword }) }); const data = await response.json(); if (!response.ok) { setError(data.error || "Failed to change password"); setLoading(false); return; } setSuccess("Password changed successfully"); setPasswordDialogOpen(false); setCurrentPassword(""); setNewPassword(""); setConfirmPassword(""); setLoading(false); } catch { setError("An error occurred while changing password"); setLoading(false); } }; const handleUnlinkOAuth = async () => { if (!hasPassword) { setError("Cannot unlink OAuth: You must set a password first"); return; } setError(null); setSuccess(null); setLoading(true); try { const response = await fetch("/api/user/unlink-oauth", { method: "POST", headers: { "Content-Type": "application/json" } }); const data = await response.json(); if (!response.ok) { setError(data.error || "Failed to unlink OAuth"); setLoading(false); return; } setSuccess("OAuth account unlinked successfully. Reloading..."); setUnlinkDialogOpen(false); setLoading(false); // Reload page to reflect changes setTimeout(() => window.location.reload(), 1500); } catch { setError("An error occurred while unlinking OAuth"); setLoading(false); } }; const handleLinkOAuth = async (providerId: string) => { setError(null); setSuccess(null); setLoading(true); try { // Set a cookie to indicate this is a linking attempt const response = await fetch("/api/user/link-oauth-start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider: providerId }) }); if (!response.ok) { const data = await response.json(); setError(data.error || "Failed to start OAuth linking"); setLoading(false); return; } // Now initiate OAuth flow await authClient.signIn.social({ provider: providerId, callbackURL: "/profile" }); } catch { setError("An error occurred while linking OAuth"); setLoading(false); } }; const handleAvatarUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; // Validate file type if (!file.type.startsWith("image/")) { setError("Please upload an image file"); return; } // Validate file size (max 2MB) if (file.size > 2 * 1024 * 1024) { setError("Image must be smaller than 2MB"); return; } setError(null); setLoading(true); try { // Convert to base64 const reader = new FileReader(); reader.onloadend = async () => { const base64 = reader.result as string; const response = await fetch("/api/user/update-avatar", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ avatarUrl: base64 }) }); const data = await response.json(); if (!response.ok) { setError(data.error || "Failed to upload avatar"); setLoading(false); return; } setAvatarUrl(base64); setSuccess("Avatar updated successfully. Refreshing..."); setLoading(false); setTimeout(() => window.location.reload(), 1000); }; reader.readAsDataURL(file); } catch { setError("An error occurred while uploading avatar"); setLoading(false); } }; const handleAvatarDelete = async () => { setError(null); setLoading(true); try { const response = await fetch("/api/user/update-avatar", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ avatarUrl: null }) }); const data = await response.json(); if (!response.ok) { setError(data.error || "Failed to delete avatar"); setLoading(false); return; } setAvatarUrl(null); setSuccess("Avatar removed successfully. Refreshing..."); setLoading(false); setTimeout(() => window.location.reload(), 1000); } catch { setError("An error occurred while deleting avatar"); setLoading(false); } }; const handleCreateToken = async (formData: FormData) => { setError(null); setNewToken(null); const result = await createApiTokenAction(formData); if ("error" in result) { setError(result.error); } else { setNewToken(result.rawToken); setSuccess("API token created successfully"); } }; const handleCopyToken = () => { if (newToken) { navigator.clipboard.writeText(newToken); setCopied(true); setTimeout(() => setCopied(false), 2000); } }; const formatDate = (iso: string | null): string => { if (!iso) return "Never"; return new Date(iso).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); }; const isExpired = (expiresAt: string | null): boolean => { if (!expiresAt) return false; return new Date(expiresAt) <= new Date(); }; const getProviderName = (provider: string) => { if (provider === "credentials") return "Username/Password"; if (provider === "oauth2") return "OAuth2"; if (provider === "authentik") return "Authentik"; return provider; }; return (

Profile & Account Settings

{error && ( {error} )} {success && ( {success} )}
{/* Account Information */}

Account Information

{/* Avatar Section */}

Profile Picture

{(!avatarUrl && user.name) ? user.name.charAt(0).toUpperCase() : user.email.charAt(0).toUpperCase()}
{avatarUrl && ( )}

Recommended: Square image, max 2MB

Email

{user.email}

Name

{user.name || "Not set"}

Role

{user.role}

Authentication Method

{getProviderName(user.provider ?? "")}
{hasPassword && (

Password

✓ Password is set

)}
{/* Password Management */}

Password Management

{hasPassword ? (

Change your password to maintain account security

) : (
You are using OAuth-only authentication. Setting a password will allow you to sign in with either OAuth or credentials.
)}
{/* OAuth Management */} {enabledProviders.length > 0 && (

OAuth Connections

{hasOAuth ? (

Your account is linked to {getProviderName(user.provider ?? "")}

{hasPassword ? ( ) : ( To unlink OAuth, you must first set a password as a fallback authentication method. )}
) : (

Link an OAuth provider to enable single sign-on

{enabledProviders.map((provider) => ( ))}
)}
)} {/* API Tokens */}

API Tokens

Create tokens for programmatic access to the API using Authorization: Bearer {''}

{/* Newly created token */} {newToken && (

Copy this token now — it will not be shown again.

{newToken}
)} {/* Existing tokens */} {apiTokens.length > 0 && (
{apiTokens.map((token) => { const expired = isExpired(token.expiresAt); return (

{token.name}

{expired && ( Expired )}

Created {formatDate(token.createdAt)}

Used {formatDate(token.lastUsedAt)}

{token.expiresAt && (

{expired ? "Expired" : "Expires"} {formatDate(token.expiresAt)}

)}
); })}
)} {apiTokens.length === 0 && !newToken && (
No API tokens yet — create one below.
)} {/* Create new token */}
{/* Change Password Dialog */} {hasPassword ? "Change Password" : "Set Password"}
{hasPassword && (
setCurrentPassword(e.target.value)} autoComplete="current-password" />
)}
setNewPassword(e.target.value)} autoComplete="new-password" />

Minimum 12 characters

setConfirmPassword(e.target.value)} autoComplete="new-password" />
{/* Unlink OAuth Dialog */} Unlink OAuth Account Are you sure you want to unlink your {getProviderName(user.provider ?? "")} account? You will only be able to sign in with your username and password after this.
); }