Files
webshop/shop/lib/actions/users.ts
DanielS a64b6790c6
All checks were successful
Staging Build / build (push) Successful in 3m33s
feat: Add partner account settings page under /account-kunden
2026-07-04 21:06:46 +02:00

347 lines
14 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 { createClient } from '@/lib/supabase/server';
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 }) {
try {
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) {
return { success: false, error: error.message };
}
if (!user) {
return { success: false, 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) {
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) {
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) {
return { success: false, 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);
return { success: false, 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);
return { success: false, error: `Auth-Fehler beim Löschen des Benutzers: ${error.message}` };
}
revalidatePath('/admin/users');
return { success: true };
} catch (err: any) {
console.error('deleteUser Server Action failed:', err);
return { success: false, error: err.message || 'Benutzer konnte nicht gelöscht werden.' };
}
}
export async function updateUserRole(id: string, role: 'admin' | 'partner' | 'verwaltung' | 'gesperrt') {
try {
const admin = createAdminClient();
const { error } = await admin
.from('users')
.upsert({ id, role }, { onConflict: 'id' });
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) {
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`;
const messageHtml = `
<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;">der Administrator hat das Zurücksetzen Ihres Passworts angefordert.</p>
<p style="color: #475569; font-size: 16px; line-height: 1.6;">Bitte klicken Sie auf den untenstehenden Button, um ein neues Passwort für Ihr Konto festzulegen:</p>
`;
await sendMail({
to: email,
subject: 'Passwort zurücksetzen CASPOS Shop',
text: `Hallo,\n\nder Administrator hat das Zurücksetzen Ihres Passworts angefordert.\n\nBitte klicken Sie auf den folgenden Link, um ein neues Passwort festzulegen:\n${resetLink}\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
html: getBeautifulEmailHtml('Passwort zurücksetzen', messageHtml, 'Passwort zurücksetzen', resetLink),
});
}
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 }) {
try {
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) {
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.' };
}
}
export async function updateCompanyDetails(companyId: string, data: { street: string | null; zip: string | null; city: string | null; email: string | null }) {
try {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return { success: false, error: 'Nicht authentifiziert.' };
}
// Berechtigung prüfen: Ist der User Mitglied dieser Company oder Admin?
const { data: dbUser } = await supabase
.from('users')
.select('role, company_id')
.eq('id', user.id)
.single();
if (!dbUser) {
return { success: false, error: 'Benutzer nicht gefunden.' };
}
if (dbUser.role !== 'admin' && dbUser.company_id !== companyId) {
return { success: false, error: 'Keine Berechtigung zur Bearbeitung dieses Unternehmens.' };
}
const admin = createAdminClient();
const { error } = await admin
.from('companies')
.update({
street: data.street,
zip: data.zip,
city: data.city,
email: data.email
})
.eq('id', companyId);
if (error) {
return { success: false, error: error.message };
}
return { success: true };
} catch (err: any) {
console.error('updateCompanyDetails Server Action failed:', err);
return { success: false, error: err.message || 'Unternehmen konnte nicht aktualisiert werden.' };
}
}
function getBeautifulEmailHtml(title: string, messageHtml: string, buttonText?: string, buttonUrl?: string) {
return `
<div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f8fafc; padding: 40px 10px; margin: 0; width: 100%;">
<div style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0;">
<!-- Header banner with gradient -->
<div style="background: linear-gradient(135deg, #1e3a8a 0%, #312e81 100%); padding: 32px; text-align: center;">
<h1 style="color: #ffffff; margin: 0; font-size: 24px; font-weight: 800; letter-spacing: -0.5px;">${title}</h1>
</div>
<!-- Content -->
<div style="padding: 40px 32px; background-color: #ffffff;">
${messageHtml}
${buttonText && buttonUrl ? `
<div style="text-align: center; margin: 32px 0;">
<a href="${buttonUrl}" style="background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: #ffffff; padding: 14px 28px; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 15px; display: inline-block; box-shadow: 0 4px 10px rgba(37, 99, 235, 0.2); transition: all 0.2s ease;">
${buttonText}
</a>
</div>
<p style="color: #64748b; font-size: 13px; line-height: 1.6; margin-top: 32px; border-top: 1px solid #f1f5f9; padding-top: 20px;">
Falls der Button nicht funktioniert, kopieren Sie bitte diesen Link in Ihren Browser:<br>
<a href="${buttonUrl}" style="color: #2563eb; text-decoration: none; word-break: break-all;">${buttonUrl}</a>
</p>
` : ''}
<hr style="border: 0; border-top: 1px solid #f1f5f9; margin: 32px 0;">
<p style="color: #94a3b8; font-size: 11px; text-align: center; margin: 0; line-height: 1.6;">
Dies ist eine automatisch generierte E-Mail.<br>
© ${new Date().getFullYear()} CASPOS Shop. Alle Rechte vorbehalten.
</p>
</div>
</div>
</div>
`;
}