Step 2 of the build order. The admin mints an invite link, the recipient registers with it, and from then on has a session. - The invite is spent in the same transaction that creates the account, so a failed signup leaves the code usable - Sessions are idle timeouts, 24h or 30 days with remember me, read from a cookie or a bearer header, extended at most once a minute - Ban is a reversible toggle that drops the member's live sessions - No password minimum; login is rate limited instead, 10 failures per email in 15 minutes, cleared by a correct password - Invite codes render as links carrying ?code=, which the register form prefills; PUBLIC_URL makes them pasteable from the loopback admin panel Tests cover invite spending, the idle timeout, ban, and the rate limiter.
148 lines
3.6 KiB
CSS
148 lines
3.6 KiB
CSS
/* Theme tokens first — the dark rock/metal look lives here and nowhere else. */
|
|
:root {
|
|
--bg: #121212;
|
|
--surface: #1c1c1e;
|
|
--surface-2: #26262a;
|
|
--border: #35353a;
|
|
--text: #ece9e6;
|
|
--muted: #9a948d;
|
|
--accent: #ff5722;
|
|
--accent-2: #c62828;
|
|
--error: #ef5350;
|
|
--radius: 4px;
|
|
/* ponytail: system stack until an Oswald woff2 is vendored into /static. */
|
|
--font-head: "Oswald", "Fira Sans Condensed", "Arial Narrow", system-ui, sans-serif;
|
|
--font-body: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
body {
|
|
margin: 0;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: var(--font-body);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
h1, h2, h3 {
|
|
font-family: var(--font-head);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
margin: 0 0 0.6rem;
|
|
}
|
|
|
|
h1 { color: var(--accent); font-size: 1.9rem; }
|
|
h2 { font-size: 1.2rem; border-bottom: 1px solid var(--border); padding-bottom: 0.3rem; }
|
|
|
|
a { color: var(--accent); }
|
|
|
|
header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
padding: 0.8rem 1.2rem;
|
|
background: var(--surface);
|
|
border-bottom: 2px solid var(--accent-2);
|
|
}
|
|
|
|
.brand {
|
|
font-family: var(--font-head);
|
|
font-size: 1.3rem;
|
|
text-transform: uppercase;
|
|
text-decoration: none;
|
|
color: var(--text);
|
|
}
|
|
|
|
nav { display: flex; align-items: center; gap: 1rem; }
|
|
nav a { text-decoration: none; }
|
|
|
|
main { max-width: 52rem; margin: 0 auto; padding: 1.5rem 1.2rem 4rem; }
|
|
section { margin-bottom: 2.5rem; }
|
|
|
|
.muted { color: var(--muted); }
|
|
.error { color: var(--error); display: block; font-size: 0.9rem; }
|
|
|
|
.tag {
|
|
font-size: 0.7rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
background: var(--accent-2);
|
|
padding: 0.1rem 0.4rem;
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
.flash {
|
|
max-width: 52rem;
|
|
margin: 1rem auto 0;
|
|
padding: 0.7rem 1rem;
|
|
background: var(--surface-2);
|
|
border-left: 3px solid var(--accent);
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
.avatar {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 2rem;
|
|
height: 2rem;
|
|
border-radius: 50%;
|
|
background: var(--accent-2);
|
|
font-family: var(--font-head);
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
form.stack { display: flex; flex-direction: column; gap: 0.9rem; max-width: 24rem; }
|
|
form.stack label { display: flex; flex-direction: column; gap: 0.25rem; }
|
|
form.stack label.row { flex-direction: row; align-items: center; gap: 0.5rem; }
|
|
|
|
input {
|
|
background: var(--surface-2);
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 0.5rem 0.6rem;
|
|
font: inherit;
|
|
}
|
|
|
|
input:focus-visible, button:focus-visible, a:focus-visible {
|
|
outline: 2px solid var(--accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
button {
|
|
background: var(--accent);
|
|
color: #150c07;
|
|
border: 0;
|
|
border-radius: var(--radius);
|
|
padding: 0.5rem 0.9rem;
|
|
font: inherit;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
|
|
button:hover { background: #ff7043; }
|
|
|
|
button.link {
|
|
background: none;
|
|
color: var(--accent);
|
|
padding: 0;
|
|
font-weight: normal;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
table { width: 100%; border-collapse: collapse; margin-top: 0.8rem; }
|
|
th, td { text-align: left; padding: 0.5rem 0.4rem; border-bottom: 1px solid var(--border); }
|
|
th { font-family: var(--font-head); text-transform: uppercase; font-size: 0.8rem; color: var(--muted); }
|
|
tr.banned { opacity: 0.55; }
|
|
|
|
.actions { display: flex; flex-wrap: wrap; gap: 0.4rem; }
|
|
.actions form { display: flex; gap: 0.3rem; }
|
|
.actions input { width: 10rem; }
|
|
|
|
code { background: var(--surface-2); padding: 0.1rem 0.35rem; border-radius: var(--radius); }
|
|
|
|
.invite { word-break: break-all; }
|