@xk2800/nextjs-template 0.6.1-beta.3 → 0.7.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/components/dashboard/deleteAccountCard.tsx +137 -0
- package/dist/activity/queries.d.mts +1 -1
- package/dist/activity/queries.d.ts +1 -1
- package/dist/auth/helpers.js +21 -1
- package/dist/auth/helpers.js.map +1 -1
- package/dist/auth/helpers.mjs +1 -1
- package/dist/auth/index.d.mts +8 -2
- package/dist/auth/index.d.ts +8 -2
- package/dist/auth/index.js +21 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/index.mjs +1 -1
- package/dist/{chunk-V72CFFU2.mjs → chunk-7Q5T4NZ7.mjs} +24 -4
- package/dist/chunk-7Q5T4NZ7.mjs.map +1 -0
- package/dist/db/index.d.mts +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/schema.d.mts +1 -1
- package/dist/db/schema.d.ts +1 -1
- package/dist/{schema-BghJ8N4X.d.mts → schema-BrBmQFkn.d.mts} +1 -1
- package/dist/{schema-BghJ8N4X.d.ts → schema-BrBmQFkn.d.ts} +1 -1
- package/dist/settings/queries.d.mts +1 -1
- package/dist/settings/queries.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-V72CFFU2.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -99,6 +99,7 @@ bun --env-file=.env.production server/test-connection/index.ts
|
|
|
99
99
|
7. Password reset and self-serve email verification, both via Resend-delivered React Email templates
|
|
100
100
|
8. Security email alerts — new-device sign-ins and passkey add/remove, mirrored to the activity log
|
|
101
101
|
9. Cookie-consent banner — "Necessary only" / "Accept all", with a "Cookie settings" link in the footer to change it later. The device fingerprint and Google One Tap only run after "Accept all"
|
|
102
|
+
10. Self-serve account deletion from the dashboard Settings page (password-confirmed, cascades all user data)
|
|
102
103
|
|
|
103
104
|
## WIP
|
|
104
105
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { toast } from 'sonner'
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '../ui/card'
|
|
6
|
+
import { Button } from '../ui/button'
|
|
7
|
+
import { Input } from '../ui/input'
|
|
8
|
+
import { PasswordInput } from '../ui/password-input'
|
|
9
|
+
import {
|
|
10
|
+
AlertDialog,
|
|
11
|
+
AlertDialogCancel,
|
|
12
|
+
AlertDialogContent,
|
|
13
|
+
AlertDialogDescription,
|
|
14
|
+
AlertDialogFooter,
|
|
15
|
+
AlertDialogHeader,
|
|
16
|
+
AlertDialogTitle,
|
|
17
|
+
} from '../ui/alert-dialog'
|
|
18
|
+
import { authClient, useSession } from '../../lib/auth-client'
|
|
19
|
+
|
|
20
|
+
export default function DeleteAccountCard() {
|
|
21
|
+
const { data: session } = useSession()
|
|
22
|
+
const email = session?.user.email ?? ''
|
|
23
|
+
// null = still checking. Credential users confirm with their password;
|
|
24
|
+
// OAuth-only users have none, so they type their email instead and the
|
|
25
|
+
// server requires a recent sign-in (better-auth's freshAge check).
|
|
26
|
+
const [hasPassword, setHasPassword] = useState<boolean | null>(null)
|
|
27
|
+
const [open, setOpen] = useState(false)
|
|
28
|
+
const [password, setPassword] = useState('')
|
|
29
|
+
const [typedEmail, setTypedEmail] = useState('')
|
|
30
|
+
const [deleting, setDeleting] = useState(false)
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
authClient.listAccounts().then(({ data }) => {
|
|
34
|
+
setHasPassword(!!data?.some((a) => a.providerId === 'credential'))
|
|
35
|
+
})
|
|
36
|
+
}, [])
|
|
37
|
+
|
|
38
|
+
const canSubmit = hasPassword ? password.length > 0 : typedEmail.trim().toLowerCase() === email.toLowerCase()
|
|
39
|
+
|
|
40
|
+
const close = (next: boolean) => {
|
|
41
|
+
if (deleting) return
|
|
42
|
+
setOpen(next)
|
|
43
|
+
if (!next) {
|
|
44
|
+
setPassword('')
|
|
45
|
+
setTypedEmail('')
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const deleteAccount = async (e: React.FormEvent) => {
|
|
50
|
+
e.preventDefault()
|
|
51
|
+
if (!canSubmit) return
|
|
52
|
+
setDeleting(true)
|
|
53
|
+
try {
|
|
54
|
+
const { error } = await authClient.deleteUser(hasPassword ? { password } : {})
|
|
55
|
+
if (error) {
|
|
56
|
+
toast.error(
|
|
57
|
+
error.code === 'SESSION_EXPIRED'
|
|
58
|
+
? 'For your security, sign out and sign back in, then try again.'
|
|
59
|
+
: error.message || 'Could not delete account',
|
|
60
|
+
)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
// Full reload, not router.push: drops every cached session/client state.
|
|
64
|
+
window.location.href = '/'
|
|
65
|
+
} finally {
|
|
66
|
+
setDeleting(false)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
<Card className="border-destructive/50">
|
|
73
|
+
<CardHeader>
|
|
74
|
+
<CardTitle>Delete account</CardTitle>
|
|
75
|
+
<CardDescription>
|
|
76
|
+
Permanently delete your account, sessions, passkeys, and activity
|
|
77
|
+
history. This can't be undone.
|
|
78
|
+
</CardDescription>
|
|
79
|
+
</CardHeader>
|
|
80
|
+
<CardContent>
|
|
81
|
+
<Button variant="destructive" onClick={() => setOpen(true)} disabled={hasPassword === null}>
|
|
82
|
+
Delete account
|
|
83
|
+
</Button>
|
|
84
|
+
</CardContent>
|
|
85
|
+
</Card>
|
|
86
|
+
|
|
87
|
+
<AlertDialog open={open} onOpenChange={close}>
|
|
88
|
+
<AlertDialogContent>
|
|
89
|
+
<form onSubmit={deleteAccount} className="space-y-4">
|
|
90
|
+
<AlertDialogHeader>
|
|
91
|
+
<AlertDialogTitle>Delete your account?</AlertDialogTitle>
|
|
92
|
+
<AlertDialogDescription>
|
|
93
|
+
Everything tied to <strong>{email}</strong> is removed immediately
|
|
94
|
+
and you'll be signed out on every device.
|
|
95
|
+
</AlertDialogDescription>
|
|
96
|
+
</AlertDialogHeader>
|
|
97
|
+
|
|
98
|
+
{hasPassword ? (
|
|
99
|
+
<div className="flex flex-col gap-1">
|
|
100
|
+
<label htmlFor="delete-password" className="text-sm font-medium">
|
|
101
|
+
Enter your password to confirm
|
|
102
|
+
</label>
|
|
103
|
+
<PasswordInput
|
|
104
|
+
id="delete-password"
|
|
105
|
+
autoComplete="current-password"
|
|
106
|
+
value={password}
|
|
107
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
108
|
+
autoFocus
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
) : (
|
|
112
|
+
<div className="flex flex-col gap-1">
|
|
113
|
+
<label htmlFor="delete-email" className="text-sm font-medium">
|
|
114
|
+
Type <strong>{email}</strong> to confirm
|
|
115
|
+
</label>
|
|
116
|
+
<Input
|
|
117
|
+
id="delete-email"
|
|
118
|
+
autoComplete="off"
|
|
119
|
+
value={typedEmail}
|
|
120
|
+
onChange={(e) => setTypedEmail(e.target.value)}
|
|
121
|
+
autoFocus
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
<AlertDialogFooter>
|
|
127
|
+
<AlertDialogCancel type="button" disabled={deleting}>Cancel</AlertDialogCancel>
|
|
128
|
+
<Button type="submit" variant="destructive" disabled={!canSubmit || deleting}>
|
|
129
|
+
{deleting ? 'Deleting...' : 'Delete account'}
|
|
130
|
+
</Button>
|
|
131
|
+
</AlertDialogFooter>
|
|
132
|
+
</form>
|
|
133
|
+
</AlertDialogContent>
|
|
134
|
+
</AlertDialog>
|
|
135
|
+
</>
|
|
136
|
+
)
|
|
137
|
+
}
|
package/dist/auth/helpers.js
CHANGED
|
@@ -787,7 +787,13 @@ function createAuth(overrides = {}) {
|
|
|
787
787
|
input: false
|
|
788
788
|
// Prevent users from setting their own role
|
|
789
789
|
}
|
|
790
|
-
}
|
|
790
|
+
},
|
|
791
|
+
// Self-service "Delete account" (dashboard Settings → Danger zone).
|
|
792
|
+
// Credential users confirm with their password; OAuth-only users have
|
|
793
|
+
// no password, so better-auth instead requires a session younger than
|
|
794
|
+
// `freshAge` (default 1 day). Every user-owned table cascades on delete.
|
|
795
|
+
// Impersonation and last-admin guards live in hooks.before below.
|
|
796
|
+
deleteUser: { enabled: true }
|
|
791
797
|
},
|
|
792
798
|
// Advanced options
|
|
793
799
|
advanced: {
|
|
@@ -891,6 +897,20 @@ function createAuth(overrides = {}) {
|
|
|
891
897
|
}
|
|
892
898
|
return;
|
|
893
899
|
}
|
|
900
|
+
if (ctx.path === "/delete-user") {
|
|
901
|
+
const current2 = await api.getSessionFromCtx(ctx, { disableCookieCache: true });
|
|
902
|
+
if (!current2) return;
|
|
903
|
+
if (current2.session.impersonatedBy) {
|
|
904
|
+
throw new api.APIError("FORBIDDEN", { message: "Stop impersonating before deleting an account." });
|
|
905
|
+
}
|
|
906
|
+
if (current2.user.role === "admin") {
|
|
907
|
+
const [{ count }] = await db.select({ count: drizzleOrm.sql`count(*)` }).from(users).where(drizzleOrm.eq(users.role, "admin"));
|
|
908
|
+
if (Number(count) <= 1) {
|
|
909
|
+
throw new api.APIError("FORBIDDEN", { message: "You're the only admin. Promote another user to admin first." });
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
894
914
|
if (ctx.path !== "/admin/stop-impersonating") return;
|
|
895
915
|
const current = await api.getSessionFromCtx(ctx, { disableCookieCache: true });
|
|
896
916
|
if (current == null ? void 0 : current.session.impersonatedBy) {
|