Skip to content
1,964 linesCodeBlameRaw
1//! The identity service: accounts, credentials and sessions.
2//!
3//! Each `*Args` struct is the argument of the method of the same name,
4//! served at `POST /rpc/<method>`.
5
6use serde::{Deserialize, Serialize};
7
8use crate::User;
9
10#[derive(Clone, Debug, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct SshKey {
13 pub id: String,
14 pub title: String,
15 pub fingerprint: String,
16 /// RFC 3339.
17 pub created_at: String,
18 /// When it was last used to sign in over SSH, RFC 3339, to within 5
19 /// minutes; null when it never was.
20 #[serde(default)]
21 pub last_used_at: Option<String>,
22}
23
24#[derive(Clone, Debug, Default, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct AccessToken {
27 pub id: String,
28 pub name: String,
29 /// RFC 3339.
30 pub created_at: String,
31 /// RFC 3339, to within a few minutes. Null until it is first used.
32 pub last_used_at: Option<String>,
33 /// For a workspace's token, the username of the member who made it.
34 /// Null once that account is gone, and on personal tokens.
35 pub created_by: Option<String>,
36 /// Its scopes, as `resource:level`, the highest of each resource.
37 /// Null: full access (an application's or an agent's credential).
38 #[serde(default)]
39 pub scopes: Option<Vec<String>>,
40 /// Made before tokens had scopes: full access until someone narrows it.
41 #[serde(default)]
42 pub legacy: bool,
43 /// RFC 3339. Null: it does not expire.
44 #[serde(default)]
45 pub expires_at: Option<String>,
46 /// Its scopes as permissions: each resource it may use, by name, at
47 /// the highest level, such as `{"issues": "write"}`. Every resource at
48 /// its highest when `scopes` is null.
49 #[serde(default)]
50 pub permissions: std::collections::BTreeMap<String, String>,
51 /// What it is for, as its owner wrote it.
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub description: Option<String>,
54 /// A personal token's reach: the workspace it is made for, by slug;
55 /// null for every workspace its owner belongs to (or, with
56 /// `repository_selection` public, none). Null on a workspace's own
57 /// token, which reaches its workspace.
58 #[serde(default)]
59 pub workspace: Option<String>,
60 /// Which repositories of that workspace it reaches.
61 #[serde(default)]
62 pub repository_selection: crate::scopes::RepositorySelection,
63 /// With `selected`: the repositories, as `owner/name`, that the viewer
64 /// can see.
65 #[serde(default)]
66 pub repositories: Vec<String>,
67 /// Whether a token made for a workspace that approves tokens may be
68 /// used there yet.
69 #[serde(default)]
70 pub status: crate::tokens::TokenStatus,
71 /// Why an owner denied or revoked it.
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub review_reason: Option<String>,
74 /// Whether it is a workspace's own token, acting as the workspace.
75 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
76 pub workspace_owned: bool,
77 /// A workspace's own token with Repositories: admin, which acts as an
78 /// admin of the workspace's repositories rather than with Write.
79 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
80 pub admin: bool,
81 /// A person's token its owner let use the website (g1t.sh) as them,
82 /// with `Authorization: Bearer`. See [`crate::scopes::TokenAccess::website`].
83 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
84 pub website: bool,
85}
86
87/// `sign_in`: verifies a username, or any confirmed email address of the
88/// account, and its password, for website sign-in. Wrong passwords are
89/// counted against the account and `client`, and past a limit nothing is
90/// checked for a while (see identity's `throttle.rs`).
91/// Returns `Outcome<SignedIn>`.
92#[derive(Debug, Serialize, Deserialize)]
93pub struct SignInArgs {
94 pub username: String,
95 pub password: String,
96 /// Who is asking, such as the visitor's IP address, for rate limits.
97 #[serde(default)]
98 pub client: Option<String>,
99}
100
101#[derive(Debug, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct SignedIn {
104 pub user: User,
105 /// Empty while `two_factor_challenge` is set: no session is made until
106 /// the code is given.
107 pub session_token: String,
108 /// Set when the account has two-factor authentication on: the token to
109 /// pass to `two_factor_sign_in` with a code. Valid for
110 /// `accounts::TWO_FACTOR_CHALLENGE_SECONDS`.
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub two_factor_challenge: Option<String>,
113}
114
115/// `sign_out` and `user_for_session`.
116#[derive(Debug, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct SessionArgs {
119 pub session_token: String,
120}
121
122/// `user_for_git_credentials`: the account password or an access token.
123#[derive(Debug, Serialize, Deserialize)]
124pub struct GitCredentialsArgs {
125 pub username: String,
126 pub secret: String,
127}
128
129/// `user_for_access_token`.
130#[derive(Debug, Serialize, Deserialize)]
131pub struct TokenArgs {
132 pub token: String,
133}
134
135/// `user_for_ssh_key`, and `principal_for_ssh_key` (see [`crate::deploy_keys`]).
136#[derive(Debug, Serialize, Deserialize)]
137pub struct FingerprintArgs {
138 pub fingerprint: String,
139}
140
141/// `user_by_username`.
142#[derive(Debug, Serialize, Deserialize)]
143pub struct UsernameArgs {
144 pub username: String,
145}
146
147/// `usernames`: the names behind account and workspace ids, as events and
148/// other records store them. Returns a map from id to name; ids it does
149/// not know are left out. Also `accounts`: the accounts behind user ids,
150/// each with its username and avatar (`HashMap<String, accounts::EmailOwner>`).
151#[derive(Debug, Serialize, Deserialize)]
152pub struct UsernamesArgs {
153 pub ids: Vec<String>,
154}
155
156/// `display_usernames`: how each of these people (by lowercased username,
157/// at most 200) wrote their username, for showing it beside the key.
158/// Returns a map from the lowercased username to its chosen case; people
159/// who chose none, and names nobody has, are left out.
160#[derive(Debug, Default, Serialize, Deserialize)]
161pub struct DisplayUsernamesArgs {
162 pub usernames: Vec<String>,
163}
164
165/// `list_ssh_keys` and `list_access_tokens`.
166#[derive(Debug, Serialize, Deserialize)]
167pub struct UserArgs {
168 pub user: User,
169}
170
171/// `ssh_key_owners`: services only. The account (user id) that registered
172/// each key, by fingerprint (`SHA256:…`, as `ssh-keygen -lf` prints it),
173/// for verifying commits signed with SSH keys. Returns a map of the
174/// fingerprints found to user ids.
175#[derive(Debug, Serialize, Deserialize)]
176pub struct SshKeyOwnersArgs {
177 pub fingerprints: Vec<String>,
178}
179
180/// `add_ssh_key`: `public_key` is one line in OpenSSH format.
181/// Returns `Outcome<SshKey>`.
182#[derive(Debug, Serialize, Deserialize)]
183#[serde(rename_all = "camelCase")]
184pub struct AddSshKeyArgs {
185 pub user: User,
186 pub title: String,
187 pub public_key: String,
188}
189
190/// `remove_ssh_key` and `remove_access_token`.
191#[derive(Debug, Serialize, Deserialize)]
192pub struct RemoveArgs {
193 pub user: User,
194 pub id: String,
195}
196
197/// `create_access_token`: a token that acts as `user`. For a workspace
198/// acting through a token of its own, the new token belongs to that
199/// workspace too.
200#[derive(Debug, Serialize, Deserialize)]
201#[serde(rename_all = "camelCase")]
202pub struct CreateAccessTokenArgs {
203 pub user: User,
204 pub name: String,
205 /// When set, the token stops working after this many seconds and is
206 /// left out of the user's token list, unless `listed`. Used for hosted
207 /// attempts.
208 #[serde(default)]
209 pub ttl_seconds: Option<u64>,
210 /// Its scopes, as `resource:level`; unknown names are left out. Null:
211 /// full access.
212 #[serde(default)]
213 pub scopes: Option<Vec<String>>,
214 /// Listed with the person's tokens although it expires: one they made
215 /// themselves, with an expiry.
216 #[serde(default)]
217 pub listed: bool,
218}
219
220/// `create_job_token`: a workflow job's `G1T_TOKEN`. It acts as the
221/// repository's workspace, reaches that repository only, holds `scopes`
222/// (from the job's `permissions`), and is never listed. The actions service
223/// revokes it when the job ends (`revoke_job_tokens`); `ttl_seconds` is a
224/// backstop. Returns `CreatedAccessToken`.
225#[derive(Debug, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase")]
227pub struct CreateJobTokenArgs {
228 /// The workspace the repository belongs to, as its own principal.
229 pub workspace: User,
230 pub repo: crate::repos::RepoPath,
231 pub run_id: String,
232 pub job_id: String,
233 /// What the token is listed as in logs: `G1T_TOKEN for acme/web run 4`.
234 pub name: String,
235 pub ttl_seconds: u64,
236 /// As `resource:level`; unknown names are left out.
237 pub scopes: Vec<String>,
238 /// Whether it may open and approve pull requests (`JobToken::pull_requests`).
239 #[serde(default)]
240 pub pull_requests: bool,
241}
242
243/// `revoke_job_tokens`: ends a workflow job's tokens at once, when the job
244/// finishes or is cancelled. Only job tokens are touched. Returns `bool`.
245#[derive(Debug, Default, Serialize, Deserialize)]
246#[serde(rename_all = "camelCase")]
247pub struct RevokeJobTokensArgs {
248 pub job_id: String,
249}
250
251/// The plaintext token is returned once and never stored.
252#[derive(Debug, Serialize, Deserialize)]
253pub struct CreatedAccessToken {
254 pub token: String,
255 pub info: AccessToken,
256}
257
258/// `register`: creates an account and signs it in.
259/// Returns `Outcome<SignedIn>`.
260///
261/// While registration is invite-only (`REGISTRATION_MODE=invite`), every
262/// new account needs `invite_code`: an unused, unexpired invite, and, when
263/// the invite names an email, that address. See [`CreateInviteArgs`].
264#[derive(Debug, Serialize, Deserialize)]
265pub struct RegisterArgs {
266 pub username: String,
267 pub email: String,
268 pub password: String,
269 /// An invite code such as `g1t-k7m2-q9xd-4hpw-…`. Ignored while
270 /// registration is open.
271 #[serde(default)]
272 pub invite_code: Option<String>,
273 /// The `proof` from the invite email's link. When it is the invite's
274 /// own and `email` is the address the invite was sent to, the account
275 /// starts with that address confirmed; otherwise it is ignored.
276 #[serde(default)]
277 pub email_proof: Option<String>,
278 /// Who is asking, such as the visitor's IP address, for rate limits.
279 #[serde(default)]
280 pub client: Option<String>,
281}
282
283/// `verify_email`: the token from the emailed link. Returns `Outcome<User>`.
284#[derive(Debug, Serialize, Deserialize)]
285pub struct EmailTokenArgs {
286 pub token: String,
287}
288
289/// `request_password_reset`. Always succeeds, so it cannot be used to find
290/// out which addresses have accounts. Any confirmed address of an account
291/// works: the link goes to the address given, and the primary (and the
292/// backup) are told a reset was asked for. A few requests an hour per
293/// address and per `client`; past that, nothing is sent.
294#[derive(Debug, Serialize, Deserialize)]
295pub struct EmailArgs {
296 pub email: String,
297 /// Who is asking, such as the visitor's IP address, for rate limits.
298 #[serde(default)]
299 pub client: Option<String>,
300}
301
302/// `reset_password`: sets a new password and ends every session.
303/// Returns `Outcome<User>`.
304#[derive(Debug, Serialize, Deserialize)]
305pub struct ResetPasswordArgs {
306 pub token: String,
307 pub password: String,
308}
309
310/// `device_start`: begins a device sign-in. Returns `DeviceStart`.
311#[derive(Debug, Serialize, Deserialize)]
312#[serde(rename_all = "camelCase")]
313pub struct DeviceStartArgs {
314 /// What is asking, shown to the person approving, e.g. "Claude Code".
315 pub client_name: String,
316}
317
318#[derive(Debug, Serialize, Deserialize)]
319#[serde(rename_all = "camelCase")]
320pub struct DeviceStart {
321 /// Secret held by the tool and exchanged for a token once approved.
322 pub device_code: String,
323 /// Short code shown to the person, e.g. `WDJB-MJHT`.
324 pub user_code: String,
325 /// Seconds until both codes stop working.
326 pub expires_in: u32,
327 /// Seconds the tool should wait between polls.
328 pub interval: u32,
329}
330
331/// `device_lookup`: what a user code is asking for, or null if it is not
332/// valid. Returns `Option<DeviceRequest>`.
333#[derive(Debug, Serialize, Deserialize)]
334#[serde(rename_all = "camelCase")]
335pub struct DeviceLookupArgs {
336 pub user_code: String,
337}
338
339#[derive(Debug, Serialize, Deserialize)]
340#[serde(rename_all = "camelCase")]
341pub struct DeviceRequest {
342 pub user_code: String,
343 pub client_name: String,
344}
345
346/// `device_resolve`: the signed-in person approves or denies a request.
347/// Returns `Outcome<bool>`.
348#[derive(Debug, Serialize, Deserialize)]
349#[serde(rename_all = "camelCase")]
350pub struct DeviceResolveArgs {
351 pub user_code: String,
352 pub user: User,
353 pub approve: bool,
354}
355
356/// `device_claim`: the tool asks whether its request was approved.
357#[derive(Debug, Serialize, Deserialize)]
358#[serde(rename_all = "camelCase")]
359pub struct DeviceClaimArgs {
360 pub device_code: String,
361}
362
363/// The answer to a `device_claim`.
364#[derive(Debug, Serialize, Deserialize)]
365#[serde(tag = "status", rename_all = "snake_case")]
366pub enum DeviceClaim {
367 /// Nobody has approved or denied it yet; ask again after the interval.
368 Pending,
369 Denied,
370 /// The code was never issued, has expired, or was already used.
371 Expired,
372 /// The access token, returned once.
373 Approved {
374 token: String,
375 user: User,
376 },
377}
378
379/// A workspace: the owner of repositories, and the first segment of their
380/// URLs. A person's own space and a team's are the same thing.
381#[derive(Clone, Debug, Serialize, Deserialize)]
382#[serde(rename_all = "camelCase")]
383pub struct Workspace {
384 pub id: String,
385 pub slug: String,
386 pub name: String,
387 /// One line saying what the workspace is for.
388 pub description: Option<String>,
389 /// RFC 3339.
390 pub created_at: String,
391 pub member_count: u32,
392 /// The workspace's uploaded icon: the SHA-256 of its bytes, served at
393 /// `/avatars/<avatar>`. Null means the generated letter avatar.
394 #[serde(default)]
395 pub avatar: Option<String>,
396 /// What every member gets on each of its repositories; owners have
397 /// Admin. See [`crate::access`].
398 #[serde(default)]
399 pub base_permission: crate::access::BasePermission,
400 /// Who may create its teams. See [`crate::teams::TeamCreation`].
401 #[serde(default)]
402 pub team_creation: crate::teams::TeamCreation,
403 /// What members may do, by GitHub's names for each
404 /// (`members_can_create_public_repositories`...), at the top level as
405 /// GitHub's organization has them. See [`crate::MemberPrivileges`].
406 #[serde(flatten)]
407 pub privileges: crate::MemberPrivileges,
408 /// Whether members and outside collaborators need two-factor
409 /// authentication to use it.
410 #[serde(default)]
411 pub two_factor_requirement_enabled: bool,
412}
413
414#[derive(Clone, Debug, Serialize, Deserialize)]
415pub struct Member {
416 pub username: String,
417 /// The username as its owner wrote it (`Ana`), when that differs from
418 /// `username`: what pages show.
419 #[serde(default, skip_serializing_if = "Option::is_none")]
420 pub display_username: Option<String>,
421 pub role: crate::Role,
422 /// The roles they hold besides `role`.
423 #[serde(default)]
424 pub org_roles: Vec<crate::OrgRole>,
425 /// Whether they have two-factor authentication on. Shown to owners
426 /// only; null for anyone else.
427 #[serde(default)]
428 pub two_factor: Option<bool>,
429 /// Their display name, when they set one.
430 #[serde(default)]
431 pub name: Option<String>,
432 /// Their uploaded avatar: the SHA-256 of its bytes, served at
433 /// `/avatars/<avatar>`. None means the generated letter avatar.
434 #[serde(default)]
435 pub avatar: Option<String>,
436}
437
438/// Where a workspace keeps its repositories' git data: anywhere g1t
439/// stores it (the default), or in the EU only. It applies to repositories
440/// made after it is set; the repos service reads it when it places a new
441/// one (`storage_options` says whether the EU can be chosen).
442#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
443#[serde(rename_all = "lowercase")]
444pub enum DataResidency {
445 #[default]
446 Anywhere,
447 Eu,
448}
449
450impl DataResidency {
451 pub fn as_str(self) -> &'static str {
452 match self {
453 DataResidency::Anywhere => "anywhere",
454 DataResidency::Eu => "eu",
455 }
456 }
457
458 pub fn parse(text: &str) -> Option<Self> {
459 match text.trim().to_ascii_lowercase().as_str() {
460 "anywhere" => Some(DataResidency::Anywhere),
461 "eu" => Some(DataResidency::Eu),
462 _ => None,
463 }
464 }
465}
466
467/// `workspace_residency` takes [`SlugArgs`] and returns
468/// `Option<DataResidency>` (null when there is no such workspace).
469/// `set_workspace_residency`: owners only. Returns `Outcome<DataResidency>`.
470#[derive(Debug, Serialize, Deserialize)]
471pub struct SetResidencyArgs {
472 pub actor: User,
473 pub slug: String,
474 pub residency: DataResidency,
475}
476
477/// `create_workspace`. Returns `Outcome<Workspace>`.
478#[derive(Debug, Serialize, Deserialize)]
479pub struct CreateWorkspaceArgs {
480 pub user: User,
481 pub slug: String,
482 #[serde(default)]
483 pub name: String,
484}
485
486/// `get_workspace`: public details, or null. Returns `Option<Workspace>`.
487#[derive(Debug, Serialize, Deserialize)]
488pub struct SlugArgs {
489 pub slug: String,
490}
491
492/// `list_members`: members only. Owners also see each member's
493/// `two_factor`. Returns `Outcome<Vec<Member>>`.
494#[derive(Debug, Serialize, Deserialize)]
495pub struct ListMembersArgs {
496 pub slug: String,
497 pub viewer: crate::Viewer,
498}
499
500/// `add_member` and `remove_member`: owners only. `add_member` never adds a
501/// person at once: it sends them a workspace invitation to accept or
502/// decline, as `invite_member` with a username does. Only g1t's own agent
503/// is added at once. Removing yourself is
504/// leaving (`members::LeaveWorkspaceArgs`); removing an owner is refused
505/// when they are the last. Each returns `Outcome<bool>`.
506#[derive(Debug, Serialize, Deserialize)]
507pub struct MemberArgs {
508 pub actor: User,
509 pub slug: String,
510 pub username: String,
511 #[serde(default)]
512 pub surface: Option<crate::audit::Surface>,
513}
514
515/// `update_workspace`: owners only. An empty name falls back to the slug;
516/// an empty description clears it. Returns `Outcome<Workspace>`.
517#[derive(Debug, Serialize, Deserialize)]
518pub struct UpdateWorkspaceArgs {
519 pub actor: User,
520 pub slug: String,
521 pub name: String,
522 pub description: String,
523}
524
525/// `rename_workspace`: owners only. Changes the workspace's slug, the first
526/// segment of its URLs, to `new_slug`; the display name is untouched. The
527/// old slug redirects to the new one, and is held for this workspace, for
528/// [`SLUG_HOLD_DAYS`]. Publishes `workspace.renamed`. Returns
529/// `Outcome<Workspace>`.
530///
531/// `check_workspace_rename` takes the same arguments and answers whether
532/// the rename would be allowed, changing nothing. Returns `Outcome<bool>`.
533#[derive(Debug, Serialize, Deserialize)]
534#[serde(rename_all = "camelCase")]
535pub struct RenameWorkspaceArgs {
536 pub actor: User,
537 pub slug: String,
538 pub new_slug: String,
539}
540
541/// `delete_workspace`: owners only, and only a person. `confirm` must be
542/// the workspace's slug, typed out. Refused for a protected workspace
543/// ([`protected_names`]), whoever asks, and while billing cannot settle it
544/// (`close_workspace`). Everything in it goes with it at once: nobody can
545/// reach it, its tokens stop working, its pages are not found, and its
546/// repositories, projects and apps are deleted with it. It is kept for
547/// [`WORKSPACE_RESTORE_DAYS`] so g1t's staff can restore it, then purged:
548/// its memberships, access tokens and old-slug redirects go, and billing's
549/// ledger and the audit log keep its history. The slug is never given to
550/// another workspace; the person whose username it is may make a workspace
551/// of that name again once it is purged. Publishes `workspace.deleting`,
552/// and `workspace.deleted` at the purge. Returns `Outcome<bool>`.
553///
554/// `check_workspace_deletion` takes the same arguments (with `confirm`
555/// ignored) and says what would go and whether anything stands in the way,
556/// changing nothing. Returns `Outcome<WorkspaceDeletion>`.
557#[derive(Debug, Serialize, Deserialize)]
558pub struct DeleteWorkspaceArgs {
559 pub actor: User,
560 pub slug: String,
561 #[serde(default)]
562 pub confirm: String,
563 /// Where the request came in, for the audit log; g1t.sh when absent.
564 #[serde(default)]
565 pub surface: Option<crate::audit::Surface>,
566}
567
568/// What deleting a workspace takes with it, and what stands in the way.
569/// Nothing does when `billing` is null and it is not `protected`.
570#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
571pub struct WorkspaceDeletion {
572 /// Its live repositories, which are deleted with it.
573 pub repositories: u32,
574 /// Its projects, hidden with it.
575 pub projects: u32,
576 #[serde(default)]
577 pub members: u32,
578 /// Why billing cannot close the workspace yet, in words for its owner.
579 pub billing: Option<String>,
580 /// It can never be deleted, by anyone ([`protected_names`]).
581 #[serde(default)]
582 pub protected: bool,
583}
584
585impl WorkspaceDeletion {
586 pub fn blocked(&self) -> bool {
587 self.protected || self.billing.is_some()
588 }
589
590 /// Why the workspace cannot be deleted, as one sentence, or `None`.
591 pub fn reason(&self, slug: &str) -> Option<String> {
592 if self.protected {
593 return Some(protected_refusal(slug));
594 }
595 self.billing.clone()
596 }
597}
598
599/// How long a deleted workspace is kept, for staff to restore, before it is
600/// purged.
601pub const WORKSPACE_RESTORE_DAYS: u64 = 30;
602
603/// Workspaces nobody can delete, whatever identity's `PROTECTED_WORKSPACES`
604/// says: Flagon's, which runs g1t.
605pub const ALWAYS_PROTECTED: &[&str] = &["flagon-io"];
606
607/// The protected workspaces: `configured` (comma-separated slugs or
608/// workspace ids, as identity's `PROTECTED_WORKSPACES` holds them), and
609/// [`ALWAYS_PROTECTED`] whatever it says, so an empty or missing variable
610/// still protects them. Lowercased, without duplicates.
611pub fn protected_names(configured: Option<&str>) -> Vec<String> {
612 let mut names: Vec<String> = Vec::new();
613 let given = configured.unwrap_or_default().split(',');
614 for name in ALWAYS_PROTECTED.iter().copied().chain(given) {
615 let name = name.trim().to_lowercase();
616 if !name.is_empty() && !names.contains(&name) {
617 names.push(name);
618 }
619 }
620 names
621}
622
623/// Why a protected workspace is not deleted, purged or acted on.
624pub fn protected_refusal(slug: &str) -> String {
625 format!("{slug} is protected and can never be deleted.")
626}
627
628/// `admin_deleted_workspaces` takes no arguments (`{}`) and returns
629/// `Vec<DeletedWorkspace>`, newest first. Staff only.
630///
631/// A workspace an owner deleted, kept until `purge_after` for staff to
632/// restore.
633#[derive(Clone, Debug, Serialize, Deserialize)]
634#[serde(rename_all = "camelCase")]
635pub struct DeletedWorkspace {
636 pub workspace_id: String,
637 pub slug: String,
638 pub name: String,
639 /// RFC 3339.
640 pub deleted_at: String,
641 /// The username of the owner who deleted it, or the staff member (by
642 /// email) who deleted it with the account that was its only owner.
643 pub deleted_by: String,
644 /// RFC 3339: when it is purged unless restored first.
645 pub purge_after: String,
646 /// What went with it, counted when it was deleted.
647 pub went: WorkspaceDeletion,
648 /// Whether staff can still restore it.
649 pub restorable: bool,
650}
651
652/// `admin_restore_workspace` and `admin_purge_workspace`: staff restore a
653/// deleted workspace within [`WORKSPACE_RESTORE_DAYS`], or purge it now.
654/// `staff` is who, for the audit logs. Purging needs `confirm`, the slug
655/// typed out, and is refused for a protected workspace. Restoring publishes
656/// `workspace.restored`; purging, `workspace.deleted`. Both return
657/// `Outcome<bool>`.
658#[derive(Debug, Serialize, Deserialize)]
659#[serde(rename_all = "camelCase")]
660pub struct AdminDeletedWorkspaceArgs {
661 pub workspace_id: String,
662 pub staff: String,
663 #[serde(default)]
664 pub confirm: String,
665}
666
667/// `transfer_repo_scopes`: a repository moved from `from` to `to`; the
668/// tokens of agents at work on it are kept pointing at it. For repos'
669/// `transfer`. Returns `bool`.
670#[derive(Debug, Serialize, Deserialize)]
671pub struct TransferRepoScopesArgs {
672 pub from: crate::repos::RepoPath,
673 pub to: crate::repos::RepoPath,
674}
675
676/// How long a workspace's old slug keeps redirecting to it, and stays
677/// reserved for it, after a rename.
678pub const SLUG_HOLD_DAYS: u64 = 90;
679
680/// How long a workspace must wait between renames.
681pub const RENAME_COOLDOWN_HOURS: u64 = 24;
682
683// `resolve_slug` takes `SlugArgs` and returns `Option<String>`: the
684// workspace's current slug when `slug` is one it was renamed from within
685// the last `SLUG_HOLD_DAYS`, and null otherwise (including for a slug that
686// is in use), or the workspace's slug when `slug` is one of its aliases.
687
688// `resolve_alias` takes `SlugArgs` and returns `Option<String>`: the slug
689// now of the workspace `slug` is an alias of, and null when it is none.
690// Aliases are set by g1t's staff only: `g1t` is Flagon, Inc.'s `flagon-io`.
691// An alias follows its workspace through renames.
692
693/// `admin_aliases` takes no arguments (`{}`) and returns
694/// `Vec<WorkspaceAlias>`, by alias. Staff only.
695///
696/// A name staff point at a workspace, so that its addresses (pages, git,
697/// the API, packages) lead to the workspace under its own name.
698#[derive(Clone, Debug, Serialize, Deserialize)]
699#[serde(rename_all = "camelCase")]
700pub struct WorkspaceAlias {
701 pub alias: String,
702 pub workspace_id: String,
703 /// The workspace's slug and name now.
704 pub workspace: String,
705 pub workspace_name: String,
706 /// Why it exists, as staff wrote it.
707 pub note: String,
708 /// The staff member who set it, or `migration`.
709 pub created_by: String,
710 /// RFC 3339.
711 pub created_at: String,
712}
713
714/// `admin_set_alias`: points `alias` at the workspace whose slug is
715/// `workspace`. The alias must have a namespace's shape, must not be one of
716/// the site's routes, and must not be anyone's username, a workspace's slug
717/// (deleted, or held after a rename) or another alias. `note` is required:
718/// it is the reason, kept with the alias and in sudo's audit log. Staff
719/// only. Returns `Outcome<WorkspaceAlias>`.
720#[derive(Debug, Serialize, Deserialize)]
721#[serde(rename_all = "camelCase")]
722pub struct AdminSetAliasArgs {
723 pub alias: String,
724 pub workspace: String,
725 pub note: String,
726 pub staff: String,
727}
728
729/// `admin_remove_alias`: the alias stops leading anywhere, and the name is
730/// nobody's again unless it is reserved. `reason` goes in sudo's audit log.
731/// Staff only. Returns `Outcome<bool>`.
732#[derive(Debug, Serialize, Deserialize)]
733#[serde(rename_all = "camelCase")]
734pub struct AdminRemoveAliasArgs {
735 pub alias: String,
736 pub reason: String,
737 pub staff: String,
738}
739
740/// `set_workspace_avatar`: owners only. `image` is the file's bytes in
741/// base64: PNG, JPEG, WebP or GIF, at most `MAX_AVATAR_BYTES`. Null removes
742/// the icon. Returns `Outcome<Workspace>`.
743#[derive(Debug, Serialize, Deserialize)]
744pub struct SetWorkspaceAvatarArgs {
745 pub actor: User,
746 pub slug: String,
747 pub image: Option<String>,
748}
749
750/// `set_user_avatar`: a person's own avatar, as `SetWorkspaceAvatarArgs`.
751/// Returns `Outcome<Option<String>>`: the new avatar, or null once removed.
752#[derive(Debug, Serialize, Deserialize)]
753pub struct SetUserAvatarArgs {
754 pub user: User,
755 pub image: Option<String>,
756}
757
758/// The largest avatar that can be uploaded, in bytes.
759pub const MAX_AVATAR_BYTES: usize = 1024 * 1024;
760
761/// `list_workspace_tokens`: members only. Returns
762/// `Outcome<Vec<AccessToken>>`.
763#[derive(Debug, Serialize, Deserialize)]
764pub struct WorkspaceTokensArgs {
765 pub slug: String,
766 pub viewer: crate::Viewer,
767}
768
769/// `remove_workspace_token`: owners only. Returns `Outcome<bool>`.
770#[derive(Debug, Serialize, Deserialize)]
771pub struct RemoveWorkspaceTokenArgs {
772 pub actor: User,
773 pub slug: String,
774 pub id: String,
775}
776
777/// `oauth_authorize`: the signed-in person approved an application. The
778/// caller has checked the client and that it may be redirected to
779/// `redirect_uri`. Returns `OAuthCode`.
780#[derive(Debug, Serialize, Deserialize)]
781#[serde(rename_all = "camelCase")]
782pub struct OAuthAuthorizeArgs {
783 pub user: User,
784 pub client_id: String,
785 /// Shown wherever the application's access is listed.
786 pub client_name: String,
787 pub redirect_uri: String,
788 /// PKCE challenge, method S256.
789 pub code_challenge: String,
790 /// What the person granted, as `resource:level`. Null: full access.
791 #[serde(default)]
792 pub scopes: Option<Vec<String>>,
793}
794
795#[derive(Debug, Serialize, Deserialize)]
796pub struct OAuthCode {
797 pub code: String,
798}
799
800/// `oauth_exchange`: redeems an authorization code.
801/// Returns `Outcome<OAuthTokens>`.
802#[derive(Debug, Serialize, Deserialize)]
803#[serde(rename_all = "camelCase")]
804pub struct OAuthExchangeArgs {
805 pub code: String,
806 pub code_verifier: String,
807 pub client_id: String,
808 pub redirect_uri: String,
809}
810
811/// `oauth_refresh`: trades a refresh token for new tokens.
812/// Returns `Outcome<OAuthTokens>`.
813#[derive(Debug, Serialize, Deserialize)]
814#[serde(rename_all = "camelCase")]
815pub struct OAuthRefreshArgs {
816 pub refresh_token: String,
817 pub client_id: String,
818}
819
820#[derive(Debug, Serialize, Deserialize)]
821#[serde(rename_all = "camelCase")]
822pub struct OAuthTokens {
823 pub access_token: String,
824 /// Works once; using it returns the next one.
825 pub refresh_token: String,
826 /// Seconds until the access token stops working.
827 pub expires_in: u64,
828 /// The scopes granted, space-separated, or `*` for full access.
829 #[serde(default)]
830 pub scope: Option<String>,
831}
832
833/// An application a person has signed in to. Listed by `list_oauth_grants`
834/// and ended by `revoke_oauth_grant`.
835#[derive(Debug, Serialize, Deserialize)]
836#[serde(rename_all = "camelCase")]
837pub struct OAuthGrant {
838 pub id: String,
839 pub client_name: String,
840 /// RFC 3339.
841 pub created_at: String,
842 /// RFC 3339.
843 pub last_used_at: String,
844 /// What the person granted. Null: full access.
845 #[serde(default)]
846 pub scopes: Option<Vec<String>>,
847 /// Signed in before applications were given scopes: full access until
848 /// someone narrows it.
849 #[serde(default)]
850 pub legacy: bool,
851}
852
853/// `update_oauth_grant`: changes what an application the person signed in
854/// to may do, at once and when it refreshes. Returns `Outcome<OAuthGrant>`.
855#[derive(Debug, Serialize, Deserialize)]
856pub struct UpdateOAuthGrantArgs {
857 pub user: User,
858 pub id: String,
859 #[serde(default)]
860 pub scopes: Option<Vec<String>>,
861}
862
863
864/// What an agent's token may do: these operations, in this repository.
865#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
866pub struct AgentScope {
867 pub repo: crate::repos::RepoPath,
868 /// API and MCP operation names, such as `create_issue`.
869 pub operations: Vec<String>,
870 /// Set on a run credential: the run it belongs to, and what it may do
871 /// with git. See [`crate::credentials`].
872 #[serde(default, skip_serializing_if = "Option::is_none")]
873 pub run: Option<crate::credentials::RunBinding>,
874}
875
876/// `create_agent_token`: a token for a g1t agent working on someone's
877/// behalf. It acts as `g1t`, a member of the repository's workspace,
878/// and only for the operations in `scope`. Returns `CreatedAccessToken`.
879#[derive(Debug, Serialize, Deserialize)]
880#[serde(rename_all = "camelCase")]
881pub struct CreateAgentTokenArgs {
882 /// The person the agent works for; the token is recorded as theirs.
883 pub on_behalf_of: User,
884 pub scope: AgentScope,
885 pub ttl_seconds: u64,
886}
887
888// `agent_scope` takes `TokenArgs` and returns `Option<AgentScope>`: what an
889// agent's token may do, or null for any other token.
890
891/// The id g1t's agent acts under. Only ever stored, never shown: it keeps
892/// the agent's work apart from g1t's own ([`crate::system::ID`]) where
893/// that matters, such as whether its approval counts.
894pub const AGENT_ID: &str = "usr_g1t_agent";
895/// The name g1t's agent is shown by: g1t's own, [`crate::system::USERNAME`].
896/// Everything it does, people see g1t do.
897pub const AGENT_NAME: &str = crate::system::USERNAME;
898
899// --- Staff ---------------------------------------------------------------
900//
901// Staff-only methods, for sudo.g1t.sh. They take no viewer and check no
902// membership: only sudo calls them, over its service binding, after it has
903// verified a Cloudflare Access sign-in and its staff list. Nothing a
904// customer can reach should ever forward to them.
905
906/// `notify_owners`: emails a short notice, with one link, to each owner of
907/// a workspace with a confirmed address. Called by other services (billing
908/// warns owners near their usage limit), never on a person's behalf.
909/// Returns how many were sent.
910#[derive(Clone, Debug, Serialize, Deserialize)]
911pub struct NotifyOwnersArgs {
912 pub workspace: String,
913 pub subject: String,
914 /// One or two sentences: what happened and what it means.
915 pub intro: String,
916 /// The button's words, such as `Open billing`.
917 pub action: String,
918 /// Where the button goes; must be on g1t.sh.
919 pub link: String,
920 /// Small print: why they got it.
921 pub footer: String,
922}
923
924/// `admin_workspaces`: every workspace, newest first, at most
925/// [`ADMIN_WORKSPACES_LIMIT`], optionally only those whose slug, name or
926/// an owner's username or email contains `query`. Returns
927/// `Vec<AdminWorkspace>`. Staff only.
928#[derive(Debug, Default, Serialize, Deserialize)]
929pub struct AdminWorkspacesArgs {
930 #[serde(default)]
931 pub query: Option<String>,
932}
933
934/// The most workspaces one `admin_workspaces` call returns.
935pub const ADMIN_WORKSPACES_LIMIT: usize = 500;
936
937/// An owner of a workspace, as staff see them.
938#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
939pub struct AdminOwner {
940 pub username: String,
941 pub email: Option<String>,
942}
943
944/// A workspace as staff see it: who owns it and how many belong to it.
945#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
946#[serde(rename_all = "camelCase")]
947pub struct AdminWorkspace {
948 pub slug: String,
949 pub name: String,
950 /// RFC 3339.
951 pub created_at: String,
952 pub owners: Vec<AdminOwner>,
953 pub member_count: u32,
954}
955
956/// `admin_workspace`: one workspace with every member, or null. Takes
957/// `SlugArgs`; returns `Option<AdminWorkspaceDetail>`. Staff only.
958#[derive(Clone, Debug, Serialize, Deserialize)]
959#[serde(rename_all = "camelCase")]
960pub struct AdminWorkspaceDetail {
961 pub slug: String,
962 pub name: String,
963 pub description: Option<String>,
964 /// RFC 3339.
965 pub created_at: String,
966 /// Owners first, then by username.
967 pub members: Vec<AdminMember>,
968 /// It can never be deleted ([`protected_names`]).
969 #[serde(default)]
970 pub protected: bool,
971}
972
973/// A member of a workspace, as staff see them.
974#[derive(Clone, Debug, Serialize, Deserialize)]
975pub struct AdminMember {
976 pub username: String,
977 pub email: Option<String>,
978 pub role: crate::Role,
979 /// When they joined the workspace. RFC 3339.
980 pub joined: String,
981}
982
983// --- Profiles ------------------------------------------------------------
984//
985// A person's public page at `g1t.sh/u/<username>`. Everything in a
986// `Profile` is shown to anyone, signed in or not; an email address never is.
987
988/// The most characters each profile field takes.
989pub const MAX_PROFILE_NAME: usize = 80;
990pub const MAX_PROFILE_BIO: usize = 160;
991pub const MAX_PROFILE_LOCATION: usize = 80;
992pub const MAX_PROFILE_WEBSITE: usize = 200;
993pub const MAX_PROFILE_PRONOUNS: usize = 40;
994pub const MAX_PROFILE_TIMEZONE: usize = 64;
995
996/// What anyone may see about a person.
997#[derive(Clone, Debug, Default, Serialize, Deserialize)]
998#[serde(rename_all = "camelCase")]
999pub struct Profile {
1000 /// Lowercased: what the profile is found and linked by.
1001 pub username: String,
1002 /// The username as its owner wrote it, when that differs: what the page shows.
1003 #[serde(default, skip_serializing_if = "Option::is_none")]
1004 pub display_username: Option<String>,
1005 /// The name they go by, if they gave one.
1006 pub name: Option<String>,
1007 /// One or two lines about them, at most [`MAX_PROFILE_BIO`] characters.
1008 pub bio: Option<String>,
1009 pub location: Option<String>,
1010 /// An `https://` address.
1011 pub website: Option<String>,
1012 pub pronouns: Option<String>,
1013 /// The time zone they are in, an IANA name such as `America/Denver`.
1014 #[serde(default)]
1015 pub timezone: Option<String>,
1016 /// The uploaded avatar's hash, served at `/avatars/<avatar>`.
1017 pub avatar: Option<String>,
1018 /// When the account was made. RFC 3339.
1019 pub created_at: String,
1020}
1021
1022// `profile` takes `UsernameArgs` and returns `Option<Profile>`: null for
1023// an account that does not exist.
1024
1025/// `update_profile`: a person changes their own profile. Every field is
1026/// replaced; an empty one is cleared. Returns `Outcome<Profile>`.
1027#[derive(Debug, Default, Serialize, Deserialize)]
1028#[serde(rename_all = "camelCase")]
1029pub struct UpdateProfileArgs {
1030 pub actor: User,
1031 #[serde(default)]
1032 pub name: String,
1033 #[serde(default)]
1034 pub bio: String,
1035 #[serde(default)]
1036 pub location: String,
1037 #[serde(default)]
1038 pub website: String,
1039 #[serde(default)]
1040 pub pronouns: String,
1041 /// An IANA time zone name, such as `America/Denver`.
1042 #[serde(default)]
1043 pub timezone: String,
1044}
1045
1046/// `profile_workspaces`: the workspaces shown on a person's profile, as
1047/// `viewer` may see them. A membership is shown only when it is no secret
1048/// from the viewer: a workspace the viewer belongs to as well, or one of
1049/// `public`, the workspaces the caller found the person has made a public
1050/// project in (whose page shows that already). Returns
1051/// `Vec<ProfileWorkspace>`; empty for an account that does not exist.
1052#[derive(Debug, Serialize, Deserialize)]
1053pub struct ProfileWorkspacesArgs {
1054 pub username: String,
1055 pub viewer: crate::Viewer,
1056 #[serde(default)]
1057 pub public: Vec<String>,
1058}
1059
1060/// A workspace on a person's profile.
1061#[derive(Clone, Debug, Serialize, Deserialize)]
1062pub struct ProfileWorkspace {
1063 pub slug: String,
1064 pub name: String,
1065 pub avatar: Option<String>,
1066}
1067
1068// --- Dock pins -------------------------------------------------------------
1069//
1070// The apps a person pins to their dock in a workspace, kept with their
1071// account so the dock follows them to every device. Each person's own:
1072// nobody else reads or sets them.
1073
1074/// The most apps a person pins in one workspace.
1075pub const MAX_DOCK_PINS: usize = 24;
1076/// The most characters an app key takes.
1077pub const MAX_DOCK_APP_KEY: usize = 32;
1078
1079/// `dock_pins`: the apps `user` pinned in the workspace `workspace` (a
1080/// slug), in the order they set. Returns `Option<Vec<String>>`: null when
1081/// they never saved any there, or are not one of its members.
1082#[derive(Debug, Default, Serialize, Deserialize)]
1083#[serde(rename_all = "camelCase")]
1084pub struct DockPinsArgs {
1085 pub user: User,
1086 pub workspace: String,
1087}
1088
1089/// `set_dock_pins`: replaces `user`'s pins in `workspace` with `apps`, in
1090/// that order. Each key is lowercase letters, digits and hyphens, at most
1091/// [`MAX_DOCK_APP_KEY`] characters; a repeat is dropped; at most
1092/// [`MAX_DOCK_PINS`]. Which keys name real apps is the web app's to say.
1093/// Returns `Outcome<Vec<String>>`: the pins as saved.
1094#[derive(Debug, Default, Serialize, Deserialize)]
1095#[serde(rename_all = "camelCase")]
1096pub struct SetDockPinsArgs {
1097 pub user: User,
1098 pub workspace: String,
1099 #[serde(default)]
1100 pub apps: Vec<String>,
1101}
1102
1103// --- Sounds and desktop notifications --------------------------------------
1104//
1105// What a person hears when chat moves, kept with their account so a phone,
1106// a laptop and the desktop app all sound the same. Which sound plays when
1107// is the web app's rule (apps/web/app/lib/chat-sounds.ts); identity keeps
1108// the choices. Do not disturb is not here: it is the person's presence
1109// (`dnd_until`, services/notify), which also silences pop-ups and pushes.
1110// Mirrors `SoundSettings` in packages/contracts/src/identity.ts.
1111
1112/// The sets of sounds a person can choose from.
1113pub const SOUND_SETS: [&str; 2] = ["soft", "bright"];
1114/// The cues a person can turn on and off. `call` is reserved and has no setting.
1115pub const SOUND_CUES: [&str; 5] = ["message", "direct", "mention", "agent_done", "sent"];
1116/// Before anyone changes them: every cue but the tick as you send.
1117pub const SOUND_CUES_OFF_BY_DEFAULT: [&str; 1] = ["sent"];
1118pub const DEFAULT_SOUND_SET: &str = "soft";
1119pub const DEFAULT_SOUND_VOLUME: u8 = 60;
1120
1121/// A person's sound settings, as kept and as answered. Wire fields are
1122/// snake_case, as the site's JSON is.
1123#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1124pub struct SoundSettings {
1125 /// Whether anything plays at all.
1126 pub sounds_enabled: bool,
1127 /// One of [`SOUND_SETS`].
1128 pub sound_set: String,
1129 /// 0 to 100.
1130 pub sound_volume: u8,
1131 /// Each cue on or off, by name; a cue left out is on, except `sent`.
1132 #[serde(default)]
1133 pub sound_cues: std::collections::BTreeMap<String, bool>,
1134 /// Whether an open tab shows a system notification for a message to
1135 /// you while its window is not in front.
1136 pub desktop_toasts: bool,
1137}
1138
1139impl Default for SoundSettings {
1140 fn default() -> Self {
1141 SoundSettings {
1142 sounds_enabled: true,
1143 sound_set: DEFAULT_SOUND_SET.to_owned(),
1144 sound_volume: DEFAULT_SOUND_VOLUME,
1145 sound_cues: SOUND_CUES
1146 .iter()
1147 .map(|cue| ((*cue).to_owned(), !SOUND_CUES_OFF_BY_DEFAULT.contains(cue)))
1148 .collect(),
1149 desktop_toasts: false,
1150 }
1151 }
1152}
1153
1154/// A change to them: only what is sent changes; `sound_cues` merges by cue.
1155#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1156pub struct SoundSettingsChange {
1157 #[serde(default)]
1158 pub sounds_enabled: Option<bool>,
1159 #[serde(default)]
1160 pub sound_set: Option<String>,
1161 #[serde(default)]
1162 pub sound_volume: Option<i64>,
1163 #[serde(default)]
1164 pub sound_cues: Option<std::collections::BTreeMap<String, bool>>,
1165 #[serde(default)]
1166 pub desktop_toasts: Option<bool>,
1167}
1168
1169/// `sound_settings`: `user`'s own, or the defaults when they never changed
1170/// them. Returns `SoundSettings`. People only.
1171#[derive(Debug, Default, Serialize, Deserialize)]
1172#[serde(rename_all = "camelCase")]
1173pub struct SoundSettingsArgs {
1174 pub user: User,
1175}
1176
1177/// `set_sound_settings`: changes `user`'s own. Returns
1178/// `Outcome<SoundSettings>`: the settings as kept, or `invalid` for a set
1179/// or cue g1t does not have, or a volume outside 0 to 100.
1180#[derive(Debug, Default, Serialize, Deserialize)]
1181#[serde(rename_all = "camelCase")]
1182pub struct SetSoundSettingsArgs {
1183 pub user: User,
1184 #[serde(default)]
1185 pub change: SoundSettingsChange,
1186}
1187
1188/// `directory`: every account or every workspace, as their public pages
1189/// show them, a page at a time in name order. For services that index
1190/// them, such as search; nothing private is in it. Returns
1191/// `DirectoryPage`.
1192#[derive(Debug, Default, Serialize, Deserialize)]
1193pub struct DirectoryArgs {
1194 /// `user` or `workspace`.
1195 pub kind: String,
1196 /// Names after this one.
1197 #[serde(default)]
1198 pub after: Option<String>,
1199 pub limit: u32,
1200}
1201
1202/// One account or workspace in the directory.
1203#[derive(Clone, Debug, Serialize, Deserialize)]
1204#[serde(rename_all = "camelCase")]
1205pub struct DirectoryEntry {
1206 /// The account's or workspace's id.
1207 pub id: String,
1208 /// A username or a workspace's slug.
1209 pub slug: String,
1210 /// A person's display name or a workspace's name.
1211 pub name: Option<String>,
1212 /// A person's bio or a workspace's description.
1213 pub bio: Option<String>,
1214 pub avatar: Option<String>,
1215 /// RFC 3339.
1216 pub created_at: String,
1217}
1218
1219#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1220pub struct DirectoryPage {
1221 pub entries: Vec<DirectoryEntry>,
1222 /// Where the next page starts; null on the last.
1223 pub next: Option<String>,
1224}
1225
1226// --- Invites ---------------------------------------------------------------
1227//
1228// While registration is invite-only, every new account (with a password or
1229// through GitHub) needs an invite code. Each person may have
1230// `INVITES_PER_USER` invites out at a time; staff grant more to a person or
1231// to a workspace, whose owners share them. Inviting an email with no
1232// account into a workspace makes an invite bound to that address, which
1233// registers and joins in one step. See services/identity/src/invites.rs.
1234
1235/// Whether anyone may make an account, or only someone with an invite. Set
1236/// by identity's `REGISTRATION_MODE` var; anything but `open`, including
1237/// leaving it unset, is `invite`, so a missing setting never opens sign-up.
1238#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1239#[serde(rename_all = "snake_case")]
1240pub enum RegistrationMode {
1241 #[default]
1242 Invite,
1243 Open,
1244}
1245
1246impl RegistrationMode {
1247 pub fn parse(text: Option<&str>) -> RegistrationMode {
1248 match text.map(|text| text.trim().to_ascii_lowercase()).as_deref() {
1249 Some("open") => RegistrationMode::Open,
1250 _ => RegistrationMode::Invite,
1251 }
1252 }
1253}
1254
1255/// How many invites a person may have out at once, unless identity's
1256/// `INVITES_PER_USER` var says otherwise.
1257pub const INVITES_PER_USER: u32 = 5;
1258
1259/// How long an invite works, unless identity's `INVITE_TTL_DAYS` var says
1260/// otherwise.
1261pub const INVITE_TTL_DAYS: u64 = 30;
1262
1263/// Where an invite stands. Only a pending invite can be used. A pending
1264/// invite can be revoked, and so can one awaiting confirmation. An expired
1265/// or revoked invite that was never used gives its inviter the invite back.
1266#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1267#[serde(rename_all = "snake_case")]
1268pub enum InviteStatus {
1269 Pending,
1270 /// Used to make an account that has not confirmed its email address
1271 /// yet. The code is spent; the workspace (or repository) it gives is
1272 /// joined when the address is confirmed, unless it is revoked first.
1273 AwaitingConfirmation,
1274 /// Used to make an account that has confirmed its address, for a
1275 /// workspace it has not yet joined or declined: the workspace
1276 /// invitation waits for the person's answer (`accept_invitation`).
1277 AwaitingAnswer,
1278 Redeemed,
1279 /// Its person declined the workspace it invited them to.
1280 Declined,
1281 Expired,
1282 Revoked,
1283}
1284
1285/// What using an invite does.
1286#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1287#[serde(rename_all = "snake_case")]
1288pub enum InviteKind {
1289 /// Makes a new account, and joins `workspace` when one is set.
1290 Account,
1291 /// An existing account joins `workspace`. Never makes an account.
1292 Workspace,
1293}
1294
1295/// Whose allowance an invite uses.
1296#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1297#[serde(rename_all = "snake_case")]
1298pub enum InviteCharge {
1299 /// Its inviter's own.
1300 User,
1301 /// The workspace's, granted by staff and shared by its owners.
1302 Workspace,
1303 /// Nobody's: staff minted it, or it invites an existing account.
1304 None,
1305}
1306
1307/// One invite, as the person who made it sees it.
1308#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1309#[serde(rename_all = "camelCase")]
1310pub struct Invite {
1311 pub id: String,
1312 /// The code, such as `g1t-k7m2-q9xd-…`: returned once when the invite
1313 /// is made, and afterwards to whoever made it while it is pending.
1314 /// Null otherwise.
1315 pub code: Option<String>,
1316 /// The code's first group, such as `g1t-k7m2`, to recognise it by.
1317 pub hint: String,
1318 /// Only an account with this address can use it. Null: anyone with
1319 /// the code.
1320 pub email: Option<String>,
1321 pub kind: InviteKind,
1322 /// The workspace it joins, by slug.
1323 pub workspace: Option<String>,
1324 pub status: InviteStatus,
1325 pub charged_to: InviteCharge,
1326 /// Who made it, by username. Null when g1t staff did.
1327 pub invited_by: Option<String>,
1328 /// The account that used it, by username.
1329 pub redeemed_by: Option<String>,
1330 /// RFC 3339.
1331 pub created_at: String,
1332 /// RFC 3339.
1333 pub expires_at: String,
1334 /// RFC 3339.
1335 pub redeemed_at: Option<String>,
1336 /// RFC 3339.
1337 pub revoked_at: Option<String>,
1338 /// The account a workspace invitation is for, by username: someone
1339 /// invited by username, or the account the invite made.
1340 #[serde(default)]
1341 pub invitee: Option<String>,
1342 /// The role `workspace` is joined with. Null when it names none.
1343 #[serde(default)]
1344 pub role: Option<crate::Role>,
1345 /// The staff member who minted it. Only in staff views.
1346 #[serde(default, skip_serializing_if = "Option::is_none")]
1347 pub staff: Option<String>,
1348}
1349
1350/// How many invites someone may have out, and how many they have.
1351#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1352pub struct Allowance {
1353 /// Null: no limit.
1354 pub limit: Option<u32>,
1355 /// Pending and used invites; revoked and expired ones are not counted.
1356 pub used: u32,
1357 /// Null: no limit.
1358 pub remaining: Option<u32>,
1359}
1360
1361impl Allowance {
1362 pub fn new(limit: Option<u32>, used: u32) -> Allowance {
1363 Allowance {
1364 limit,
1365 used,
1366 remaining: limit.map(|limit| limit.saturating_sub(used)),
1367 }
1368 }
1369
1370 pub fn exhausted(&self) -> bool {
1371 self.remaining == Some(0)
1372 }
1373}
1374
1375/// A workspace's shared invites, for one of its owners.
1376#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1377pub struct WorkspaceAllowance {
1378 pub slug: String,
1379 pub allowance: Allowance,
1380}
1381
1382/// `list_invites` (takes `UserArgs`): a person's invites, newest first,
1383/// and what they have left. Returns `InvitesOverview`.
1384#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1385pub struct InvitesOverview {
1386 pub mode: RegistrationMode,
1387 pub allowance: Allowance,
1388 /// Workspaces the person owns that staff granted invites to.
1389 pub workspaces: Vec<WorkspaceAllowance>,
1390 pub invites: Vec<Invite>,
1391}
1392
1393/// `create_invite`: a person makes an invite, optionally for one email
1394/// address. People only; never an agent or a workspace's token, and not
1395/// before their email is confirmed. Uses one of the person's invites, or,
1396/// with `workspace`, one of the invites staff granted that workspace (its
1397/// owners only). Emails the address when one is given. Returns
1398/// `Outcome<Invite>`, with the code.
1399///
1400/// `revoke_invite` (takes `RemoveArgs`): its maker revokes a pending
1401/// invite; a workspace's owners may revoke one made for the workspace.
1402/// The invite comes back to whoever it was charged to. Returns
1403/// `Outcome<Invite>`.
1404#[derive(Debug, Serialize, Deserialize)]
1405pub struct CreateInviteArgs {
1406 pub user: User,
1407 #[serde(default)]
1408 pub email: Option<String>,
1409 /// Use this workspace's granted invites, by slug.
1410 #[serde(default)]
1411 pub workspace: Option<String>,
1412 /// The workspace the new account is invited to, by slug: one the
1413 /// person owns that can add members (not on the free plan). Once the
1414 /// account is confirmed it gets a workspace invitation to accept, as a
1415 /// member, and no workspace of its own is made for it.
1416 #[serde(default)]
1417 pub join: Option<String>,
1418 /// The role `join` invites them with; member when absent. Ignored
1419 /// without `join`.
1420 #[serde(default)]
1421 pub join_role: Option<crate::Role>,
1422 /// Where the request came in, for the audit log; g1t.sh when absent.
1423 #[serde(default)]
1424 pub surface: Option<crate::audit::Surface>,
1425}
1426
1427/// `check_invite`: what an invite code is for, before using it. Returns
1428/// `Outcome<InvitePreview>`; a code that is unknown, used, revoked or
1429/// expired gets the same answer, so codes cannot be probed. With
1430/// `any_status`, a real code that can no longer be used is described
1431/// instead (its `status` says why), so the page can say whom to ask for a
1432/// new one; an unknown code still gets the one answer.
1433#[derive(Debug, Serialize, Deserialize)]
1434pub struct InviteCodeArgs {
1435 pub code: String,
1436 /// Who is asking, such as the visitor's IP address, for rate limits.
1437 #[serde(default)]
1438 pub client: Option<String>,
1439 /// Who is looking, if signed in: sets `InvitePreview::for_viewer`.
1440 #[serde(default)]
1441 pub viewer: Option<User>,
1442 #[serde(default)]
1443 pub any_status: bool,
1444 /// The `proof` from the invite email's link, if the page was opened
1445 /// from it: sets `InvitePreview::email_proven`.
1446 #[serde(default)]
1447 pub email_proof: Option<String>,
1448}
1449
1450/// Someone shown on an invite.
1451#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1452pub struct InviteFrom {
1453 pub username: String,
1454 pub name: Option<String>,
1455 pub avatar: Option<String>,
1456}
1457
1458/// A repository an invite code was sent with: using the code accepts the
1459/// invitation to collaborate on it.
1460#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1461pub struct InviteRepository {
1462 /// `workspace/repo`.
1463 pub name: String,
1464 /// The role it gives, such as `write`.
1465 pub role: String,
1466}
1467
1468/// What a valid invite code is for.
1469#[derive(Clone, Debug, Serialize, Deserialize)]
1470#[serde(rename_all = "camelCase")]
1471pub struct InvitePreview {
1472 pub kind: InviteKind,
1473 /// Pending, unless `any_status` asked about a code that is spent.
1474 pub status: InviteStatus,
1475 /// Null when g1t staff sent it.
1476 pub invited_by: Option<InviteFrom>,
1477 pub workspace: Option<ProfileWorkspace>,
1478 /// The repository it accepts an invitation to, if it was sent with one.
1479 pub repository: Option<InviteRepository>,
1480 /// The address it is for, partly hidden, such as `a•••@example.com`.
1481 pub email: Option<String>,
1482 /// The address in full, while it is pending: whoever holds the code
1483 /// was sent it there. Fills in and locks the sign-up form.
1484 pub address: Option<String>,
1485 /// Whether the address it is for has a g1t account already, so the
1486 /// page asks them to sign in rather than sign up.
1487 pub has_account: bool,
1488 /// With a viewer: whether the invite is theirs (it is for one of their
1489 /// confirmed addresses, or they used it). Null without a viewer or,
1490 /// for a pending invite, when it is for anyone with the code.
1491 pub for_viewer: Option<bool>,
1492 /// RFC 3339.
1493 pub expires_at: String,
1494 /// For a shared invite link ([`SharedInvite`]): the group it was made
1495 /// for, such as `Cloudflare judges`. Not secret; the sign-up page shows
1496 /// it. Null for a one-person invite.
1497 #[serde(default)]
1498 pub shared_label: Option<String>,
1499 /// For a shared invite link limited to some email domains: those
1500 /// domains, such as `["cloudflare.com"]`. Empty for any address.
1501 #[serde(default)]
1502 pub shared_domains: Vec<String>,
1503 /// Whether `email_proof` was this pending invite's own, from the email
1504 /// it was sent in: the account made with it starts with `address`
1505 /// confirmed. False without a proof, with a wrong one, and for an
1506 /// invite bound to no address.
1507 #[serde(default)]
1508 pub email_proven: bool,
1509}
1510
1511/// `accept_invite`: a signed-in person uses a workspace invite made for
1512/// their confirmed address, and joins the workspace, or an invite sent with
1513/// a repository invitation, and accepts it. Returns `Outcome<String>`: the
1514/// workspace's slug, or `workspace/repo`.
1515#[derive(Debug, Serialize, Deserialize)]
1516pub struct AcceptInviteArgs {
1517 pub user: User,
1518 pub code: String,
1519}
1520
1521/// `invite_member`: an owner invites an email address into a workspace.
1522/// It always makes an invite bound to that address and emails it, so the
1523/// answer never says whether the address has an account. Without one, the
1524/// invite registers and joins in one step, and uses one of the workspace's
1525/// granted invites or else one of the owner's own. With one, it costs
1526/// nothing. Returns `Outcome<Invite>`, with the code.
1527#[derive(Debug, Serialize, Deserialize)]
1528pub struct InviteMemberArgs {
1529 pub actor: User,
1530 pub slug: String,
1531 /// An email address. Give this or `username`.
1532 #[serde(default)]
1533 pub email: String,
1534 /// A g1t username: that account gets a workspace invitation to accept
1535 /// or decline, in its inbox and by email. Nobody joins without saying
1536 /// yes.
1537 #[serde(default)]
1538 pub username: Option<String>,
1539 /// The role they join with; member when absent.
1540 #[serde(default)]
1541 pub role: Option<crate::Role>,
1542 /// Where the request came in, for the audit log; g1t.sh when absent.
1543 #[serde(default)]
1544 pub surface: Option<crate::audit::Surface>,
1545}
1546
1547/// A workspace invitation waiting for its person's answer, as they see it.
1548/// `list_invitations` (takes `UserArgs`) returns `Vec<WorkspaceInvitation>`,
1549/// newest first: pending ones only, never expired, revoked or answered.
1550#[derive(Clone, Debug, Serialize, Deserialize)]
1551#[serde(rename_all = "camelCase")]
1552pub struct WorkspaceInvitation {
1553 pub id: String,
1554 pub workspace: ProfileWorkspace,
1555 /// The role accepting joins with.
1556 pub role: crate::Role,
1557 /// Null when g1t staff sent it.
1558 pub invited_by: Option<InviteFrom>,
1559 /// RFC 3339.
1560 pub created_at: String,
1561 /// RFC 3339.
1562 pub expires_at: String,
1563}
1564
1565/// `accept_invitation`: the person it is for joins the workspace with the
1566/// role it names. Returns `Outcome<String>`, the workspace's slug.
1567///
1568/// `decline_invitation`: they say no; whoever sent it is told in their
1569/// inbox. Returns `Outcome<bool>`.
1570#[derive(Debug, Serialize, Deserialize)]
1571pub struct InvitationArgs {
1572 pub user: User,
1573 pub id: String,
1574 /// Where the request came in, for the audit log; g1t.sh when absent.
1575 #[serde(default)]
1576 pub surface: Option<crate::audit::Surface>,
1577}
1578
1579/// `find_people`: accounts whose username starts with `query`, or whose
1580/// name contains it, for picking someone to invite. Only what a profile
1581/// shows: a username, a name and an avatar, never an email address.
1582/// Returns `Vec<InviteFrom>`, at most `limit` (10 at most, 8 when absent).
1583#[derive(Debug, Serialize, Deserialize)]
1584pub struct FindPeopleArgs {
1585 pub query: String,
1586 #[serde(default)]
1587 pub limit: Option<u32>,
1588}
1589
1590/// `workspace_invites` (takes `ListMembersArgs`): a workspace's invites,
1591/// newest first. Owners only. Returns `Outcome<Vec<Invite>>`.
1592///
1593/// `revoke_workspace_invite`: owners only. Returns `Outcome<Invite>`.
1594#[derive(Debug, Serialize, Deserialize)]
1595pub struct WorkspaceInviteArgs {
1596 pub actor: User,
1597 pub slug: String,
1598 pub id: String,
1599}
1600
1601/// `request_access`: someone without an invite asks for one. Kept on the
1602/// waitlist, one entry per address. Answers the same way whether or not
1603/// the address is already on it. Returns `Outcome<bool>`.
1604#[derive(Debug, Default, Serialize, Deserialize)]
1605pub struct RequestAccessArgs {
1606 pub email: String,
1607 /// What they will build, if they said.
1608 #[serde(default)]
1609 pub about: String,
1610 /// Who is asking, such as the visitor's IP address, for rate limits.
1611 #[serde(default)]
1612 pub client: Option<String>,
1613}
1614
1615/// The most characters `RequestAccessArgs::about` keeps.
1616pub const MAX_WAITLIST_ABOUT: usize = 1000;
1617
1618// `registration` takes `{}` and returns `RegistrationMode`.
1619
1620// --- Invites, staff only ---
1621
1622#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1623#[serde(rename_all = "snake_case")]
1624pub enum WaitlistStatus {
1625 Waiting,
1626 Invited,
1627 Dismissed,
1628}
1629
1630impl WaitlistStatus {
1631 pub fn as_str(self) -> &'static str {
1632 match self {
1633 WaitlistStatus::Waiting => "waiting",
1634 WaitlistStatus::Invited => "invited",
1635 WaitlistStatus::Dismissed => "dismissed",
1636 }
1637 }
1638}
1639
1640/// Someone who asked for access.
1641#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1642#[serde(rename_all = "camelCase")]
1643pub struct WaitlistEntry {
1644 pub id: String,
1645 pub email: String,
1646 pub about: Option<String>,
1647 pub status: WaitlistStatus,
1648 pub invite_id: Option<String>,
1649 pub decided_by: Option<String>,
1650 /// RFC 3339.
1651 pub decided_at: Option<String>,
1652 /// What staff wrote when approving; it went in the invite email.
1653 #[serde(default)]
1654 pub note: Option<String>,
1655 /// The account made with the invite, once it was used.
1656 #[serde(default)]
1657 pub joined_as: Option<String>,
1658 /// When they first asked. RFC 3339.
1659 pub created_at: String,
1660 /// When they last asked. RFC 3339.
1661 pub updated_at: String,
1662}
1663
1664/// `admin_waitlist`: the waitlist, newest first, at most
1665/// [`ADMIN_INVITES_LIMIT`]. Returns `Vec<WaitlistEntry>`.
1666///
1667/// `admin_waitlist_pending` takes `{}` and returns the number of requests
1668/// still waiting, for sudo's navigation.
1669#[derive(Debug, Default, Serialize, Deserialize)]
1670pub struct AdminWaitlistArgs {
1671 /// Part of an email address or of what they said.
1672 #[serde(default)]
1673 pub query: Option<String>,
1674 /// Null: every status.
1675 #[serde(default)]
1676 pub status: Option<WaitlistStatus>,
1677}
1678
1679/// The most rows one staff listing of invites or the waitlist returns.
1680pub const ADMIN_INVITES_LIMIT: usize = 500;
1681
1682/// `admin_decide_waitlist`: approving mints an invite bound to the
1683/// address, charged to nobody, and emails it, with `note` if given;
1684/// dismissing only marks the entry. Returns `Outcome<WaitlistEntry>`.
1685#[derive(Debug, Serialize, Deserialize)]
1686pub struct AdminDecideWaitlistArgs {
1687 pub id: String,
1688 pub approve: bool,
1689 /// The staff member, by email.
1690 pub staff: String,
1691 /// A line for the invite email, up to [`MAX_WAITLIST_NOTE`] characters.
1692 #[serde(default)]
1693 pub note: Option<String>,
1694}
1695
1696/// The most characters an approval's note keeps.
1697pub const MAX_WAITLIST_NOTE: usize = 500;
1698
1699/// `admin_invites`: every invite, newest first, at most
1700/// [`ADMIN_INVITES_LIMIT`], optionally only those whose code starts with
1701/// `query`, or whose email, inviter or redeemer contains it. Returns
1702/// `Vec<Invite>`.
1703#[derive(Debug, Default, Serialize, Deserialize)]
1704pub struct AdminInvitesArgs {
1705 #[serde(default)]
1706 pub query: Option<String>,
1707}
1708
1709/// `admin_revoke_invite`: revokes any pending invite. Returns
1710/// `Outcome<Invite>`.
1711#[derive(Debug, Serialize, Deserialize)]
1712pub struct AdminRevokeInviteArgs {
1713 pub id: String,
1714 pub staff: String,
1715}
1716
1717/// `admin_mint_invite`: staff make an invite that uses nobody's
1718/// allowance, optionally bound to (and emailed to) an address. Returns
1719/// `Outcome<Invite>`, with the code.
1720#[derive(Debug, Serialize, Deserialize)]
1721pub struct AdminMintInviteArgs {
1722 #[serde(default)]
1723 pub email: Option<String>,
1724 pub staff: String,
1725}
1726
1727/// Who staff grant invites to.
1728#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1729#[serde(rename_all = "snake_case")]
1730pub enum GrantTarget {
1731 User,
1732 Workspace,
1733}
1734
1735impl GrantTarget {
1736 pub fn as_str(self) -> &'static str {
1737 match self {
1738 GrantTarget::User => "user",
1739 GrantTarget::Workspace => "workspace",
1740 }
1741 }
1742}
1743
1744/// `admin_grant_invites`: gives a person (by username) or a workspace (by
1745/// slug) `amount` more invites; a negative amount takes some back. Returns
1746/// `Outcome<Allowance>`: theirs afterwards.
1747#[derive(Debug, Serialize, Deserialize)]
1748pub struct AdminGrantInvitesArgs {
1749 pub target: GrantTarget,
1750 pub name: String,
1751 pub amount: i32,
1752 #[serde(default)]
1753 pub note: String,
1754 pub staff: String,
1755}
1756
1757/// The most invites one grant gives or takes back.
1758pub const MAX_INVITE_GRANT: i32 = 1000;
1759
1760/// Invites staff granted.
1761#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1762#[serde(rename_all = "camelCase")]
1763pub struct InviteGrant {
1764 pub amount: i32,
1765 pub note: Option<String>,
1766 pub granted_by: String,
1767 /// RFC 3339.
1768 pub created_at: String,
1769}
1770
1771/// Someone a person invited, and whom they invited in turn.
1772#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1773#[serde(rename_all = "camelCase")]
1774pub struct InviteTreeNode {
1775 pub username: String,
1776 /// When they used the invite. RFC 3339.
1777 pub joined_at: String,
1778 pub invited: Vec<InviteTreeNode>,
1779}
1780
1781/// `admin_invite_tree` (takes `UsernameArgs`): where a person came from
1782/// and whom they brought, for tracing abuse. Returns `Option<InviteTree>`.
1783///
1784/// `admin_workspace_invites` (takes `SlugArgs`): a workspace's granted
1785/// invites, grants and invites. Returns `Option<InviteTree>` with
1786/// `username` the slug and no `invited_by`.
1787#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1788#[serde(rename_all = "camelCase")]
1789pub struct InviteTree {
1790 pub username: String,
1791 /// Who invited them, then who invited that person, and so on. Empty
1792 /// for an account made without an invite.
1793 pub invited_by: Vec<String>,
1794 /// The staff member who minted their invite, when staff did.
1795 pub staff: Option<String>,
1796 pub allowance: Allowance,
1797 pub grants: Vec<InviteGrant>,
1798 /// Their invites, newest first.
1799 pub invites: Vec<Invite>,
1800 /// Whom they invited, three levels down.
1801 pub invited: Vec<InviteTreeNode>,
1802 /// The shared invite link the account was made with, if it was.
1803 #[serde(default)]
1804 pub shared: Option<SharedInviteSource>,
1805}
1806
1807// --- Shared invite links, staff only ---
1808//
1809// One link for a group (a conference's judges, a post, a community): up
1810// to `max_uses` new accounts, until it expires or staff revoke it,
1811// optionally only for addresses at some domains. Each use makes a new
1812// account, which then makes its own workspace; a shared link never joins
1813// anyone to an existing workspace, and uses nobody's allowance. Its code
1814// looks and is stored like any invite code (only a hash, and a sealed copy
1815// staff can copy again while it is live); the link is
1816// `https://g1t.sh/register?invite=<code>`. See
1817// services/identity/src/shared_invites.rs.
1818
1819/// How long a shared invite link works when staff give no date.
1820pub const SHARED_INVITE_TTL_DAYS: u64 = 14;
1821/// The furthest ahead a shared invite link's last day may be set.
1822pub const SHARED_INVITE_MAX_DAYS: u64 = 365;
1823/// The most accounts one shared invite link makes.
1824pub const MAX_SHARED_INVITE_USES: u32 = 1000;
1825/// The most characters a shared invite link's label keeps.
1826pub const MAX_SHARED_INVITE_LABEL: usize = 80;
1827/// The most email domains one shared invite link may be limited to.
1828pub const MAX_SHARED_INVITE_DOMAINS: usize = 10;
1829
1830/// Where a shared invite link stands. Only a live one makes accounts.
1831#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1832#[serde(rename_all = "snake_case")]
1833pub enum SharedInviteStatus {
1834 Live,
1835 /// Every use is taken.
1836 UsedUp,
1837 Expired,
1838 Revoked,
1839}
1840
1841/// The shared invite link an account was made with.
1842#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1843pub struct SharedInviteSource {
1844 pub id: String,
1845 pub label: String,
1846}
1847
1848/// An account made with a shared invite link.
1849#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1850#[serde(rename_all = "camelCase")]
1851pub struct SharedInviteAccount {
1852 /// Null once the account is purged.
1853 pub username: Option<String>,
1854 /// When it was made with the link. RFC 3339.
1855 pub joined_at: String,
1856}
1857
1858/// One shared invite link, as staff see it.
1859#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1860#[serde(rename_all = "camelCase")]
1861pub struct SharedInvite {
1862 /// `sinv_…`.
1863 pub id: String,
1864 /// Whom it is for, such as `Cloudflare judges`.
1865 pub label: String,
1866 /// The code, while it is live (and IDENTITY_KEY is set).
1867 pub code: Option<String>,
1868 /// The code's first group, such as `g1t-k7m2`.
1869 pub hint: String,
1870 pub max_uses: u32,
1871 /// Accounts made with it so far.
1872 pub uses: u32,
1873 /// Only addresses at these domains may use it; empty for any.
1874 pub domains: Vec<String>,
1875 pub status: SharedInviteStatus,
1876 /// The staff member who made it, by email.
1877 pub staff: String,
1878 /// RFC 3339.
1879 pub created_at: String,
1880 /// RFC 3339.
1881 pub expires_at: String,
1882 pub revoked_at: Option<String>,
1883 pub revoked_by: Option<String>,
1884 /// The accounts made with it, oldest first.
1885 pub accounts: Vec<SharedInviteAccount>,
1886}
1887
1888/// `admin_shared_invites` takes `{}`: shared invite links, newest first,
1889/// at most 200, each with the accounts it made. Returns
1890/// `Vec<SharedInvite>`.
1891///
1892/// `admin_create_shared_invite`: staff make a shared invite link. Recorded
1893/// in sudo's audit log. Returns `Outcome<SharedInvite>`, with the code.
1894#[derive(Debug, Default, Serialize, Deserialize)]
1895pub struct AdminCreateSharedInviteArgs {
1896 /// Required, up to [`MAX_SHARED_INVITE_LABEL`] characters.
1897 pub label: String,
1898 /// 1 to [`MAX_SHARED_INVITE_USES`].
1899 pub max_uses: u32,
1900 /// The last day it works, `YYYY-MM-DD` (UTC; it works until the end of
1901 /// that day), at most [`SHARED_INVITE_MAX_DAYS`] ahead. Null for
1902 /// [`SHARED_INVITE_TTL_DAYS`] from now.
1903 #[serde(default)]
1904 pub expires_on: Option<String>,
1905 /// Email domains it is limited to, such as `cloudflare.com`; empty for
1906 /// any address. Up to [`MAX_SHARED_INVITE_DOMAINS`].
1907 #[serde(default)]
1908 pub domains: Vec<String>,
1909 /// The staff member, by email.
1910 pub staff: String,
1911}
1912
1913/// `admin_revoke_shared_invite`: stops a shared invite link making any
1914/// more accounts. Those it made stay. Recorded in sudo's audit log.
1915/// Returns `Outcome<SharedInvite>`.
1916#[derive(Debug, Serialize, Deserialize)]
1917pub struct AdminRevokeSharedInviteArgs {
1918 pub id: String,
1919 pub staff: String,
1920}
1921
1922#[cfg(test)]
1923mod deletion_tests {
1924 use super::{WorkspaceDeletion, protected_names};
1925
1926 #[test]
1927 fn only_billing_or_protection_stands_in_the_way() {
1928 let clear = WorkspaceDeletion {
1929 repositories: 2,
1930 projects: 1,
1931 members: 3,
1932 ..WorkspaceDeletion::default()
1933 };
1934 assert!(!clear.blocked());
1935 assert_eq!(clear.reason("acme"), None);
1936 let owing = WorkspaceDeletion {
1937 billing: Some("Pay first.".into()),
1938 ..WorkspaceDeletion::default()
1939 };
1940 assert!(owing.blocked());
1941 assert_eq!(owing.reason("acme").as_deref(), Some("Pay first."));
1942 let protected = WorkspaceDeletion {
1943 billing: Some("Pay first.".into()),
1944 protected: true,
1945 ..WorkspaceDeletion::default()
1946 };
1947 assert!(protected.blocked());
1948 assert_eq!(
1949 protected.reason("flagon-io").as_deref(),
1950 Some("flagon-io is protected and can never be deleted.")
1951 );
1952 }
1953
1954 #[test]
1955 fn flagon_is_protected_whatever_the_variable_says() {
1956 assert_eq!(protected_names(None), ["flagon-io"]);
1957 assert_eq!(protected_names(Some("")), ["flagon-io"]);
1958 assert_eq!(protected_names(Some(" , ")), ["flagon-io"]);
1959 assert_eq!(
1960 protected_names(Some("Flagon-IO, acme ,wsp_1")),
1961 ["flagon-io", "acme", "wsp_1"]
1962 );
1963 }
1964}