anvilsign in

collin/anvil

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