Email: one branded HTML layout with a text part, and invites can be emailed (#5917)
Each email was a subject and a plain-text f-string at its call site. mail_templates.py now holds one layout: the site name, a heading, the message, a button for the link and a footer saying who sent it and why. Every email (password reset, test, invite) is a function returning its subject, text and HTML. mailer.send sends them as multipart/alternative, text first. The HTML is email-shaped: tables, inline styles, no images or fonts, and every value from settings or a person escaped. POST /api/invites takes send_email. It is refused before anything is made when email is off or the invite names no address. The link is built from the public address. A failed send leaves the invite standing with email_error, so the admin still gets the link once. The invite form offers "Email the link to …" when email is on and an address is filled in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { api } from "../api/client";
|
||||
import { errorMessage } from "../api/errors";
|
||||
import { appLink } from "../router/links";
|
||||
@@ -7,6 +7,7 @@ import BaseButton from "./BaseButton.vue";
|
||||
import BaseInput from "./BaseInput.vue";
|
||||
import OneTimeSecret from "./OneTimeSecret.vue";
|
||||
import { toastOnFailure, useLoad } from "../composables/useAction";
|
||||
import { useConfigStore } from "../stores/config";
|
||||
|
||||
// Admin: invite one person to register while registration stays closed (#5172).
|
||||
// The server keeps only a hash of each invite's token, so the link is shown once,
|
||||
@@ -38,17 +39,30 @@ const days = ref(7);
|
||||
const creating = ref(false);
|
||||
// The link for the invite just made. Never retrievable again once dismissed.
|
||||
const freshLink = ref("");
|
||||
const freshNote = ref("");
|
||||
|
||||
// Emailing the link (#5917) needs email set up and an address to send it to. The
|
||||
// server says whether email is on in the same flag the sign-in page reads.
|
||||
const config = useConfigStore();
|
||||
const sendEmail = ref(true);
|
||||
const canEmail = computed(() => config.passwordResetByEmail && email.value.trim() !== "");
|
||||
|
||||
async function create() {
|
||||
creating.value = true;
|
||||
error.value = "";
|
||||
freshLink.value = "";
|
||||
try {
|
||||
const res = await api.post<{ invite: Invite; token: string }>("/api/invites", {
|
||||
email: email.value.trim() || null,
|
||||
days: days.value,
|
||||
});
|
||||
const to = email.value.trim();
|
||||
const res = await api.post<{ invite: Invite; token: string; emailed?: boolean; email_error?: string }>(
|
||||
"/api/invites",
|
||||
{ email: to || null, days: days.value, send_email: canEmail.value && sendEmail.value },
|
||||
);
|
||||
freshLink.value = appLink({ name: "register", query: { invite: res.token } });
|
||||
freshNote.value = res.emailed
|
||||
? `Emailed to ${to}. The link is here too, in case it's easier to send yourself.`
|
||||
: res.email_error
|
||||
? `${res.email_error} Send this link to ${to} yourself.`
|
||||
: "Send this to the person you're inviting.";
|
||||
invites.value = [res.invite, ...invites.value];
|
||||
email.value = "";
|
||||
} catch (e) {
|
||||
@@ -100,7 +114,7 @@ onMounted(() => {
|
||||
<OneTimeSecret
|
||||
v-if="freshLink"
|
||||
:value="freshLink"
|
||||
note="Send this to the person you're inviting."
|
||||
:note="freshNote"
|
||||
@done="freshLink = ''"
|
||||
/>
|
||||
|
||||
@@ -123,7 +137,13 @@ onMounted(() => {
|
||||
<option v-for="l in LIFETIMES" :key="l.days" :value="l.days">{{ l.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<BaseButton type="submit" :loading="creating">Create invite</BaseButton>
|
||||
<BaseButton type="submit" :loading="creating">{{
|
||||
canEmail && sendEmail ? "Create and email" : "Create invite"
|
||||
}}</BaseButton>
|
||||
<label v-if="canEmail" class="flex w-full items-center gap-2 text-sm text-neutral-700 dark:text-neutral-300">
|
||||
<input v-model="sendEmail" type="checkbox" class="h-4 w-4 accent-brand" />
|
||||
Email the link to {{ email.trim() }}
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<p v-if="error" class="form-error">{{ error }}</p>
|
||||
|
||||
Reference in New Issue
Block a user