Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)
# Conflicts: # apps/api/src/operations.rs
| 45 | 45 | ReportQueueArgs, ReportReviewArgs, | |
| 46 | 46 | }; | |
| 47 | 47 | use g1t_contracts::identity::AgentScope; | |
| 48 | − | use g1t_contracts::{Failure, FailureCode, Outcome, PrincipalKind, Viewer}; | |
| 48 | + | use g1t_contracts::{Failure, FailureCode, Outcome, PrincipalKind, User, Viewer}; | |
| 49 | 49 | use g1t_kit::wire; | |
| 50 | 50 | use serde_json::{Value, json}; | |
| 51 | 51 | use worker::{Context, Env, Method, Request, Response, Result, event}; | |
| ⋯ | |||
| 94 | 94 | } | |
| 95 | 95 | } | |
| 96 | 96 | ||
| 97 | + | /// What a person whose account has not confirmed its email address may | |
| 98 | + | /// call: who they are, their addresses, and confirming one with the code | |
| 99 | + | /// from the email. Nothing over MCP. | |
| 100 | + | fn pending_may(method: &str, path: &str, on_mcp: bool) -> bool { | |
| 101 | + | !on_mcp | |
| 102 | + | && matches!( | |
| 103 | + | (method, path.trim_end_matches('/')), | |
| 104 | + | ("GET", "/user") | ("GET", "/user/emails") | ("POST", "/user/emails/confirm") | |
| 105 | + | ) | |
| 106 | + | } | |
| 107 | + | ||
| 97 | 108 | /// An error in the shape every endpoint uses. | |
| 98 | 109 | fn failure(failure: &Failure) -> Result<Response> { | |
| 99 | 110 | Ok(reply(&json!({ "error": failure }))?.with_status(failure.code.http_status())) | |
| ⋯ | |||
| 614 | 625 | Ok(viewer) => viewer, | |
| 615 | 626 | Err(refused) => return Ok(refused), | |
| 616 | 627 | }; | |
| 628 | + | // A person who has not confirmed their email address: who they are, | |
| 629 | + | // their addresses, and confirming one, nothing else (REST or MCP). | |
| 630 | + | if viewer.as_ref().is_some_and(User::awaits_confirmation) && !pending_may(method, &path, on_mcp) { | |
| 631 | + | return fail( | |
| 632 | + | FailureCode::Forbidden, | |
| 633 | + | &g1t_contracts::accounts::confirm_email_first(&services.addresses.site), | |
| 634 | + | ); | |
| 635 | + | } | |
| 617 | 636 | services.audit = audit::AuditContext::of(&request, on_mcp); | |
| 618 | 637 | // An agent's token: what it may do comes with it, on the composite | |
| 619 | 638 | // identity identity resolved it to. | |
| ⋯ | |||
| 868 | 887 | use serde_json::json; | |
| 869 | 888 | ||
| 870 | 889 | #[test] | |
| 890 | + | fn an_unconfirmed_account_may_only_see_itself_and_confirm_its_address() { | |
| 891 | + | assert!(super::pending_may("GET", "/user", false)); | |
| 892 | + | assert!(super::pending_may("GET", "/user/emails/", false)); | |
| 893 | + | assert!(super::pending_may("POST", "/user/emails/confirm", false)); | |
| 894 | + | assert!(!super::pending_may("POST", "/user/emails", false)); | |
| 895 | + | assert!(!super::pending_may("POST", "/workspaces", false)); | |
| 896 | + | assert!(!super::pending_may("GET", "/repos/acme/rocket", false)); | |
| 897 | + | assert!(!super::pending_may("POST", "/user/emails/confirm", true)); | |
| 898 | + | assert!(!super::pending_may("POST", "/", true)); | |
| 899 | + | } | |
| 900 | + | ||
| 901 | + | #[test] | |
| 871 | 902 | fn a_job_spec_gets_the_toolkits_variables() { | |
| 872 | 903 | // As the actions service sends it, converted as the API does. | |
| 873 | 904 | let sent = json!({ "variables": { "GITHUB_SHA": "abc" }, "runtime": { "token": "h.p.s", "idToken": true } }); | |
| 26 | 26 | ( | |
| 27 | 27 | "Accounts", | |
| 28 | 28 | "Signing in from a tool, who a token acts as, and your email addresses.", | |
| 29 | − | &[Op::Whoami, Op::ListEmails, Op::AddEmail, Op::RemoveEmail, Op::UpdateEmailSettings], | |
| 29 | + | &[Op::Whoami, Op::ListEmails, Op::AddEmail, Op::ConfirmEmail, Op::RemoveEmail, Op::UpdateEmailSettings], | |
| 30 | 30 | ), | |
| 31 | 31 | ( | |
| 32 | 32 | "Notifications", | |
| ⋯ | |||
| 541 | 541 | Op::LeaveWorkspace => "Leave a workspace", | |
| 542 | 542 | Op::ListEmails => "List your email addresses", | |
| 543 | 543 | Op::AddEmail => "Add an email address", | |
| 544 | + | Op::ConfirmEmail => "Confirm an email address", | |
| 544 | 545 | Op::RemoveEmail => "Remove an email address", | |
| 545 | 546 | Op::UpdateEmailSettings => "Change your email settings", | |
| 546 | 547 | Op::ListInvites => "List your invites", | |
| 114 | 114 | LeaveWorkspace, | |
| 115 | 115 | ListEmails, | |
| 116 | 116 | AddEmail, | |
| 117 | + | ConfirmEmail, | |
| 117 | 118 | RemoveEmail, | |
| 118 | 119 | UpdateEmailSettings, | |
| 119 | 120 | ListInvites, | |
| ⋯ | |||
| 682 | 683 | } | |
| 683 | 684 | ||
| 684 | 685 | impl Op { | |
| 685 | − | pub const ALL: [Op; 314] = [ | |
| 686 | + | pub const ALL: [Op; 315] = [ | |
| 686 | 687 | Op::Whoami, | |
| 687 | 688 | Op::GetWorkspace, | |
| 688 | 689 | Op::CreateWorkspace, | |
| ⋯ | |||
| 695 | 696 | Op::LeaveWorkspace, | |
| 696 | 697 | Op::ListEmails, | |
| 697 | 698 | Op::AddEmail, | |
| 699 | + | Op::ConfirmEmail, | |
| 698 | 700 | Op::RemoveEmail, | |
| 699 | 701 | Op::UpdateEmailSettings, | |
| 700 | 702 | Op::ListInvites, | |
| ⋯ | |||
| 1018 | 1020 | Op::LeaveWorkspace => "leave_workspace", | |
| 1019 | 1021 | Op::ListEmails => "list_emails", | |
| 1020 | 1022 | Op::AddEmail => "add_email", | |
| 1023 | + | Op::ConfirmEmail => "confirm_email", | |
| 1021 | 1024 | Op::RemoveEmail => "remove_email", | |
| 1022 | 1025 | Op::UpdateEmailSettings => "update_email_settings", | |
| 1023 | 1026 | Op::ListInvites => "list_invites", | |
| ⋯ | |||
| 1221 | 1224 | Op::AddEmail => { | |
| 1222 | 1225 | "Add an email address to your account. g1t emails it a link to confirm it; until then it cannot be primary and does not sign you in. Adding an address you added before and have not confirmed sends the link again. An address another account has confirmed cannot be added. An account has at most 10. Needs your account `password`; your confirmed addresses are told. People only." | |
| 1223 | 1226 | } | |
| 1227 | + | Op::ConfirmEmail => { | |
| 1228 | + | "Confirm an email address with the six-digit `code` from the confirmation email g1t sent it. The same email has a link that does the same; either one works, once, for 60 minutes, and asking for a new email ends both. A new account must confirm its address before it can do anything else: until then this, `GET /user` and `GET /user/emails` are the only calls its token can make, and everything else, MCP included, is refused with `403`. Confirming a new account's address also joins the workspace its invite named, when the invite still applies: the answer's `joined` names it, or `invite_lapsed` says why not. Ten wrong codes in an hour pause checking for the account. People only." | |
| 1229 | + | } | |
| 1224 | 1230 | Op::RemoveEmail => { | |
| 1225 | 1231 | "Remove an email address from your account. Never your primary address (make another primary first) and never your last confirmed one. Needs your account `password`; every confirmed address, the removed one included, is told. People only." | |
| 1226 | 1232 | } | |
| ⋯ | |||
| 1775 | 1781 | &[], | |
| 1776 | 1782 | ), | |
| 1777 | 1783 | Op::ListEmails => object(json!({}), &[]), | |
| 1784 | + | Op::ConfirmEmail => object( | |
| 1785 | + | json!({ | |
| 1786 | + | "code": { | |
| 1787 | + | "type": "string", | |
| 1788 | + | "description": "The six-digit code from the confirmation email. Spaces and hyphens are ignored.", | |
| 1789 | + | }, | |
| 1790 | + | }), | |
| 1791 | + | &["code"], | |
| 1792 | + | ), | |
| 1778 | 1793 | Op::AddEmail => object( | |
| 1779 | 1794 | json!({ | |
| 1780 | 1795 | "email": { "type": "string", "description": "The address to add." }, | |
| ⋯ | |||
| 3303 | 3318 | | Op::LeaveWorkspace | |
| 3304 | 3319 | | Op::ListEmails | |
| 3305 | 3320 | | Op::AddEmail | |
| 3321 | + | | Op::ConfirmEmail | |
| 3306 | 3322 | | Op::RemoveEmail | |
| 3307 | 3323 | | Op::UpdateEmailSettings | |
| 3308 | 3324 | | Op::ListInvites | |
| ⋯ | |||
| 3578 | 3594 | // A person's addresses: identity refuses anyone but a person, and | |
| 3579 | 3595 | // the password is the proof a sensitive change needs. | |
| 3580 | 3596 | Op::ListEmails => pass(identity, "list_emails", &json!({ "user": actor() })).await, | |
| 3597 | + | Op::ConfirmEmail => { | |
| 3598 | + | pass( | |
| 3599 | + | identity, | |
| 3600 | + | "confirm_email_code", | |
| 3601 | + | &g1t_contracts::accounts::ConfirmEmailCodeArgs { user: actor(), code: text(input, "code"), client: None }, | |
| 3602 | + | ) | |
| 3603 | + | .await | |
| 3604 | + | } | |
| 3581 | 3605 | Op::AddEmail | Op::RemoveEmail => { | |
| 3582 | 3606 | let method = if self == Op::AddEmail { "add_email" } else { "remove_email" }; | |
| 3583 | 3607 | pass( | |
| 282 | 282 | "commit_email": "6c1d0efg+ada@users.noreply.g1t.sh", | |
| 283 | 283 | "limit": 10 | |
| 284 | 284 | }, | |
| 285 | − | "notes": "Answers `403` with code `reauth_required` when `password` is missing or wrong, and `409` when another account has confirmed the address. The new address stays unconfirmed until its link is followed." | |
| 285 | + | "notes": "Answers `403` with code `reauth_required` when `password` is missing or wrong, and `409` when another account has confirmed the address. g1t emails the address a six-digit code and a link; it stays unconfirmed until the code is sent to [`confirm_email`](/reference/api/accounts/confirm-email/) or the link is followed." | |
| 286 | + | }, | |
| 287 | + | "confirm_email": { | |
| 288 | + | "request": { | |
| 289 | + | "code": "482913" | |
| 290 | + | }, | |
| 291 | + | "response": { | |
| 292 | + | "username": "ada", | |
| 293 | + | "email": "ada@example.com", | |
| 294 | + | "verified": true, | |
| 295 | + | "joined": "acme", | |
| 296 | + | "invite_lapsed": null | |
| 297 | + | }, | |
| 298 | + | "notes": "The code confirms the address it was sent to, and ends the link sent with it. A wrong, used or expired code answers `422` with one message for all three; after ten wrong codes in an hour the account answers `409` for a while, and the link in the email still works. `verified` says whether the account is confirmed: whether its primary address is. `joined` is set when confirming a new account's address joined the workspace its invite named; when the invite was revoked or expired, or the workspace deleted, while the account waited, the address is confirmed all the same and `invite_lapsed` says so. See [Confirming your email address](/guides/authentication/#confirming-your-email-address)." | |
| 286 | 299 | }, | |
| 287 | 300 | "remove_email": { | |
| 288 | 301 | "request": { |
| 204 | 204 | Op::ListEmails | Op::AddEmail | Op::RemoveEmail | Op::UpdateEmailSettings => { | |
| 205 | 205 | through::<g1t_contracts::accounts::AccountEmails>(op, sent) | |
| 206 | 206 | } | |
| 207 | + | Op::ConfirmEmail => through::<g1t_contracts::accounts::EmailConfirmed>(op, sent), | |
| 207 | 208 | Op::ListInvites => through::<g1t_contracts::identity::InvitesOverview>(op, sent), | |
| 208 | 209 | Op::CreateInvite | Op::RevokeInvite | Op::InviteMember | Op::RevokeWorkspaceInvite => { | |
| 209 | 210 | through::<g1t_contracts::identity::Invite>(op, sent) |
| 44 | 44 | route("DELETE", "/workspaces/:workspace", Op::DeleteWorkspace, &[]), | |
| 45 | 45 | route("GET", "/user/emails", Op::ListEmails, &[]), | |
| 46 | 46 | route("POST", "/user/emails", Op::AddEmail, &[]), | |
| 47 | + | route("POST", "/user/emails/confirm", Op::ConfirmEmail, &[]), | |
| 47 | 48 | route("DELETE", "/user/emails/:email", Op::RemoveEmail, &[]), | |
| 48 | 49 | route("PATCH", "/user/email-settings", Op::UpdateEmailSettings, &[]), | |
| 49 | 50 | route("GET", "/user/invites", Op::ListInvites, &[]), |
| 500 | 500 | a("whoami", Op::Whoami, "Who the token acts as, and its workspaces"), | |
| 501 | 501 | a("list_emails", Op::ListEmails, "Your addresses"), | |
| 502 | 502 | a("add_email", Op::AddEmail, "Add an address"), | |
| 503 | + | a("confirm_email", Op::ConfirmEmail, "Confirm an address with the code from its email"), | |
| 503 | 504 | a("remove_email", Op::RemoveEmail, "Remove an address"), | |
| 504 | 505 | a("update_email_settings", Op::UpdateEmailSettings, "Primary, backup and privacy"), | |
| 505 | 506 | a("list_invites", Op::ListInvites, "Your invites to g1t"), |
| 11 | 11 | on the same page. Usernames are lowercase letters, digits and single | |
| 12 | 12 | hyphens, up to 39 characters. | |
| 13 | 13 | ||
| 14 | + | Before you can do anything else, you [confirm your email | |
| 15 | + | address](#confirming-your-email-address) with the code g1t emails you. | |
| 16 | + | ||
| 14 | 17 | Accounts can only be created in a browser. There is no API for it, by | |
| 15 | 18 | design: it keeps passwords out of scripts and agents, and lets g1t protect | |
| 16 | 19 | the one place accounts are made. | |
| ⋯ | |||
| 115 | 118 | - works once, for one new account; | |
| 116 | 119 | - works for 30 days; | |
| 117 | 120 | - when it was made for an email address, works only with that address; | |
| 118 | − | - can be revoked by whoever made it until it is used. | |
| 121 | + | - can be revoked by whoever made it until it is used, and after that until | |
| 122 | + | the new account [confirms its email address](#confirming-your-email-address). | |
| 119 | 123 | ||
| 120 | 124 | ### Using an invite | |
| 121 | 125 | ||
| ⋯ | |||
| 124 | 128 | repository, or just making an account), and finishes the job there: | |
| 125 | 129 | ||
| 126 | 130 | 1. **No account yet**: sign up on the page. When the invite was sent to | |
| 127 | − | your address, the email field is filled in and locked, and the address | |
| 128 | − | is confirmed already, so no confirmation email follows. Choose a | |
| 129 | − | username (one is suggested from your address) and a password, or select | |
| 131 | + | your address, the email field is filled in and locked. Choose a | |
| 132 | + | username (one is suggested from your address) and a password, then | |
| 133 | + | [confirm the address](#confirming-your-email-address) with the code g1t | |
| 134 | + | emails it, even though the invite came there: an invite link can be | |
| 135 | + | forwarded, so it does not prove the inbox is yours. Or select | |
| 130 | 136 | **Continue with GitHub**: the invite rides along, and the account uses | |
| 131 | − | the invited address when GitHub has verified it too. | |
| 137 | + | the invited address when GitHub has verified it too, in which case no | |
| 138 | + | confirmation is needed. | |
| 132 | 139 | 2. **The address already has an account**: select **Sign in to accept**. | |
| 133 | 140 | After you sign in, the invite is accepted for you. | |
| 134 | 141 | 3. **Signed in as someone else**: an invite sent to one address works only | |
| 135 | 142 | for an account that has confirmed that address. The page says so and | |
| 136 | 143 | offers **Sign out and continue**. | |
| 137 | 144 | ||
| 138 | − | Once the account exists or you have signed in, you land in the workspace | |
| 139 | − | (or the repository) the invite was for, already a member, with a one-time | |
| 140 | − | "You're in" banner, and it becomes the workspace your sidebar shows. A | |
| 145 | + | Once you have signed in, or your new account has confirmed its address, | |
| 146 | + | you land in the workspace (or the repository) the invite was for, already | |
| 147 | + | a member, with a one-time "You're in" banner, and it becomes the workspace | |
| 148 | + | your sidebar shows. | |
| 149 | + | ||
| 150 | + | Signing up spends the invite at once, so nobody else can use it while you | |
| 151 | + | confirm your address, but you join its workspace only when you confirm, in | |
| 152 | + | the same step. Until then the invite shows as **confirming their email** to | |
| 153 | + | whoever made it, and they can still revoke it. If the invite is revoked or | |
| 154 | + | expires, or its workspace is deleted, before you confirm, your address is | |
| 155 | + | confirmed all the same and g1t tells you the invite no longer applies: ask | |
| 156 | + | whoever invited you to add you again. A | |
| 141 | 157 | code typed at [g1t.sh/register](https://g1t.sh/register) goes to the | |
| 142 | 158 | same page. | |
| 143 | 159 | ||
| ⋯ | |||
| 155 | 171 | Each person can have **5** invites out at a time. Pending and used invites | |
| 156 | 172 | count; an invite you revoke, or one that expires before anyone uses it, | |
| 157 | 173 | comes back to you. The list under the form shows each invite's state: | |
| 158 | − | pending, joined (with the username of who joined), expired or revoked. You | |
| 174 | + | pending, confirming their email (used to sign up by someone who has not | |
| 175 | + | confirmed their address yet), joined (with the username of who joined), | |
| 176 | + | expired or revoked. You | |
| 159 | 177 | must confirm your email before you can make invites. An agent's token and | |
| 160 | 178 | a workspace's token cannot make them. | |
| 161 | 179 | ||
| ⋯ | |||
| 163 | 181 | ||
| 164 | 182 | An owner can invite an email address straight into a workspace from its | |
| 165 | 183 | People page; see [members and roles](/guides/workspaces/#members-and-roles). | |
| 166 | − | When the address has no g1t account, accepting makes the account and joins | |
| 167 | − | the workspace in one step, and it uses one invite. Inviting someone who is | |
| 184 | + | When the address has no g1t account, the invite makes the account, which | |
| 185 | + | joins the workspace once it confirms its email address, and it uses one | |
| 186 | + | invite. Inviting someone who is | |
| 168 | 187 | already on g1t costs nothing. | |
| 169 | 188 | ||
| 170 | 189 | ### Need more invites? | |
| ⋯ | |||
| 193 | 212 | | --- | --- | --- | | |
| 194 | 213 | | [`GET /user/invites`](/reference/api/invites/list-invites/) | `account` `list_invites` | Your invites and how many you have left | | |
| 195 | 214 | | [`POST /user/invites`](/reference/api/invites/create-invite/) | `account` `create_invite` | Make an invite, optionally for one `email` | | |
| 196 | − | | [`DELETE /user/invites/{id}`](/reference/api/invites/revoke-invite/) | `account` `revoke_invite` | Revoke a pending invite | | |
| 215 | + | | [`DELETE /user/invites/{id}`](/reference/api/invites/revoke-invite/) | `account` `revoke_invite` | Revoke a pending invite, or one whose new account has not confirmed its address | | |
| 197 | 216 | | [`POST /workspaces/{workspace}/invitations`](/reference/api/invites/invite-member/) | `workspace` `invite_member` | Invite an address into a workspace. Owners only. | | |
| 198 | 217 | ||
| 199 | − | ## Confirming your email | |
| 218 | + | ## Confirming your email address | |
| 219 | + | ||
| 220 | + | A new account confirms its email address before it can do anything else on | |
| 221 | + | g1t. Right after you sign up, g1t emails the address from `noreply@g1t.sh` | |
| 222 | + | with two ways to confirm it, either one enough: | |
| 223 | + | ||
| 224 | + | - a **six-digit code**, shown large in the email (and in its subject, so a | |
| 225 | + | phone's notification shows it). Type it on the **Confirm your email** | |
| 226 | + | page g1t takes you to. On a phone, the keyboard offers it from the | |
| 227 | + | message. | |
| 228 | + | - a **link**, for when you would rather click than type. It works whether | |
| 229 | + | or not you are signed in, in any browser. | |
| 230 | + | ||
| 231 | + | The code and the link work for **60 minutes**, once. Using either ends the | |
| 232 | + | other. **Send a new code** on the confirmation page sends a fresh code and | |
| 233 | + | link, at most once a minute and 10 times an hour, and the ones before stop | |
| 234 | + | working. | |
| 235 | + | ||
| 236 | + | | On the confirmation page | What it does | | |
| 237 | + | | --- | --- | | |
| 238 | + | | **Confirm email** | Checks the code. A wrong, used or expired code gets the same answer. After 10 wrong codes in an hour, codes for the account are not checked for a while (a minute, then longer); the link in the email still works. Wrong codes from one network are limited the same way. | | |
| 239 | + | | **Send a new code** | A new code and link; the ones before stop working. | | |
| 240 | + | | **Wrong address? Change it** | Replaces the address you signed up with and sends the new one a code. Only while the account has no confirmed address. | | |
| 241 | + | | **Sign out** | Signs out. Sign in again to come back to the page. | | |
| 242 | + | ||
| 243 | + | ### Until you confirm | |
| 244 | + | ||
| 245 | + | An account that has not confirmed its address can only confirm it: | |
| 246 | + | ||
| 247 | + | - **The site** sends every page to the confirmation page, and back to where | |
| 248 | + | you were going once you confirm. Signing in and out, password resets, | |
| 249 | + | the confirmation link, and g1t's [policies](https://g1t.sh/policies), | |
| 250 | + | security, support, status and pricing pages stay open. | |
| 251 | + | - **The API** answers `403` with a message saying to confirm your address, | |
| 252 | + | except for [`GET /user`](/reference/api/accounts/whoami/), | |
| 253 | + | [`GET /user/emails`](/reference/api/accounts/list-emails/) and | |
| 254 | + | [`POST /user/emails/confirm`](/reference/api/accounts/confirm-email/). | |
| 255 | + | - **The MCP server** answers `403` with the same message. | |
| 256 | + | - **Git** over HTTPS refuses pushes and fetches with your credentials, with | |
| 257 | + | the same message. Package registries treat them as wrong credentials. | |
| 258 | + | - You cannot create a workspace, join the one your invite named, make | |
| 259 | + | invites or tokens, or approve a tool's sign-in. | |
| 200 | 260 | ||
| 201 | − | g1t sends a confirmation link from `noreply@g1t.sh`. It works for 24 hours. | |
| 261 | + | You cannot make a token before you confirm, so the API and MCP refusals | |
| 262 | + | matter only for an account that made one before this rule existed. | |
| 202 | 263 | ||
| 203 | − | Until you follow it you can sign in and look around, but you cannot create | |
| 204 | − | repositories, push, or open issues and pull requests. Those requests fail with `403` and a | |
| 205 | − | message telling you to confirm your address. To get a new link, sign in and | |
| 206 | − | use the banner at the top of the site. | |
| 264 | + | ### Addresses GitHub has confirmed | |
| 207 | 265 | ||
| 266 | + | An account made with **Continue with GitHub** starts confirmed: its address | |
| 267 | + | is one GitHub has verified, so GitHub has already proved the inbox is | |
| 268 | + | yours, and no code is sent. | |
| 269 | + | ||
| 270 | + | ### Accounts that never confirmed | |
| 271 | + | ||
| 272 | + | Accounts are confirmed once and stay confirmed. An account made before this | |
| 273 | + | rule that never confirmed its address, or whose address another account | |
| 274 | + | confirmed first, is held at the confirmation page the same way the next | |
| 275 | + | time it signs in; **Send a new code** gets it a code, and **Change it** | |
| 276 | + | gives it a new address. | |
| 277 | + | ||
| 278 | + | ### Confirming through the API | |
| 279 | + | ||
| 280 | + | | Route | MCP tool and action | What it does | | |
| 281 | + | | --- | --- | --- | | |
| 282 | + | | [`POST /user/emails/confirm`](/reference/api/accounts/confirm-email/) | `account` `confirm_email` | Confirm an address with the `code` from its email | | |
| 283 | + | ||
| 284 | + | The answer says whether the account is now confirmed (`verified`), the | |
| 285 | + | workspace confirming joined it to (`joined`), or why its invite no longer | |
| 286 | + | applies (`invite_lapsed`). | |
| 287 | + | ||
| 208 | 288 | ## Email addresses | |
| 209 | 289 | ||
| 210 | 290 | An account can have up to 10 email addresses. Manage them in | |
| ⋯ | |||
| 215 | 295 | | Primary | Get account mail and password reset links. Exactly one, always confirmed once any address is. | | |
| 216 | 296 | | Confirmed | Sign you in (type it instead of your username), ask for a password reset, and mark commits that carry it as yours. | | |
| 217 | 297 | | Backup | Get security notices as well as the primary. Optional, and a confirmed address other than the primary. | | |
| 218 | − | | Unconfirmed | Nothing yet. It is not yours until you follow the link g1t sent it. | | |
| 298 | + | | Unconfirmed | Nothing yet. It is not yours until you enter the code or follow the link g1t sent it. | | |
| 219 | 299 | ||
| 220 | 300 | A confirmed address belongs to one account. Anyone can add an address they | |
| 221 | − | have not confirmed; the first account to follow its link keeps it, and the | |
| 301 | + | have not confirmed; the first account to confirm it keeps it, and the | |
| 222 | 302 | address leaves every other account that added it. An address another | |
| 223 | 303 | account has confirmed cannot be added. | |
| 224 | 304 | ||
| ⋯ | |||
| 226 | 306 | ||
| 227 | 307 | 1. Open [Settings → Emails](https://g1t.sh/settings/emails). | |
| 228 | 308 | 2. Enter the address under **Add an email address** and select **Add**. | |
| 229 | − | 3. Follow the link g1t sends it. The link works for 24 hours; **Resend | |
| 230 | − | link** sends a new one, at most once a minute and 10 times an hour. | |
| 309 | + | 3. Enter the code g1t emails it, or follow the link in the same email. | |
| 310 | + | Both work for 60 minutes; **Resend link** sends a new code and link, at | |
| 311 | + | most once a minute and 10 times an hour, and ends the ones before. | |
| 231 | 312 | ||
| 232 | 313 | If your account had no confirmed address yet, the first one you confirm | |
| 233 | 314 | becomes your primary. | |
| ⋯ | |||
| 312 | 393 | | --- | --- | --- | | |
| 313 | 394 | | [`GET /user/emails`](/reference/api/accounts/list-emails/) | `account` `list_emails` | Your addresses and email settings | | |
| 314 | 395 | | [`POST /user/emails`](/reference/api/accounts/add-email/) | `account` `add_email` | Add an address; takes `email` and `password` | | |
| 396 | + | | [`POST /user/emails/confirm`](/reference/api/accounts/confirm-email/) | `account` `confirm_email` | Confirm an address with the `code` from its email | | |
| 315 | 397 | | [`DELETE /user/emails/{email}`](/reference/api/accounts/remove-email/) | `account` `remove_email` | Remove an address; takes `password` | | |
| 316 | 398 | | [`PATCH /user/email-settings`](/reference/api/accounts/update-email-settings/) | `account` `update_email_settings` | Change `primary`, `backup`, `private_email` or `block_private_pushes` | | |
| 317 | 399 | ||
| 62 | 62 | 3. Open [http://localhost:8787](http://localhost:8787) and create an | |
| 63 | 63 | account. | |
| 64 | 64 | 4. Open the Mailpit inbox at [http://localhost:8025](http://localhost:8025) | |
| 65 | − | and follow the link in the confirmation email. | |
| 65 | + | and enter the code from the confirmation email on the page the site | |
| 66 | + | shows you, or follow the link in the same email. Set `IDENTITY_KEY` | |
| 67 | + | (the setup does) so codes are kept as keyed hashes. | |
| 66 | 68 | 5. Create a workspace, then a repository. | |
| 67 | 69 | ||
| 68 | 70 | ## Push and clone |
| 332 | 332 | never says which it was. | |
| 333 | 333 | ||
| 334 | 334 | The email names you and the workspace and links to the invite's page. | |
| 335 | − | Someone new signs up right there, with the invited address filled in and | |
| 336 | − | already confirmed; someone with an account signs in. Either way they land | |
| 337 | − | in the workspace as a member, with a one-time welcome. See | |
| 335 | + | Someone new signs up right there, with the invited address filled in, and | |
| 336 | + | joins once they confirm it with the code g1t emails them; someone with an | |
| 337 | + | account signs in. Either way they land in the workspace as a member, with | |
| 338 | + | a one-time welcome. Until a new account confirms its address, its invite | |
| 339 | + | shows as **confirming their email** under the members, and you can still | |
| 340 | + | revoke it. See | |
| 338 | 341 | [using an invite](/guides/authentication/#using-an-invite). | |
| 339 | 342 | ||
| 340 | 343 | Pending invites are listed under the members, with a link to copy and |
| 23 | 23 | ||
| 24 | 24 | [Sign up](https://g1t.sh/register) with your invite (g1t is invite-only | |
| 25 | 25 | for now; see [invites](/guides/authentication/#invites)) and confirm your | |
| 26 | − | email. Then create a | |
| 26 | + | email with the code g1t sends it (or the link in the same email). Then create a | |
| 27 | 27 | **workspace**: it owns repositories and is the first part of their | |
| 28 | 28 | address, `g1t.sh/<workspace>/<repo>`. See [workspaces](/guides/workspaces/). | |
| 29 | 29 |
| 735 | 735 | invites while g1t is [invite-only](/guides/authentication/#invites), and | |
| 736 | 736 | invitations to repositories waiting for you. `whoami` is the default | |
| 737 | 737 | action, and needs no scope. An agent's token and a workspace's token cannot | |
| 738 | − | use the email and invite actions. | |
| 738 | + | use the email and invite actions. An account that has not confirmed its | |
| 739 | + | email address gets `403` from every tool until it does; see | |
| 740 | + | [until you confirm](/guides/authentication/#until-you-confirm). | |
| 739 | 741 | ||
| 740 | 742 | | Action | What it does | Required | Scope | | |
| 741 | 743 | | --- | --- | --- | --- | | |
| 742 | 744 | | [`whoami`](/reference/api/accounts/whoami/) | Who the access token acts as, and the workspaces it can work in. `kind` is `user`, `workspace` or `agent`. | None | None | | |
| 743 | 745 | | [`list_emails`](/reference/api/accounts/list-emails/) | Your email addresses and email settings. People only. | None | `account:read` | | |
| 744 | − | | [`add_email`](/reference/api/accounts/add-email/) | Add an address; g1t emails it a link to confirm it. | `email`, `password` | `account:write` | | |
| 746 | + | | [`add_email`](/reference/api/accounts/add-email/) | Add an address; g1t emails it a code and a link to confirm it. | `email`, `password` | `account:write` | | |
| 747 | + | | [`confirm_email`](/reference/api/accounts/confirm-email/) | Confirm an address with the six-digit code from its email. See [confirming your email address](/guides/authentication/#confirming-your-email-address). | `code` | `account:write` | | |
| 745 | 748 | | [`remove_email`](/reference/api/accounts/remove-email/) | Remove an address; never the primary or the last confirmed one. | `email`, `password` | `account:write` | | |
| 746 | 749 | | [`update_email_settings`](/reference/api/accounts/update-email-settings/) | Change `primary` or `backup` (with `password`), `private_email` or `block_private_pushes`. See [email addresses](/guides/authentication/#email-addresses). | None | `account:write` | | |
| 747 | 750 | | [`list_invites`](/reference/api/invites/list-invites/) | Your invites, newest first, and how many you have left. | None | `account:read` | |
Binary or large file; its contents are not shown.
| 137 | 137 | switch (invite.status) { | |
| 138 | 138 | case "pending": | |
| 139 | 139 | return <Badge tone="lavender">Pending</Badge>; | |
| 140 | + | case "awaiting_confirmation": | |
| 141 | + | return <Badge tone="lavender">Used{invite.redeemedBy ? ` by ${invite.redeemedBy}` : ""}, email not confirmed</Badge>; | |
| 140 | 142 | case "redeemed": | |
| 141 | 143 | return <Badge tone="mint">Used{invite.redeemedBy ? ` by ${invite.redeemedBy}` : ""}</Badge>; | |
| 142 | 144 | case "expired": |
| 19 | 19 | <div className="flex flex-wrap items-center gap-x-3 gap-y-1"> | |
| 20 | 20 | <span className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs ${TONE[state.tone]}`}>{state.label}</span> | |
| 21 | 21 | <span className="min-w-0 truncate text-sm">{inviteFor(invite)}</span> | |
| 22 | − | {invite.status === "pending" && ( | |
| 22 | + | {(invite.status === "pending" || invite.status === "awaiting_confirmation") && ( | |
| 23 | 23 | <Form method="post" className="ml-auto"> | |
| 24 | 24 | <input type="hidden" name="intent" value="revoke-invite" /> | |
| 25 | 25 | <input type="hidden" name="id" value={invite.id} /> |
| 25 | 25 | }); | |
| 26 | 26 | ||
| 27 | 27 | test("a visitor gets the marketing frame on the front, pricing and sign-in pages", () => { | |
| 28 | − | for (const path of ["/", "/pricing", "/pricing/", "/login", "/register", "/verify", "/forgot", "/reset", "/device", "/oauth/authorize"]) { | |
| 28 | + | for (const path of ["/", "/pricing", "/pricing/", "/login", "/register", "/verify", "/confirm-email", "/forgot", "/reset", "/device", "/oauth/authorize"]) { | |
| 29 | 29 | assert.equal(usesAppShell(path, false), false, path); | |
| 30 | 30 | } | |
| 31 | 31 | }); |
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
This change is too large to show in full.