@mdguggenbichler/slugbase-core 0.0.40 → 0.0.42

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.
@@ -1 +1 @@
1
- {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/routes/users.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAuJxB,eAAe,MAAM,CAAC"}
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":"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"}
1
+ {"version":3,"file":"users.js","sourceRoot":"","sources":["../../src/routes/users.ts"],"names":[],"mappings":"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"}
@@ -50,18 +50,6 @@ function AdminRoute({ children }: { children: React.ReactNode }) {
50
50
  return <>{children}</>;
51
51
  }
52
52
 
53
- function ExtraAdminRoutes() {
54
- const { extraAdminRoutes } = useAppConfig();
55
- if (!extraAdminRoutes?.length) return null;
56
- return (
57
- <>
58
- {extraAdminRoutes.map(({ path, element }) => (
59
- <Route key={path} path={path} element={element} />
60
- ))}
61
- </>
62
- );
63
- }
64
-
65
53
  function SharedRedirect() {
66
54
  const { pathPrefixForLinks } = useAppConfig();
67
55
  const to = `${pathPrefixForLinks || ''}/bookmarks?scope=shared_with_me`.replace(/\/+/g, '/') || '/bookmarks?scope=shared_with_me';
@@ -90,7 +78,7 @@ class LoginRouteErrorBoundary extends Component<{ children: ReactNode }, { error
90
78
  function AppRoutes() {
91
79
  const { user, loading } = useAuth();
92
80
  const { t } = useTranslation();
93
- const { appRootPath, skipSetupFlow, hideAdminOidcAndSmtp } = useAppConfig();
81
+ const { appRootPath, skipSetupFlow, hideAdminOidcAndSmtp, extraAdminRoutes } = useAppConfig();
94
82
  const [setupStatus, setSetupStatus] = React.useState<{ initialized: boolean } | null>(() =>
95
83
  skipSetupFlow ? { initialized: true } : null
96
84
  );
@@ -149,7 +137,9 @@ function AppRoutes() {
149
137
  </>
150
138
  )}
151
139
  <Route path="ai" element={<AdminAIPage />} />
152
- <ExtraAdminRoutes />
140
+ {extraAdminRoutes?.map(({ path, element }) => (
141
+ <Route key={path} path={path} element={element} />
142
+ ))}
153
143
  </Route>
154
144
  </Route>
155
145
  </Routes>
@@ -260,9 +250,11 @@ export interface AppProps {
260
250
  extraAdminRoutes?: { path: string; element: ReactNode }[];
261
251
  /** Optional extra admin nav items (e.g. cloud billing). Passed to sidebar when extraAdminRoutes are used. */
262
252
  extraAdminNavItems?: { path: string; label: string }[];
253
+ /** Optional guard for profile/account deletion (e.g. cloud blocks billing owner). */
254
+ profileDeleteGuard?: () => Promise<{ allowed: boolean; message?: string }>;
263
255
  }
264
256
 
265
- function App({ basePath, apiBaseUrl, routerBasename, skipSetupFlow, hideAdminOidcAndSmtp, adminAiOnlyToggle, extraAdminRoutes, extraAdminNavItems }: AppProps = {}) {
257
+ function App({ basePath, apiBaseUrl, routerBasename, skipSetupFlow, hideAdminOidcAndSmtp, adminAiOnlyToggle, extraAdminRoutes, extraAdminNavItems, profileDeleteGuard }: AppProps = {}) {
266
258
  const appRootPath = routerBasename !== undefined ? '/' : (basePath === '/' || !basePath ? '/' : basePath);
267
259
  const pathPrefixForLinks = routerBasename !== undefined ? '' : (basePath ?? '');
268
260
  const content = (
@@ -278,7 +270,7 @@ function App({ basePath, apiBaseUrl, routerBasename, skipSetupFlow, hideAdminOid
278
270
  );
279
271
  return (
280
272
  <AppErrorBoundary>
281
- <AppConfigProvider appBasePath={basePath} apiBaseUrl={apiBaseUrl} appRootPath={appRootPath} skipSetupFlow={skipSetupFlow} pathPrefixForLinks={pathPrefixForLinks} hideAdminOidcAndSmtp={hideAdminOidcAndSmtp} adminAiOnlyToggle={adminAiOnlyToggle} extraAdminNavItems={extraAdminNavItems} extraAdminRoutes={extraAdminRoutes}>
273
+ <AppConfigProvider appBasePath={basePath} apiBaseUrl={apiBaseUrl} appRootPath={appRootPath} skipSetupFlow={skipSetupFlow} pathPrefixForLinks={pathPrefixForLinks} hideAdminOidcAndSmtp={hideAdminOidcAndSmtp} adminAiOnlyToggle={adminAiOnlyToggle} extraAdminNavItems={extraAdminNavItems} extraAdminRoutes={extraAdminRoutes} profileDeleteGuard={profileDeleteGuard}>
282
274
  {routerBasename !== undefined ? (
283
275
  content
284
276
  ) : (
@@ -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 { pathPrefixForLinks } = useAppConfig();
64
+ const navigate = useNavigate();
65
+ const { pathPrefixForLinks, profileDeleteGuard } = useAppConfig();
65
66
  const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
66
- const { user, updateUser, checkAuth } = useAuth();
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
  }
@@ -19,9 +19,11 @@ export default function SearchEngineGuide() {
19
19
  const searchUrl = `${baseUrl}${goPath}`;
20
20
 
21
21
  const tabOptions = [
22
- { value: 'chromium', label: t('searchEngineGuide.tabChromium') },
23
- { value: 'firefox', label: t('searchEngineGuide.tabFirefox') },
22
+ { value: 'chromium', label: t('searchEngineGuide.tabChromium') || 'Chromium-based' },
23
+ { value: 'firefox', label: t('searchEngineGuide.tabFirefox') || 'Firefox' },
24
24
  ];
25
+ const pageTitle = t('searchEngineGuide.title') || 'Custom Search Engine Setup Guide';
26
+ const pageSubtitle = t('searchEngineGuide.description') || '';
25
27
 
26
28
  return (
27
29
  <div className="space-y-6 max-w-4xl mx-auto">
@@ -33,14 +35,14 @@ export default function SearchEngineGuide() {
33
35
  </Button>
34
36
  </Link>
35
37
  <PageHeader
36
- title={t('searchEngineGuide.title')}
37
- subtitle={t('searchEngineGuide.description')}
38
+ title={pageTitle}
39
+ subtitle={pageSubtitle}
38
40
  actions={
39
41
  <ScopeSegmentedControl
40
42
  value={activeTab}
41
43
  onChange={(v) => setActiveTab(v as 'chromium' | 'firefox')}
42
44
  options={tabOptions}
43
- ariaLabel={t('searchEngineGuide.title')}
45
+ ariaLabel={pageTitle}
44
46
  />
45
47
  }
46
48
  />
@@ -93,7 +95,7 @@ export default function SearchEngineGuide() {
93
95
  <Card>
94
96
  <CardHeader className="pb-2">
95
97
  <CardTitle className="text-base">
96
- {t('searchEngineGuide.stepTitle', { n: 1 })}
98
+ {(t('searchEngineGuide.stepTitle', { n: 1 }) as string) || 'Step 1'}
97
99
  </CardTitle>
98
100
  </CardHeader>
99
101
  <CardContent>
@@ -103,7 +105,7 @@ export default function SearchEngineGuide() {
103
105
  <Card>
104
106
  <CardHeader className="pb-2">
105
107
  <CardTitle className="text-base">
106
- {t('searchEngineGuide.stepTitle', { n: 2 })}
108
+ {(t('searchEngineGuide.stepTitle', { n: 2 }) as string) || 'Step 2'}
107
109
  </CardTitle>
108
110
  </CardHeader>
109
111
  <CardContent>
@@ -113,7 +115,7 @@ export default function SearchEngineGuide() {
113
115
  <Card>
114
116
  <CardHeader className="pb-2">
115
117
  <CardTitle className="text-base">
116
- {t('searchEngineGuide.stepTitle', { n: 3 })}
118
+ {(t('searchEngineGuide.stepTitle', { n: 3 }) as string) || 'Step 3'}
117
119
  </CardTitle>
118
120
  </CardHeader>
119
121
  <CardContent>
@@ -123,7 +125,7 @@ export default function SearchEngineGuide() {
123
125
  <Card>
124
126
  <CardHeader className="pb-2">
125
127
  <CardTitle className="text-base">
126
- {t('searchEngineGuide.stepTitle', { n: 4 })}
128
+ {(t('searchEngineGuide.stepTitle', { n: 4 }) as string) || 'Step 4'}
127
129
  </CardTitle>
128
130
  </CardHeader>
129
131
  <CardContent className="space-y-2">
@@ -138,7 +140,7 @@ export default function SearchEngineGuide() {
138
140
  <Card>
139
141
  <CardHeader className="pb-2">
140
142
  <CardTitle className="text-base">
141
- {t('searchEngineGuide.stepTitle', { n: 5 })}
143
+ {(t('searchEngineGuide.stepTitle', { n: 5 }) as string) || 'Step 5'}
142
144
  </CardTitle>
143
145
  </CardHeader>
144
146
  <CardContent>
@@ -162,7 +164,7 @@ export default function SearchEngineGuide() {
162
164
  <Card>
163
165
  <CardHeader className="pb-2">
164
166
  <CardTitle className="text-base">
165
- {t('searchEngineGuide.stepTitle', { n: 1 })}
167
+ {(t('searchEngineGuide.stepTitle', { n: 1 }) as string) || 'Step 1'}
166
168
  </CardTitle>
167
169
  </CardHeader>
168
170
  <CardContent>
@@ -172,7 +174,7 @@ export default function SearchEngineGuide() {
172
174
  <Card>
173
175
  <CardHeader className="pb-2">
174
176
  <CardTitle className="text-base">
175
- {t('searchEngineGuide.stepTitle', { n: 2 })}
177
+ {(t('searchEngineGuide.stepTitle', { n: 2 }) as string) || 'Step 2'}
176
178
  </CardTitle>
177
179
  </CardHeader>
178
180
  <CardContent>
@@ -182,7 +184,7 @@ export default function SearchEngineGuide() {
182
184
  <Card>
183
185
  <CardHeader className="pb-2">
184
186
  <CardTitle className="text-base">
185
- {t('searchEngineGuide.stepTitle', { n: 3 })}
187
+ {(t('searchEngineGuide.stepTitle', { n: 3 }) as string) || 'Step 3'}
186
188
  </CardTitle>
187
189
  </CardHeader>
188
190
  <CardContent>
@@ -192,7 +194,7 @@ export default function SearchEngineGuide() {
192
194
  <Card>
193
195
  <CardHeader className="pb-2">
194
196
  <CardTitle className="text-base">
195
- {t('searchEngineGuide.stepTitle', { n: 4 })}
197
+ {(t('searchEngineGuide.stepTitle', { n: 4 }) as string) || 'Step 4'}
196
198
  </CardTitle>
197
199
  </CardHeader>
198
200
  <CardContent className="space-y-2">
@@ -207,7 +209,7 @@ export default function SearchEngineGuide() {
207
209
  <Card>
208
210
  <CardHeader className="pb-2">
209
211
  <CardTitle className="text-base">
210
- {t('searchEngineGuide.stepTitle', { n: 5 })}
212
+ {(t('searchEngineGuide.stepTitle', { n: 5 }) as string) || 'Step 5'}
211
213
  </CardTitle>
212
214
  </CardHeader>
213
215
  <CardContent>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mdguggenbichler/slugbase-core",
3
- "version": "0.0.40",
3
+ "version": "0.0.42",
4
4
  "description": "SlugBase core: backend and frontend entrypoints for self-hosted and cloud apps",
5
5
  "type": "module",
6
6
  "exports": {