| 1 | //! Persisted domain types, defined as Toasty models. Tables are created from |
| 2 | //! these definitions via [`crate::db`]'s `push_schema`. |
| 3 | //! |
| 4 | //! Foreign keys are kept as plain scalar fields (`owner_id`, `user_id`) and |
| 5 | //! queried explicitly, rather than declaring Toasty relations — simpler and a |
| 6 | //! good fit for our small schema. |
| 7 | |
| 8 | /// A registered user account. |
| 9 | #[derive(Debug, toasty::Model)] |
| 10 | pub struct User { |
| 11 | #[key] |
| 12 | #[auto] |
| 13 | pub id: i64, |
| 14 | #[unique] |
| 15 | pub username: String, |
| 16 | pub email: String, |
| 17 | /// Argon2 PHC-format password hash. |
| 18 | pub password_hash: String, |
| 19 | pub is_admin: bool, |
| 20 | /// Unix timestamp (seconds) of account creation. |
| 21 | pub created_at: i64, |
| 22 | } |
| 23 | |
| 24 | /// A hosted repository, owned by a [`User`]. |
| 25 | #[derive(Debug, toasty::Model)] |
| 26 | pub struct Repository { |
| 27 | #[key] |
| 28 | #[auto] |
| 29 | pub id: i64, |
| 30 | #[index] |
| 31 | pub owner_id: i64, |
| 32 | pub name: String, |
| 33 | pub description: String, |
| 34 | pub is_private: bool, |
| 35 | /// Short name of the default branch, e.g. `main`. |
| 36 | pub default_branch: String, |
| 37 | pub created_at: i64, |
| 38 | /// Push-mirror remote: after every successful push, refs are mirrored to |
| 39 | /// this git URL (`git push --mirror`). Empty disables mirroring. New |
| 40 | /// columns go last so `ALTER TABLE ADD COLUMN` on existing databases |
| 41 | /// agrees with the fresh-schema column order. |
| 42 | pub mirror_url: String, |
| 43 | /// SHA-256 hash of the preview image extracted from README (empty if none). |
| 44 | pub preview_image_hash: String, |
| 45 | /// Primary language detected in the repository (e.g. "Rust", empty if no files). |
| 46 | pub primary_language: String, |
| 47 | /// JSON array of language percentages: [{"lang": "Rust", "percent": 75.5}, ...]. |
| 48 | pub languages_json: String, |
| 49 | } |
| 50 | |
| 51 | /// A CI run for a pushed commit. |
| 52 | /// |
| 53 | /// `status` is one of `queued`, `running`, `success`, `failure` (a step exited |
| 54 | /// non-zero), or `error` (the runner itself failed). `started_at`/`finished_at` |
| 55 | /// are 0 until they occur. |
| 56 | #[derive(Clone, Debug, toasty::Model)] |
| 57 | pub struct CiRun { |
| 58 | #[key] |
| 59 | #[auto] |
| 60 | pub id: i64, |
| 61 | #[index] |
| 62 | pub repo_id: i64, |
| 63 | /// Full commit SHA the run is for. |
| 64 | pub commit: String, |
| 65 | /// Short branch name that was pushed (e.g. `main`). |
| 66 | pub ref_name: String, |
| 67 | pub status: String, |
| 68 | /// Accumulated run log. |
| 69 | pub log: String, |
| 70 | pub created_at: i64, |
| 71 | pub started_at: i64, |
| 72 | pub finished_at: i64, |
| 73 | } |
| 74 | |
| 75 | /// One artifact produced by a CI run, stored on disk under |
| 76 | /// `data_dir/artifacts/{repo_id}/{commit}/` (see `docs/ci-artifacts.md`). |
| 77 | /// |
| 78 | /// `commit` is denormalized from the run so per-commit lookups (the |
| 79 | /// latest-on-branch alias) don't join through runs. |
| 80 | #[derive(Clone, Debug, toasty::Model)] |
| 81 | pub struct CiArtifact { |
| 82 | #[key] |
| 83 | #[auto] |
| 84 | pub id: i64, |
| 85 | #[index] |
| 86 | pub run_id: i64, |
| 87 | #[index] |
| 88 | pub repo_id: i64, |
| 89 | /// Full commit SHA the producing run was for. |
| 90 | pub commit: String, |
| 91 | /// Declared artifact name (unique within a pipeline, not globally). |
| 92 | pub name: String, |
| 93 | /// Total size in bytes (summed over files for directory artifacts). |
| 94 | pub size: i64, |
| 95 | /// Directory artifact (stored as a tarball, or extracted when `browse`). |
| 96 | pub is_dir: bool, |
| 97 | /// Served as a browsable static site rather than a download. |
| 98 | pub browse: bool, |
| 99 | /// JSON object of metadata-extractor key → output. |
| 100 | pub meta: String, |
| 101 | pub created_at: i64, |
| 102 | } |
| 103 | |
| 104 | /// An issue on a repository. `number` is the user-facing per-repo sequence |
| 105 | /// (`#1`, `#2`, …); `id` stays the global key. `state` is `open` or `closed`. |
| 106 | /// |
| 107 | /// Numbering is assigned as max+1 at creation; with a single server process |
| 108 | /// (our deployment shape) that cannot race. |
| 109 | #[derive(Clone, Debug, toasty::Model)] |
| 110 | pub struct Issue { |
| 111 | #[key] |
| 112 | #[auto] |
| 113 | pub id: i64, |
| 114 | #[index] |
| 115 | pub repo_id: i64, |
| 116 | pub number: i64, |
| 117 | pub title: String, |
| 118 | /// Markdown body (may be empty). |
| 119 | pub body: String, |
| 120 | pub author_id: i64, |
| 121 | pub state: String, |
| 122 | pub created_at: i64, |
| 123 | /// Bumped on comments and state changes, for "recently active" ordering. |
| 124 | pub updated_at: i64, |
| 125 | } |
| 126 | |
| 127 | /// A comment on an [`Issue`]. |
| 128 | #[derive(Clone, Debug, toasty::Model)] |
| 129 | pub struct IssueComment { |
| 130 | #[key] |
| 131 | #[auto] |
| 132 | pub id: i64, |
| 133 | #[index] |
| 134 | pub issue_id: i64, |
| 135 | pub author_id: i64, |
| 136 | /// Markdown body. |
| 137 | pub body: String, |
| 138 | pub created_at: i64, |
| 139 | } |
| 140 | |
| 141 | /// A web login session, keyed by an opaque random token stored in a cookie. |
| 142 | #[derive(Debug, toasty::Model)] |
| 143 | pub struct Session { |
| 144 | #[key] |
| 145 | pub token: String, |
| 146 | #[index] |
| 147 | pub user_id: i64, |
| 148 | pub created_at: i64, |
| 149 | /// Unix timestamp (seconds) after which the session is invalid. |
| 150 | pub expires_at: i64, |
| 151 | } |
| 152 | |
| 153 | /// An uploaded file (e.g. an image pasted into the file editor), stored |
| 154 | /// outside git at `data_dir/attachments/{repo_id}/{hash}` so large binaries |
| 155 | /// never enter the repository's history. Markdown carries only the serve URL. |
| 156 | /// |
| 157 | /// Content-addressed: `hash` is the lowercase hex SHA-256 of the bytes, so the |
| 158 | /// same content uploaded twice to a repo dedupes to one file. Lookups and GC |
| 159 | /// scope by `repo_id`, which also gates serving by the repo's read access. |
| 160 | #[derive(Clone, Debug, toasty::Model)] |
| 161 | pub struct Attachment { |
| 162 | #[key] |
| 163 | #[auto] |
| 164 | pub id: i64, |
| 165 | #[index] |
| 166 | pub repo_id: i64, |
| 167 | /// Lowercase hex SHA-256 of the content — both the dedup key and the path |
| 168 | /// component under the repo's attachment directory. |
| 169 | pub hash: String, |
| 170 | /// MIME type to serve the bytes with (e.g. `image/png`). |
| 171 | pub content_type: String, |
| 172 | pub size: i64, |
| 173 | /// The user who first uploaded this content to the repo. |
| 174 | pub uploader_id: i64, |
| 175 | pub created_at: i64, |
| 176 | } |
| 177 | |
| 178 | /// A personal access token: a long-lived, scoped bearer credential for |
| 179 | /// non-browser API clients (e.g. tooling that fetches attachments). Only the |
| 180 | /// SHA-256 hash of the token is stored; the plaintext is shown once at |
| 181 | /// creation. A PAT is least-privilege by design — its `scopes` bound what it |
| 182 | /// can do, and the only scope today (`read`) authenticates safe (GET/HEAD) |
| 183 | /// requests only, so a leaked token can never mutate. |
| 184 | #[derive(Clone, Debug, toasty::Model)] |
| 185 | pub struct ApiToken { |
| 186 | #[key] |
| 187 | #[auto] |
| 188 | pub id: i64, |
| 189 | #[index] |
| 190 | pub user_id: i64, |
| 191 | /// A human label for the token (e.g. "claude"), for listing/revoking. |
| 192 | pub name: String, |
| 193 | /// Lowercase hex SHA-256 of the token; the lookup key. |
| 194 | #[unique] |
| 195 | pub token_hash: String, |
| 196 | /// Comma-separated scopes granted to this token (e.g. `read`). |
| 197 | pub scopes: String, |
| 198 | pub created_at: i64, |
| 199 | } |
| 200 | |
| 201 | /// A registered SSH public key, used to authenticate git-over-SSH connections. |
| 202 | #[derive(Debug, toasty::Model)] |
| 203 | pub struct SshKey { |
| 204 | #[key] |
| 205 | #[auto] |
| 206 | pub id: i64, |
| 207 | #[index] |
| 208 | pub user_id: i64, |
| 209 | pub title: String, |
| 210 | /// Canonical SHA256 fingerprint, e.g. `SHA256:…`. |
| 211 | #[unique] |
| 212 | pub fingerprint: String, |
| 213 | /// Normalized OpenSSH public-key line. |
| 214 | pub content: String, |
| 215 | pub created_at: i64, |
| 216 | } |
| 217 | |
| 218 | /// A registered passkey (WebAuthn credential) used to sign in. |
| 219 | /// |
| 220 | /// Only public material is here: the credential id, its public key, and the |
| 221 | /// counters the spec asks a relying party to track. The private key lives in |
| 222 | /// the authenticator and is never transmitted, so this table is not a |
| 223 | /// credential store in the way a password hash is — losing it costs users |
| 224 | /// their registrations, not their secrets. See [`crate::passkeys`]. |
| 225 | #[derive(Clone, Debug, toasty::Model)] |
| 226 | pub struct Passkey { |
| 227 | #[key] |
| 228 | #[auto] |
| 229 | pub id: i64, |
| 230 | #[index] |
| 231 | pub user_id: i64, |
| 232 | /// User-supplied label, e.g. "MacBook Touch ID". |
| 233 | pub name: String, |
| 234 | /// Base64url credential id, as the authenticator reports it. |
| 235 | #[unique] |
| 236 | pub credential_id: String, |
| 237 | /// Base64 WebAuthn user handle: opaque, per account, shared by that |
| 238 | /// account's passkeys. |
| 239 | pub user_handle: String, |
| 240 | /// Base64 of the credential's immutable state (its public key). |
| 241 | pub static_state: String, |
| 242 | /// Base64 of the mutable state (signature counter, backup and |
| 243 | /// user-verification flags), rewritten after every sign-in. |
| 244 | pub dynamic_state: String, |
| 245 | /// Encoded transport hints (USB, NFC, internal, …) for re-prompting. |
| 246 | pub transports: i64, |
| 247 | pub created_at: i64, |
| 248 | /// Unix time of the last successful sign-in, or 0 if never used. |
| 249 | pub last_used_at: i64, |
| 250 | } |
| 251 | |
| 252 | /// A per-repository secret, stored only as a sealed envelope. |
| 253 | /// |
| 254 | /// The server cannot read `envelope`: it is encrypted to the owner's |
| 255 | /// ssh-ed25519 keys by the client that set it (see [`crate::secrets`]). |
| 256 | /// `recipients` denormalizes the envelope's fingerprints so the UI can tell, |
| 257 | /// without opening anything, which secrets a newly registered key still cannot |
| 258 | /// decrypt — those need `anvild secret rekey`. |
| 259 | #[derive(Clone, Debug, toasty::Model)] |
| 260 | pub struct RepoSecret { |
| 261 | #[key] |
| 262 | #[auto] |
| 263 | pub id: i64, |
| 264 | #[index] |
| 265 | pub repo_id: i64, |
| 266 | /// Environment variable name, e.g. `DEPLOY_TOKEN`. Unique per repository. |
| 267 | pub name: String, |
| 268 | /// The sealed envelope, as JSON (`anvil-secret-v1`). |
| 269 | pub envelope: String, |
| 270 | /// Comma-separated SSH fingerprints the envelope is sealed to. |
| 271 | pub recipients: String, |
| 272 | pub created_at: i64, |
| 273 | pub updated_at: i64, |
| 274 | } |
| 275 | |
| 276 | /// Cached admin metrics computed periodically (e.g., disk usage snapshot). |
| 277 | #[derive(Clone, Debug, toasty::Model)] |
| 278 | pub struct AdminCache { |
| 279 | #[key] |
| 280 | #[auto] |
| 281 | pub id: i64, |
| 282 | /// Cache key (e.g., "disk_usage"). |
| 283 | pub key: String, |
| 284 | /// JSON-encoded cached data. |
| 285 | pub value: String, |
| 286 | /// Unix timestamp (seconds) of when this snapshot was taken. |
| 287 | pub computed_at: i64, |
| 288 | } |