235 lines
9.4 KiB
TypeScript
235 lines
9.4 KiB
TypeScript
'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' | '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 {
|
||
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\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' | 'verwaltung' | '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/verify-link?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');
|
||
}
|
||
|