Files
website/app/beach/actions.ts
T
WilliamSoderberg Verified 7de7d11d11
Publish Docker image / Build and Push Docker image (release) Successful in 20s
Fixed Stalwart API
2026-08-17 14:16:05 +02:00

305 lines
13 KiB
TypeScript

// app/beach/actions.ts
'use server';
export async function submitRegistration(prevState: unknown, formData: FormData) {
// 1. Spara all rådata direkt för att kunna skicka tillbaka den vid fel
const rawData = {
name: formData.get('name') as string,
email: formData.get('email') as string,
tel: formData.get('tel') as string,
team: formData.get('team') as string,
count: formData.get('count') as string,
level: formData.get('level') as string,
message: formData.get('message') as string,
};
const honeypot = formData.get('website');
if (honeypot) {
return { success: false, message: 'Spam detekterat.', values: rawData };
}
// 2. Hämta Turnstile Token och Secret
const turnstileToken = formData.get('cf-turnstile-response');
const secretKey = process.env.TURNSTILE_SECRET;
if (!secretKey) {
console.error("Saknar TURNSTILE_SECRET i miljön.");
return {
success: false,
message: 'Serverkonfiguration saknas. Kontakta administratör.',
values: rawData
};
}
// 3. Verifiera mot Cloudflare
try {
const turnstileRes = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
secret: secretKey,
response: String(turnstileToken || ''),
}),
});
const turnstileData = await turnstileRes.json();
if (!turnstileData.success) {
console.error("Turnstile failed:", turnstileData);
return {
success: false,
message: 'Säkerhetsverifieringen misslyckades. Ladda om sidan och försök igen.',
values: rawData
};
}
} catch (error) {
console.error("Turnstile fetch error:", error);
return {
success: false,
message: 'Kunde inte nå verifieringsservern.',
values: rawData
};
}
// 4. Formatera telefonnummer
let cleanTel = rawData.tel.replace(/\D/g, '');
if (cleanTel.startsWith('46')) cleanTel = cleanTel.substring(2);
else if (cleanTel.startsWith('0')) cleanTel = cleanTel.substring(1);
const formattedTel = cleanTel.length === 9
? `+46 (0)${cleanTel.substring(0, 2)}-${cleanTel.substring(2, 5)} ${cleanTel.substring(5, 7)} ${cleanTel.substring(7, 9)}`
: rawData.tel;
// 5. Stalwart JMAP Setup
const jmapUrl = process.env.STALWART_JMAP_URL;
const jmapToken = process.env.STALWART_API_TOKEN;
const accountId = process.env.STALWART_ACCOUNT_ID;
if (!jmapUrl || !jmapToken || !accountId) {
console.error("Saknar JMAP-konfiguration i miljön.");
return {
success: false,
message: 'E-postserver är inte konfigurerad. Kontakta administratör.',
values: rawData
};
}
async function sendMailJMAP(toEmail: string, replyToEmail: string, subject: string, htmlContent: string, fromName: string, fromEmail: string) {
// 1. Hämta Mapp-ID (Drafts/Utkast) och Identitets-ID från Stalwart
const setupPayload = {
using: ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:submission"],
methodCalls: [
["Mailbox/query", { accountId: accountId, filter: { role: "drafts" } }, "getMailbox"],
["Identity/get", { accountId: accountId }, "getIdentity"]
]
};
const setupRes = await fetch(jmapUrl as string, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${jmapToken}` },
body: JSON.stringify(setupPayload)
});
const setupData = await setupRes.json();
const mailboxRes = setupData.methodResponses.find((r: any) => r[0] === "Mailbox/query");
const identityRes = setupData.methodResponses.find((r: any) => r[0] === "Identity/get");
let mailboxId = mailboxRes && mailboxRes[1].ids && mailboxRes[1].ids.length > 0 ? mailboxRes[1].ids[0] : null;
// ---- NY LOGIK HÄR: Hitta rätt identitet baserat på fromEmail ----
let identityId = null;
let actualFromEmail = fromEmail;
if (identityRes && identityRes[1].list) {
// Leta efter identiteten som matchar adressen vi vill skicka från
const matchedIdentity = identityRes[1].list.find((i: any) => i.email === fromEmail);
if (matchedIdentity) {
identityId = matchedIdentity.id;
} else if (identityRes[1].list.length > 0) {
// Fallback om den exakta adressen inte fanns i listan
identityId = identityRes[1].list[0].id;
actualFromEmail = identityRes[1].list[0].email;
}
}
// Om ingen mapp med rollen "drafts" finns, hämta första bästa mapp
if (!mailboxId) {
const fallbackRes = await fetch(jmapUrl as string, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${jmapToken}` },
body: JSON.stringify({
using: ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
methodCalls: [["Mailbox/query", { accountId: accountId }, "0"]]
})
});
const fallbackData = await fallbackRes.json();
mailboxId = fallbackData.methodResponses[0][1].ids[0];
}
if (!mailboxId || !identityId) {
throw new Error("Kunde inte hitta mailboxId eller identityId i Stalwart.");
}
// 2. Skapa och skicka mailet
const payload = {
using: [
"urn:ietf:params:jmap:core",
"urn:ietf:params:jmap:mail",
"urn:ietf:params:jmap:submission"
],
methodCalls: [
[
"Email/set",
{
accountId: accountId,
create: {
"draft-1": {
mailboxIds: { [mailboxId as string]: true },
from: [{ email: actualFromEmail, name: fromName }], // Nu använder vi rätt adress
to: [{ email: toEmail }],
replyTo: [{ email: replyToEmail }],
subject: subject,
bodyValues: {
"body-html": {
value: htmlContent,
isTruncated: false,
isEncodingProblem: false
}
},
htmlBody: [{ partId: "body-html", type: "text/html" }]
}
}
},
"0"
],
[
"EmailSubmission/set",
{
accountId: accountId,
create: {
"sub-1": {
emailId: "#draft-1",
identityId: identityId // Nu använder vi rätt ID
}
}
},
"1"
]
]
};
const res = await fetch(jmapUrl as string, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${jmapToken}`
},
body: JSON.stringify(payload)
});
const data = await res.json();
const hasErrors = data.methodResponses.some(
(response: any) =>
response[0] === "error" ||
(response[1] && response[1].notCreated)
);
if (hasErrors) {
console.error("--- JMAP METHOD ERROR ---");
console.error(JSON.stringify(data.methodResponses, null, 2));
console.error("-------------------------");
throw new Error("JMAP accepterade anropet, men vägrade skapa/skicka mailet.");
}
return data;
}
// 6. Skapa och skicka mail
try {
const dateStr = new Date().toLocaleString('sv-SE', {
timeZone: 'Europe/Stockholm',
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit'
});
const cleanMessage = rawData.message ? rawData.message.replace(/[\r\n;]/g, " ") : "";
// HTML för mail till klubben
const mailTillKlubbHtml = `
<html>
<body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 800px; padding: 20px;'>
<h2 style='color: #39979f;'>Beach-anmälan</h2>
<table style='width: 100%; max-width: 600px; border-collapse: collapse;'>
<tr><td style='padding: 5px; font-weight: bold; width: 150px;'>Lagnamn:</td><td>${rawData.team}</td></tr>
<tr><td style='padding: 5px; font-weight: bold;'>Kontakt:</td><td>${rawData.name}</td></tr>
<tr><td style='padding: 5px; font-weight: bold;'>E-post:</td><td>${rawData.email}</td></tr>
<tr><td style='padding: 5px; font-weight: bold;'>Telefon:</td><td>${formattedTel}</td></tr>
<tr><td style='padding: 5px; font-weight: bold;'>Spelare:</td><td>${rawData.count}</td></tr>
<tr><td style='padding: 5px; font-weight: bold;'>Nivå:</td><td>${rawData.level}</td></tr>
</table>
<p><strong>Meddelande:</strong><br>${rawData.message ? rawData.message.replace(/\n/g, '<br>') : '<em>Inget meddelande</em>'}</p>
<hr style='border: none; border-top: 1px solid #ddd; margin: 30px 0;'>
<details style="margin-top: 20px;">
<summary style="background-color: #fdb84b; color: #fff; padding: 12px 20px; border-radius: 8px; font-weight: bold; cursor: pointer; display: inline-block; font-size: 14px; text-transform: uppercase; letter-spacing: 1px;">
Visa kopierbar tabell för Excel
</summary>
<div style="margin-top: 15px; overflow-x: auto;">
<table border='1' style='border-collapse: collapse; font-family: Calibri, sans-serif; font-size: 11pt; width: 100%; text-align: left;'>
<tr style="background-color: #f9f9f9;">
<td style='padding: 8px; border: 1px solid #ccc;'>${rawData.team}</td>
<td style='padding: 8px; border: 1px solid #ccc;'>${rawData.count}</td>
<td style='padding: 8px; border: 1px solid #ccc;'>${rawData.level}</td>
<td style='padding: 8px; border: 1px solid #ccc;'>${rawData.name}</td>
<td style='padding: 8px; border: 1px solid #ccc;'>${formattedTel}</td>
<td style='padding: 8px; border: 1px solid #ccc;'>${rawData.email}</td>
<td style='padding: 8px; border: 1px solid #ccc;'>${dateStr}</td>
<td style='padding: 8px; border: 1px solid #ccc;'>${cleanMessage}</td>
</tr>
</table>
</div>
</details>
</body>
</html>
`;
// HTML för bekräftelse till användaren
const mailTillAnvandareHtml = `
<div style="font-family: sans-serif; padding: 20px; color: #333;">
<h2 style="color: #39979f;">Hej ${rawData.name}!</h2>
<p>Tack för din anmälan till beachturneringen. Vi återkommer så snart vi har granskat era uppgifter.</p>
<p>Ses på stranden!<br><strong style="color: #fdb84b;">Lerbergets Volleybollsällskap</strong></p>
</div>
`;
// Skicka mailen parallellt
await Promise.all([
sendMailJMAP(
'beach@lerbergetsvolleyboll.se', // To
rawData.email, // ReplyTo
`Anmälan: ${rawData.team}`, // Subject
mailTillKlubbHtml, // HTML Content
`Beach-anmälan: ${rawData.name}`,// From Name
'beach@lerbergetsvolleyboll.se' // From Email (VIKTIGT!)
),
sendMailJMAP(
rawData.email, // To
'beach@lerbergetsvolleyboll.se', // ReplyTo
`Vi har tagit emot din anmälan för ${rawData.team}`, // Subject
mailTillAnvandareHtml, // HTML Content
"LVS Beach", // From Name
'beach@lerbergetsvolleyboll.se' // From Email (VIKTIGT!)
)
]);
return { success: true, message: 'Tack! Din anmälan har skickats. Håll utkik i din inkorg efter bekräftelse.' };
} catch (e) {
console.error("JMAP Fetch error:", e);
return {
success: false,
message: 'Kunde inte skicka mail. Kontakta oss manuellt istället.',
values: rawData
};
}
}