fix(users): refactor user admin actions to return status objects instead of throwing to prevent production error obfuscation
All checks were successful
Staging Build / build (push) Successful in 3m29s

This commit is contained in:
DanielS
2026-07-03 13:51:00 +02:00
parent 12d06e46ad
commit 38651037bc
3 changed files with 154 additions and 94 deletions

View File

@@ -96,12 +96,17 @@ export function UserDialog({ companies }: UserDialogProps) {
first_name: values.first_name || undefined, first_name: values.first_name || undefined,
last_name: values.last_name || undefined, last_name: values.last_name || undefined,
} }
await createUser(payload) const res = await createUser(payload)
setOpen(false) if (res.success) {
form.reset() setOpen(false)
router.refresh() form.reset()
router.refresh()
} else {
alert(res.error || 'Fehler beim Erstellen des Benutzers.')
}
} catch (error) { } catch (error) {
console.error('Error creating user:', error) console.error('Error creating user:', error)
alert('Ein unerwarteter Fehler ist aufgetreten.')
} }
} }

View File

@@ -63,17 +63,22 @@ export function UserList({ initialUsers, companies }: UserListProps) {
if (confirm('Möchten Sie diesen Benutzer wirklich löschen?')) { if (confirm('Möchten Sie diesen Benutzer wirklich löschen?')) {
setLoadingId(id) setLoadingId(id)
try { try {
await deleteUser(id) const res = await deleteUser(id)
router.refresh() if (res.success) {
router.refresh()
} else {
console.error('Fehler beim Löschen des Benutzers:', res.error)
if (res.error?.includes('Belege')) {
if (confirm('Löschen nicht möglich! Auf diesen User existieren bereits Belege. Möchten Sie diesen Benutzer stattdessen sperren?')) {
await handleRoleChange(id, 'gesperrt')
}
} else {
alert('Fehler beim Löschen: ' + res.error)
}
}
} catch (e: any) { } catch (e: any) {
console.error('Fehler beim Löschen des Benutzers:', e) console.error('Fehler beim Löschen des Benutzers:', e)
if (e.message?.includes('Belege')) { alert('Fehler beim Löschen: ' + (e.message || JSON.stringify(e)))
if (confirm('Löschen nicht möglich! Auf diesen User existieren bereits Belege. Möchten Sie diesen Benutzer stattdessen sperren?')) {
await handleRoleChange(id, 'gesperrt')
}
} else {
alert('Fehler beim Löschen: ' + (e.message || JSON.stringify(e)))
}
} finally { } finally {
setLoadingId(null) setLoadingId(null)
} }
@@ -83,10 +88,15 @@ export function UserList({ initialUsers, companies }: UserListProps) {
const handleRoleChange = async (id: string, newRole: Role) => { const handleRoleChange = async (id: string, newRole: Role) => {
setLoadingId(id + '-role') setLoadingId(id + '-role')
try { try {
await updateUserRole(id, newRole) const res = await updateUserRole(id, newRole)
router.refresh() if (res.success) {
} catch (e) { router.refresh()
} else {
alert(res.error || 'Fehler beim Ändern der Rolle.')
}
} catch (e: any) {
console.error('Fehler beim Ändern der Rolle:', e) console.error('Fehler beim Ändern der Rolle:', e)
alert('Fehler beim Ändern der Rolle: ' + (e.message || JSON.stringify(e)))
} finally { } finally {
setLoadingId(null) setLoadingId(null)
} }
@@ -96,10 +106,15 @@ export function UserList({ initialUsers, companies }: UserListProps) {
const newRole = currentRole === 'gesperrt' ? 'partner' : 'gesperrt' const newRole = currentRole === 'gesperrt' ? 'partner' : 'gesperrt'
setLoadingId(id + '-lock') setLoadingId(id + '-lock')
try { try {
await updateUserRole(id, newRole) const res = await updateUserRole(id, newRole)
router.refresh() if (res.success) {
} catch (e) { router.refresh()
} else {
alert(res.error || 'Fehler beim Sperren/Entsperren.')
}
} catch (e: any) {
console.error('Fehler beim Sperren/Entsperren:', e) console.error('Fehler beim Sperren/Entsperren:', e)
alert('Fehler beim Sperren/Entsperren: ' + (e.message || JSON.stringify(e)))
} finally { } finally {
setLoadingId(null) setLoadingId(null)
} }
@@ -241,10 +256,15 @@ export function UserList({ initialUsers, companies }: UserListProps) {
const val = e.target.value.trim() const val = e.target.value.trim()
if (val !== (user.first_name || '')) { if (val !== (user.first_name || '')) {
try { try {
await updateUserProfile(user.id, { first_name: val || null, last_name: user.last_name }) const res = await updateUserProfile(user.id, { first_name: val || null, last_name: user.last_name })
router.refresh() if (res.success) {
} catch (err) { router.refresh()
} else {
alert(res.error || 'Fehler beim Aktualisieren des Vornamens.')
}
} catch (err: any) {
console.error('Failed to update first name:', err) console.error('Failed to update first name:', err)
alert('Fehler beim Aktualisieren des Vornamens: ' + (err.message || JSON.stringify(err)))
} }
} }
}} }}
@@ -263,10 +283,15 @@ export function UserList({ initialUsers, companies }: UserListProps) {
const val = e.target.value.trim() const val = e.target.value.trim()
if (val !== (user.last_name || '')) { if (val !== (user.last_name || '')) {
try { try {
await updateUserProfile(user.id, { first_name: user.first_name, last_name: val || null }) const res = await updateUserProfile(user.id, { first_name: user.first_name, last_name: val || null })
router.refresh() if (res.success) {
} catch (err) { router.refresh()
} else {
alert(res.error || 'Fehler beim Aktualisieren des Nachnamens.')
}
} catch (err: any) {
console.error('Failed to update last name:', err) console.error('Failed to update last name:', err)
alert('Fehler beim Aktualisieren des Nachnamens: ' + (err.message || JSON.stringify(err)))
} }
} }
}} }}

View File

@@ -50,64 +50,77 @@ export async function getUsers() {
} }
export async function createUser(data: { email: string; role?: 'admin' | 'partner' | 'verwaltung'; email_confirm?: boolean; company_id?: string; first_name?: string; last_name?: string }) { export async function createUser(data: { email: string; role?: 'admin' | 'partner' | 'verwaltung'; email_confirm?: boolean; company_id?: string; first_name?: string; last_name?: string }) {
const admin = createAdminClient();
const { data: { user }, error } = await admin.auth.admin.createUser({
email: data.email,
password: Math.random().toString(36).slice(-10) + Math.random().toString(36).slice(-10),
email_confirm: data.email_confirm ?? true,
});
if (error) throw error;
if (!user) throw new Error('Benutzer konnte nicht erstellt werden.');
const role = data.role || 'partner';
const { error: dbError } = await admin
.from('users')
.upsert({ id: user.id, role, company_id: data.company_id || null }, { onConflict: 'id' });
if (dbError) throw dbError;
const { error: profileError } = await admin
.from('profiles')
.upsert({
id: user.id,
first_name: data.first_name || null,
last_name: data.last_name || null,
}, { onConflict: 'id' });
if (profileError) throw profileError;
// Generate recovery link to let user set password
try { try {
const { data: linkData, error: linkError } = await admin.auth.admin.generateLink({ const admin = createAdminClient();
type: 'recovery', const { data: { user }, error } = await admin.auth.admin.createUser({
email: data.email, email: data.email,
password: Math.random().toString(36).slice(-10) + Math.random().toString(36).slice(-10),
email_confirm: data.email_confirm ?? true,
}); });
if (!linkError && linkData) { if (error) {
const tokenHash = (linkData as any)?.properties?.hashed_token; return { success: false, error: error.message };
if (tokenHash) { }
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de'; if (!user) {
const setupLink = `${siteUrl}/auth/verify-link?token_hash=${tokenHash}&type=recovery&next=/auth/update-password`; return { success: false, error: 'Benutzer konnte nicht erstellt werden.' };
await sendMail({
to: data.email,
subject: 'Willkommen bei CASPOS Shop Ihr Benutzerkonto wurde erstellt',
text: `Hallo,\n\nein neues Benutzerkonto wurde für Sie auf CASPOS Shop erstellt.\n\nBitte klicken Sie auf den folgenden Link, um Ihr persönliches Passwort festzulegen und Ihr Konto zu aktivieren:\n${setupLink}\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
html: getBeautifulEmailHtml(
'Willkommen bei CASPOS Shop',
`
<p style="color: #475569; font-size: 16px; line-height: 1.6; margin-top: 0;">Hallo,</p>
<p style="color: #475569; font-size: 16px; line-height: 1.6;">für Sie wurde erfolgreich ein neues Benutzerkonto auf unserer Plattform eingerichtet.</p>
<p style="color: #475569; font-size: 16px; line-height: 1.6;">Bitte klicken Sie auf den untenstehenden Button, um Ihr Passwort festzulegen und Ihr Konto zu aktivieren:</p>
`,
'Passwort festlegen',
setupLink
),
});
}
} }
} catch (mailError) {
console.error('Failed to send welcome setup mail:', mailError);
}
revalidatePath('/admin/users'); const role = data.role || 'partner';
return user; const { error: dbError } = await admin
.from('users')
.upsert({ id: user.id, role, company_id: data.company_id || null }, { onConflict: 'id' });
if (dbError) {
return { success: false, error: dbError.message };
}
const { error: profileError } = await admin
.from('profiles')
.upsert({
id: user.id,
first_name: data.first_name || null,
last_name: data.last_name || null,
}, { onConflict: 'id' });
if (profileError) {
return { success: false, error: profileError.message };
}
// Generate recovery link to let user set password
try {
const { data: linkData, error: linkError } = await admin.auth.admin.generateLink({
type: 'recovery',
email: data.email,
});
if (!linkError && linkData) {
const tokenHash = (linkData as any)?.properties?.hashed_token;
if (tokenHash) {
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de';
const setupLink = `${siteUrl}/auth/verify-link?token_hash=${tokenHash}&type=recovery&next=/auth/update-password`;
await sendMail({
to: data.email,
subject: 'Willkommen bei CASPOS Shop Ihr Benutzerkonto wurde erstellt',
text: `Hallo,\n\nein neues Benutzerkonto wurde für Sie auf CASPOS Shop erstellt.\n\nBitte klicken Sie auf den folgenden Link, um Ihr persönliches Passwort festzulegen und Ihr Konto zu aktivieren:\n${setupLink}\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
html: getBeautifulEmailHtml(
'Willkommen bei CASPOS Shop',
`
<p style="color: #475569; font-size: 16px; line-height: 1.6; margin-top: 0;">Hallo,</p>
<p style="color: #475569; font-size: 16px; line-height: 1.6;">für Sie wurde erfolgreich ein neues Benutzerkonto auf unserer Plattform eingerichtet.</p>
<p style="color: #475569; font-size: 16px; line-height: 1.6;">Bitte klicken Sie auf den untenstehenden Button, um Ihr Passwort festzulegen und Ihr Konto zu aktivieren:</p>
`,
'Passwort festlegen',
setupLink
),
});
}
}
} catch (mailError) {
console.error('Failed to send welcome setup mail:', mailError);
}
revalidatePath('/admin/users');
return { success: true, user };
} catch (err: any) {
console.error('Error creating user in action:', err);
return { success: false, error: err.message || 'Ein unerwarteter Fehler ist aufgetreten.' };
}
} }
export async function deleteUser(id: string) { export async function deleteUser(id: string) {
@@ -125,35 +138,44 @@ export async function deleteUser(id: string) {
} }
if (count && count > 0) { if (count && count > 0) {
throw new Error('Auf diesen User gibt es bereits Belege. Sperren möglich.'); return { success: false, error: 'Auf diesen User gibt es bereits Belege. Sperren möglich.' };
} }
// Delete from public.users first to satisfy foreign key constraints // Delete from public.users first to satisfy foreign key constraints
const { error: dbError } = await admin.from('users').delete().eq('id', id); const { error: dbError } = await admin.from('users').delete().eq('id', id);
if (dbError) { if (dbError) {
console.error('Database delete user error:', dbError); console.error('Database delete user error:', dbError);
throw new Error(`Datenbank-Fehler beim Löschen des Benutzers: ${dbError.message}`); return { success: false, error: `Datenbank-Fehler beim Löschen des Benutzers: ${dbError.message}` };
} }
const { error } = await admin.auth.admin.deleteUser(id); const { error } = await admin.auth.admin.deleteUser(id);
if (error) { if (error) {
console.error('Auth delete user error:', error); console.error('Auth delete user error:', error);
throw new Error(`Auth-Fehler beim Löschen des Benutzers: ${error.message}`); return { success: false, error: `Auth-Fehler beim Löschen des Benutzers: ${error.message}` };
} }
revalidatePath('/admin/users'); revalidatePath('/admin/users');
return { success: true };
} catch (err: any) { } catch (err: any) {
console.error('deleteUser Server Action failed:', err); console.error('deleteUser Server Action failed:', err);
throw new Error(err.message || 'Benutzer konnte nicht gelöscht werden.'); return { success: false, error: err.message || 'Benutzer konnte nicht gelöscht werden.' };
} }
} }
export async function updateUserRole(id: string, role: 'admin' | 'partner' | 'verwaltung' | 'gesperrt') { export async function updateUserRole(id: string, role: 'admin' | 'partner' | 'verwaltung' | 'gesperrt') {
const admin = createAdminClient(); try {
const { error } = await admin const admin = createAdminClient();
.from('users') const { error } = await admin
.upsert({ id, role }, { onConflict: 'id' }); .from('users')
if (error) throw error; .upsert({ id, role }, { onConflict: 'id' });
revalidatePath('/admin/users'); if (error) {
return { success: false, error: error.message };
}
revalidatePath('/admin/users');
return { success: true };
} catch (err: any) {
console.error('updateUserRole Server Action failed:', err);
return { success: false, error: err.message || 'Rolle konnte nicht aktualisiert werden.' };
}
} }
export async function updateUserStatus(id: string, ban: boolean) { export async function updateUserStatus(id: string, ban: boolean) {
@@ -224,12 +246,20 @@ export async function getPartners() {
} }
export async function updateUserProfile(id: string, data: { first_name: string | null; last_name: string | null }) { export async function updateUserProfile(id: string, data: { first_name: string | null; last_name: string | null }) {
const admin = createAdminClient(); try {
const { error } = await admin const admin = createAdminClient();
.from('profiles') const { error } = await admin
.upsert({ id, first_name: data.first_name, last_name: data.last_name }, { onConflict: 'id' }); .from('profiles')
if (error) throw error; .upsert({ id, first_name: data.first_name, last_name: data.last_name }, { onConflict: 'id' });
revalidatePath('/admin/users'); if (error) {
return { success: false, error: error.message };
}
revalidatePath('/admin/users');
return { success: true };
} catch (err: any) {
console.error('updateUserProfile Server Action failed:', err);
return { success: false, error: err.message || 'Profil konnte nicht aktualisiert werden.' };
}
} }
function getBeautifulEmailHtml(title: string, messageHtml: string, buttonText?: string, buttonUrl?: string) { function getBeautifulEmailHtml(title: string, messageHtml: string, buttonText?: string, buttonUrl?: string) {