Files
FabledSteward/steward/templates/settings/user_invite.html
T
bvandeusenandClaude Opus 5.5 2b6e12859a
CI / lint (push) Successful in 3s
CI / integration (push) Failing after 1m17s
CI / unit (push) Successful in 1m43s
CI / publish (push) Skipped
feat(users): invite by email — a one-time link where the invitee makes their own account (milestone 496)
The operator's ask: an invite link sent by mail, the key in the URL, and
the new user choosing their own username, password and email.

- user_invites (migration 0037): the key's sha256 only, the address, the
  role and days the admin chose, who sent it, used/revoked.
- core/invites: create (revokes an older open invite to the address),
  renew (new key, new clock), find_pending, the four states, signup checks.
- /invite/<key>, outside sign-in and throttled like login: the invitee's
  username, email (prefilled, theirs to change) and password; the account
  takes the admin's role, the key is used up in the same transaction and
  they are signed in. A bad key gets one plain message.
- Settings › Users: Invite by email (address, role, 1/7/30 days), sent from
  the admin in their zone; without a mail server, or if sending fails, the
  link is shown once to copy. Invites not yet used are listed with Resend
  and Withdraw.
- Audit: user.invited, user.invite_resent, user.invite_revoked, user.joined.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:54:44 -04:00

44 lines
2.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{# settings/user_invite.html — invite someone by email (milestone 496): they
make their own account from the link, with the role chosen here. #}
{% extends "base.html" %}
{% from "_macros.html" import crumbs, page_header %}
{% from "settings/_tabs.html" import settings_tabs %}
{% block title %}Invite by email — Settings — Steward{% endblock %}
{% block section_tabs %}{{ settings_tabs("users") }}{% endblock %}
{% block breadcrumb %}{{ crumbs([("Settings", "/settings/"), ("Users", "/settings/users/"), ("Invite by email", "")]) }}{% endblock %}
{% block content %}
{{ page_header("Invite by email", "They choose their own username, email and password from the link.") }}
{% if error %}<div class="alert alert-error" role="alert">{{ error }}</div>{% endif %}
<form method="post" action="/settings/users/invite" class="form-page">{{ csrf_field() }}
<section class="card form-section" aria-labelledby="inv-who">
<h2 id="inv-who" class="section-title">Invite</h2>
<div class="field">
<label class="field-label" for="inv-email">Send to</label>
<input class="input" id="inv-email" type="email" name="email" maxlength="255" required autofocus
value="{{ form.get('email', '') }}" autocomplete="off">
{% if not mail_ready %}<span class="field-hint tone-warn">No mail server is set, so you'll get the link to pass on yourself. Set one in Settings › <a href="/settings/notifications/">Notifications</a>.</span>{% endif %}
</div>
<div class="form-pair">
<div class="field">
<label class="field-label" for="inv-role">Role</label>
<select class="input" id="inv-role" name="role">
{% for r in roles %}<option value="{{ r.value }}" {% if form.get('role', 'viewer') == r.value %}selected{% endif %}>{{ r.value|capitalize }}</option>{% endfor %}
</select>
</div>
<div class="field">
<label class="field-label" for="inv-days">Link works for</label>
<select class="input" id="inv-days" name="days">
{% for d in days %}<option value="{{ d }}" {% if form.get('days', default_days|string) == d|string %}selected{% endif %}>{{ d }} day{{ 's' if d != 1 }}</option>{% endfor %}
</select>
</div>
</div>
<p class="field-hint">A viewer reads; an operator also runs playbooks and changes monitors, hosts and alerts; an admin also changes Settings and users. They can't change the role you pick. The link works once.</p>
</section>
<div class="form-actions">
<button type="submit" class="btn">{{ "Send invite" if mail_ready else "Make invite link" }}</button>
<a href="/settings/users/" class="btn btn-ghost">Cancel</a>
</div>
</form>
{% endblock %}