Files
webshop/shop/lib/actions/users.ts
DanielS 7cad819df8
All checks were successful
Staging Build / build (push) Successful in 2m44s
fix: Invalidate user list cache on company creation and refresh page on popup creation
2026-06-25 02:37:45 +02:00

235 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use server';
import { createAdminClient } from '@/lib/supabase/admin';
import { revalidatePath, unstable_noStore as noStore } from 'next/cache';
import { sendMail } from '@/utils/mail';
export async function getUsers() {
noStore();
const admin = createAdminClient();
const { data: { users }, error } = await admin.auth.admin.listUsers();
if (error) throw error;
const { data: dbUsers, error: dbError } = await admin
.from('users')
.select('id, role, company_id, companies(id, name)');
const { data: dbProfiles, error: profilesError } = await admin
.from('profiles')
.select('id, first_name, last_name');
const userMap: Record<string, { role: string; company_id: string | null; company_name: string | null }> = {};
if (!dbError && dbUsers) {
dbUsers.forEach((u: any) => {
userMap[u.id] = {
role: u.role,
company_id: u.company_id || null,
company_name: Array.isArray(u.companies) ? u.companies[0]?.name || null : u.companies?.name || null,
};
});
}
const profileMap: Record<string, { first_name: string | null; last_name: string | null }> = {};
if (!profilesError && dbProfiles) {
dbProfiles.forEach((p: any) => {
profileMap[p.id] = {
first_name: p.first_name || null,
last_name: p.last_name || null,
};
});
}
return users.map((u) => ({
...u,
role: userMap[u.id]?.role || 'partner',
company_id: userMap[u.id]?.company_id || null,
company_name: userMap[u.id]?.company_name || null,
first_name: profileMap[u.id]?.first_name || null,
last_name: profileMap[u.id]?.last_name || null,
}));
}
export async function createUser(data: { email: string; role?: 'admin' | 'partner'; 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 {
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/confirm?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\nDieser Link ist zeitlich begrenzt gültig.\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
html: `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
<h2 style="color: #0f172a; margin-bottom: 16px;">Willkommen bei CASPOS Shop!</h2>
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Hallo,</p>
<p style="color: #475569; font-size: 16px; line-height: 1.5;">für Sie wurde erfolgreich ein neues Benutzerkonto auf unserer Plattform eingerichtet.</p>
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Bitte klicken Sie auf den untenstehenden Button, um Ihr Passwort festzulegen und Ihr Konto zu aktivieren:</p>
<div style="margin: 24px 0;">
<a href="${setupLink}" style="background-color: #3b82f6; color: #ffffff; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold; display: inline-block;">Passwort jetzt festlegen</a>
</div>
<p style="color: #64748b; font-size: 14px; line-height: 1.5;">Falls der Button nicht funktioniert, können Sie auch folgenden Link in Ihren Browser kopieren:<br>
<a href="${setupLink}" style="color: #3b82f6;">${setupLink}</a></p>
<hr style="border: 0; border-top: 1px solid #e2e8f0; margin: 24px 0;">
<p style="color: #94a3b8; font-size: 12px;">Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht darauf.</p>
</div>
`,
});
}
}
} catch (mailError) {
console.error('Failed to send welcome setup mail:', mailError);
}
revalidatePath('/admin/users');
return user;
}
export async function deleteUser(id: string) {
try {
const admin = createAdminClient();
// Check if user has orders (documents/belege)
const { count, error: countError } = await admin
.from('orders')
.select('*', { count: 'exact', head: true })
.eq('user_id', id);
if (countError) {
console.error('Failed to count user orders:', countError);
}
if (count && count > 0) {
throw new Error('Auf diesen User gibt es bereits Belege. Sperren möglich.');
}
// Delete from public.users first to satisfy foreign key constraints
const { error: dbError } = await admin.from('users').delete().eq('id', id);
if (dbError) {
console.error('Database delete user error:', dbError);
throw new Error(`Datenbank-Fehler beim Löschen des Benutzers: ${dbError.message}`);
}
const { error } = await admin.auth.admin.deleteUser(id);
if (error) {
console.error('Auth delete user error:', error);
throw new Error(`Auth-Fehler beim Löschen des Benutzers: ${error.message}`);
}
revalidatePath('/admin/users');
} catch (err: any) {
console.error('deleteUser Server Action failed:', err);
throw new Error(err.message || 'Benutzer konnte nicht gelöscht werden.');
}
}
export async function updateUserRole(id: string, role: 'admin' | 'partner' | 'gesperrt') {
const admin = createAdminClient();
const { error } = await admin
.from('users')
.upsert({ id, role }, { onConflict: 'id' });
if (error) throw error;
revalidatePath('/admin/users');
}
export async function updateUserStatus(id: string, ban: boolean) {
const admin = createAdminClient();
const { error } = await admin.auth.admin.updateUserById(id, {
ban_duration: ban ? '876000h' : '0h',
});
if (error) throw error;
revalidatePath('/admin/users');
}
export async function resetPassword(email: string) {
const admin = createAdminClient();
const { data, error } = await admin.auth.admin.generateLink({
type: 'recovery',
email,
});
if (error) throw error;
const linkData = data as any;
// Extract token_hash from the generated link properties
const tokenHash = linkData?.properties?.hashed_token;
if (!tokenHash) throw new Error('Recovery token not generated');
// Build link pointing to our own /auth/confirm route
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de';
const resetLink = `${siteUrl}/auth/confirm?token_hash=${tokenHash}&type=recovery&next=/auth/update-password`;
await sendMail({
to: email,
subject: 'Passwort zurücksetzen',
text: `Klicken Sie auf den folgenden Link, um Ihr Passwort zurückzusetzen: ${resetLink}`,
html: `<p>Klicken Sie <a href="${resetLink}">hier</a>, um Ihr Passwort zurückzusetzen.</p>`,
});
}
export async function getPartners() {
const admin = createAdminClient();
const { data: { users }, error } = await admin.auth.admin.listUsers();
if (error) throw error;
const { data: dbUsers, error: dbError } = await admin
.from('users')
.select('id, role, company_id, companies(id, name)')
.in('role', ['partner', 'gesperrt']);
if (dbError) throw dbError;
const userMap: Record<string, { role: string; company_id: string | null; company_name: string | null }> = {};
dbUsers.forEach((u: any) => {
userMap[u.id] = {
role: u.role,
company_id: u.company_id || null,
company_name: Array.isArray(u.companies) ? u.companies[0]?.name || null : u.companies?.name || null,
};
});
const partnerIds = new Set(dbUsers.map((u: any) => u.id));
return users
.filter((u) => partnerIds.has(u.id))
.map((u) => ({
...u,
role: userMap[u.id]?.role || 'partner',
company_id: userMap[u.id]?.company_id || null,
company_name: userMap[u.id]?.company_name || null,
}));
}
export async function updateUserProfile(id: string, data: { first_name: string | null; last_name: string | null }) {
const admin = createAdminClient();
const { error } = await admin
.from('profiles')
.upsert({ id, first_name: data.first_name, last_name: data.last_name }, { onConflict: 'id' });
if (error) throw error;
revalidatePath('/admin/users');
}