@mdguggenbichler/slugbase-core 0.0.41 → 0.0.43
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/backend/dist/routes/users.d.ts.map +1 -1
- package/backend/dist/routes/users.js +20 -0
- package/backend/dist/routes/users.js.map +1 -1
- package/frontend/src/App.tsx +17 -4
- package/frontend/src/components/AppSidebar.tsx +11 -2
- package/frontend/src/components/GlobalSearch.tsx +1 -1
- package/frontend/src/components/UserDropdown.tsx +1 -1
- package/frontend/src/contexts/AppConfigContext.tsx +6 -0
- package/frontend/src/locales/de.json +8 -1
- package/frontend/src/locales/en.json +8 -1
- package/frontend/src/locales/es.json +8 -1
- package/frontend/src/locales/fr.json +8 -1
- package/frontend/src/locales/it.json +8 -1
- package/frontend/src/locales/ja.json +8 -1
- package/frontend/src/locales/nl.json +8 -1
- package/frontend/src/locales/pl.json +8 -1
- package/frontend/src/locales/pt.json +8 -1
- package/frontend/src/locales/ru.json +8 -1
- package/frontend/src/locales/zh.json +8 -1
- package/frontend/src/pages/Profile.tsx +75 -4
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/routes/users.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/routes/users.ts"],"names":[],"mappings":"AASA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA0KxB,eAAe,MAAM,CAAC"}
|
|
@@ -5,6 +5,7 @@ import { validateEmail, normalizeEmail, validateLength, sanitizeString } from '.
|
|
|
5
5
|
import { v4 as uuidv4 } from 'uuid';
|
|
6
6
|
import crypto from 'crypto';
|
|
7
7
|
import { sendEmailVerificationEmail } from '../utils/email.js';
|
|
8
|
+
import { getClearAuthCookieOptions } from '../config/cookies.js';
|
|
8
9
|
const router = Router();
|
|
9
10
|
router.use(requireAuth());
|
|
10
11
|
// List users for sharing (same tenant/org). No admin required. Used by sharing modal.
|
|
@@ -128,5 +129,24 @@ router.put('/me', async (req, res) => {
|
|
|
128
129
|
res.status(500).json({ error: error.message });
|
|
129
130
|
}
|
|
130
131
|
});
|
|
132
|
+
// Delete current user account. Cascades via DB FKs (api_tokens, team_members, etc.).
|
|
133
|
+
// Clears auth cookie so client is logged out.
|
|
134
|
+
router.delete('/me', async (req, res) => {
|
|
135
|
+
const authReq = req;
|
|
136
|
+
try {
|
|
137
|
+
const userId = authReq.user.id;
|
|
138
|
+
const user = await queryOne('SELECT id FROM users WHERE id = ?', [userId]);
|
|
139
|
+
if (!user) {
|
|
140
|
+
return res.status(404).json({ error: 'User not found' });
|
|
141
|
+
}
|
|
142
|
+
await execute('DELETE FROM users WHERE id = ?', [userId]);
|
|
143
|
+
const clearOpts = getClearAuthCookieOptions();
|
|
144
|
+
res.clearCookie('token', clearOpts);
|
|
145
|
+
res.json({ message: 'Account deleted' });
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
res.status(500).json({ error: error.message });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
131
151
|
export default router;
|
|
132
152
|
//# sourceMappingURL=users.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"users.js","sourceRoot":"","sources":["../../src/routes/users.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAe,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACvG,OAAO,EAAE,EAAE,IAAI,MAAM,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"users.js","sourceRoot":"","sources":["../../src/routes/users.ts"],"names":[],"mappings":"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"}
|
package/frontend/src/App.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-route
|
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
|
5
5
|
import { AppConfigProvider, useAppConfig } from './contexts/AppConfigContext';
|
|
6
|
-
import { PlanProvider } from './contexts/PlanContext';
|
|
6
|
+
import { PlanProvider, usePlan } from './contexts/PlanContext';
|
|
7
7
|
import { ToastProvider } from './components/ui/Toast';
|
|
8
8
|
import { TooltipProvider } from './components/ui/tooltip-base';
|
|
9
9
|
import Layout from './components/Layout';
|
|
@@ -50,6 +50,17 @@ function AdminRoute({ children }: { children: React.ReactNode }) {
|
|
|
50
50
|
return <>{children}</>;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/** Redirect /admin to first visible admin tab (e.g. in cloud free plan, Users/Teams are hidden so redirect to ai or billing). */
|
|
54
|
+
function AdminIndexRedirect() {
|
|
55
|
+
const planInfo = usePlan();
|
|
56
|
+
const { extraAdminNavItems } = useAppConfig();
|
|
57
|
+
const showUsersAndTeams = !planInfo || planInfo.canShareWithTeams;
|
|
58
|
+
const firstPath = showUsersAndTeams
|
|
59
|
+
? 'members'
|
|
60
|
+
: (extraAdminNavItems?.[0]?.path ?? 'ai');
|
|
61
|
+
return <Navigate to={firstPath} replace />;
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
function SharedRedirect() {
|
|
54
65
|
const { pathPrefixForLinks } = useAppConfig();
|
|
55
66
|
const to = `${pathPrefixForLinks || ''}/bookmarks?scope=shared_with_me`.replace(/\/+/g, '/') || '/bookmarks?scope=shared_with_me';
|
|
@@ -122,7 +133,7 @@ function AppRoutes() {
|
|
|
122
133
|
<Route path="go-preferences" element={<GoPreferences />} />
|
|
123
134
|
<Route path="search-engine-guide" element={<SearchEngineGuide />} />
|
|
124
135
|
<Route path="admin" element={<AdminRoute><AdminLayout /></AdminRoute>}>
|
|
125
|
-
<Route index element={<
|
|
136
|
+
<Route index element={<AdminIndexRedirect />} />
|
|
126
137
|
<Route path="members" element={<AdminMembersPage />} />
|
|
127
138
|
<Route path="teams" element={<AdminTeamsPage />} />
|
|
128
139
|
{hideAdminOidcAndSmtp ? (
|
|
@@ -250,9 +261,11 @@ export interface AppProps {
|
|
|
250
261
|
extraAdminRoutes?: { path: string; element: ReactNode }[];
|
|
251
262
|
/** Optional extra admin nav items (e.g. cloud billing). Passed to sidebar when extraAdminRoutes are used. */
|
|
252
263
|
extraAdminNavItems?: { path: string; label: string }[];
|
|
264
|
+
/** Optional guard for profile/account deletion (e.g. cloud blocks billing owner). */
|
|
265
|
+
profileDeleteGuard?: () => Promise<{ allowed: boolean; message?: string }>;
|
|
253
266
|
}
|
|
254
267
|
|
|
255
|
-
function App({ basePath, apiBaseUrl, routerBasename, skipSetupFlow, hideAdminOidcAndSmtp, adminAiOnlyToggle, extraAdminRoutes, extraAdminNavItems }: AppProps = {}) {
|
|
268
|
+
function App({ basePath, apiBaseUrl, routerBasename, skipSetupFlow, hideAdminOidcAndSmtp, adminAiOnlyToggle, extraAdminRoutes, extraAdminNavItems, profileDeleteGuard }: AppProps = {}) {
|
|
256
269
|
const appRootPath = routerBasename !== undefined ? '/' : (basePath === '/' || !basePath ? '/' : basePath);
|
|
257
270
|
const pathPrefixForLinks = routerBasename !== undefined ? '' : (basePath ?? '');
|
|
258
271
|
const content = (
|
|
@@ -268,7 +281,7 @@ function App({ basePath, apiBaseUrl, routerBasename, skipSetupFlow, hideAdminOid
|
|
|
268
281
|
);
|
|
269
282
|
return (
|
|
270
283
|
<AppErrorBoundary>
|
|
271
|
-
<AppConfigProvider appBasePath={basePath} apiBaseUrl={apiBaseUrl} appRootPath={appRootPath} skipSetupFlow={skipSetupFlow} pathPrefixForLinks={pathPrefixForLinks} hideAdminOidcAndSmtp={hideAdminOidcAndSmtp} adminAiOnlyToggle={adminAiOnlyToggle} extraAdminNavItems={extraAdminNavItems} extraAdminRoutes={extraAdminRoutes}>
|
|
284
|
+
<AppConfigProvider appBasePath={basePath} apiBaseUrl={apiBaseUrl} appRootPath={appRootPath} skipSetupFlow={skipSetupFlow} pathPrefixForLinks={pathPrefixForLinks} hideAdminOidcAndSmtp={hideAdminOidcAndSmtp} adminAiOnlyToggle={adminAiOnlyToggle} extraAdminNavItems={extraAdminNavItems} extraAdminRoutes={extraAdminRoutes} profileDeleteGuard={profileDeleteGuard}>
|
|
272
285
|
{routerBasename !== undefined ? (
|
|
273
286
|
content
|
|
274
287
|
) : (
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
useSidebar,
|
|
31
31
|
} from './ui/sidebar';
|
|
32
32
|
import { useAppConfig } from '../contexts/AppConfigContext';
|
|
33
|
+
import { usePlan } from '../contexts/PlanContext';
|
|
33
34
|
import type { User } from '../contexts/AuthContext';
|
|
34
35
|
import { cn } from '../lib/utils';
|
|
35
36
|
|
|
@@ -45,6 +46,7 @@ export default function AppSidebar({ user, version = null }: AppSidebarProps) {
|
|
|
45
46
|
const location = useLocation();
|
|
46
47
|
const pathname = location.pathname;
|
|
47
48
|
const { appBasePath, pathPrefixForLinks, hideAdminOidcAndSmtp, extraAdminNavItems } = useAppConfig();
|
|
49
|
+
const planInfo = usePlan();
|
|
48
50
|
const { setOpenMobile, toggleSidebar, isMobile, state } = useSidebar();
|
|
49
51
|
const prefix = pathPrefixForLinks || '';
|
|
50
52
|
// For active matching use pathPrefixForLinks so it matches useLocation().pathname (e.g. when Router has basename="/app", pathname is "/bookmarks" not "/app/bookmarks").
|
|
@@ -52,9 +54,16 @@ export default function AppSidebar({ user, version = null }: AppSidebarProps) {
|
|
|
52
54
|
const adminBaseFull = `${pathBaseForActive}/admin`.replace(/\/+/g, '/') || '/admin';
|
|
53
55
|
const adminBaseLink = `${prefix}/admin`.replace(/\/+/g, '/') || '/admin';
|
|
54
56
|
|
|
57
|
+
// In cloud, show Users and Teams only on team plan; self-hosted (planInfo null) always shows them.
|
|
58
|
+
const showAdminUsersAndTeams = !planInfo || planInfo.canShareWithTeams;
|
|
59
|
+
|
|
55
60
|
const adminNavItems = [
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
...(showAdminUsersAndTeams
|
|
62
|
+
? [
|
|
63
|
+
{ pathForLink: `${adminBaseLink}/members`, pathForActive: `${adminBaseFull}/members`, label: t('admin.users'), icon: Users },
|
|
64
|
+
{ pathForLink: `${adminBaseLink}/teams`, pathForActive: `${adminBaseFull}/teams`, label: t('admin.teams'), icon: UserCog },
|
|
65
|
+
]
|
|
66
|
+
: []),
|
|
58
67
|
...(!hideAdminOidcAndSmtp
|
|
59
68
|
? [
|
|
60
69
|
{ pathForLink: `${adminBaseLink}/oidc`, pathForActive: `${adminBaseFull}/oidc`, label: t('admin.oidcProviders'), icon: Key },
|
|
@@ -52,7 +52,7 @@ export default function GlobalSearch() {
|
|
|
52
52
|
{ type: 'navigation', title: t('folders.title'), path: `${prefix}/folders`.replace(/\/+/g, '/') || '/folders', id: 'nav-folders' },
|
|
53
53
|
{ type: 'navigation', title: t('tags.title'), path: `${prefix}/tags`.replace(/\/+/g, '/') || '/tags', id: 'nav-tags' },
|
|
54
54
|
{ type: 'navigation', title: t('shared.title'), path: `${prefix}/shared`.replace(/\/+/g, '/') || '/shared', id: 'nav-shared' },
|
|
55
|
-
...(showAdmin ? [{ type: 'navigation' as const, title: t('admin.title'), path: `${prefix}/admin
|
|
55
|
+
...(showAdmin ? [{ type: 'navigation' as const, title: t('admin.title'), path: `${prefix}/admin`.replace(/\/+/g, '/') || '/admin', id: 'nav-admin' }] : []),
|
|
56
56
|
], [showAdmin, t, prefix]);
|
|
57
57
|
|
|
58
58
|
const actionItems: SearchResult[] = useMemo(() => [
|
|
@@ -67,7 +67,7 @@ export default function UserDropdown({ user }: UserDropdownProps) {
|
|
|
67
67
|
</DropdownMenuItem>
|
|
68
68
|
{showAdmin && (
|
|
69
69
|
<DropdownMenuItem asChild>
|
|
70
|
-
<Link to={`${prefix}/admin/
|
|
70
|
+
<Link to={`${prefix}/admin`.replace(/\/+/g, '/') || '/admin'} className="flex items-center gap-2 cursor-pointer">
|
|
71
71
|
<Settings className="h-4 w-4" />
|
|
72
72
|
{t('admin.title')}
|
|
73
73
|
</Link>
|
|
@@ -23,6 +23,8 @@ export interface AppConfig {
|
|
|
23
23
|
extraAdminNavItems?: { path: string; label: string }[];
|
|
24
24
|
/** Optional extra admin route definitions (e.g. cloud billing). Generic extension. */
|
|
25
25
|
extraAdminRoutes?: { path: string; element: React.ReactNode }[];
|
|
26
|
+
/** Optional guard called before allowing profile/account deletion (e.g. cloud blocks billing owner). Returns { allowed, message }. */
|
|
27
|
+
profileDeleteGuard?: () => Promise<{ allowed: boolean; message?: string }>;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
const defaultConfig: AppConfig = {
|
|
@@ -50,6 +52,7 @@ export function AppConfigProvider({
|
|
|
50
52
|
canShareWithTeams,
|
|
51
53
|
extraAdminNavItems,
|
|
52
54
|
extraAdminRoutes,
|
|
55
|
+
profileDeleteGuard,
|
|
53
56
|
}: {
|
|
54
57
|
children: React.ReactNode;
|
|
55
58
|
appBasePath?: string;
|
|
@@ -72,6 +75,8 @@ export function AppConfigProvider({
|
|
|
72
75
|
extraAdminNavItems?: { path: string; label: string }[];
|
|
73
76
|
/** Optional extra admin route definitions (e.g. cloud billing). Generic extension. */
|
|
74
77
|
extraAdminRoutes?: { path: string; element: React.ReactNode }[];
|
|
78
|
+
/** Optional guard for profile/account deletion (e.g. cloud blocks billing owner). */
|
|
79
|
+
profileDeleteGuard?: () => Promise<{ allowed: boolean; message?: string }>;
|
|
75
80
|
}) {
|
|
76
81
|
const base = appBasePath ?? defaultConfig.appBasePath;
|
|
77
82
|
const value: AppConfig = {
|
|
@@ -87,6 +92,7 @@ export function AppConfigProvider({
|
|
|
87
92
|
canShareWithTeams,
|
|
88
93
|
extraAdminNavItems,
|
|
89
94
|
extraAdminRoutes,
|
|
95
|
+
profileDeleteGuard,
|
|
90
96
|
};
|
|
91
97
|
return <AppConfigContext.Provider value={value}>{children}</AppConfigContext.Provider>;
|
|
92
98
|
}
|
|
@@ -307,7 +307,14 @@
|
|
|
307
307
|
"yourTokens": "Deine Tokens",
|
|
308
308
|
"aiSuggestions": "KI-Vorschläge",
|
|
309
309
|
"aiSuggestionsDescription": "Titel, Tags und Slug beim Anlegen von Lesezeichen vorschlagen",
|
|
310
|
-
"aiSuggestionsUpgradePlan": "Verfügbar im Personal-, Team- und Early-Supporter-Plan."
|
|
310
|
+
"aiSuggestionsUpgradePlan": "Verfügbar im Personal-, Team- und Early-Supporter-Plan.",
|
|
311
|
+
"dangerZone": "Gefahrenzone",
|
|
312
|
+
"dangerZoneDescription": "Unwiderrufliche Aktionen. Diese können nicht rückgängig gemacht werden.",
|
|
313
|
+
"deleteAccount": "Mein Konto löschen",
|
|
314
|
+
"deleteAccountDescription": "Ihr Konto und alle Daten werden dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.",
|
|
315
|
+
"deleteAccountConfirm": "Möchten Sie Ihr Konto wirklich löschen? Alle Lesezeichen, Ordner, Tags und Daten werden dauerhaft gelöscht. Dies kann nicht rückgängig gemacht werden.",
|
|
316
|
+
"deleteAccountSuccess": "Ihr Konto wurde gelöscht.",
|
|
317
|
+
"deleteAccountGuardMessage": "Sie können Ihr Konto nicht löschen. Übertragen Sie zuerst die Inhaberschaft oder regeln Sie die Abrechnung."
|
|
311
318
|
},
|
|
312
319
|
"common": {
|
|
313
320
|
"loading": "Lädt...",
|
|
@@ -334,7 +334,14 @@
|
|
|
334
334
|
"yourTokens": "Your tokens",
|
|
335
335
|
"aiSuggestions": "AI suggestions",
|
|
336
336
|
"aiSuggestionsDescription": "Suggest title, tags, and slug when creating bookmarks",
|
|
337
|
-
"aiSuggestionsUpgradePlan": "Available on Personal, Team, and Early Supporter plans."
|
|
337
|
+
"aiSuggestionsUpgradePlan": "Available on Personal, Team, and Early Supporter plans.",
|
|
338
|
+
"dangerZone": "Danger zone",
|
|
339
|
+
"dangerZoneDescription": "Irreversible actions. These cannot be undone.",
|
|
340
|
+
"deleteAccount": "Delete my account",
|
|
341
|
+
"deleteAccountDescription": "Permanently delete your account and all your data. This action cannot be undone.",
|
|
342
|
+
"deleteAccountConfirm": "Are you sure you want to delete your account? All your bookmarks, folders, tags, and data will be permanently deleted. This cannot be undone.",
|
|
343
|
+
"deleteAccountSuccess": "Your account has been deleted.",
|
|
344
|
+
"deleteAccountGuardMessage": "You cannot delete your account. Transfer ownership or resolve billing first."
|
|
338
345
|
},
|
|
339
346
|
"common": {
|
|
340
347
|
"loading": "Loading...",
|
|
@@ -308,7 +308,14 @@
|
|
|
308
308
|
"yourTokens": "Tus tokens",
|
|
309
309
|
"aiSuggestions": "Sugerencias con IA",
|
|
310
310
|
"aiSuggestionsDescription": "Sugerir título, etiquetas y slug al crear marcadores",
|
|
311
|
-
"aiSuggestionsUpgradePlan": "Disponible en planes Personal, Team y Early Supporter."
|
|
311
|
+
"aiSuggestionsUpgradePlan": "Disponible en planes Personal, Team y Early Supporter.",
|
|
312
|
+
"dangerZone": "Zona de peligro",
|
|
313
|
+
"dangerZoneDescription": "Acciones irreversibles. No se pueden deshacer.",
|
|
314
|
+
"deleteAccount": "Eliminar mi cuenta",
|
|
315
|
+
"deleteAccountDescription": "Elimina permanentemente tu cuenta y todos tus datos. Esta acción no se puede deshacer.",
|
|
316
|
+
"deleteAccountConfirm": "¿Estás seguro de que quieres eliminar tu cuenta? Todos tus marcadores, carpetas, etiquetas y datos se eliminarán permanentemente. No se puede deshacer.",
|
|
317
|
+
"deleteAccountSuccess": "Tu cuenta ha sido eliminada.",
|
|
318
|
+
"deleteAccountGuardMessage": "No puedes eliminar tu cuenta. Transfiere la propiedad o resuelve la facturación primero."
|
|
312
319
|
},
|
|
313
320
|
"common": {
|
|
314
321
|
"loading": "Cargando...",
|
|
@@ -232,7 +232,14 @@
|
|
|
232
232
|
"viewApiDocs": "Voir la documentation API",
|
|
233
233
|
"noTokens": "Aucun jeton API",
|
|
234
234
|
"noTokensDescription": "Créez un jeton pour vous authentifier avec l'API REST depuis des scripts, CLI ou CI/CD.",
|
|
235
|
-
"yourTokens": "Vos jetons"
|
|
235
|
+
"yourTokens": "Vos jetons",
|
|
236
|
+
"dangerZone": "Zone de danger",
|
|
237
|
+
"dangerZoneDescription": "Actions irréversibles. Elles ne peuvent pas être annulées.",
|
|
238
|
+
"deleteAccount": "Supprimer mon compte",
|
|
239
|
+
"deleteAccountDescription": "Supprimez définitivement votre compte et toutes vos données. Cette action est irréversible.",
|
|
240
|
+
"deleteAccountConfirm": "Êtes-vous sûr de vouloir supprimer votre compte ? Tous vos favoris, dossiers, tags et données seront définitivement supprimés. Cette action est irréversible.",
|
|
241
|
+
"deleteAccountSuccess": "Votre compte a été supprimé.",
|
|
242
|
+
"deleteAccountGuardMessage": "Vous ne pouvez pas supprimer votre compte. Transférez la propriété ou réglez la facturation d'abord."
|
|
236
243
|
},
|
|
237
244
|
"common": {
|
|
238
245
|
"loading": "Chargement...",
|
|
@@ -217,7 +217,14 @@
|
|
|
217
217
|
"viewApiDocs": "Visualizza documentazione API",
|
|
218
218
|
"noTokens": "Nessun token API",
|
|
219
219
|
"noTokensDescription": "Crea un token per autenticarti con l'API REST da script, CLI o CI/CD.",
|
|
220
|
-
"yourTokens": "I tuoi token"
|
|
220
|
+
"yourTokens": "I tuoi token",
|
|
221
|
+
"dangerZone": "Zona pericolosa",
|
|
222
|
+
"dangerZoneDescription": "Azioni irreversibili. Non possono essere annullate.",
|
|
223
|
+
"deleteAccount": "Elimina il mio account",
|
|
224
|
+
"deleteAccountDescription": "Elimina definitivamente il tuo account e tutti i tuoi dati. Questa azione non può essere annullata.",
|
|
225
|
+
"deleteAccountConfirm": "Sei sicuro di voler eliminare il tuo account? Tutti i segnalibri, cartelle, tag e dati verranno eliminati definitivamente. Non può essere annullato.",
|
|
226
|
+
"deleteAccountSuccess": "Il tuo account è stato eliminato.",
|
|
227
|
+
"deleteAccountGuardMessage": "Non puoi eliminare il tuo account. Trasferisci prima la proprietà o risolvi la fatturazione."
|
|
221
228
|
},
|
|
222
229
|
"common": {
|
|
223
230
|
"loading": "Caricamento...",
|
|
@@ -217,7 +217,14 @@
|
|
|
217
217
|
"viewApiDocs": "APIドキュメントを見る",
|
|
218
218
|
"noTokens": "APIトークンがありません",
|
|
219
219
|
"noTokensDescription": "スクリプト、CLI、CI/CDからREST APIで認証するためのトークンを作成します。",
|
|
220
|
-
"yourTokens": "あなたのトークン"
|
|
220
|
+
"yourTokens": "あなたのトークン",
|
|
221
|
+
"dangerZone": "危険ゾーン",
|
|
222
|
+
"dangerZoneDescription": "元に戻せない操作です。取り消すことはできません。",
|
|
223
|
+
"deleteAccount": "アカウントを削除",
|
|
224
|
+
"deleteAccountDescription": "アカウントとすべてのデータが完全に削除されます。この操作は元に戻せません。",
|
|
225
|
+
"deleteAccountConfirm": "アカウントを削除してもよろしいですか?ブックマーク、フォルダ、タグ、データはすべて完全に削除され、元に戻せません。",
|
|
226
|
+
"deleteAccountSuccess": "アカウントが削除されました。",
|
|
227
|
+
"deleteAccountGuardMessage": "アカウントを削除できません。まず所有権を譲渡するか、請求を解決してください。"
|
|
221
228
|
},
|
|
222
229
|
"common": {
|
|
223
230
|
"loading": "読み込み中...",
|
|
@@ -232,7 +232,14 @@
|
|
|
232
232
|
"viewApiDocs": "API-documentatie bekijken",
|
|
233
233
|
"noTokens": "Geen API-tokens",
|
|
234
234
|
"noTokensDescription": "Maak een token om te authenticeren met de REST API vanuit scripts, CLI of CI/CD.",
|
|
235
|
-
"yourTokens": "Je tokens"
|
|
235
|
+
"yourTokens": "Je tokens",
|
|
236
|
+
"dangerZone": "Gevarenzone",
|
|
237
|
+
"dangerZoneDescription": "Onomkeerbare acties. Deze kunnen niet ongedaan worden gemaakt.",
|
|
238
|
+
"deleteAccount": "Mijn account verwijderen",
|
|
239
|
+
"deleteAccountDescription": "Verwijder je account en alle gegevens permanent. Deze actie kan niet ongedaan worden gemaakt.",
|
|
240
|
+
"deleteAccountConfirm": "Weet je zeker dat je je account wilt verwijderen? Al je bladwijzers, mappen, tags en gegevens worden permanent verwijderd. Dit kan niet ongedaan worden gemaakt.",
|
|
241
|
+
"deleteAccountSuccess": "Je account is verwijderd.",
|
|
242
|
+
"deleteAccountGuardMessage": "Je kunt je account niet verwijderen. Draag eerst het eigendom over of los de facturatie op."
|
|
236
243
|
},
|
|
237
244
|
"common": {
|
|
238
245
|
"loading": "Laden...",
|
|
@@ -217,7 +217,14 @@
|
|
|
217
217
|
"viewApiDocs": "Zobacz dokumentację API",
|
|
218
218
|
"noTokens": "Brak tokenów API",
|
|
219
219
|
"noTokensDescription": "Utwórz token, aby uwierzytelnić się w REST API ze skryptów, CLI lub CI/CD.",
|
|
220
|
-
"yourTokens": "Twoje tokeny"
|
|
220
|
+
"yourTokens": "Twoje tokeny",
|
|
221
|
+
"dangerZone": "Strefa niebezpieczna",
|
|
222
|
+
"dangerZoneDescription": "Nieodwracalne czynności. Nie można ich cofnąć.",
|
|
223
|
+
"deleteAccount": "Usuń moje konto",
|
|
224
|
+
"deleteAccountDescription": "Trwale usuń swoje konto i wszystkie dane. Tej czynności nie można cofnąć.",
|
|
225
|
+
"deleteAccountConfirm": "Czy na pewno chcesz usunąć konto? Wszystkie zakładki, foldery, tagi i dane zostaną trwale usunięte. Nie można tego cofnąć.",
|
|
226
|
+
"deleteAccountSuccess": "Twoje konto zostało usunięte.",
|
|
227
|
+
"deleteAccountGuardMessage": "Nie możesz usunąć konta. Najpierw przenieś własność lub rozwiąż kwestię rozliczeń."
|
|
221
228
|
},
|
|
222
229
|
"common": {
|
|
223
230
|
"loading": "Ładowanie...",
|
|
@@ -217,7 +217,14 @@
|
|
|
217
217
|
"viewApiDocs": "Ver documentação da API",
|
|
218
218
|
"noTokens": "Nenhum token de API",
|
|
219
219
|
"noTokensDescription": "Crie um token para se autenticar na API REST a partir de scripts, CLI ou CI/CD.",
|
|
220
|
-
"yourTokens": "Seus tokens"
|
|
220
|
+
"yourTokens": "Seus tokens",
|
|
221
|
+
"dangerZone": "Zona de perigo",
|
|
222
|
+
"dangerZoneDescription": "Ações irreversíveis. Não podem ser desfeitas.",
|
|
223
|
+
"deleteAccount": "Excluir minha conta",
|
|
224
|
+
"deleteAccountDescription": "Exclua permanentemente sua conta e todos os seus dados. Esta ação não pode ser desfeita.",
|
|
225
|
+
"deleteAccountConfirm": "Tem certeza de que deseja excluir sua conta? Todos os seus favoritos, pastas, tags e dados serão excluídos permanentemente. Isso não pode ser desfeito.",
|
|
226
|
+
"deleteAccountSuccess": "Sua conta foi excluída.",
|
|
227
|
+
"deleteAccountGuardMessage": "Você não pode excluir sua conta. Transfira a propriedade ou resolva a cobrança primeiro."
|
|
221
228
|
},
|
|
222
229
|
"common": {
|
|
223
230
|
"loading": "Carregando...",
|
|
@@ -217,7 +217,14 @@
|
|
|
217
217
|
"viewApiDocs": "Просмотр документации API",
|
|
218
218
|
"noTokens": "Нет токенов API",
|
|
219
219
|
"noTokensDescription": "Создайте токен для аутентификации в REST API из скриптов, CLI или CI/CD.",
|
|
220
|
-
"yourTokens": "Ваши токены"
|
|
220
|
+
"yourTokens": "Ваши токены",
|
|
221
|
+
"dangerZone": "Опасная зона",
|
|
222
|
+
"dangerZoneDescription": "Необратимые действия. Их нельзя отменить.",
|
|
223
|
+
"deleteAccount": "Удалить мой аккаунт",
|
|
224
|
+
"deleteAccountDescription": "Навсегда удалить аккаунт и все данные. Это действие нельзя отменить.",
|
|
225
|
+
"deleteAccountConfirm": "Вы уверены, что хотите удалить аккаунт? Все закладки, папки, теги и данные будут удалены безвозвратно. Это нельзя отменить.",
|
|
226
|
+
"deleteAccountSuccess": "Ваш аккаунт удалён.",
|
|
227
|
+
"deleteAccountGuardMessage": "Вы не можете удалить аккаунт. Сначала передайте право владения или решите вопрос с оплатой."
|
|
221
228
|
},
|
|
222
229
|
"common": {
|
|
223
230
|
"loading": "Загрузка...",
|
|
@@ -217,7 +217,14 @@
|
|
|
217
217
|
"viewApiDocs": "查看 API 文档",
|
|
218
218
|
"noTokens": "暂无 API 令牌",
|
|
219
219
|
"noTokensDescription": "创建令牌以通过脚本、CLI 或 CI/CD 进行 REST API 身份验证。",
|
|
220
|
-
"yourTokens": "您的令牌"
|
|
220
|
+
"yourTokens": "您的令牌",
|
|
221
|
+
"dangerZone": "危险区域",
|
|
222
|
+
"dangerZoneDescription": "不可逆操作。无法撤销。",
|
|
223
|
+
"deleteAccount": "删除我的账户",
|
|
224
|
+
"deleteAccountDescription": "永久删除您的账户及所有数据。此操作无法撤销。",
|
|
225
|
+
"deleteAccountConfirm": "确定要删除您的账户吗?您的所有书签、文件夹、标签和数据将被永久删除。无法撤销。",
|
|
226
|
+
"deleteAccountSuccess": "您的账户已删除。",
|
|
227
|
+
"deleteAccountGuardMessage": "您无法删除账户。请先转移所有权或解决账单问题。"
|
|
221
228
|
},
|
|
222
229
|
"common": {
|
|
223
230
|
"loading": "加载中...",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { Link } from 'react-router-dom';
|
|
3
|
+
import { Link, useNavigate } from 'react-router-dom';
|
|
4
4
|
import { useAuth } from '../contexts/AuthContext';
|
|
5
5
|
import { useAppConfig } from '../contexts/AppConfigContext';
|
|
6
6
|
import { usePlan } from '../contexts/PlanContext';
|
|
7
|
-
import { AlertCircle, Key } from 'lucide-react';
|
|
7
|
+
import { AlertCircle, Key, AlertTriangle } from 'lucide-react';
|
|
8
8
|
import Select from '../components/ui/Select';
|
|
9
9
|
import Button from '../components/ui/Button';
|
|
10
10
|
import { Switch } from '../components/ui/switch';
|
|
@@ -61,9 +61,11 @@ function SettingsRow({
|
|
|
61
61
|
|
|
62
62
|
export default function Profile() {
|
|
63
63
|
const { t } = useTranslation();
|
|
64
|
-
const
|
|
64
|
+
const navigate = useNavigate();
|
|
65
|
+
const { pathPrefixForLinks, profileDeleteGuard } = useAppConfig();
|
|
65
66
|
const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
|
|
66
|
-
const
|
|
67
|
+
const loginPath = `${prefix}/login`.replace(/\/+/g, '/') || '/login';
|
|
68
|
+
const { user, updateUser, checkAuth, logout } = useAuth();
|
|
67
69
|
const planInfo = usePlan();
|
|
68
70
|
const isFreePlan = planInfo?.plan === 'free';
|
|
69
71
|
const { showToast } = useToast();
|
|
@@ -83,6 +85,8 @@ export default function Profile() {
|
|
|
83
85
|
const [tokensLoading, setTokensLoading] = useState(true);
|
|
84
86
|
const [createTokenOpen, setCreateTokenOpen] = useState(false);
|
|
85
87
|
const [revokeTokenId, setRevokeTokenId] = useState<string | null>(null);
|
|
88
|
+
const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
|
|
89
|
+
const [deleteAccountLoading, setDeleteAccountLoading] = useState(false);
|
|
86
90
|
|
|
87
91
|
useEffect(() => {
|
|
88
92
|
if (user) {
|
|
@@ -173,6 +177,38 @@ export default function Profile() {
|
|
|
173
177
|
}
|
|
174
178
|
}
|
|
175
179
|
|
|
180
|
+
async function handleDeleteAccountClick() {
|
|
181
|
+
if (profileDeleteGuard) {
|
|
182
|
+
try {
|
|
183
|
+
const result = await profileDeleteGuard();
|
|
184
|
+
if (!result.allowed) {
|
|
185
|
+
showToast(result.message || t('profile.deleteAccountGuardMessage'), 'error');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
showToast(t('common.error'), 'error');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
setDeleteAccountOpen(true);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function handleConfirmDeleteAccount() {
|
|
197
|
+
setDeleteAccountLoading(true);
|
|
198
|
+
try {
|
|
199
|
+
await api.delete('/users/me');
|
|
200
|
+
setDeleteAccountOpen(false);
|
|
201
|
+
showToast(t('profile.deleteAccountSuccess'), 'success');
|
|
202
|
+
await logout();
|
|
203
|
+
navigate(loginPath, { replace: true });
|
|
204
|
+
} catch (err: unknown) {
|
|
205
|
+
const e = err as { response?: { data?: { error?: string } } };
|
|
206
|
+
showToast(e.response?.data?.error || t('common.error'), 'error');
|
|
207
|
+
} finally {
|
|
208
|
+
setDeleteAccountLoading(false);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
176
212
|
function formatDate(iso: string) {
|
|
177
213
|
try {
|
|
178
214
|
return new Date(iso).toLocaleDateString(undefined, {
|
|
@@ -582,6 +618,32 @@ export default function Profile() {
|
|
|
582
618
|
)}
|
|
583
619
|
</CardContent>
|
|
584
620
|
</Card>
|
|
621
|
+
|
|
622
|
+
{/* Danger Zone: delete account */}
|
|
623
|
+
<Card className="border border-destructive/50 bg-muted/30 shadow-sm">
|
|
624
|
+
<CardHeader>
|
|
625
|
+
<CardTitle className="flex items-center gap-2 text-destructive">
|
|
626
|
+
<AlertTriangle className="h-5 w-5" />
|
|
627
|
+
{t('profile.dangerZone')}
|
|
628
|
+
</CardTitle>
|
|
629
|
+
<CardDescription>{t('profile.dangerZoneDescription')}</CardDescription>
|
|
630
|
+
</CardHeader>
|
|
631
|
+
<CardContent>
|
|
632
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
633
|
+
{t('profile.deleteAccountDescription')}
|
|
634
|
+
</p>
|
|
635
|
+
<Button
|
|
636
|
+
type="button"
|
|
637
|
+
variant="ghost"
|
|
638
|
+
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
|
639
|
+
onClick={handleDeleteAccountClick}
|
|
640
|
+
disabled={deleteAccountLoading}
|
|
641
|
+
aria-label={t('profile.deleteAccount')}
|
|
642
|
+
>
|
|
643
|
+
{t('profile.deleteAccount')}
|
|
644
|
+
</Button>
|
|
645
|
+
</CardContent>
|
|
646
|
+
</Card>
|
|
585
647
|
</div>
|
|
586
648
|
|
|
587
649
|
<CreateTokenModal
|
|
@@ -598,6 +660,15 @@ export default function Profile() {
|
|
|
598
660
|
onConfirm={() => revokeTokenId && handleRevokeToken(revokeTokenId)}
|
|
599
661
|
onCancel={() => setRevokeTokenId(null)}
|
|
600
662
|
/>
|
|
663
|
+
<ConfirmDialog
|
|
664
|
+
isOpen={deleteAccountOpen}
|
|
665
|
+
title={t('profile.deleteAccount')}
|
|
666
|
+
message={t('profile.deleteAccountConfirm')}
|
|
667
|
+
variant="danger"
|
|
668
|
+
confirmText={t('profile.deleteAccount')}
|
|
669
|
+
onConfirm={handleConfirmDeleteAccount}
|
|
670
|
+
onCancel={() => setDeleteAccountOpen(false)}
|
|
671
|
+
/>
|
|
601
672
|
</div>
|
|
602
673
|
);
|
|
603
674
|
}
|