AlgoJudge documentation0.2
Domain model

User

An account, what "deleted" means here, and the session rows behind the users screen.

User extends ASP.NET Core Identity's IdentityUser. Identity supplies the credential half — login, address, password hash, lockout — and the product adds the rest.

FieldMeaning
FirstName, LastNameoptional
Notea sentence about the account, written by staff. Not a tag
Tagsfree display metadata, never queried. Opaque jsonb
ApprovedAtwhen staff approved the account. Absent means pending
IsTemporarya bulk account handed out on a slip of paper
ExpiresAtwhen it runs out
BlockedReasonwhy the account is blocked
CreatedAt, LastSeenAt
Anonymizedthe account has been deleted, which here means emptied

Approval is not confirmation

ApprovedAt is deliberately not the same fact as EmailConfirmed. Confirming an address proves somebody reads that mailbox; approving an account is a decision a person made. One state holding both would make an installation that confirms no addresses unable to approve anybody.

Blocked and expired are computed, not stored twice

Blocking is LockoutEnd, and never a second boolean: two fields answering is this account blocked is two fields that can disagree, and Identity already enforces the first one at sign-in. BlockedReason says why; it does not say whether.

Expiry is one expression over ExpiresAt, never written back as a block. Setting a block from the date would put two writers on one field — a manager and the clock — and both ways they disagree are silent: unblocking would defeat the expiry, and moving the date would leave a stale block behind.

Deletion is anonymization

A user row is never removed

Submissions and results name a user id and it has to stay resolvable, so deletion empties the row in place — immediately, with no grace period. Anonymized exists so a screen can say "deleted account" rather than showing a blank name and leaving a reader to guess.

An account merge follows the same rule: both accounts keep their rows, and the emptied one is anonymized when the undo window closes. user:merge is its own permission because it hands one person's submissions and points to somebody else.

Emptying the name is only the first half

What somebody wrote, and what they left in a queue, carries their name as surely as the name field does. Deletion reaches those too.

Questions they asked keep their row and lose their content. Topic and body both become [deleted]. A question signed with a class and a surname is not anonymous because the account row is.

Print requests still outstanding are disposed of — the ones waiting in the queue and the ones somebody at a printer has already taken. Bytes somebody asked to have printed are theirs, and a deleted account must not leave them sitting in a queue for whoever is next at the printer. Disposal is the same act an operator performs on a request they finish with: the reference goes, the file goes with it, and the record of the request stays. The outcome is recorded as discarded, with nobody named as having resolved it.

Every session is closed and stripped, not only the open ones. The rows stay, because what is deleted is the person and not the record that somebody signed in; the address and the user agent are cleared from all of them. An anonymization that leaves personal data behind is not one.

Submissions stay and their origin does not. A submission is somebody's record in a contest and survives erasure by design, so IpAddress and DeviceId are cleared and the submission is not. The exclusion reason goes with them; the exclusion itself stays. Whether a submission counted is contest history, and clearing that would quietly move a board — the sentence about a named person is not history.

None of this reaches a backup

Every step above rewrites rows and deletes bytes in the live store. A backup taken before the deletion still holds the name, the addresses and the source somebody asked to have printed, and restoring one brings them back.

Passwords

Ordinary participants authenticate through OIDC; see Identity. The embedded Identity stays permanently for administrator, local and temporary accounts — a room of bulk logins handed out on paper has no mailboxes and no provider. Nothing prevents an installation from running entirely on local accounts, and that is not the recommendation: registering a provider is.

An address is optional on an account, deliberately, and the seeded administrator is such an account.

UserSession

One sign-in, which is neither a person nor a browser tab.

FieldMeaning
StartedAt, EndedAtEndedAt is set on sign-out, so history survives it
LastRequestAt, LastRequestPathan API path, not the screen somebody was looking at
IpAddressinet, normalized before it is stored
UserAgent
DeviceIdthe name this browser gives itself, or null
ExpiresAt

IpAddress is inet rather than text because the question anybody will ever ask of it — is this inside the examination room's network — is a containment test, and over a string it is a string comparison.

The number of open WebSockets for a session is counted live from the connection registry and never stored. A count written to a row is a count that survives a crash and tells the users screen somebody is present who left hours ago.

DeviceId is self-declared: it arrives in a header the page writes, so whoever is using the browser can read, change or clear it. It is weaker evidence than the session cookie, which is HttpOnly. It is stored as a Guid rather than as the text that arrived — client-supplied text stored whole is text that reaches a screen unvalidated.

On this page