Skip to content

Commit cd6d35a

Browse files
committed
mailgun send owner notifications,
1 parent c820a5c commit cd6d35a

5 files changed

Lines changed: 55 additions & 28 deletions

File tree

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# MB3R Lab Landing
22

3-
Static landing page + Supabase Edge Function backend for pilot onboarding. The UI lives on any static host (GitHub Pages), while a Supabase function receives submissions, stores them (with Geo-IP country) in Postgres, and triggers Mailgun emails.
3+
Static landing page + Supabase Edge Function backend for pilot onboarding. The UI lives on any static host (GitHub Pages), while a Supabase function receives submissions, stores them (with Geo-IP country) in Postgres, and sends owner notifications via Mailgun.
44

55
## Architecture
66

@@ -34,6 +34,7 @@ If the function is unreachable, the UI falls back to localStorage so leads are n
3434
MAIL_FROM="MB3R Lab <noreply@mb3r-lab.org>" \
3535
MAILGUN_API_KEY="key-..." \
3636
MAILGUN_DOMAIN="mg.example.com" \
37+
MAIL_NOTIFY_TO="owner@company.com" \
3738
ALLOWED_ORIGINS="https://mb3r-lab.github.io,http://localhost:5500"
3839
# Optional: MAILGUN_API_BASE_URL=https://api.eu.mailgun.net/v3
3940
# Optional admin brute-force controls:
@@ -47,6 +48,10 @@ If the function is unreachable, the UI falls back to localStorage so leads are n
4748
```bash
4849
supabase functions deploy applications --project-ref YOUR_PROJECT_REF
4950
```
51+
If production still uses the legacy endpoint name, deploy the same code as:
52+
```bash
53+
supabase functions deploy database-access --project-ref YOUR_PROJECT_REF
54+
```
5055
The function expects an existing `public.applications` table (see SQL below).
5156

5257
### 2. Point the frontend at the function
@@ -67,15 +72,16 @@ Push the static site (e.g., to GitHub Pages). The landing page and `/admin.html`
6772

6873
- **CTA + modal form** — collects email/company/context and sends the payload to the Supabase function.
6974
- **Supabase storage** — submissions persist in `public.applications`; schema (including `country` column) is auto-created on first call.
70-
- **Mailgun confirmations** — the function posts to Mailgun so every lead receives an acknowledgement email.
75+
- **Mailgun owner notifications** — the function posts to Mailgun so the owner mailbox receives every new lead.
76+
- **No auto-reply to requester** — submitters are not emailed automatically; follow-up is manual from the owner side.
7177
- **Admin dashboard**`/admin.html` lists submissions. Access requires the password that you stored in the function secret (`x-admin-pass` header). If the function is offline, the dashboard shows the locally cached leads.
7278
- **Offline fallback** — when the API is unreachable (or not configured) leads are saved in `localStorage`, so you can later recover them from `/admin`.
7379

7480
## Supabase function behavior
7581

7682
The deployed function handles:
7783

78-
- `POST /database-access` — validate payload, insert into `applications`, trigger Mailgun email, respond with the created ID.
84+
- `POST /database-access` — validate payload, insert into `applications`, send owner notification to `MAIL_NOTIFY_TO` (if configured), respond with the created ID.
7985
- `GET /database-access` — require `x-admin-pass` header, validate request `Origin` against `ALLOWED_ORIGINS`, apply per-client failed-login throttling (delay + temporary block), return ordered submissions.
8086
- `OPTIONS` — CORS preflight for allowlisted origins (`content-type` + `x-admin-pass` headers).
8187
- **Schema requirement** — create the table once in Supabase (SQL editor):

assets/i18n/en.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,11 +284,11 @@
284284
"companyPlaceholder": "Company name",
285285
"commentLabel": "Context (optional)",
286286
"commentPlaceholder": "Key systems, desired outcomes, timelines",
287-
"note": "We will send a confirmation email after the request is delivered.",
287+
"note": "After we receive your request, we will contact you by work email.",
288288
"submit": "Submit request",
289289
"status": {
290290
"sending": "Sending your request...",
291-
"success": "All set! We just confirmed via email.",
291+
"success": "All set! Your request was received. We will contact you soon.",
292292
"savedOffline": "No backend connection. Saved locally for now.",
293293
"savedMissingEndpoint": "API endpoint missing. Saved locally for now."
294294
},

assets/i18n/ru.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,11 +284,11 @@
284284
"companyPlaceholder": "Название компании",
285285
"commentLabel": "Контекст (необязательно)",
286286
"commentPlaceholder": "Ключевые системы, цели, сроки",
287-
"note": "Мы отправим подтверждение по почте после получения заявки.",
287+
"note": "После получения заявки мы свяжемся с вами по рабочей почте.",
288288
"submit": "Отправить заявку",
289289
"status": {
290290
"sending": "Отправляем заявку...",
291-
"success": "Готово! Подтверждение отправлено на почту.",
291+
"success": "Готово! Заявка принята, мы скоро свяжемся.",
292292
"savedOffline": "Нет соединения с бэкендом. Пока сохранили локально.",
293293
"savedMissingEndpoint": "API-эндпоинт не указан. Пока сохранили локально."
294294
},

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ <h3 id="pilot-modal-title" data-i18n="modal.title">Request a pilot with MB3R Lab
464464
<textarea id="application-comment" name="comment" rows="3" placeholder="" data-i18n-attr="placeholder:form.commentPlaceholder"></textarea>
465465
</div>
466466
<div class="form-meta">
467-
<small data-i18n="form.note">We will send a confirmation email after the request is delivered.</small>
467+
<small data-i18n="form.note">After we receive your request, we will contact you by work email.</small>
468468
</div>
469469
<button type="submit" class="button button-primary" id="application-submit" data-i18n="form.submit">Submit request</button>
470470
<p class="form-status" id="application-status" role="status" aria-live="polite"></p>

supabase/functions/applications/index.ts

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const ADMIN_PASSWORD = Deno.env.get('ADMIN_PASSWORD') ?? '';
77
const MAIL_FROM = Deno.env.get('MAIL_FROM') ?? 'MB3R Lab <noreply@mb3r-lab.org>';
88
const MAILGUN_API_KEY = Deno.env.get('MAILGUN_API_KEY') ?? '';
99
const MAILGUN_DOMAIN = Deno.env.get('MAILGUN_DOMAIN') ?? '';
10+
const MAIL_NOTIFY_TO = Deno.env.get('MAIL_NOTIFY_TO') ?? '';
1011
const MAILGUN_API_BASE_URL =
1112
Deno.env.get('MAILGUN_API_BASE_URL') ?? 'https://api.mailgun.net/v3';
1213
const DEFAULT_ALLOWED_ORIGINS = [
@@ -209,10 +210,20 @@ async function handlePost(req: Request): Promise<Response> {
209210
return jsonResponse(req, { message: 'Unable to save your request.' }, 500);
210211
}
211212

213+
let ownerNotificationStatus: 'sent' | 'skipped' | 'failed' = 'skipped';
212214
try {
213-
await sendConfirmation(email, company);
215+
ownerNotificationStatus = await sendOwnerNotification({
216+
id: data?.id,
217+
createdAt: data?.created_at,
218+
applicantEmail: email,
219+
company,
220+
comment,
221+
country,
222+
clientIp: getClientIp(req)
223+
});
214224
} catch (mailError) {
215-
console.error('[applications] mailgun error', mailError);
225+
ownerNotificationStatus = 'failed';
226+
console.error('[applications] mailgun notify error', mailError);
216227
}
217228

218229
return jsonResponse(
@@ -221,7 +232,8 @@ async function handlePost(req: Request): Promise<Response> {
221232
id: data?.id,
222233
created_at: data?.created_at,
223234
country: data?.country,
224-
message: 'Request received.'
235+
message: 'Request received.',
236+
owner_notification_status: ownerNotificationStatus
225237
},
226238
201
227239
);
@@ -273,35 +285,42 @@ async function handleGet(req: Request): Promise<Response> {
273285
return jsonResponse(req, data || []);
274286
}
275287

276-
async function sendConfirmation(to: string, company: string) {
277-
if (!to || !MAILGUN_API_KEY || !MAILGUN_DOMAIN) {
278-
return;
288+
type OwnerNotificationPayload = {
289+
id: number | null | undefined;
290+
createdAt: string | null | undefined;
291+
applicantEmail: string;
292+
company: string;
293+
comment: string | null;
294+
country: string | null;
295+
clientIp: string;
296+
};
297+
298+
async function sendOwnerNotification(payload: OwnerNotificationPayload): Promise<'sent' | 'skipped'> {
299+
if (!MAIL_NOTIFY_TO || !MAILGUN_API_KEY || !MAILGUN_DOMAIN) {
300+
return 'skipped';
279301
}
280302

281-
const subject = 'MB3R Lab — pilot request received';
303+
const subject = 'MB3R Lab — new pilot request';
282304
const plainText = [
283-
'Hi there,',
305+
'New pilot request received:',
284306
'',
285-
'Thanks for your interest in running a pilot with MB3R Lab.',
286-
`Company: ${company || '—'}`,
307+
`ID: ${payload.id ?? '—'}`,
308+
`Created at: ${payload.createdAt ?? '—'}`,
309+
`Email: ${payload.applicantEmail || '—'}`,
310+
`Company: ${payload.company || '—'}`,
311+
`Comment: ${payload.comment || '—'}`,
312+
`Country: ${payload.country || '—'}`,
313+
`Client IP: ${payload.clientIp || '—'}`,
287314
'',
288-
'Our team will follow up shortly with next steps.',
289-
'',
290-
'— MB3R Lab'
315+
'Open admin page to review full details.'
291316
].join('\n');
292317

293-
const html = plainText
294-
.split('\n')
295-
.map((line) => (line ? `<p>${line}</p>` : '<br>'))
296-
.join('');
297-
298318
const endpoint = `${MAILGUN_API_BASE_URL.replace(/\/$/, '')}/${MAILGUN_DOMAIN}/messages`;
299319
const params = new URLSearchParams();
300320
params.append('from', MAIL_FROM);
301-
params.append('to', to);
321+
params.append('to', MAIL_NOTIFY_TO);
302322
params.append('subject', subject);
303323
params.append('text', plainText);
304-
params.append('html', html);
305324

306325
const authHeader = `Basic ${btoa(`api:${MAILGUN_API_KEY}`)}`;
307326

@@ -318,6 +337,8 @@ async function sendConfirmation(to: string, company: string) {
318337
const body = await response.text();
319338
throw new Error(`Mailgun request failed: ${body}`);
320339
}
340+
341+
return 'sent';
321342
}
322343

323344
function jsonResponse(

0 commit comments

Comments
 (0)