added messages templates

This commit is contained in:
onysd 2026-09-01 14:40:06 +03:00
parent e8dc967e6a
commit 7cd1f64d0d
29 changed files with 1266 additions and 84 deletions

View file

@ -30,6 +30,7 @@ export function ServerSettingsPage() {
{tab === "settings" ? (
<div className="stacked-sections">
<IdentitySection />
<LoginNotificationsSection />
<EnvSection />
</div>
) : (
@ -127,6 +128,168 @@ function IdentitySection() {
);
}
// --- Login notifications ------------------------------------------------
// LoginNotificationsSection edits the 777000 login-notification message's
// per-method (phone/email) template -- a different concern from brand
// identity above even though both live in the same identity.json (see
// cmd/telesrv-admin/serversettings.go's handleSetWelcomeMessageTemplatesAPI
// doc comment), so it gets its own card and its own save action.
function LoginNotificationsSection() {
const [identity, setIdentity] = useState<ServerIdentity | null>(null);
const [phoneTemplate, setPhoneTemplate] = useState("");
const [emailTemplate, setEmailTemplate] = useState("");
const [codeTemplate, setCodeTemplate] = useState("");
const [error, setError] = useState("");
async function load() {
setError("");
try {
const info = await api.serverIdentity();
setIdentity(info);
setPhoneTemplate(info.welcome_message_phone_template ?? "");
setEmailTemplate(info.welcome_message_email_template ?? "");
setCodeTemplate(info.login_code_message_template ?? "");
} catch (err) {
setError(errorMessage(err));
}
}
useEffect(() => { void load(); }, []);
const phoneIsOverridden = phoneTemplate.trim() !== "";
const emailIsOverridden = emailTemplate.trim() !== "";
const codeIsOverridden = codeTemplate.trim() !== "";
// Mirrors the server-side check in handleSetLoginCodeMessageTemplateAPI --
// disable the save button instead of letting the operator submit a
// template that would silently never deliver the actual OTP code.
const codeOccurrences = (codeTemplate.match(/\{\{code\}\}/g) ?? []).length;
const codeTemplateInvalid = codeIsOverridden && codeOccurrences !== 1;
return (
<section className="section-block">
<SectionHead title={"Login notifications"} />
{error && <Alert>{error}</Alert>}
{!identity ? (
<LoadingSurface label={"Loading login notification templates..."} />
) : (
<div className="card-body">
<p style={{ color: "var(--muted)", marginTop: 0 }}>
{"Sent from the official system account on every completed sign-in. Use "}
<code>{"{{server_name}}"}</code>
{" to insert the server's configured name."}
</p>
<label className="form-field">
<span>
{"Phone sign-in template"}
{" "}
{phoneIsOverridden ? <span className="badge good">{"custom"}</span> : <span className="badge">{"default"}</span>}
</span>
<textarea
rows={4}
value={phoneTemplate}
onChange={(event) => setPhoneTemplate(event.target.value)}
placeholder={identity.default_welcome_message_phone_template}
/>
</label>
<div className="gift-table-actions">
<ActionButton
tone="neutral"
compact
label={"Reset to default"}
path="/api/actions/set-welcome-message-templates"
payload={() => ({ phone_template: "", email_template: emailTemplate })}
disabled={!phoneIsOverridden}
onDone={() => { setPhoneTemplate(""); void load(); }}
/>
</div>
<label className="form-field">
<span>
{"Email sign-in template"}
{" "}
{emailIsOverridden ? <span className="badge good">{"custom"}</span> : <span className="badge">{"default"}</span>}
</span>
<textarea
rows={4}
value={emailTemplate}
onChange={(event) => setEmailTemplate(event.target.value)}
placeholder={identity.default_welcome_message_email_template}
/>
</label>
<div className="gift-table-actions">
<ActionButton
tone="neutral"
compact
label={"Reset to default"}
path="/api/actions/set-welcome-message-templates"
payload={() => ({ phone_template: phoneTemplate, email_template: "" })}
disabled={!emailIsOverridden}
onDone={() => { setEmailTemplate(""); void load(); }}
/>
</div>
<div className="gift-table-actions identity-save-row">
<ActionButton
tone="neutral"
label={"Save login notification templates"}
path="/api/actions/set-welcome-message-templates"
payload={() => ({ phone_template: phoneTemplate, email_template: emailTemplate })}
onDone={() => void load()}
/>
</div>
<p style={{ color: "var(--muted)", marginTop: "1.5em", borderTop: "1px solid var(--line)", paddingTop: "1em" }}>
{"Sent from the official system account with every login code (SMS and email alike). Must contain "}
<code>{"{{code}}"}</code>
{" exactly once -- that's where the actual code is inserted and bolded. "}
<code>{"{{server_name}}"}</code>
{" is optional and may appear any number of times."}
</p>
<label className="form-field">
<span>
{"Login-code message template"}
{" "}
{codeIsOverridden ? <span className="badge good">{"custom"}</span> : <span className="badge">{"default"}</span>}
</span>
<textarea
rows={5}
value={codeTemplate}
onChange={(event) => setCodeTemplate(event.target.value)}
placeholder={identity.default_login_code_message_template}
/>
{codeTemplateInvalid && (
<span style={{ color: "var(--danger-text)", fontSize: "0.85em" }}>
{codeOccurrences === 0
? "Must contain {{code}} exactly once -- it is currently missing."
: `Must contain {{code}} exactly once -- it currently appears ${codeOccurrences} times.`}
</span>
)}
</label>
<div className="gift-table-actions">
<ActionButton
tone="neutral"
compact
label={"Reset to default"}
path="/api/actions/set-login-code-message-template"
payload={() => ({ template: "" })}
disabled={!codeIsOverridden}
onDone={() => { setCodeTemplate(""); void load(); }}
/>
</div>
<div className="gift-table-actions identity-save-row">
<ActionButton
tone="neutral"
label={"Save login-code message template"}
path="/api/actions/set-login-code-message-template"
payload={() => ({ template: codeTemplate })}
disabled={codeTemplateInvalid}
onDone={() => void load()}
/>
</div>
</div>
)}
</section>
);
}
function ServerIconModal({ hasIcon, onClose, onDone }: { hasIcon: boolean; onClose: () => void; onDone: () => void }) {
const [file, setFile] = useState<File | null>(null);
const [previewURL, setPreviewURL] = useState("");

View file

@ -851,6 +851,26 @@ export type ServerIdentity = {
name: string;
description: string;
icon_ext?: string;
// welcome_message_*_template: raw admin-panel override for the 777000
// login-notification message, empty when unset (falls back to the
// TELESRV_WELCOME_MESSAGE_*_TEMPLATE env var, then a built-in default).
welcome_message_phone_template?: string;
welcome_message_email_template?: string;
// default_welcome_message_*_template: the effective fallback text this
// admin process currently reads (env var if set, else the compiled-in
// copy) -- shown when the override above is empty.
default_welcome_message_phone_template: string;
default_welcome_message_email_template: string;
// login_code_message_template: raw admin-panel override for the 777000
// login-code delivery message, empty when unset (falls back to the
// TELESRV_LOGIN_CODE_MESSAGE_TEMPLATE env var, then a built-in default).
// Unlike the welcome_message_* templates there is only one -- the
// message never varies by delivery channel. Must contain the {{code}}
// placeholder exactly once (enforced server-side on save).
login_code_message_template?: string;
// default_login_code_message_template: the effective fallback text this
// admin process currently reads -- shown when the override above is empty.
default_login_code_message_template: string;
};
export type EnvField = {