misc
This commit is contained in:
1 parent
490444032c
commit
efecc500c4
15 files changed
+349
-47
No files matched your search
@@ -51,6 +51,7 @@ const f = ui.contact.form;
|
||||
<button class="button" type="submit">{f.submit}</button>
|
||||
<p class="small muted"><span aria-hidden="true">* </span>{f.required}</p>
|
||||
</div>
|
||||
<noscript><p class="field--full small">{f.noscript}</p></noscript>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -227,8 +228,13 @@ const f = ui.contact.form;
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// With scripting the form sends in place and answers in a toast. Without it, the server
|
||||
// redirects to /contact/sent or /contact/error.
|
||||
// The form sends in place and answers in a toast. Every send carries the answer to a
|
||||
// proof-of-work challenge (ALTCHA), worked out in the background from the moment the visitor
|
||||
// first touches the form. Without scripting the server refuses the post and redirects to
|
||||
// /contact/error, which points to the email address.
|
||||
import { solveChallenge, type Challenge } from 'altcha-lib';
|
||||
import { deriveKey } from 'altcha-lib/algorithms/web/pbkdf2';
|
||||
|
||||
const form = document.querySelector<HTMLFormElement>('[data-contact]');
|
||||
const toast = document.querySelector<HTMLElement>('[data-toast]');
|
||||
if (form && toast) {
|
||||
@@ -238,6 +244,22 @@ const f = ui.contact.form;
|
||||
const close = toast.querySelector<HTMLButtonElement>('[data-toast-close]')!;
|
||||
const submit = form.querySelector<HTMLButtonElement>('[type="submit"]')!;
|
||||
|
||||
// One challenge per message: fetched and solved once, spent by the send that carries it.
|
||||
let proof: Promise<string | null> | null = null;
|
||||
const solve = async (): Promise<string | null> => {
|
||||
try {
|
||||
const res = await fetch('/contact/challenge', { headers: { Accept: 'application/json' } });
|
||||
if (!res.ok) return null;
|
||||
const challenge: Challenge = await res.json();
|
||||
const solution = await solveChallenge({ challenge, deriveKey });
|
||||
return solution ? btoa(JSON.stringify({ challenge, solution })) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const prepare = () => (proof ??= solve());
|
||||
form.addEventListener('focusin', prepare);
|
||||
|
||||
const show = (state: 'sending' | 'sent' | 'failed', text: string) => {
|
||||
toast.dataset.state = state;
|
||||
toast.setAttribute('data-open', '');
|
||||
@@ -264,15 +286,21 @@ const f = ui.contact.form;
|
||||
form.setAttribute('aria-busy', 'true');
|
||||
show('sending', '');
|
||||
let outcome = 'failed';
|
||||
try {
|
||||
const res = await fetch(form.action, {
|
||||
method: 'POST',
|
||||
headers: { Accept: 'application/json' },
|
||||
body: new URLSearchParams(new FormData(form) as unknown as Record<string, string>),
|
||||
});
|
||||
outcome = (await res.json()).outcome ?? 'failed';
|
||||
} catch {
|
||||
outcome = 'failed';
|
||||
const altcha = await prepare();
|
||||
proof = null;
|
||||
if (altcha) {
|
||||
const body = new URLSearchParams(new FormData(form) as unknown as Record<string, string>);
|
||||
body.set('altcha', altcha);
|
||||
try {
|
||||
const res = await fetch(form.action, {
|
||||
method: 'POST',
|
||||
headers: { Accept: 'application/json' },
|
||||
body,
|
||||
});
|
||||
outcome = (await res.json()).outcome ?? 'failed';
|
||||
} catch {
|
||||
outcome = 'failed';
|
||||
}
|
||||
}
|
||||
if (outcome === 'sent') {
|
||||
form.reset();
|
||||
|
||||
+6
-4
@@ -143,7 +143,7 @@ const en = {
|
||||
},
|
||||
contact: {
|
||||
title: 'Talk to us.',
|
||||
body: 'Complex software, a tight compliance deadline, or a problem no one has solved yet. We respond within 24 hours.',
|
||||
body: 'Complex software, a tight deadline, or a problem no one has solved yet. We respond within 24 hours.',
|
||||
address: ['Innstr. 43', '94032 Passau, Germany'],
|
||||
phone: '+49 (0)851 / 509-3218',
|
||||
phoneHref: 'tel:+498515093218',
|
||||
@@ -159,6 +159,7 @@ const en = {
|
||||
sent: 'Message sent. We respond within 24 hours.',
|
||||
invalid: 'Please fill in every field and give a valid email address.',
|
||||
failed: 'Your message did not go through. Please try again, or write to office@ciphercell.de.',
|
||||
noscript: 'The form needs JavaScript. Without it, write to office@ciphercell.de.',
|
||||
toast: { sending: 'Sending', sent: 'Sent', failed: 'Not sent', close: 'Close' },
|
||||
},
|
||||
result: {
|
||||
@@ -309,7 +310,7 @@ const en = {
|
||||
},
|
||||
research: {
|
||||
title: ['Published', 'research.'],
|
||||
intro: 'Papers by our team on Open RAN, cloud-native infrastructure and hardware security. The method we sell grew out of this work.',
|
||||
intro: 'Expert perspectives on Open RAN security, cloud-native infrastructure, and hardware security, grounded in real-world engineering and backed by research.',
|
||||
sortLabel: 'Sort',
|
||||
sort: { cites: 'Most cited', newest: 'Newest', oldest: 'Oldest' },
|
||||
filterLabel: 'Category',
|
||||
@@ -458,7 +459,7 @@ const de: typeof en = {
|
||||
},
|
||||
contact: {
|
||||
title: 'Sprechen Sie mit uns.',
|
||||
body: 'Komplexe Software, eine knappe Compliance-Frist oder ein Problem, das noch niemand gelöst hat. Wir antworten innerhalb von 24 Stunden.',
|
||||
body: 'Komplexe Software, eine knappe Frist oder ein Problem, das noch niemand gelöst hat. Wir antworten innerhalb von 24 Stunden.',
|
||||
address: ['Innstr. 43', '94032 Passau, Deutschland'],
|
||||
phone: '+49 (0)851 / 509-3218',
|
||||
phoneHref: 'tel:+498515093218',
|
||||
@@ -474,6 +475,7 @@ const de: typeof en = {
|
||||
sent: 'Nachricht gesendet. Wir antworten innerhalb von 24 Stunden.',
|
||||
invalid: 'Bitte füllen Sie alle Felder aus und geben Sie eine gültige E-Mail-Adresse an.',
|
||||
failed: 'Ihre Nachricht ist nicht angekommen. Bitte versuchen Sie es erneut oder schreiben Sie an office@ciphercell.de.',
|
||||
noscript: 'Das Formular braucht JavaScript. Ohne schreiben Sie an office@ciphercell.de.',
|
||||
toast: { sending: 'Wird gesendet', sent: 'Gesendet', failed: 'Nicht gesendet', close: 'Schließen' },
|
||||
},
|
||||
result: {
|
||||
@@ -624,7 +626,7 @@ const de: typeof en = {
|
||||
},
|
||||
research: {
|
||||
title: ['Veröffentlichte', 'Forschung.'],
|
||||
intro: 'Arbeiten unseres Teams zu Open RAN, cloud-nativer Infrastruktur und Hardware-Sicherheit. Die Methode, die wir anbieten, ist aus dieser Arbeit entstanden.',
|
||||
intro: 'Experteneinblicke in Open-RAN-Sicherheit, Cloud-native Infrastruktur und Hardware-Sicherheit, basierend auf praxiserprobter Ingenieursarbeit und wissenschaftlich fundiert.',
|
||||
sortLabel: 'Sortieren',
|
||||
sort: { cites: 'Meistzitiert', newest: 'Neueste', oldest: 'Älteste' },
|
||||
filterLabel: 'Kategorie',
|
||||
|
||||
@@ -40,7 +40,8 @@ const altDe = new URL(switchLang(Astro.url, 'de'), Astro.site);
|
||||
<link rel="alternate" hreflang="en" href={altEn} />
|
||||
<link rel="alternate" hreflang="de" href={altDe} />
|
||||
<link rel="alternate" hreflang="x-default" href={altEn} />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="preload" href="/fonts/inter-tight-latin-wght-normal.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content={ui.meta.siteName} />
|
||||
|
||||
Reference in new issue
Block a user