"use client"; import { useState } from "react"; import { Alert, Avatar, Box, Button, Card, CardContent, Chip, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, IconButton, Stack, TextField, Typography } from "@mui/material"; import { signIn } from "next-auth/react"; import PersonIcon from "@mui/icons-material/Person"; import LockIcon from "@mui/icons-material/Lock"; import LinkIcon from "@mui/icons-material/Link"; import LinkOffIcon from "@mui/icons-material/LinkOff"; import LoginIcon from "@mui/icons-material/Login"; import PhotoCamera from "@mui/icons-material/PhotoCamera"; import DeleteIcon from "@mui/icons-material/Delete"; interface User { id: number; email: string; name: string | null; provider: string; subject: string; password_hash: string | null; role: string; avatar_url: string | null; } interface ProfileClientProps { user: User; enabledProviders: Array<{ id: string; name: string }>; } export default function ProfileClient({ user, enabledProviders }: 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.avatar_url); const hasPassword = !!user.password_hash; const hasOAuth = user.provider !== "credentials"; const isCredentialsOnly = 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 (err) { 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 (err) { 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 signIn(providerId, { callbackUrl: "/profile" }); } catch (err) { 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 (err) { 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 (err) { setError("An error occurred while deleting avatar"); setLoading(false); } }; const getProviderName = (provider: string) => { if (provider === "credentials") return "Username/Password"; if (provider === "oauth2") return "OAuth2"; if (provider === "authentik") return "Authentik"; return provider; }; const getProviderColor = (provider: string) => { if (provider === "credentials") return "default"; return "primary"; }; return ( Profile & Account Settings {error && ( setError(null)}> {error} )} {success && ( setSuccess(null)}> {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 Authentication Method {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) => ( ))} )} )} {/* Change Password Dialog */} setPasswordDialogOpen(false)} maxWidth="sm" fullWidth> {hasPassword ? "Change Password" : "Set Password"} {hasPassword && ( setCurrentPassword(e.target.value)} fullWidth autoComplete="current-password" /> )} setNewPassword(e.target.value)} fullWidth autoComplete="new-password" helperText="Minimum 12 characters" /> setConfirmPassword(e.target.value)} fullWidth autoComplete="new-password" /> {/* Unlink OAuth Dialog */} setUnlinkDialogOpen(false)} maxWidth="sm" fullWidth> 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. ); }