Publish Docker image / Build and Push Docker image (release) Successful in 20s
305 lines
13 KiB
TypeScript
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
|
|
};
|
|
}
|
|
} |