diff options
Diffstat (limited to 'http/public')
| -rw-r--r-- | http/public/do/send_email.php | 124 | ||||
| -rw-r--r-- | http/public/do/send_otp.php | 104 | ||||
| -rw-r--r-- | http/public/do/verify_otp.php | 63 | ||||
| l--------- | http/public/images | 1 | ||||
| -rw-r--r-- | http/public/scripts/contact/actions.js | 60 | ||||
| -rw-r--r-- | http/public/scripts/contact/post-error-message.js | 34 | ||||
| -rw-r--r-- | http/public/scripts/contact/resend-otp.js | 31 | ||||
| -rw-r--r-- | http/public/scripts/contact/reset-resend-button.js | 32 | ||||
| -rw-r--r-- | http/public/scripts/contact/selectors.js | 25 | ||||
| -rw-r--r-- | http/public/scripts/contact/submit-contact-form.js | 53 | ||||
| -rw-r--r-- | http/public/scripts/contact/submit-otp-form.js | 77 | ||||
| -rw-r--r-- | http/public/scripts/otp-form-wc.js | 68 |
12 files changed, 0 insertions, 672 deletions
diff --git a/http/public/do/send_email.php b/http/public/do/send_email.php deleted file mode 100644 index 51d8b19..0000000 --- a/http/public/do/send_email.php +++ /dev/null @@ -1,124 +0,0 @@ -<?php - -use JoeacNet\Http\Config; -use JoeacNet\Http\Db; -use JoeacNet\Http\Mail; - -require_once __DIR__ . "/../../vendor/autoload.php"; -require_once __DIR__ . "/../../php/config.php"; -require_once __DIR__ . "/../../php/db.php"; -require_once __DIR__ . "/../../php/mail.php"; - -$db = Db\connectDb(); - -$payload = json_decode(file_get_contents("php://input"), true); -$name = getNameFromPayloadAndValidate($payload); -$email = getEmailFromPayloadAndValidate($payload); -$message = getMessageFromPayloadAndValidate($payload); -$token = getTokenFromPayloadAndValidate($payload); -$userId = getUserIdFromPayloadAndValidate($payload); - -limitMaxDailyEmails($db); -verifyToken(db: $db, userId: $userId, token: $token); -Db\deleteAllTokensForUser($db, $userId); - -$messageId = Mail\sendEmail( - subject: "joeac.net: $name left a message", - replyToEmail: $email, - replyToName: $name, - toEmail: Config\contactMailbox(), - toName: Config\contactMailboxName(), - body: "$name <$email> sent you a message:\n\n\n$message", -); -Db\recordSentEmail($db, $messageId); -syslog(LOG_INFO, "Sent an email to Joe. Message ID: $messageId"); - -http_response_code(200); -echo $messageId; -exit(); - -/// functions /// - -function verifyToken(PDO $db, string $userId, string $token) -{ - if (!Db\isTokenValid($db, $userId, $token)) { - http_response_code(400); - echo "token is not valid"; - exit(); - } -} - -function limitMaxDailyEmails(PDO $db) -{ - $countEmailsSentLast24Hours = Db\countEmailsSentLast24Hours($db); - if ($countEmailsSentLast24Hours > Config\maxDailyEmails()) { - $msg = - "$countEmailsSentLast24Hours emails have been sent in the last 24 hours, but the max daily load is " . - Config\maxDailyEmails() . - "."; - syslog(LOG_WARNING, $msg); - http_response_code(500); - echo $msg; - exit(); - } -} - -function getNameFromPayloadAndValidate(array $payload): string -{ - $name = (string) $payload["name"]; - if (is_null($name) || $name == "") { - http_response_code(400); - echo "name must not be empty"; - exit(); - } - return $name; -} - -function getEmailFromPayloadAndValidate(array $payload): string -{ - $email = (string) $payload["email"]; - if (is_null($email) || $email == "") { - http_response_code(400); - echo "email must not be empty"; - exit(); - } - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { - http_response_code(400); - echo "<$email> is not a valid email address."; - exit(); - } - return $email; -} - -function getMessageFromPayloadAndValidate(array $payload): string -{ - $message = (string) $payload["message"]; - if (is_null($message) || $message == "") { - http_response_code(400); - echo "message must not be empty"; - exit(); - } - return $message; -} - -function getTokenFromPayloadAndValidate(array $payload): string -{ - $token = (string) $payload["token"]; - if (is_null($token) || $token == "") { - http_response_code(400); - echo "token must not be empty"; - exit(); - } - return $token; -} - -function getUserIdFromPayloadAndValidate(array $payload): string -{ - $userId = (string) $payload["userId"]; - if (is_null($userId) || $userId == "") { - http_response_code(400); - echo "userId must not be empty"; - exit(); - } - return $userId; -} diff --git a/http/public/do/send_otp.php b/http/public/do/send_otp.php deleted file mode 100644 index 5ca4d85..0000000 --- a/http/public/do/send_otp.php +++ /dev/null @@ -1,104 +0,0 @@ -<?php - -use JoeacNet\Http\Config; -use JoeacNet\Http\Db; -use JoeacNet\Http\Mail; - -require_once __DIR__ . "/../../php/config.php"; -require_once __DIR__ . "/../../php/db.php"; -require_once __DIR__ . "/../../php/mail.php"; - -const SEND_OTP_TYPES = ["email"]; - -$db = Db\connectDb(); - -$payload = json_decode(file_get_contents("php://input"), true); -$email = getEmailFromPayloadAndValidate($payload); -$name = getNameFromPayloadAndValidate($payload); -$type = getTypeFromPayloadAndValidate($payload); - -limitMaxDailyEmails(db: $db, email: $email, name: $name); - -$otp = strtoupper(bin2hex(random_bytes(3))); -$prettyOtp = substr($otp, 0, 3) . "-" . substr($otp, 3, 6); -Db\insertOtp(db: $db, userId: $email, otp: $otp); - -try { - $messageId = Mail\sendEmail( - toEmail: $email, - toName: $name, - subject: "joeac.net: your OTP is $prettyOtp", - body: <<<BODY - Someone tried to use this email address on joeac.net. If this was you, - your one-time passcode is $prettyOtp. If this wasn't you, you don't need - to do anything. - BODY, - ); -} catch (Exception $e) { - error_log("Email could not be sent: $e"); - http_response_code(500); - echo "Email could not be sent: $e"; - exit(); -} - -Db\recordSentEmail($db, $messageId); -syslog(LOG_INFO, "Sent OTP ($prettyOtp) to $email. Message ID: $messageId"); - -http_response_code(200); -echo $messageId; -exit(); - -/// functions /// - -function limitMaxDailyEmails(PDO $db, string $email, string $name) -{ - $countEmailsSentLast24Hours = Db\countEmailsSentLast24Hours($db); - if ($countEmailsSentLast24Hours > Config\maxDailyEmails()) { - $msg = - "$name <$email> requested an OTP email, but $countEmailsSentLast24Hours emails have already been sent, whereas the max daily load is " . - Config\maxDailyEmails() . - "."; - syslog(LOG_WARNING, $msg); - http_response_code(500); - echo $msg; - exit(); - } -} - -function getNameFromPayloadAndValidate(array $payload): string -{ - $name = (string) $payload["name"]; - if (is_null($name) || $name == "") { - http_response_code(400); - echo "name must not be empty"; - exit(); - } - return $name; -} - -function getEmailFromPayloadAndValidate(array $payload): string -{ - $email = (string) $payload["email"]; - if (is_null($email) || $email == "") { - http_response_code(400); - echo "email must not be empty"; - exit(); - } - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { - http_response_code(400); - echo "<$email> is not a valid email address."; - exit(); - } - return $email; -} - -function getTypeFromPayloadAndValidate(array $payload): string -{ - $type = (string) $payload["type"]; - if (!in_array($type, SEND_OTP_TYPES)) { - http_response_code(400); - echo "type must be one of: " . SEND_OTP_TYPES; - exit(); - } - return $type; -} diff --git a/http/public/do/verify_otp.php b/http/public/do/verify_otp.php deleted file mode 100644 index 7f86858..0000000 --- a/http/public/do/verify_otp.php +++ /dev/null @@ -1,63 +0,0 @@ -<?php - -use JoeacNet\Http\Db; - -require_once __DIR__ . "/../../php/db.php"; - -$db = Db\connectDb(); - -$payload = json_decode(file_get_contents("php://input"), true); -$guess = getGuessFromPayloadAndValidate($payload); -$leniencySecs = getLeniencySecsFromPayloadAndValidate($payload); -$userId = getUserIdFromPayloadAndValidate($payload); - -if ( - !Db\isOtpCorrect( - db: $db, - userId: $userId, - guess: $guess, - leniencySecs: $leniencySecs, - ) -) { - http_response_code(400); - echo "OTP is not valid"; - exit(); -} -Db\deleteAllOtpsForUser($db, $userId); -$token = bin2hex(random_bytes(256)); -Db\insertSendEmailToken(db: $db, userId: $userId, token: $token); -http_response_code(200); -echo $token; -exit(); - -/// functions /// - -function getGuessFromPayloadAndValidate(array $payload): string -{ - $guess = (string) $payload["guess"]; - if (strlen($guess) != 6) { - http_response_code(400); - echo "guess must be six characters long"; - exit(); - } - return $guess; -} - -function getLeniencySecsFromPayloadAndValidate(array $payload): int -{ - if ((bool) $payload["lenient"] ?? false) { - return 60; - } - return 0; -} - -function getUserIdFromPayloadAndValidate(array $payload): string -{ - $userId = (string) $payload["userId"]; - if (is_null($userId) || $userId == "") { - http_response_code(400); - echo "userId must not be empty"; - exit(); - } - return $userId; -} diff --git a/http/public/images b/http/public/images deleted file mode 120000 index ccc7a5b..0000000 --- a/http/public/images +++ /dev/null @@ -1 +0,0 @@ -../../common/images/
\ No newline at end of file diff --git a/http/public/scripts/contact/actions.js b/http/public/scripts/contact/actions.js deleted file mode 100644 index 5a7e32a..0000000 --- a/http/public/scripts/contact/actions.js +++ /dev/null @@ -1,60 +0,0 @@ -export const actions = { - /** - * @param {{type: 'email', name: string, email: string}} - * @returns {Promise<void>} - */ - sendOtp: async ({ type, name, email }) => { - const url = "/do/send_otp.php"; - const req = new Request(url, { - method: "POST", - body: JSON.stringify({ type, name, email }), - }); - const res = await fetch(req); - if (!res.ok) { - throw new Error( - `Request to ${url} failed: ${res.status} ${res.statusText} ${await res.text()}`, - ); - } - }, - - /** - * @param {{guess: string, lenient?: bool, userId: string}} - * @returns {Promise<string | false>} - */ - verifyOtp: async ({ guess, lenient, userId }) => { - const url = "/do/verify_otp.php"; - const req = new Request(url, { - method: "POST", - body: JSON.stringify({ guess, lenient, userId }), - }); - const res = await fetch(req); - - if (res.status === 400) { - return false; - } else if (!res.ok) { - throw new Error( - `Request to ${url} failed: ${res.status} ${res.statusText} ${await res.text()}`, - ); - } - - return res.text(); - }, - - /** - * @param {{email: string, message: string, name: string, userId: string, token: string}} - * @returns {Promise<void>} - */ - sendEmail: async ({ email, message, name, userId, token }) => { - const url = "/do/send_email.php"; - const req = new Request(url, { - method: "POST", - body: JSON.stringify({ email, message, name, userId, token }), - }); - const res = await fetch(req); - if (!res.ok) { - throw new Error( - `Request to ${url} failed: ${res.status} ${res.statusText} ${await res.text()}`, - ); - } - }, -}; diff --git a/http/public/scripts/contact/post-error-message.js b/http/public/scripts/contact/post-error-message.js deleted file mode 100644 index 121cbb6..0000000 --- a/http/public/scripts/contact/post-error-message.js +++ /dev/null @@ -1,34 +0,0 @@ -/** @typedef {import("./selectors.js").Selectors} Selectors */ - -/** - * @param {{contactForm: Selectors}} contactForm - * @param {string} errorMsg - */ -export function postErrorMessageOnContactForm({ contactForm }, errorMsg) { - const errorElem = contactForm.errorElem(); - if (errorElem) { - errorElem.textContent = errorMsg; - errorElem.removeAttribute("hidden"); - } else { - alert(errorMsg); - } -} - -/** - * @param {{otpDialog: Selectors}} otpDialog - * @param {string} errorMsg - */ -export function postErrorMessageOnOtpForm({ otpDialog }, errorMsg) { - const errorElem = otpDialog.errorElem(); - if (errorElem) { - errorElem.textContent = errorMsg; - errorElem.removeAttribute("hidden"); - } else { - alert(errorMsg); - } - /** @type NodeListOf<HTMLInputElement> */ - const inputs = otpDialog.allOtpInputs(); - for (const input of inputs) { - input.value = ""; - } -} diff --git a/http/public/scripts/contact/resend-otp.js b/http/public/scripts/contact/resend-otp.js deleted file mode 100644 index e083d95..0000000 --- a/http/public/scripts/contact/resend-otp.js +++ /dev/null @@ -1,31 +0,0 @@ -/** @typedef {import("./selectors.js").Selectors} Selectors */ - -import { actions } from "./actions.js"; -import { resetResendButton } from "./reset-resend-button.js"; -import { postErrorMessageOnOtpForm } from "./post-error-message.js"; - -/** @typedef {{ resendButtonInterval: NodeJS.Timeout | undefined }} Result */ - -const fallbackErrorMsg = - "No can do. I'm afraid joeac.net is a bit broken right now - sorry about that."; - -/** - * @param {Selectors} selectors - * @param {NodeJS.Timeout | undefined} resetButtonInterval - * @returns {Promise<Result>} - */ -export async function resendOtp(selectors, resetButtonInterval) { - const result = resetResendButton(selectors, resetButtonInterval); - const name = selectors.contactForm.nameElem()?.value; - const email = selectors.contactForm.emailElem().value; - - try { - await actions.sendOtp({ type: "email", name, email }); - } catch (sendOtpError) { - const errorMsg = sendOtpError?.toString() ?? fallbackErrorMsg; - postErrorMessageOnOtpForm(selectors, errorMsg); - throw sendOtpError; - } - - return result; -} diff --git a/http/public/scripts/contact/reset-resend-button.js b/http/public/scripts/contact/reset-resend-button.js deleted file mode 100644 index 47bc9ac..0000000 --- a/http/public/scripts/contact/reset-resend-button.js +++ /dev/null @@ -1,32 +0,0 @@ -/** @typedef {import("./selectors.js").Selectors} Selectors */ - -/** @typedef {{ resendButtonInterval: NodeJS.Timeout | undefined }} Result */ - -/** - * @param {Selectors} selectors - * @param {NodeJS.Timeout | undefined} resetButtonInterval - * @returns {Result} - */ -export function resetResendButton({ otpDialog }, resendButtonInterval) { - clearInterval(resendButtonInterval); - - const resendButton = otpDialog.resendButton(); - if (resendButton) { - resendButton.setAttribute("data-countdown", "60"); - resendButton.setAttribute("disabled", ""); - - resendButtonInterval = setInterval(() => { - const countdown = +(resendButton.getAttribute("data-countdown") ?? 1) - 1; - resendButton.setAttribute("data-countdown", countdown.toString()); - resendButton.textContent = `Resend (${countdown}s)`; - }, 1000); - - setTimeout(() => { - clearInterval(resendButtonInterval); - resendButton.textContent = "Resend"; - resendButton.removeAttribute("disabled"); - }, 1000 * 60); - } - - return { resendButtonInterval }; -} diff --git a/http/public/scripts/contact/selectors.js b/http/public/scripts/contact/selectors.js deleted file mode 100644 index 7676414..0000000 --- a/http/public/scripts/contact/selectors.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @typedef {Object} Selectors - * @property {Object} contactForm - * @property {() => HTMLInputElement} contactForm.emailElem - * @property {() => Element | null} contactForm.errorElem - * @property {() => HTMLInputElement} contactForm.nameElem - * @property {() => HTMLTextAreaElement} contactForm.messageElem - * @property {() => HTMLFormElement | null} contactForm.self - * @property {() => HTMLInputElement | null} contactForm.submitButton - * @property {Object} otpDialog - * @property {() => NodeListOf<HTMLInputElement>} otpDialog.allOtpInputs - * @property {() => Element | null} otpDialog.errorElem - * @property {() => HTMLInputElement | null} otpDialog.firstOtpInput - * @property {() => HTMLFormElement | null} otpDialog.otpForm - * @property {() => Element | null} otpDialog.otpRecipient - * @property {() => Element | null} otpDialog.otpValidUntil - * @property {() => HTMLButtonElement | null} otpDialog.resendButton - * @property {() => HTMLDialogElement} otpDialog.self - * @property {() => HTMLInputElement | null} otpDialog.submitButton - * @property {Object} successSection - * @property {() => Element | null} successSection.email - * @property {() => Element | null} successSection.message - * @property {() => Element | null} successSection.name - * @property {() => Element | null} successSection.self - */ diff --git a/http/public/scripts/contact/submit-contact-form.js b/http/public/scripts/contact/submit-contact-form.js deleted file mode 100644 index a90f420..0000000 --- a/http/public/scripts/contact/submit-contact-form.js +++ /dev/null @@ -1,53 +0,0 @@ -import { actions } from "./actions.js"; -import { postErrorMessageOnContactForm } from "./post-error-message.js"; -import { resetResendButton } from "./reset-resend-button.js"; - -/** @typedef {import("./selectors.js").Selectors} Selectors */ - -const fallbackErrorMsg = - "No can do. I'm afraid joeac.net is a bit broken right now - sorry about that."; - -/** - * @param {Selectors} selectors - * @param {NodeJS.Timeout | undefined} resetButtonInterval - * @returns {Promise<Result>} - */ -export async function submitContactForm(selectors, resendButtonInterval) { - const { contactForm, otpDialog } = selectors; - - const name = contactForm.nameElem()?.value; - const email = contactForm.emailElem().value; - - contactForm.submitButton()?.setAttribute("disabled", ""); - try { - await actions.sendOtp({ type: "email", name, email }); - } catch (sendOtpError) { - const errorMsg = sendOtpError?.toString() ?? fallbackErrorMsg; - postErrorMessageOnContactForm(selectors, errorMsg); - throw sendOtpError; - } - - const otpRecipient = otpDialog.otpRecipient(); - email && otpRecipient && (otpRecipient.textContent = `<${email}>`); - const otpValidUntil = otpDialog.otpValidUntil(); - const validUntil = new Date(Date.now() + 1000 * 60 * 5); - otpValidUntil && - (otpValidUntil.textContent = `until ${validUntil.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`); - - const dialog = otpDialog.self(); - dialog.showModal(); - contactForm.submitButton()?.removeAttribute("disabled"); - dialog.addEventListener("click", function (event) { - const rect = dialog.getBoundingClientRect(); - const isInDialog = - rect.top <= event.clientY && - event.clientY <= rect.top + rect.height && - rect.left <= event.clientX && - event.clientX <= rect.left + rect.width; - if (!isInDialog) { - dialog.close(); - } - }); - - return resetResendButton(selectors, resendButtonInterval); -} diff --git a/http/public/scripts/contact/submit-otp-form.js b/http/public/scripts/contact/submit-otp-form.js deleted file mode 100644 index 8c09314..0000000 --- a/http/public/scripts/contact/submit-otp-form.js +++ /dev/null @@ -1,77 +0,0 @@ -import { actions } from "./actions.js"; -import { postErrorMessageOnOtpForm } from "./post-error-message.js"; - -/** @typedef {import("./selectors.js").Selectors} Selectors */ - -const fallbackErrorMsg = - "No can do. I'm afraid joeac.net is a bit broken right now - sorry about that."; - -/** - * @param {Selectors} selectors - */ -export async function submitOtpForm(selectors) { - const { contactForm, otpDialog, successSection } = selectors; - const otpForm = otpDialog.otpForm(); - otpDialog.submitButton()?.setAttribute("disabled", ""); - - const otpFormData = new FormData(otpForm ?? undefined); - const guess = [ - otpFormData.get("1"), - otpFormData.get("2"), - otpFormData.get("3"), - otpFormData.get("4"), - otpFormData.get("5"), - otpFormData.get("6"), - ].join(""); - - const name = contactForm.nameElem()?.value; - const email = contactForm.emailElem().value; - const message = contactForm.messageElem()?.value; - - let verifyResult; - try { - verifyResult = await actions.verifyOtp({ guess, userId: email }); - } catch (verifyError) { - otpDialog.submitButton()?.removeAttribute("disabled"); - postErrorMessageOnOtpForm( - selectors, - verifyError?.toString() ?? fallbackErrorMsg, - ); - return; - } - - if (verifyResult === false) { - otpDialog.submitButton()?.removeAttribute("disabled"); - postErrorMessageOnOtpForm(selectors, "Incorrect OTP. Check your email?"); - otpDialog.firstOtpInput()?.focus(); - return; - } - const sendmailToken = verifyResult; - - try { - await actions.sendEmail({ - email, - message: message, - name: name, - userId: email, - token: sendmailToken, - }); - } catch (sendEmailError) { - const errorMsg = sendEmailError?.toString() ?? fallbackErrorMsg; - postErrorMessageOnOtpForm(selectors, errorMsg); - otpDialog.submitButton()?.removeAttribute("disabled"); - return; - } - - const sentName = successSection.name(); - const sentEmail = successSection.email(); - const sentMessage = successSection.message(); - sentName && (sentName.textContent = name ?? "???"); - sentEmail && (sentEmail.textContent = email ?? "???"); - sentMessage && (sentMessage.textContent = message ?? "???"); - - contactForm.self()?.remove(); - successSection.self()?.removeAttribute("hidden"); - otpDialog.submitButton()?.removeAttribute("disabled"); - otpDialog.self().close(); -} diff --git a/http/public/scripts/otp-form-wc.js b/http/public/scripts/otp-form-wc.js deleted file mode 100644 index 49e1b02..0000000 --- a/http/public/scripts/otp-form-wc.js +++ /dev/null @@ -1,68 +0,0 @@ -class OtpForm extends HTMLElement { - /** @type MutationObserver? */ - observer; - - constructor() { - super(); - } - - connectedCallback() { - this.observer = new MutationObserver(() => { - this.clearInputs(); - this.configureInputs(); - }); - this.observer.observe(this, { childList: true, subtree: true }); - } - - disconnectedCallback() { - this.observer?.disconnect(); - } - - clearInputs() { - console.log("clearing all inputs"); - /** @type NodeListOf<HTMLInputElement> */ - const inputs = this.querySelectorAll('input:not([type="submit"])'); - for (const input of inputs) { - input.value = ""; - } - } - - configureInputs() { - this.observer?.disconnect(); - /** @type NodeListOf<HTMLInputElement> */ - const inputs = this.querySelectorAll('input:not([type="submit"])'); - /** @type NodeListOf<HTMLFormElement> */ - const form = this.querySelector("form"); - - for (const input of inputs) { - input.addEventListener("focus", () => { - input.select(); - }); - - input.addEventListener("input", () => { - if (input.value.length > 0) { - input.value = input.value.slice(0, 1).toLocaleUpperCase(); - /** @type HTMLInputElement */ - const nextInput = input.nextElementSibling; - if (nextInput) { - nextInput.focus(); - return; - } - - /** @type HTMLInputElement */ - const submitButton = form.querySelector('input[type="submit"]'); - submitButton.focus(); - let areAllInputsEntered = true; - inputs.forEach((input) => { - areAllInputsEntered = areAllInputsEntered && input.value.length > 0; - }); - if (areAllInputsEntered) { - form.requestSubmit(submitButton); - } - } - }); - } - } -} - -customElements.define("otp-form", OtpForm); |
