feat(backend): Discord OAuth login with DB-backed sessions (#23)

The browser UI signs in with a Discord authorization code grant
(identify + guilds.members.read) instead of a shared password. Guild
membership is the gate; the owner's Discord ID is the only identity
allowed in while registration is closed. Sessions become rows in a
sessions table with opaque random ids — the cookie carries only the id,
looked up and expiry-checked per request — so deleting a row revokes a
session. HMAC cookie signing, its derived key, and WEB_PASSWORD are
gone, and no replacement signing secret is introduced (ADR-0002).

Discord's API base is configurable (DISCORD_API_BASE); the full flow is
tested through the real router against a local stub, including the
form-encoded token exchange Discord rejects if sent as JSON.
This commit is contained in:
2026-08-08 08:31:46 +07:00
parent 8cebb94b92
commit 13e8e73da7
19 changed files with 1272 additions and 357 deletions
+6 -20
View File
@@ -774,26 +774,6 @@ button { cursor: pointer; }
filter: drop-shadow(0 0 34px var(--ember-wash)) drop-shadow(0 18px 24px rgba(0,0,0,.5));
}
.login-card form { display: flex; flex-direction: column; gap: 18px; }
.login-card label {
font: 500 10px/1 var(--font-mono);
letter-spacing: .16em;
text-transform: uppercase;
color: var(--mute);
}
.login-card input {
width: 100%;
height: 54px;
margin-top: 9px;
padding: 0 2px;
border: none;
border-bottom: 1px solid var(--field-line);
background: transparent;
color: var(--paper);
font: 500 20px var(--font-mono);
letter-spacing: .16em;
outline: none;
}
.login-card input:focus { border-bottom-color: var(--paper); }
.login-card .error {
margin: 0;
min-height: 20px;
@@ -812,6 +792,12 @@ button { cursor: pointer; }
border-color: var(--ember);
color: #fff;
}
.login-card .login-note {
margin: 14px 0 0;
text-align: center;
font: 400 12px/1.4 var(--font-body);
color: var(--mute);
}
/* ---- laptop and up: the whole sheet is drawn 20% larger, which is what
reading it at 120% zoom on a 1920-wide screen was doing by hand. Everything