Skip to content
1,879 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/// `directory`: every account or every workspace, as their public pages
1104/// show them, a page at a time in name order. For services that index
1105/// them, such as search; nothing private is in it. Returns
1106/// `DirectoryPage`.
1107#[derive(Debug, Default, Serialize, Deserialize)]
1108pub struct DirectoryArgs {
1109 /// `user` or `workspace`.
1110 pub kind: String,
1111 /// Names after this one.
1112 #[serde(default)]
1113 pub after: Option<String>,
1114 pub limit: u32,
1115}
1116
1117/// One account or workspace in the directory.
1118#[derive(Clone, Debug, Serialize, Deserialize)]
1119#[serde(rename_all = "camelCase")]
1120pub struct DirectoryEntry {
1121 /// The account's or workspace's id.
1122 pub id: String,
1123 /// A username or a workspace's slug.
1124 pub slug: String,
1125 /// A person's display name or a workspace's name.
1126 pub name: Option<String>,
1127 /// A person's bio or a workspace's description.
1128 pub bio: Option<String>,
1129 pub avatar: Option<String>,
1130 /// RFC 3339.
1131 pub created_at: String,
1132}
1133
1134#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1135pub struct DirectoryPage {
1136 pub entries: Vec<DirectoryEntry>,
1137 /// Where the next page starts; null on the last.
1138 pub next: Option<String>,
1139}
1140
1141// --- Invites ---------------------------------------------------------------
1142//
1143// While registration is invite-only, every new account (with a password or
1144// through GitHub) needs an invite code. Each person may have
1145// `INVITES_PER_USER` invites out at a time; staff grant more to a person or
1146// to a workspace, whose owners share them. Inviting an email with no
1147// account into a workspace makes an invite bound to that address, which
1148// registers and joins in one step. See services/identity/src/invites.rs.
1149
1150/// Whether anyone may make an account, or only someone with an invite. Set
1151/// by identity's `REGISTRATION_MODE` var; anything but `open`, including
1152/// leaving it unset, is `invite`, so a missing setting never opens sign-up.
1153#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1154#[serde(rename_all = "snake_case")]
1155pub enum RegistrationMode {
1156 #[default]
1157 Invite,
1158 Open,
1159}
1160
1161impl RegistrationMode {
1162 pub fn parse(text: Option<&str>) -> RegistrationMode {
1163 match text.map(|text| text.trim().to_ascii_lowercase()).as_deref() {
1164 Some("open") => RegistrationMode::Open,
1165 _ => RegistrationMode::Invite,
1166 }
1167 }
1168}
1169
1170/// How many invites a person may have out at once, unless identity's
1171/// `INVITES_PER_USER` var says otherwise.
1172pub const INVITES_PER_USER: u32 = 5;
1173
1174/// How long an invite works, unless identity's `INVITE_TTL_DAYS` var says
1175/// otherwise.
1176pub const INVITE_TTL_DAYS: u64 = 30;
1177
1178/// Where an invite stands. Only a pending invite can be used. A pending
1179/// invite can be revoked, and so can one awaiting confirmation. An expired
1180/// or revoked invite that was never used gives its inviter the invite back.
1181#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1182#[serde(rename_all = "snake_case")]
1183pub enum InviteStatus {
1184 Pending,
1185 /// Used to make an account that has not confirmed its email address
1186 /// yet. The code is spent; the workspace (or repository) it gives is
1187 /// joined when the address is confirmed, unless it is revoked first.
1188 AwaitingConfirmation,
1189 /// Used to make an account that has confirmed its address, for a
1190 /// workspace it has not yet joined or declined: the workspace
1191 /// invitation waits for the person's answer (`accept_invitation`).
1192 AwaitingAnswer,
1193 Redeemed,
1194 /// Its person declined the workspace it invited them to.
1195 Declined,
1196 Expired,
1197 Revoked,
1198}
1199
1200/// What using an invite does.
1201#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1202#[serde(rename_all = "snake_case")]
1203pub enum InviteKind {
1204 /// Makes a new account, and joins `workspace` when one is set.
1205 Account,
1206 /// An existing account joins `workspace`. Never makes an account.
1207 Workspace,
1208}
1209
1210/// Whose allowance an invite uses.
1211#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1212#[serde(rename_all = "snake_case")]
1213pub enum InviteCharge {
1214 /// Its inviter's own.
1215 User,
1216 /// The workspace's, granted by staff and shared by its owners.
1217 Workspace,
1218 /// Nobody's: staff minted it, or it invites an existing account.
1219 None,
1220}
1221
1222/// One invite, as the person who made it sees it.
1223#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1224#[serde(rename_all = "camelCase")]
1225pub struct Invite {
1226 pub id: String,
1227 /// The code, such as `g1t-k7m2-q9xd-…`: returned once when the invite
1228 /// is made, and afterwards to whoever made it while it is pending.
1229 /// Null otherwise.
1230 pub code: Option<String>,
1231 /// The code's first group, such as `g1t-k7m2`, to recognise it by.
1232 pub hint: String,
1233 /// Only an account with this address can use it. Null: anyone with
1234 /// the code.
1235 pub email: Option<String>,
1236 pub kind: InviteKind,
1237 /// The workspace it joins, by slug.
1238 pub workspace: Option<String>,
1239 pub status: InviteStatus,
1240 pub charged_to: InviteCharge,
1241 /// Who made it, by username. Null when g1t staff did.
1242 pub invited_by: Option<String>,
1243 /// The account that used it, by username.
1244 pub redeemed_by: Option<String>,
1245 /// RFC 3339.
1246 pub created_at: String,
1247 /// RFC 3339.
1248 pub expires_at: String,
1249 /// RFC 3339.
1250 pub redeemed_at: Option<String>,
1251 /// RFC 3339.
1252 pub revoked_at: Option<String>,
1253 /// The account a workspace invitation is for, by username: someone
1254 /// invited by username, or the account the invite made.
1255 #[serde(default)]
1256 pub invitee: Option<String>,
1257 /// The role `workspace` is joined with. Null when it names none.
1258 #[serde(default)]
1259 pub role: Option<crate::Role>,
1260 /// The staff member who minted it. Only in staff views.
1261 #[serde(default, skip_serializing_if = "Option::is_none")]
1262 pub staff: Option<String>,
1263}
1264
1265/// How many invites someone may have out, and how many they have.
1266#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1267pub struct Allowance {
1268 /// Null: no limit.
1269 pub limit: Option<u32>,
1270 /// Pending and used invites; revoked and expired ones are not counted.
1271 pub used: u32,
1272 /// Null: no limit.
1273 pub remaining: Option<u32>,
1274}
1275
1276impl Allowance {
1277 pub fn new(limit: Option<u32>, used: u32) -> Allowance {
1278 Allowance {
1279 limit,
1280 used,
1281 remaining: limit.map(|limit| limit.saturating_sub(used)),
1282 }
1283 }
1284
1285 pub fn exhausted(&self) -> bool {
1286 self.remaining == Some(0)
1287 }
1288}
1289
1290/// A workspace's shared invites, for one of its owners.
1291#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1292pub struct WorkspaceAllowance {
1293 pub slug: String,
1294 pub allowance: Allowance,
1295}
1296
1297/// `list_invites` (takes `UserArgs`): a person's invites, newest first,
1298/// and what they have left. Returns `InvitesOverview`.
1299#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1300pub struct InvitesOverview {
1301 pub mode: RegistrationMode,
1302 pub allowance: Allowance,
1303 /// Workspaces the person owns that staff granted invites to.
1304 pub workspaces: Vec<WorkspaceAllowance>,
1305 pub invites: Vec<Invite>,
1306}
1307
1308/// `create_invite`: a person makes an invite, optionally for one email
1309/// address. People only; never an agent or a workspace's token, and not
1310/// before their email is confirmed. Uses one of the person's invites, or,
1311/// with `workspace`, one of the invites staff granted that workspace (its
1312/// owners only). Emails the address when one is given. Returns
1313/// `Outcome<Invite>`, with the code.
1314///
1315/// `revoke_invite` (takes `RemoveArgs`): its maker revokes a pending
1316/// invite; a workspace's owners may revoke one made for the workspace.
1317/// The invite comes back to whoever it was charged to. Returns
1318/// `Outcome<Invite>`.
1319#[derive(Debug, Serialize, Deserialize)]
1320pub struct CreateInviteArgs {
1321 pub user: User,
1322 #[serde(default)]
1323 pub email: Option<String>,
1324 /// Use this workspace's granted invites, by slug.
1325 #[serde(default)]
1326 pub workspace: Option<String>,
1327 /// The workspace the new account is invited to, by slug: one the
1328 /// person owns that can add members (not on the free plan). Once the
1329 /// account is confirmed it gets a workspace invitation to accept, as a
1330 /// member, and no workspace of its own is made for it.
1331 #[serde(default)]
1332 pub join: Option<String>,
1333 /// The role `join` invites them with; member when absent. Ignored
1334 /// without `join`.
1335 #[serde(default)]
1336 pub join_role: Option<crate::Role>,
1337 /// Where the request came in, for the audit log; g1t.sh when absent.
1338 #[serde(default)]
1339 pub surface: Option<crate::audit::Surface>,
1340}
1341
1342/// `check_invite`: what an invite code is for, before using it. Returns
1343/// `Outcome<InvitePreview>`; a code that is unknown, used, revoked or
1344/// expired gets the same answer, so codes cannot be probed. With
1345/// `any_status`, a real code that can no longer be used is described
1346/// instead (its `status` says why), so the page can say whom to ask for a
1347/// new one; an unknown code still gets the one answer.
1348#[derive(Debug, Serialize, Deserialize)]
1349pub struct InviteCodeArgs {
1350 pub code: String,
1351 /// Who is asking, such as the visitor's IP address, for rate limits.
1352 #[serde(default)]
1353 pub client: Option<String>,
1354 /// Who is looking, if signed in: sets `InvitePreview::for_viewer`.
1355 #[serde(default)]
1356 pub viewer: Option<User>,
1357 #[serde(default)]
1358 pub any_status: bool,
1359 /// The `proof` from the invite email's link, if the page was opened
1360 /// from it: sets `InvitePreview::email_proven`.
1361 #[serde(default)]
1362 pub email_proof: Option<String>,
1363}
1364
1365/// Someone shown on an invite.
1366#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1367pub struct InviteFrom {
1368 pub username: String,
1369 pub name: Option<String>,
1370 pub avatar: Option<String>,
1371}
1372
1373/// A repository an invite code was sent with: using the code accepts the
1374/// invitation to collaborate on it.
1375#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1376pub struct InviteRepository {
1377 /// `workspace/repo`.
1378 pub name: String,
1379 /// The role it gives, such as `write`.
1380 pub role: String,
1381}
1382
1383/// What a valid invite code is for.
1384#[derive(Clone, Debug, Serialize, Deserialize)]
1385#[serde(rename_all = "camelCase")]
1386pub struct InvitePreview {
1387 pub kind: InviteKind,
1388 /// Pending, unless `any_status` asked about a code that is spent.
1389 pub status: InviteStatus,
1390 /// Null when g1t staff sent it.
1391 pub invited_by: Option<InviteFrom>,
1392 pub workspace: Option<ProfileWorkspace>,
1393 /// The repository it accepts an invitation to, if it was sent with one.
1394 pub repository: Option<InviteRepository>,
1395 /// The address it is for, partly hidden, such as `a•••@example.com`.
1396 pub email: Option<String>,
1397 /// The address in full, while it is pending: whoever holds the code
1398 /// was sent it there. Fills in and locks the sign-up form.
1399 pub address: Option<String>,
1400 /// Whether the address it is for has a g1t account already, so the
1401 /// page asks them to sign in rather than sign up.
1402 pub has_account: bool,
1403 /// With a viewer: whether the invite is theirs (it is for one of their
1404 /// confirmed addresses, or they used it). Null without a viewer or,
1405 /// for a pending invite, when it is for anyone with the code.
1406 pub for_viewer: Option<bool>,
1407 /// RFC 3339.
1408 pub expires_at: String,
1409 /// For a shared invite link ([`SharedInvite`]): the group it was made
1410 /// for, such as `Cloudflare judges`. Not secret; the sign-up page shows
1411 /// it. Null for a one-person invite.
1412 #[serde(default)]
1413 pub shared_label: Option<String>,
1414 /// For a shared invite link limited to some email domains: those
1415 /// domains, such as `["cloudflare.com"]`. Empty for any address.
1416 #[serde(default)]
1417 pub shared_domains: Vec<String>,
1418 /// Whether `email_proof` was this pending invite's own, from the email
1419 /// it was sent in: the account made with it starts with `address`
1420 /// confirmed. False without a proof, with a wrong one, and for an
1421 /// invite bound to no address.
1422 #[serde(default)]
1423 pub email_proven: bool,
1424}
1425
1426/// `accept_invite`: a signed-in person uses a workspace invite made for
1427/// their confirmed address, and joins the workspace, or an invite sent with
1428/// a repository invitation, and accepts it. Returns `Outcome<String>`: the
1429/// workspace's slug, or `workspace/repo`.
1430#[derive(Debug, Serialize, Deserialize)]
1431pub struct AcceptInviteArgs {
1432 pub user: User,
1433 pub code: String,
1434}
1435
1436/// `invite_member`: an owner invites an email address into a workspace.
1437/// It always makes an invite bound to that address and emails it, so the
1438/// answer never says whether the address has an account. Without one, the
1439/// invite registers and joins in one step, and uses one of the workspace's
1440/// granted invites or else one of the owner's own. With one, it costs
1441/// nothing. Returns `Outcome<Invite>`, with the code.
1442#[derive(Debug, Serialize, Deserialize)]
1443pub struct InviteMemberArgs {
1444 pub actor: User,
1445 pub slug: String,
1446 /// An email address. Give this or `username`.
1447 #[serde(default)]
1448 pub email: String,
1449 /// A g1t username: that account gets a workspace invitation to accept
1450 /// or decline, in its inbox and by email. Nobody joins without saying
1451 /// yes.
1452 #[serde(default)]
1453 pub username: Option<String>,
1454 /// The role they join with; member when absent.
1455 #[serde(default)]
1456 pub role: Option<crate::Role>,
1457 /// Where the request came in, for the audit log; g1t.sh when absent.
1458 #[serde(default)]
1459 pub surface: Option<crate::audit::Surface>,
1460}
1461
1462/// A workspace invitation waiting for its person's answer, as they see it.
1463/// `list_invitations` (takes `UserArgs`) returns `Vec<WorkspaceInvitation>`,
1464/// newest first: pending ones only, never expired, revoked or answered.
1465#[derive(Clone, Debug, Serialize, Deserialize)]
1466#[serde(rename_all = "camelCase")]
1467pub struct WorkspaceInvitation {
1468 pub id: String,
1469 pub workspace: ProfileWorkspace,
1470 /// The role accepting joins with.
1471 pub role: crate::Role,
1472 /// Null when g1t staff sent it.
1473 pub invited_by: Option<InviteFrom>,
1474 /// RFC 3339.
1475 pub created_at: String,
1476 /// RFC 3339.
1477 pub expires_at: String,
1478}
1479
1480/// `accept_invitation`: the person it is for joins the workspace with the
1481/// role it names. Returns `Outcome<String>`, the workspace's slug.
1482///
1483/// `decline_invitation`: they say no; whoever sent it is told in their
1484/// inbox. Returns `Outcome<bool>`.
1485#[derive(Debug, Serialize, Deserialize)]
1486pub struct InvitationArgs {
1487 pub user: User,
1488 pub id: String,
1489 /// Where the request came in, for the audit log; g1t.sh when absent.
1490 #[serde(default)]
1491 pub surface: Option<crate::audit::Surface>,
1492}
1493
1494/// `find_people`: accounts whose username starts with `query`, or whose
1495/// name contains it, for picking someone to invite. Only what a profile
1496/// shows: a username, a name and an avatar, never an email address.
1497/// Returns `Vec<InviteFrom>`, at most `limit` (10 at most, 8 when absent).
1498#[derive(Debug, Serialize, Deserialize)]
1499pub struct FindPeopleArgs {
1500 pub query: String,
1501 #[serde(default)]
1502 pub limit: Option<u32>,
1503}
1504
1505/// `workspace_invites` (takes `ListMembersArgs`): a workspace's invites,
1506/// newest first. Owners only. Returns `Outcome<Vec<Invite>>`.
1507///
1508/// `revoke_workspace_invite`: owners only. Returns `Outcome<Invite>`.
1509#[derive(Debug, Serialize, Deserialize)]
1510pub struct WorkspaceInviteArgs {
1511 pub actor: User,
1512 pub slug: String,
1513 pub id: String,
1514}
1515
1516/// `request_access`: someone without an invite asks for one. Kept on the
1517/// waitlist, one entry per address. Answers the same way whether or not
1518/// the address is already on it. Returns `Outcome<bool>`.
1519#[derive(Debug, Default, Serialize, Deserialize)]
1520pub struct RequestAccessArgs {
1521 pub email: String,
1522 /// What they will build, if they said.
1523 #[serde(default)]
1524 pub about: String,
1525 /// Who is asking, such as the visitor's IP address, for rate limits.
1526 #[serde(default)]
1527 pub client: Option<String>,
1528}
1529
1530/// The most characters `RequestAccessArgs::about` keeps.
1531pub const MAX_WAITLIST_ABOUT: usize = 1000;
1532
1533// `registration` takes `{}` and returns `RegistrationMode`.
1534
1535// --- Invites, staff only ---
1536
1537#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1538#[serde(rename_all = "snake_case")]
1539pub enum WaitlistStatus {
1540 Waiting,
1541 Invited,
1542 Dismissed,
1543}
1544
1545impl WaitlistStatus {
1546 pub fn as_str(self) -> &'static str {
1547 match self {
1548 WaitlistStatus::Waiting => "waiting",
1549 WaitlistStatus::Invited => "invited",
1550 WaitlistStatus::Dismissed => "dismissed",
1551 }
1552 }
1553}
1554
1555/// Someone who asked for access.
1556#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1557#[serde(rename_all = "camelCase")]
1558pub struct WaitlistEntry {
1559 pub id: String,
1560 pub email: String,
1561 pub about: Option<String>,
1562 pub status: WaitlistStatus,
1563 pub invite_id: Option<String>,
1564 pub decided_by: Option<String>,
1565 /// RFC 3339.
1566 pub decided_at: Option<String>,
1567 /// What staff wrote when approving; it went in the invite email.
1568 #[serde(default)]
1569 pub note: Option<String>,
1570 /// The account made with the invite, once it was used.
1571 #[serde(default)]
1572 pub joined_as: Option<String>,
1573 /// When they first asked. RFC 3339.
1574 pub created_at: String,
1575 /// When they last asked. RFC 3339.
1576 pub updated_at: String,
1577}
1578
1579/// `admin_waitlist`: the waitlist, newest first, at most
1580/// [`ADMIN_INVITES_LIMIT`]. Returns `Vec<WaitlistEntry>`.
1581///
1582/// `admin_waitlist_pending` takes `{}` and returns the number of requests
1583/// still waiting, for sudo's navigation.
1584#[derive(Debug, Default, Serialize, Deserialize)]
1585pub struct AdminWaitlistArgs {
1586 /// Part of an email address or of what they said.
1587 #[serde(default)]
1588 pub query: Option<String>,
1589 /// Null: every status.
1590 #[serde(default)]
1591 pub status: Option<WaitlistStatus>,
1592}
1593
1594/// The most rows one staff listing of invites or the waitlist returns.
1595pub const ADMIN_INVITES_LIMIT: usize = 500;
1596
1597/// `admin_decide_waitlist`: approving mints an invite bound to the
1598/// address, charged to nobody, and emails it, with `note` if given;
1599/// dismissing only marks the entry. Returns `Outcome<WaitlistEntry>`.
1600#[derive(Debug, Serialize, Deserialize)]
1601pub struct AdminDecideWaitlistArgs {
1602 pub id: String,
1603 pub approve: bool,
1604 /// The staff member, by email.
1605 pub staff: String,
1606 /// A line for the invite email, up to [`MAX_WAITLIST_NOTE`] characters.
1607 #[serde(default)]
1608 pub note: Option<String>,
1609}
1610
1611/// The most characters an approval's note keeps.
1612pub const MAX_WAITLIST_NOTE: usize = 500;
1613
1614/// `admin_invites`: every invite, newest first, at most
1615/// [`ADMIN_INVITES_LIMIT`], optionally only those whose code starts with
1616/// `query`, or whose email, inviter or redeemer contains it. Returns
1617/// `Vec<Invite>`.
1618#[derive(Debug, Default, Serialize, Deserialize)]
1619pub struct AdminInvitesArgs {
1620 #[serde(default)]
1621 pub query: Option<String>,
1622}
1623
1624/// `admin_revoke_invite`: revokes any pending invite. Returns
1625/// `Outcome<Invite>`.
1626#[derive(Debug, Serialize, Deserialize)]
1627pub struct AdminRevokeInviteArgs {
1628 pub id: String,
1629 pub staff: String,
1630}
1631
1632/// `admin_mint_invite`: staff make an invite that uses nobody's
1633/// allowance, optionally bound to (and emailed to) an address. Returns
1634/// `Outcome<Invite>`, with the code.
1635#[derive(Debug, Serialize, Deserialize)]
1636pub struct AdminMintInviteArgs {
1637 #[serde(default)]
1638 pub email: Option<String>,
1639 pub staff: String,
1640}
1641
1642/// Who staff grant invites to.
1643#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1644#[serde(rename_all = "snake_case")]
1645pub enum GrantTarget {
1646 User,
1647 Workspace,
1648}
1649
1650impl GrantTarget {
1651 pub fn as_str(self) -> &'static str {
1652 match self {
1653 GrantTarget::User => "user",
1654 GrantTarget::Workspace => "workspace",
1655 }
1656 }
1657}
1658
1659/// `admin_grant_invites`: gives a person (by username) or a workspace (by
1660/// slug) `amount` more invites; a negative amount takes some back. Returns
1661/// `Outcome<Allowance>`: theirs afterwards.
1662#[derive(Debug, Serialize, Deserialize)]
1663pub struct AdminGrantInvitesArgs {
1664 pub target: GrantTarget,
1665 pub name: String,
1666 pub amount: i32,
1667 #[serde(default)]
1668 pub note: String,
1669 pub staff: String,
1670}
1671
1672/// The most invites one grant gives or takes back.
1673pub const MAX_INVITE_GRANT: i32 = 1000;
1674
1675/// Invites staff granted.
1676#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1677#[serde(rename_all = "camelCase")]
1678pub struct InviteGrant {
1679 pub amount: i32,
1680 pub note: Option<String>,
1681 pub granted_by: String,
1682 /// RFC 3339.
1683 pub created_at: String,
1684}
1685
1686/// Someone a person invited, and whom they invited in turn.
1687#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1688#[serde(rename_all = "camelCase")]
1689pub struct InviteTreeNode {
1690 pub username: String,
1691 /// When they used the invite. RFC 3339.
1692 pub joined_at: String,
1693 pub invited: Vec<InviteTreeNode>,
1694}
1695
1696/// `admin_invite_tree` (takes `UsernameArgs`): where a person came from
1697/// and whom they brought, for tracing abuse. Returns `Option<InviteTree>`.
1698///
1699/// `admin_workspace_invites` (takes `SlugArgs`): a workspace's granted
1700/// invites, grants and invites. Returns `Option<InviteTree>` with
1701/// `username` the slug and no `invited_by`.
1702#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1703#[serde(rename_all = "camelCase")]
1704pub struct InviteTree {
1705 pub username: String,
1706 /// Who invited them, then who invited that person, and so on. Empty
1707 /// for an account made without an invite.
1708 pub invited_by: Vec<String>,
1709 /// The staff member who minted their invite, when staff did.
1710 pub staff: Option<String>,
1711 pub allowance: Allowance,
1712 pub grants: Vec<InviteGrant>,
1713 /// Their invites, newest first.
1714 pub invites: Vec<Invite>,
1715 /// Whom they invited, three levels down.
1716 pub invited: Vec<InviteTreeNode>,
1717 /// The shared invite link the account was made with, if it was.
1718 #[serde(default)]
1719 pub shared: Option<SharedInviteSource>,
1720}
1721
1722// --- Shared invite links, staff only ---
1723//
1724// One link for a group (a conference's judges, a post, a community): up
1725// to `max_uses` new accounts, until it expires or staff revoke it,
1726// optionally only for addresses at some domains. Each use makes a new
1727// account, which then makes its own workspace; a shared link never joins
1728// anyone to an existing workspace, and uses nobody's allowance. Its code
1729// looks and is stored like any invite code (only a hash, and a sealed copy
1730// staff can copy again while it is live); the link is
1731// `https://g1t.sh/register?invite=<code>`. See
1732// services/identity/src/shared_invites.rs.
1733
1734/// How long a shared invite link works when staff give no date.
1735pub const SHARED_INVITE_TTL_DAYS: u64 = 14;
1736/// The furthest ahead a shared invite link's last day may be set.
1737pub const SHARED_INVITE_MAX_DAYS: u64 = 365;
1738/// The most accounts one shared invite link makes.
1739pub const MAX_SHARED_INVITE_USES: u32 = 1000;
1740/// The most characters a shared invite link's label keeps.
1741pub const MAX_SHARED_INVITE_LABEL: usize = 80;
1742/// The most email domains one shared invite link may be limited to.
1743pub const MAX_SHARED_INVITE_DOMAINS: usize = 10;
1744
1745/// Where a shared invite link stands. Only a live one makes accounts.
1746#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1747#[serde(rename_all = "snake_case")]
1748pub enum SharedInviteStatus {
1749 Live,
1750 /// Every use is taken.
1751 UsedUp,
1752 Expired,
1753 Revoked,
1754}
1755
1756/// The shared invite link an account was made with.
1757#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1758pub struct SharedInviteSource {
1759 pub id: String,
1760 pub label: String,
1761}
1762
1763/// An account made with a shared invite link.
1764#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1765#[serde(rename_all = "camelCase")]
1766pub struct SharedInviteAccount {
1767 /// Null once the account is purged.
1768 pub username: Option<String>,
1769 /// When it was made with the link. RFC 3339.
1770 pub joined_at: String,
1771}
1772
1773/// One shared invite link, as staff see it.
1774#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1775#[serde(rename_all = "camelCase")]
1776pub struct SharedInvite {
1777 /// `sinv_…`.
1778 pub id: String,
1779 /// Whom it is for, such as `Cloudflare judges`.
1780 pub label: String,
1781 /// The code, while it is live (and IDENTITY_KEY is set).
1782 pub code: Option<String>,
1783 /// The code's first group, such as `g1t-k7m2`.
1784 pub hint: String,
1785 pub max_uses: u32,
1786 /// Accounts made with it so far.
1787 pub uses: u32,
1788 /// Only addresses at these domains may use it; empty for any.
1789 pub domains: Vec<String>,
1790 pub status: SharedInviteStatus,
1791 /// The staff member who made it, by email.
1792 pub staff: String,
1793 /// RFC 3339.
1794 pub created_at: String,
1795 /// RFC 3339.
1796 pub expires_at: String,
1797 pub revoked_at: Option<String>,
1798 pub revoked_by: Option<String>,
1799 /// The accounts made with it, oldest first.
1800 pub accounts: Vec<SharedInviteAccount>,
1801}
1802
1803/// `admin_shared_invites` takes `{}`: shared invite links, newest first,
1804/// at most 200, each with the accounts it made. Returns
1805/// `Vec<SharedInvite>`.
1806///
1807/// `admin_create_shared_invite`: staff make a shared invite link. Recorded
1808/// in sudo's audit log. Returns `Outcome<SharedInvite>`, with the code.
1809#[derive(Debug, Default, Serialize, Deserialize)]
1810pub struct AdminCreateSharedInviteArgs {
1811 /// Required, up to [`MAX_SHARED_INVITE_LABEL`] characters.
1812 pub label: String,
1813 /// 1 to [`MAX_SHARED_INVITE_USES`].
1814 pub max_uses: u32,
1815 /// The last day it works, `YYYY-MM-DD` (UTC; it works until the end of
1816 /// that day), at most [`SHARED_INVITE_MAX_DAYS`] ahead. Null for
1817 /// [`SHARED_INVITE_TTL_DAYS`] from now.
1818 #[serde(default)]
1819 pub expires_on: Option<String>,
1820 /// Email domains it is limited to, such as `cloudflare.com`; empty for
1821 /// any address. Up to [`MAX_SHARED_INVITE_DOMAINS`].
1822 #[serde(default)]
1823 pub domains: Vec<String>,
1824 /// The staff member, by email.
1825 pub staff: String,
1826}
1827
1828/// `admin_revoke_shared_invite`: stops a shared invite link making any
1829/// more accounts. Those it made stay. Recorded in sudo's audit log.
1830/// Returns `Outcome<SharedInvite>`.
1831#[derive(Debug, Serialize, Deserialize)]
1832pub struct AdminRevokeSharedInviteArgs {
1833 pub id: String,
1834 pub staff: String,
1835}
1836
1837#[cfg(test)]
1838mod deletion_tests {
1839 use super::{WorkspaceDeletion, protected_names};
1840
1841 #[test]
1842 fn only_billing_or_protection_stands_in_the_way() {
1843 let clear = WorkspaceDeletion {
1844 repositories: 2,
1845 projects: 1,
1846 members: 3,
1847 ..WorkspaceDeletion::default()
1848 };
1849 assert!(!clear.blocked());
1850 assert_eq!(clear.reason("acme"), None);
1851 let owing = WorkspaceDeletion {
1852 billing: Some("Pay first.".into()),
1853 ..WorkspaceDeletion::default()
1854 };
1855 assert!(owing.blocked());
1856 assert_eq!(owing.reason("acme").as_deref(), Some("Pay first."));
1857 let protected = WorkspaceDeletion {
1858 billing: Some("Pay first.".into()),
1859 protected: true,
1860 ..WorkspaceDeletion::default()
1861 };
1862 assert!(protected.blocked());
1863 assert_eq!(
1864 protected.reason("flagon-io").as_deref(),
1865 Some("flagon-io is protected and can never be deleted.")
1866 );
1867 }
1868
1869 #[test]
1870 fn flagon_is_protected_whatever_the_variable_says() {
1871 assert_eq!(protected_names(None), ["flagon-io"]);
1872 assert_eq!(protected_names(Some("")), ["flagon-io"]);
1873 assert_eq!(protected_names(Some(" , ")), ["flagon-io"]);
1874 assert_eq!(
1875 protected_names(Some("Flagon-IO, acme ,wsp_1")),
1876 ["flagon-io", "acme", "wsp_1"]
1877 );
1878 }
1879}