## MODIFIED Requirements

### Requirement: Google OAuth sign-in with email allowlist (FR-4.1 adapted, P0)
The system SHALL provide admin sign-in at `/admin` by two means: a Google account, and a
username and password. Every admin account SHALL be exactly one of those two kinds, fixed when
the account is created, and SHALL be usable only by its own kind — a Google account has no
password to try, and a password account cannot be reached by presenting a Google assertion for
the same address.

**Google accounts.** Sign-in succeeds only when the Google account's verified email matches an
**active** account. A verified Google account with no record SHALL NOT be rejected outright: the
system SHALL create an inactive Google-kind record with the default role and SHALL tell the
visitor their access is awaiting approval. An account that exists but is not active SHALL be
refused a session and SHALL be told which of the two states it is in — awaiting approval, or
access withdrawn. Because any verified Google account can reach the pending state, the system
SHALL rate-limit pending-account creation per source address.

**Password accounts.** Sign-in succeeds only when the username matches an active password
account and the password verifies against its stored hash. Password accounts SHALL NOT
self-register: they exist only because a super admin created them. A failed password attempt
SHALL be answered with one message that does not distinguish an unknown username from a wrong
password, since here — unlike the pending queue — there is nothing a visitor can legitimately
learn from the difference.

Passwords SHALL be stored only as a salted hash from a memory-hard algorithm, never reversibly
and never logged. `last_login_at` is recorded on every successful sign-in of either kind, and
the first-seen time is recorded when a pending record is created.

#### Scenario: Active Google admin signs in
- **WHEN** an active Google-kind admin completes Google sign-in
- **THEN** a session is established and `last_login_at` is recorded

#### Scenario: Active password admin signs in
- **WHEN** an active password-kind admin submits their correct username and password
- **THEN** a session is established and `last_login_at` is recorded

#### Scenario: First sign-in by an unknown Google account
- **WHEN** a verified Google account with no record completes sign-in
- **THEN** no session is issued, an inactive Google-kind record is created with the default role, the visitor is told their access is awaiting approval, and the attempt is recorded

#### Scenario: Second sign-in while still pending
- **WHEN** the same pending account signs in again before a super admin has acted
- **THEN** no session is issued, the visitor sees the same awaiting-approval message, and no second record is created

#### Scenario: Unverified Google email
- **WHEN** a Google sign-in completes with `email_verified` false
- **THEN** access is rejected and no record is created

#### Scenario: Wrong password
- **WHEN** a password account's holder submits an incorrect password
- **THEN** no session is issued and the message is identical to the one for a username that does not exist

#### Scenario: The two kinds do not cross
- **WHEN** a Google-kind account's email is submitted to the password form, or a password account's address arrives as a Google assertion
- **THEN** no session is issued, and the response does not reveal that an account of the other kind exists

#### Scenario: Password sign-in for an unknown username
- **WHEN** a username with no account is submitted
- **THEN** no session is issued and no account is created

#### Scenario: Deactivated admin
- **WHEN** an admin whose access was withdrawn attempts sign-in by either means
- **THEN** access is rejected, they are told access was withdrawn rather than that they are pending, and no session is issued

#### Scenario: Flood of unknown Google accounts
- **WHEN** pending-account creations from one source address exceed the configured limit within the window
- **THEN** further attempts are refused without creating records

#### Scenario: Unauthenticated access
- **WHEN** a request hits any `/admin` page or admin API without a valid session
- **THEN** it is redirected to sign-in / rejected with 401

### Requirement: Role-based permissions (§2.2, P0)
The system SHALL enforce exactly two roles.

**Super Admin** — everything: every event and every guest in the system, plus configuring
gateways, managing the admin roster, viewing the audit log, and transferring event ownership.

**Host** — everything an event owner needs, but only within the events they own: view those
events and their guests and counts, create events, add/edit/import guests, send invitations and
broadcasts, export, edit invitation content, manage reminders, generate QR codes, and delete
their own events and the guests under them. A Host SHALL NOT configure gateways, manage the
admin roster, view the audit log, or reach any event they do not own.

Role SHALL be independent of how the account signs in: a password account and a Google account
holding the same role have exactly the same permissions and the same scope.

A Host's permission to act is therefore two independent checks — the capability, and the scope —
and both SHALL be enforced by the API on its own authority regardless of what the interface
offers.

#### Scenario: Host deletes their own guest
- **WHEN** a Host deletes a guest belonging to an event they own
- **THEN** the deletion succeeds

#### Scenario: Host attempts to reach another owner's event
- **WHEN** a Host requests an event owned by someone else, by id, directly against the API
- **THEN** the request is refused and the response does not reveal whether that event exists

#### Scenario: Host requests the audit log
- **WHEN** a Host requests the audit log
- **THEN** access is denied

#### Scenario: Host attempts to manage the roster
- **WHEN** a Host calls any admin-roster endpoint
- **THEN** access is denied with a forbidden error

#### Scenario: Sign-in kind does not affect permissions
- **WHEN** a password-kind Super Admin and a Google-kind Super Admin each call the same endpoint
- **THEN** both are answered identically

#### Scenario: Enforcement is server-side
- **WHEN** a role-restricted or scope-restricted API is called directly, bypassing the UI
- **THEN** the same authorization and the same scope rules apply

#### Scenario: Super admin is unscoped
- **WHEN** a Super Admin lists events, guests or counts
- **THEN** every event in the system is included regardless of owner

## ADDED Requirements

### Requirement: JWT sessions valid for two days (P0)
A successful sign-in of either kind SHALL issue a signed JSON Web Token identifying the account,
valid for **two days**, delivered in an httpOnly, secure, sameSite cookie. The token SHALL NOT be
readable by page JavaScript and SHALL NOT be placed in browser storage.

The token SHALL carry identity only. Role, status and scope SHALL be read from the database on
every request and never taken from the token, so a role change, a withdrawal or an ownership
transfer applies on the very next request rather than when the token expires. A token whose
signature fails, whose lifetime has passed, or whose account is no longer active SHALL be
refused.

Each account SHALL carry a moment before which its tokens are invalid. Changing a password, or
having a password set for the account, SHALL move that moment to now, so every token issued
before the change stops working immediately.

#### Scenario: Token issued on sign-in
- **WHEN** an admin signs in successfully by either means
- **THEN** a signed token valid for two days is set in an httpOnly cookie

#### Scenario: Token is not reachable from JavaScript
- **WHEN** a page attempts to read the session from `document.cookie` or from browser storage
- **THEN** the token is not there

#### Scenario: Expiry
- **WHEN** two days pass after sign-in
- **THEN** the token is refused and the admin must sign in again

#### Scenario: Role change beats the token
- **WHEN** a Super Admin is demoted to Host while holding a token issued minutes earlier
- **THEN** their next request is answered with Host permissions, because the role came from the database and not from the token

#### Scenario: Withdrawal beats the token
- **WHEN** an admin's access is withdrawn while they hold a valid unexpired token
- **THEN** their next request is rejected as unauthenticated

#### Scenario: Password change invalidates outstanding tokens
- **WHEN** an admin changes their password while signed in on another device
- **THEN** the other device's token is refused on its next request

#### Scenario: Tampered token
- **WHEN** a token's payload is altered and re-presented
- **THEN** the signature check fails and the request is rejected

### Requirement: Password quality and brute-force resistance (P0)
The system SHALL enforce a minimum password length and reject passwords it can tell are trivially
weak, at every point a password is set — by an admin issuing a temporary one, and by a user
choosing their own.

Repeated failed password attempts against one account SHALL lock further attempts on that account
for a cooling-off period, and attempts SHALL additionally be rate-limited per source address so
one attacker cannot spread guesses across many accounts. A lockout SHALL NOT be reported in a way
that distinguishes a locked real account from an account that does not exist. A successful
sign-in SHALL clear the failure count.

#### Scenario: Weak password refused
- **WHEN** a password shorter than the minimum, or an obviously trivial one, is submitted at any point a password is set
- **THEN** it is refused and the requirement is stated

#### Scenario: Lockout after repeated failures
- **WHEN** failed attempts against one account exceed the threshold
- **THEN** further attempts on that account are refused for the cooling-off period, even with the correct password

#### Scenario: Lockout is not an oracle
- **WHEN** a locked account and a non-existent username are each tried
- **THEN** the responses are indistinguishable

#### Scenario: Spread attempts are limited too
- **WHEN** one source address makes failed attempts across many usernames
- **THEN** the source is rate-limited independently of any per-account lockout

#### Scenario: Success resets the count
- **WHEN** an admin signs in correctly after some failed attempts
- **THEN** the failure count returns to zero

### Requirement: A temporary password confines the session until it is replaced (P0)
When a super admin sets a password for an account, that password SHALL be marked temporary. An
account holding a temporary password SHALL be able to sign in and SHALL receive a session, and
that session SHALL be refused by **every** admin surface except reading its own identity,
changing its own password, and signing out. The confinement SHALL be enforced by the API on its
own authority, so navigating directly to any admin screen or calling any admin endpoint cannot
escape it.

Setting a new password of the user's own choosing SHALL lift the confinement immediately, without
signing in again. The new password SHALL NOT be permitted to equal the temporary one. A temporary
password SHALL itself expire after a bounded period, after which it no longer signs in and a
super admin must issue another.

#### Scenario: Signing in with a temporary password
- **WHEN** an admin signs in with a temporary password
- **THEN** a session is issued and they are sent to set a new password

#### Scenario: Confined session cannot work
- **WHEN** that admin calls any admin endpoint other than reading their identity, changing their password, or signing out
- **THEN** the request is refused and the response says a password change is required

#### Scenario: Confinement cannot be navigated past
- **WHEN** that admin types the URL of an admin screen directly
- **THEN** they are returned to the password-change screen and the underlying data is not served

#### Scenario: Setting their own password releases them
- **WHEN** they set a new password that meets the quality rules
- **THEN** the confinement lifts on the next request with no further sign-in, and the temporary mark is cleared

#### Scenario: Reusing the temporary password
- **WHEN** they submit the temporary password as their new password
- **THEN** it is refused

#### Scenario: Temporary password expires
- **WHEN** a temporary password is not used before its expiry
- **THEN** it no longer signs in, and a super admin must issue a new one

### Requirement: Self-service password change in profile settings (P0)
Every admin SHALL have a profile screen showing their own account — email, username where they
have one, role, and how they sign in. A password account's holder SHALL be able to change their
own password there, by supplying their current password and a new one meeting the quality rules.
A Google account's holder SHALL be shown no password controls, because there is no password on
that account to change.

Changing one's own password SHALL invalidate tokens issued before the change, and SHALL keep the
session that performed it working, so an admin is not signed out of the device they just used.
No admin SHALL be able to change another admin's password from this screen; that is the roster's
job and it produces a temporary password, never a chosen one.

#### Scenario: Password account changes its own password
- **WHEN** a password admin supplies their correct current password and a valid new one
- **THEN** the password is changed, they remain signed in on that device, and their other sessions stop working

#### Scenario: Wrong current password
- **WHEN** the current password supplied is incorrect
- **THEN** the change is refused and the stored password is unchanged

#### Scenario: Google account has nothing to change
- **WHEN** a Google admin opens profile settings
- **THEN** their account is shown with no password controls and no way to set one

#### Scenario: Not a route to someone else's account
- **WHEN** a request attempts to change a password for any account other than the caller's own
- **THEN** it is refused regardless of the caller's role

### Requirement: Development-only username sign-in (P0)
A development build SHALL offer sign-in by email address alone, without Google and without a
password, so the admin area can be opened locally with no OAuth client and no credential. This
SHALL be refused unless **both** the dev-bypass flag is enabled **and** the environment is not
production, and a production build SHALL refuse to start at all while the flag is set. The email
SHALL still have to match an active account, so this weakens authentication only — never
authorization. Its use SHALL be recorded in the audit log as having used the bypass,
distinguishably from a Google or password sign-in.

It SHALL be presented separately from the real password form and labelled as a development
shortcut, so the two are not mistaken for one another. It SHALL work for an account of either
kind, and SHALL NOT bypass the temporary-password confinement — a confined account signed in this
way is still confined.

#### Scenario: Dev build, flag on
- **WHEN** a developer signs in by email in a dev build with the bypass enabled and that email is an active admin
- **THEN** a session is issued and the audit entry records that the bypass was used

#### Scenario: Dev build, unknown email
- **WHEN** the email is not an active account
- **THEN** no session is issued and no pending record is created — the dev sign-in never provisions

#### Scenario: Production build
- **WHEN** the dev sign-in endpoint is called against a production build
- **THEN** it behaves as though it does not exist

#### Scenario: Production refuses to boot with the bypass on
- **WHEN** the application starts in production with the dev-bypass flag enabled
- **THEN** startup fails with an explicit error rather than serving the endpoint

#### Scenario: The dev sign-in form is absent in production
- **WHEN** the sign-in page is rendered by a production build
- **THEN** no development shortcut is offered, and only the Google control and the password form are present

#### Scenario: It does not bypass confinement
- **WHEN** an account holding a temporary password signs in through the dev shortcut
- **THEN** the session is still confined to changing that password

### Requirement: Bootstrap super admin (P0)
The system SHALL be configured with one bootstrap administrator email that is provisioned as an
active Super Admin, in production as well as in development. Provisioning SHALL be idempotent —
applying it again SHALL NOT duplicate the record, and SHALL NOT overwrite a role, a status, or a
password that a super admin has since changed by hand. A production build SHALL refuse to start
without this configured, because an empty roster means every Google sign-in lands in the pending
queue with nobody holding the permission to approve it.

#### Scenario: First production start
- **WHEN** a production deployment starts against an empty database with the bootstrap email configured
- **THEN** that email exists as an active Super Admin and can sign in with Google

#### Scenario: Same account in development
- **WHEN** a developer uses the dev sign-in with the bootstrap email
- **THEN** they sign in as that same Super Admin

#### Scenario: Re-running provisioning
- **WHEN** provisioning runs again after a super admin has changed that account's role or given it a password
- **THEN** the change is preserved and no second record is created

#### Scenario: Missing configuration in production
- **WHEN** a production build starts with no bootstrap email configured
- **THEN** startup fails with an explicit error naming the missing setting

## REMOVED Requirements

### Requirement: Admin user management (P0)
**Reason**: The admin roster is no longer a sub-concern of authentication. It now has its own
screens, its own pending-approval queue produced by first-time Google sign-in, account creation
of two different kinds, temporary-password issuance, and event-ownership transfer — none of which
fit inside a sign-in capability. The three-role vocabulary it names (Super Admin / Co-host /
Viewer) no longer exists either.

**Migration**: Superseded by the `admin-user-management` capability, which carries every
behaviour this requirement described — add by email, assign role, deactivate, change role, audit
every change, and protect the last active Super Admin — restated against the two-role model and
extended to both kinds of account.
