Break out contact form into separate component and implement OTP and rate
limiting, add iconify-icon dependency Add src/components/ContactForm.astro implementing OTP verify/send flows and server-side validation Add src/lib/Otp.ts (otplib-backed) with per-hour OTP and per-week message rate limits and helper functions Expose OTP_SUPER_SECRET_SALT in astro.config and add otplib Rename src/lib/cap.ts to src/lib/CapAdapter.ts and update imports Replace inline contact page logic with ContactForm and adjust SMS client Add iconify-icon dependency
This commit is contained in:
parent
d5a7887ad2
commit
72e57fb7ff
10 changed files with 516 additions and 129 deletions
|
|
@ -1,132 +1,11 @@
|
|||
---
|
||||
import Layout from "@layouts/BaseLayout.astro";
|
||||
import SmsClient from "@lib/SmsGatewayClient.ts";
|
||||
import CapServer from "@lib/cap";
|
||||
import ContactForm from "@components/ContactForm.astro";
|
||||
export const prerender = false;
|
||||
|
||||
const errors = { name: "", phone: "", msg: "", form: "" };
|
||||
let success = false;
|
||||
if (Astro.request.method === "POST") {
|
||||
try {
|
||||
const data = await Astro.request.formData();
|
||||
const name = data.get("name")?.toString();
|
||||
const capToken = data.get("cap-token")?.toString();
|
||||
const phone = data.get("phone")?.toString();
|
||||
const msg = data.get("msg")?.toString();
|
||||
|
||||
if (typeof capToken !== "string" || !(await CapServer.validateToken(capToken)).success) {
|
||||
throw new Error("invalid cap token");
|
||||
}
|
||||
|
||||
if (typeof name !== "string" || name.length < 1) {
|
||||
errors.name += "Please enter a name. ";
|
||||
}
|
||||
if (typeof phone !== "string") {
|
||||
errors.phone += "Phone is not valid. ";
|
||||
}
|
||||
if (typeof msg !== "string" || msg.length < 20) {
|
||||
errors.msg += "Message must be at least 20 characters. ";
|
||||
}
|
||||
|
||||
const hasErrors = Object.values(errors).some(msg => msg)
|
||||
if (!hasErrors) {
|
||||
const smsClient = new SmsClient();
|
||||
const message = "Web message from " + name + " (" + phone + "):\n\n" + msg;
|
||||
const result = await smsClient.sendSMS(message);
|
||||
if (!result.success) {
|
||||
errors.form += "Sending SMS failed; API returned error. "
|
||||
} else { success = true; }
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
errors.form += error.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
---
|
||||
<script>import CapWidget from "@cap.js/widget";</script>
|
||||
<style>
|
||||
cap-widget {
|
||||
--cap-background: #fdfdfd;
|
||||
--cap-border-color: var(--border-color);
|
||||
--cap-border-radius: 4px;
|
||||
--cap-widget-height: 100%;
|
||||
--cap-widget-width: 100%;
|
||||
--cap-widget-padding: 10px 20px;
|
||||
--cap-gap: 14px;
|
||||
--cap-color: #212121;
|
||||
--cap-checkbox-size: 25px;
|
||||
--cap-checkbox-border: 1px solid #aaaaaad1;
|
||||
--cap-checkbox-border-radius: 4px;
|
||||
--cap-checkbox-background: #fafafa91;
|
||||
--cap-checkbox-margin: 2px;
|
||||
--cap-font: system, -apple-system, "BlinkMacSystemFont", ".SFNSText-Regular", "San Francisco",
|
||||
"Roboto", "Segoe UI", "Helvetica Neue", "Lucida Grande", "Ubuntu", "arial", sans-serif;
|
||||
--cap-spinner-color: #000;
|
||||
--cap-spinner-background-color: #eee;
|
||||
--cap-spinner-thickness: 5px;
|
||||
}
|
||||
</style>
|
||||
<Layout>
|
||||
<title slot="head">Contact</title>
|
||||
<style>
|
||||
form {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
grid-template-areas:
|
||||
"header header header header "
|
||||
"name name phone phone"
|
||||
"msg msg msg msg"
|
||||
"captcha captcha submit submit";
|
||||
}
|
||||
div {
|
||||
grid-area: header;
|
||||
}
|
||||
label[for="name"] {
|
||||
grid-area: name;
|
||||
}
|
||||
label[for="phone"] {
|
||||
grid-area: phone;
|
||||
}
|
||||
label[for="msg"] {
|
||||
grid-area: msg;
|
||||
}
|
||||
label[for="captcha"] {
|
||||
grid-area: captcha;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
button#submit {
|
||||
grid-area: submit;
|
||||
}
|
||||
</style>
|
||||
<Fragment slot="content">
|
||||
<h2>Contact</h2>
|
||||
{!success && <form method="post" x-data="{}">
|
||||
<div>
|
||||
<p>Use the below form to shoot me a quick text!</p>
|
||||
{errors.form && <p>{errors.form}</p>}
|
||||
</div>
|
||||
<label for="name">
|
||||
Name
|
||||
<input type="text" id="name" name="name" placeholder="Bad Blocks" />
|
||||
{errors.name && <p>{errors.name}</p>}
|
||||
</label>
|
||||
<label for="phone">
|
||||
Phone
|
||||
<input type="text" id="phone" name="phone" placeholder="555-555-5555" />
|
||||
{errors.phone && <p>{errors.phone}</p>}
|
||||
</label>
|
||||
<label for="msg">
|
||||
Msg
|
||||
<textarea id="msg" name="msg" placeholder="I think badblocks rocks!"></textarea>
|
||||
{errors.msg && <p>{errors.msg}</p>}
|
||||
</label>
|
||||
<label for="captcha">
|
||||
<cap-widget id="captcha" data-cap-api-endpoint="/cap/"></cap-widget>
|
||||
</label>
|
||||
<button id="submit" type="submit">Submit</button>
|
||||
</form> || <p>Your message has been sent successfully!</p>}
|
||||
<ContactForm />
|
||||
</Fragment>
|
||||
</Layout>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue