466 lines
16 KiB
TypeScript
466 lines
16 KiB
TypeScript
"use client"
|
|
|
|
import { Switch } from "@/components/ui/switch"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
|
import MyAccount from "@/components/cards/dashboard/Settings/MyAccount"
|
|
import { useState, useEffect } from "react"
|
|
import { LayoutDashboard, Trash, Loader2, Check, X } from "lucide-react"
|
|
import { Button } from "@/components/ui/button"
|
|
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
|
import { Dialog } from "@/components/ui/dialog"
|
|
import {
|
|
InputOTP,
|
|
InputOTPGroup,
|
|
InputOTPSeparator,
|
|
InputOTPSlot,
|
|
} from "@/components/ui/input-otp"
|
|
import { signOut } from "next-auth/react"
|
|
import { useRouter } from "next/navigation"
|
|
|
|
export default function Settings() {
|
|
const [settings, setSettings] = useState({
|
|
hideGenAI: false,
|
|
hideUpgrades: false,
|
|
hideCrypto: false
|
|
});
|
|
const [loading, setLoading] = useState(false)
|
|
const [deleteOTP, setDeleteOTP] = useState('')
|
|
const [deleteOTPLoading, setDeleteOTPLoading] = useState(false)
|
|
const [cooldownSeconds, setCooldownSeconds] = useState(0)
|
|
const [otpError, setOtpError] = useState('')
|
|
const [showSuccess, setShowSuccess] = useState(false)
|
|
const [isDeleting, setIsDeleting] = useState(false)
|
|
const [nextcloudError, setNextcloudError] = useState('')
|
|
const [deleteSteps, setDeleteSteps] = useState({
|
|
nextcloud: { status: 'pending', message: 'Pending' },
|
|
database: { status: 'pending', message: 'Pending' }
|
|
})
|
|
const [deleteStep, setDeleteStep] = useState<'initial' | 'otp' | 'confirm'>('initial')
|
|
|
|
const router = useRouter()
|
|
|
|
const handleOTPChange = (value: string) => {
|
|
const numericValue = value.replace(/[^0-9]/g, '')
|
|
setDeleteOTP(numericValue)
|
|
}
|
|
|
|
useEffect(() => {
|
|
let successTimer: NodeJS.Timeout
|
|
if (showSuccess) {
|
|
successTimer = setTimeout(() => {
|
|
setShowSuccess(false)
|
|
}, 5000)
|
|
}
|
|
return () => {
|
|
if (successTimer) clearTimeout(successTimer)
|
|
}
|
|
}, [showSuccess])
|
|
|
|
useEffect(() => {
|
|
let cooldownTimer: NodeJS.Timeout
|
|
if (cooldownSeconds > 0) {
|
|
cooldownTimer = setInterval(() => {
|
|
setCooldownSeconds(prev => prev - 1)
|
|
}, 1000)
|
|
}
|
|
return () => {
|
|
if (cooldownTimer) clearInterval(cooldownTimer)
|
|
}
|
|
}, [cooldownSeconds])
|
|
|
|
useEffect(() => {
|
|
const fetchSettings = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await fetch('/api/users/settings')
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
setSettings(data)
|
|
} else {
|
|
console.error('[!] Failed to fetch settings')
|
|
}
|
|
} catch (error) {
|
|
console.error('[!] Error fetching settings:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
};
|
|
|
|
fetchSettings()
|
|
}, []);
|
|
|
|
const updateSetting = async (settingName: string, value: boolean) => {
|
|
setSettings(prev => ({
|
|
...prev,
|
|
[settingName]: value
|
|
}))
|
|
|
|
try {
|
|
setLoading(true)
|
|
const response = await fetch('/api/users/settings', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
...settings,
|
|
[settingName]: value
|
|
}),
|
|
})
|
|
|
|
if (response.ok) {
|
|
const updatedSettings = await response.json()
|
|
setSettings(updatedSettings)
|
|
} else {
|
|
console.error('[!] Failed to update settings')
|
|
setSettings(prev => ({
|
|
...prev,
|
|
[settingName]: !value
|
|
}))
|
|
}
|
|
} catch (error) {
|
|
console.error('[!] Error updating settings:', error)
|
|
setSettings(prev => ({
|
|
...prev,
|
|
[settingName]: !value
|
|
}))
|
|
} finally {
|
|
setLoading(false)
|
|
window.location.reload()
|
|
}
|
|
};
|
|
|
|
const deleteAccount = async () => {
|
|
setIsDeleting(true)
|
|
setOtpError('')
|
|
setNextcloudError('')
|
|
setDeleteSteps({
|
|
nextcloud: { status: 'pending', message: 'Pending' },
|
|
database: { status: 'pending', message: 'Pending' }
|
|
})
|
|
|
|
try {
|
|
// 1. Delete Nextcloud account
|
|
const nextcloudResponse = await fetch('/api/users/delete', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
otp: deleteOTP,
|
|
step: 'nextcloud'
|
|
}),
|
|
})
|
|
|
|
const nextcloudData = await nextcloudResponse.json()
|
|
|
|
if (nextcloudData.steps) {
|
|
setDeleteSteps(prev => ({
|
|
...prev,
|
|
nextcloud: nextcloudData.steps.nextcloud
|
|
}))
|
|
}
|
|
|
|
if (!nextcloudResponse.ok || nextcloudData.steps?.nextcloud.status === 'error') {
|
|
if (nextcloudData.error === 'Failed to delete user from Nextcloud') {
|
|
setNextcloudError('Failed to delete your Nextcloud account. Please try again or contact support.')
|
|
} else {
|
|
setOtpError(nextcloudData.error || 'Failed to delete Nextcloud account')
|
|
}
|
|
setIsDeleting(false)
|
|
return
|
|
}
|
|
|
|
const databaseResponse = await fetch('/api/users/delete', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
otp: deleteOTP,
|
|
step: 'database'
|
|
}),
|
|
})
|
|
|
|
const databaseData = await databaseResponse.json()
|
|
|
|
if (databaseData.steps) {
|
|
setDeleteSteps(prev => ({
|
|
...prev,
|
|
database: databaseData.steps.database
|
|
}))
|
|
}
|
|
|
|
if (databaseResponse.ok && databaseData.success) {
|
|
setDeleteSteps({
|
|
nextcloud: { status: 'success', message: 'Completed' },
|
|
database: { status: 'success', message: 'Completed' }
|
|
})
|
|
|
|
setTimeout(() => {
|
|
signOut()
|
|
router.push('/')
|
|
}, 1500)
|
|
} else {
|
|
if (databaseData.error === 'Failed to delete user from database') {
|
|
setOtpError('Failed to delete your account from the database. Please try again or contact support.')
|
|
} else {
|
|
setOtpError(databaseData.error || 'Failed to delete account')
|
|
}
|
|
setIsDeleting(false)
|
|
}
|
|
} catch (error) {
|
|
console.error('[!] Error deleting account:', error)
|
|
setOtpError(error instanceof Error ? error.message : 'Failed to delete account')
|
|
setIsDeleting(false)
|
|
}
|
|
}
|
|
|
|
const sendOTP = async () => {
|
|
setDeleteOTPLoading(true)
|
|
setOtpError('')
|
|
try {
|
|
const response = await fetch('/api/users/otp', {
|
|
method: 'GET'
|
|
})
|
|
const data = await response.json()
|
|
|
|
if (response.ok) {
|
|
console.log(data)
|
|
setShowSuccess(true)
|
|
setCooldownSeconds(60)
|
|
} else {
|
|
if (response.status === 429) {
|
|
setCooldownSeconds(data.remainingCooldown)
|
|
setOtpError(`Please wait ${data.remainingCooldown} seconds before requesting another OTP`)
|
|
} else {
|
|
setOtpError(data.error || 'Failed to send OTP')
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('[!] Error sending OTP:', error)
|
|
setOtpError('Failed to send OTP')
|
|
} finally {
|
|
setDeleteOTPLoading(false)
|
|
}
|
|
}
|
|
|
|
const renderStepIcon = (status: string) => {
|
|
switch (status) {
|
|
case 'pending':
|
|
return <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
case 'success':
|
|
return <Check className="h-4 w-4 text-green-500" />
|
|
case 'error':
|
|
return <X className="h-4 w-4 text-destructive" />
|
|
default:
|
|
return <Loader2 className="h-4 w-4 animate-spin" />
|
|
}
|
|
}
|
|
|
|
const renderStepStatus = (step: { status: string, message: string }) => {
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
{renderStepIcon(step.status)}
|
|
<span className={step.status === 'error' ? 'text-destructive' : ''}>
|
|
{step.message}
|
|
</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<h1 className="text-2xl xl:text-3xl font-bold mb-6 text-foreground">Settings</h1>
|
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
<MyAccount />
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center text-xl gap-2">
|
|
<LayoutDashboard size={20} />
|
|
Dashboard Settings
|
|
</CardTitle>
|
|
<CardDescription>Customize your dashboard experience</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="hideGenAI">Hide Generative AI</Label>
|
|
<Switch
|
|
id="hideGenAI"
|
|
checked={settings.hideGenAI}
|
|
onCheckedChange={(checked) => updateSetting('hideGenAI', checked)}
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="hideUpgrades">Hide Upgrades</Label>
|
|
<Switch
|
|
id="hideUpgrades"
|
|
checked={settings.hideUpgrades}
|
|
onCheckedChange={(checked) => updateSetting('hideUpgrades', checked)}
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="hideCrypto">Hide Crypto Exchange</Label>
|
|
<Switch
|
|
id="hideCrypto"
|
|
checked={settings.hideCrypto}
|
|
onCheckedChange={(checked) => updateSetting('hideCrypto', checked)}
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center text-xl">
|
|
<Trash size={18} className="mr-1" />
|
|
Delete Account
|
|
</CardTitle>
|
|
<CardDescription>Permanently delete your account</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Dialog>
|
|
<DialogTrigger asChild>
|
|
<Button variant="destructive" className="cursor-pointer">
|
|
<Trash size={18} />
|
|
Delete Account
|
|
</Button>
|
|
</DialogTrigger>
|
|
{deleteStep === 'initial' ? (
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Delete Account</DialogTitle>
|
|
</DialogHeader>
|
|
<DialogDescription>
|
|
Are you sure you want to delete your account? This action cannot be undone.
|
|
</DialogDescription>
|
|
|
|
<p className="text-sm mt-2">This will remove your data from the following services:</p>
|
|
<ul className="list-disc list-inside text-sm">
|
|
<li>Nextcloud</li>
|
|
<li>LibreCloud Internal DB</li>
|
|
</ul>
|
|
|
|
<DialogFooter>
|
|
<Button className="cursor-pointer" variant="destructive" onClick={() => setDeleteStep('otp')}>
|
|
I understand, continue
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
) : deleteStep === 'otp' ? (
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Confirm your identity</DialogTitle>
|
|
</DialogHeader>
|
|
<DialogDescription>
|
|
Please confirm with a 6 digit code sent to your email.
|
|
</DialogDescription>
|
|
|
|
<div className="flex items-center justify-center gap-6 mt-4">
|
|
<InputOTP maxLength={6} value={deleteOTP} onChange={handleOTPChange}>
|
|
<InputOTPGroup>
|
|
<InputOTPSlot index={0} />
|
|
<InputOTPSlot index={1} />
|
|
<InputOTPSlot index={2} />
|
|
</InputOTPGroup>
|
|
<InputOTPSeparator />
|
|
<InputOTPGroup>
|
|
<InputOTPSlot index={3} />
|
|
<InputOTPSlot index={4} />
|
|
<InputOTPSlot index={5} />
|
|
</InputOTPGroup>
|
|
</InputOTP>
|
|
{deleteOTPLoading ? (
|
|
<Button className="cursor-pointer" variant="outline" disabled>
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
Sending...
|
|
</Button>
|
|
) : showSuccess ? (
|
|
<Button className="cursor-pointer" variant="outline" disabled>
|
|
<Check className="h-4 w-4 text-green-500" />
|
|
Sent!
|
|
</Button>
|
|
) : cooldownSeconds > 0 ? (
|
|
<Button className="cursor-pointer" variant="outline" disabled>
|
|
{cooldownSeconds}s
|
|
</Button>
|
|
) : (
|
|
<Button className="cursor-pointer" variant="outline" onClick={() => sendOTP()}>
|
|
Send OTP
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{otpError && (
|
|
<p className="text-sm text-destructive mt-2">{otpError}</p>
|
|
)}
|
|
|
|
<DialogFooter className="mt-4">
|
|
<Button
|
|
className="cursor-pointer"
|
|
variant="destructive"
|
|
disabled={!deleteOTP || deleteOTP.length !== 6}
|
|
onClick={() => setDeleteStep('confirm')}
|
|
>
|
|
Verify OTP
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
) : (
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Final Confirmation</DialogTitle>
|
|
</DialogHeader>
|
|
<DialogDescription>
|
|
Please review the deletion process carefully.
|
|
</DialogDescription>
|
|
|
|
<p className="text-sm mt-2">Deleting your account will permanently remove all data associated with your account and email. This action cannot be undone.</p>
|
|
|
|
<div className="mt-4 space-y-2">
|
|
<div className="flex justify-between items-center">
|
|
<span>Nextcloud</span>
|
|
{renderStepStatus(deleteSteps.nextcloud)}
|
|
</div>
|
|
<div className="flex justify-between items-center">
|
|
<span>Database</span>
|
|
{renderStepStatus(deleteSteps.database)}
|
|
</div>
|
|
</div>
|
|
|
|
{nextcloudError && (
|
|
<p className="text-sm text-destructive mt-2">{nextcloudError}</p>
|
|
)}
|
|
|
|
<DialogFooter className="mt-4">
|
|
{isDeleting ? (
|
|
<Button className="cursor-pointer" variant="destructive" disabled>
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
Deleting data...
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
className="cursor-pointer"
|
|
variant="destructive"
|
|
disabled={deleteSteps.nextcloud.status === 'error' || deleteSteps.database.status === 'error'}
|
|
onClick={() => deleteAccount()}
|
|
>
|
|
<Trash size={18} />
|
|
Delete Account
|
|
</Button>
|
|
)}
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
)}
|
|
</Dialog>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</>
|
|
)
|
|
} |