Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 1 | import { Info, Search, ShieldAlert, TriangleAlert } from "lucide-react"; |
| 2 | import { useMemo, useState, type ReactNode } from "react"; | |
| 3 | ||
| 4 | import { | |
| 5 | PRESETS, | |
| 6 | levelsOf, | |
| 7 | permissionsOf, | |
| 8 | presetScopes, | |
| 9 | type Permissions, | |
| 10 | type PresetId, | |
| 11 | type ScopeLevel, | |
| 12 | type ScopeResource, | |
| 13 | } from "@g1t/contracts/scopes"; | |
| 14 | import type { AccessToken, RepositorySelection, TokenPolicy } from "@g1t/contracts"; | |
| 15 | ||
| 16 | import { cn } from "../lib/cn"; | |
| 17 | import { | |
| 18 | ALL_WORKSPACES, | |
| 19 | DEFAULT_EXPIRY_DAYS, | |
| 20 | NO_WORKSPACE, | |
| 21 | describeDays, | |
| 22 | expiryChoices, | |
| 23 | isDangerousLevel, | |
| 24 | keptOutOfAll, | |
| 25 | levelAbout, | |
| 26 | levelLabel, | |
| 27 | permissionGroups, | |
| 28 | policyNote, | |
| 29 | tokenPermissions, | |
| 30 | } from "../lib/access-tokens"; | |
| Merge main into Artifacts Phase 2 | 31 | import { Checkbox, CheckboxOption } from "./ui/checkbox"; |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 32 | import { Hint } from "./ui/hint"; |
| 33 | import { RadioGroup, RadioOption } from "./ui/radio-group"; | |
| 34 | import { SelectField } from "./ui/select"; | |
| 35 | import { Field, Input, Textarea } from "./ui"; | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 36 | import { Button } from "./ui/button"; |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 37 | |
| 38 | // The one form for an access token, a person's or a workspace's: its name, | |
| 39 | // when it expires, where it reaches (the workspaces and repositories it is | |
| 40 | // made for) and its permissions, a level for each resource. Every control | |
| 41 | // is a form field (lib/access-tokens.ts reads them back), so it posts the | |
| 42 | // same without JavaScript; the script hides what does not apply. | |
| 43 | ||
| 44 | /** A workspace a personal token can be made for, with its rules and repositories. */ | |
| 45 | export type WorkspaceChoice = { | |
| 46 | slug: string; | |
| 47 | /** Whether you are an owner there: your tokens never wait for approval. */ | |
| 48 | owner: boolean; | |
| 49 | policy: TokenPolicy | null; | |
| 50 | /** Its repositories you can see, as `owner/name`. */ | |
| 51 | repos: string[]; | |
| 52 | }; | |
| 53 | ||
| 54 | function PermissionRow({ | |
| 55 | resource, | |
| 56 | label, | |
| 57 | value, | |
| 58 | onChange, | |
| 59 | }: { | |
| 60 | resource: ScopeResource; | |
| 61 | label: string; | |
| 62 | value: ScopeLevel | "none"; | |
| 63 | onChange: (value: ScopeLevel | "none") => void; | |
| 64 | }) { | |
| 65 | const id = `perm-${resource}`; | |
| 66 | const about = levelAbout(resource, value); | |
| 67 | const levels: (ScopeLevel | "none")[] = ["none", ...levelsOf(resource)]; | |
| 68 | return ( | |
| 69 | <div className="flex flex-col gap-2 py-2.5 sm:flex-row sm:items-start sm:justify-between sm:gap-6"> | |
| 70 | <div className="min-w-0"> | |
| 71 | <label htmlFor={id} className="text-sm font-medium text-fg"> | |
| 72 | {label} | |
| 73 | </label> | |
| 74 | <p className={cn("mt-0.5 text-xs leading-snug", isDangerousLevel(value) ? "text-danger" : "text-faint")}> | |
| 75 | {about ?? "No access."} | |
| 76 | </p> | |
| 77 | </div> | |
| 78 | <SelectField | |
| 79 | id={id} | |
| 80 | name={`perm.${resource}`} | |
| 81 | value={value} | |
| 82 | onValueChange={(next) => onChange(next as ScopeLevel | "none")} | |
| 83 | className={cn( | |
| 84 | "w-full shrink-0 sm:w-48", | |
| 85 | value !== "none" && (isDangerousLevel(value) ? "border-danger/50" : "border-accent/50"), | |
| 86 | )} | |
| 87 | options={levels.map((level) => ({ value: level, label: levelLabel(resource, level) }))} | |
| 88 | /> | |
| 89 | </div> | |
| 90 | ); | |
| 91 | } | |
| 92 | ||
| 93 | /** The repositories a token reaches in one workspace: all, the ones picked, or none of the private ones. */ | |
| 94 | function RepositoryChoice({ | |
| 95 | slug, | |
| 96 | repos, | |
| 97 | selection, | |
| 98 | onSelection, | |
| 99 | picked, | |
| 100 | onPicked, | |
| 101 | allowPublic, | |
| 102 | }: { | |
| 103 | slug: string; | |
| 104 | repos: string[]; | |
| 105 | selection: RepositorySelection; | |
| 106 | onSelection: (value: RepositorySelection) => void; | |
| 107 | picked: string[]; | |
| 108 | onPicked: (value: string[]) => void; | |
| 109 | allowPublic: boolean; | |
| 110 | }) { | |
| 111 | const [filter, setFilter] = useState(""); | |
| 112 | const shown = useMemo(() => { | |
| 113 | const query = filter.trim().toLowerCase(); | |
| 114 | return query ? repos.filter((repo) => repo.toLowerCase().includes(query)) : repos; | |
| 115 | }, [repos, filter]); | |
| 116 | return ( | |
| 117 | <fieldset className="space-y-3"> | |
| 118 | <legend className="text-sm font-medium text-fg">Repository access</legend> | |
| 119 | <RadioGroup | |
| 120 | name="repository_selection" | |
| 121 | value={selection} | |
| 122 | onValueChange={(value) => onSelection(value as RepositorySelection)} | |
| 123 | className="gap-2.5" | |
| 124 | > | |
| 125 | <RadioOption value="all" label="All repositories" description={`Every repository of ${slug}, ones made later too.`} /> | |
| 126 | <RadioOption value="selected" label="Only select repositories" description="Up to 50, chosen below." /> | |
| 127 | {allowPublic && ( | |
| 128 | <RadioOption | |
| 129 | value="public" | |
| 130 | label="No private repositories" | |
| 131 | description={`Public repositories, read-only, and ${slug}'s own settings its permissions allow.`} | |
| 132 | /> | |
| 133 | )} | |
| 134 | </RadioGroup> | |
| 135 | {selection === "selected" && ( | |
| 136 | <div className="rounded-md border border-line"> | |
| 137 | <div className="flex items-center gap-2 border-b border-line px-3 py-2"> | |
| 138 | <Search size={14} className="shrink-0 text-faint" /> | |
| 139 | <input | |
| 140 | type="search" | |
| 141 | value={filter} | |
| 142 | onChange={(event) => setFilter(event.target.value)} | |
| 143 | placeholder={`Find a repository of ${slug}`} | |
| 144 | aria-label="Find a repository" | |
| 145 | className="min-w-0 grow bg-transparent text-sm outline-none placeholder:text-faint" | |
| 146 | /> | |
| 147 | <span className="shrink-0 text-xs text-faint">{picked.length} chosen</span> | |
| 148 | </div> | |
| 149 | <div className="max-h-56 overflow-y-auto px-3 py-1.5"> | |
| 150 | {shown.length === 0 && <p className="py-2 text-xs text-faint">No repositories match.</p>} | |
| 151 | {shown.map((repo) => ( | |
| 152 | <label key={repo} className="flex cursor-pointer items-center gap-2.5 py-1 text-sm"> | |
| 153 | <Checkbox | |
| 154 | name="repo" | |
| 155 | value={repo} | |
| 156 | checked={picked.includes(repo)} | |
| 157 | onCheckedChange={(on) => onPicked(on === true ? [...picked, repo] : picked.filter((name) => name !== repo))} | |
| 158 | /> | |
| 159 | <span className="truncate font-mono text-[0.8125rem]">{repo}</span> | |
| 160 | </label> | |
| 161 | ))} | |
| 162 | </div> | |
| 163 | </div> | |
| 164 | )} | |
| 165 | </fieldset> | |
| 166 | ); | |
| 167 | } | |
| 168 | ||
| 169 | function Note({ tone, children }: { tone: "warn" | "danger" | "info"; children: ReactNode }) { | |
| 170 | return ( | |
| 171 | <p | |
| 172 | className={cn( | |
| 173 | "flex items-start gap-2 rounded-md border px-3 py-2 text-xs", | |
| 174 | tone === "danger" && "border-danger/40 bg-danger/5 text-danger", | |
| 175 | tone === "warn" && "border-warn/40 bg-warn/5 text-warn", | |
| 176 | tone === "info" && "border-line text-muted", | |
| 177 | )} | |
| 178 | > | |
| 179 | {tone === "info" ? <Info size={14} className="mt-px shrink-0" /> : <TriangleAlert size={14} className="mt-px shrink-0" />} | |
| 180 | <span>{children}</span> | |
| 181 | </p> | |
| 182 | ); | |
| 183 | } | |
| 184 | ||
| 185 | export function TokenForm({ | |
| 186 | workspaceOwned = false, | |
| 187 | slug, | |
| 188 | workspaces = [], | |
| 189 | repos = [], | |
| 190 | editing, | |
| 191 | preset = "read_only", | |
| 192 | }: { | |
| 193 | /** A workspace's own token, made by an owner: it reaches that workspace. */ | |
| 194 | workspaceOwned?: boolean; | |
| 195 | /** For a workspace's own token: the workspace. */ | |
| 196 | slug?: string; | |
| 197 | /** For a personal token: the workspaces you belong to. */ | |
| 198 | workspaces?: WorkspaceChoice[]; | |
| 199 | /** For a workspace's own token: the workspace's repositories. */ | |
| 200 | repos?: string[]; | |
| 201 | /** The token being changed: its reach's workspace and expiry stay. */ | |
| 202 | editing?: AccessToken; | |
| 203 | /** What a new token's permissions start as. */ | |
| 204 | preset?: PresetId; | |
| 205 | }) { | |
| 206 | // Where it reaches. A personal token: every workspace (*), none (-), or one. | |
| 207 | const initialReach = editing | |
| 208 | ? editing.workspace ?? (editing.repositorySelection === "public" ? NO_WORKSPACE : ALL_WORKSPACES) | |
| 209 | : ALL_WORKSPACES; | |
| 210 | const [reach, setReach] = useState<string>(initialReach); | |
| 211 | const one = workspaceOwned ? (slug ?? "") : reach !== ALL_WORKSPACES && reach !== NO_WORKSPACE ? reach : null; | |
| 212 | const chosen = workspaces.find((choice) => choice.slug === one) ?? null; | |
| 213 | const [selection, setSelection] = useState<RepositorySelection>(editing?.repositorySelection ?? "all"); | |
| 214 | const [picked, setPicked] = useState<string[]>(editing?.repositories ?? []); | |
| 215 | const [levels, setLevels] = useState<Permissions>(() => | |
| 216 | editing ? tokenPermissions(editing) : permissionsOf(presetScopes(preset) ?? null), | |
| 217 | ); | |
| Merge main into Artifacts Phase 2 | 218 | const [website, setWebsite] = useState<boolean>(editing?.website ?? false); |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 219 | |
| 220 | // The rules of the workspaces it would reach decide how long it may last. | |
| 221 | const reached = workspaceOwned ? [] : one ? [chosen] : reach === ALL_WORKSPACES ? workspaces : []; | |
| 222 | const choices = expiryChoices(reached.map((choice) => choice?.policy ?? null)); | |
| 223 | const [expires, setExpires] = useState<string>( | |
| 224 | choices.days.includes(DEFAULT_EXPIRY_DAYS) ? String(DEFAULT_EXPIRY_DAYS) : String(choices.days[choices.days.length - 1]), | |
| 225 | ); | |
| 226 | const expiryOptions = [ | |
| 227 | ...choices.days.map((days) => ({ value: String(days), label: describeDays(days) })), | |
| 228 | ...(choices.never ? [{ value: "never", label: "No expiration" }] : []), | |
| 229 | ]; | |
| 230 | const expiresShown = expiryOptions.some((option) => option.value === expires) ? expires : expiryOptions[expiryOptions.length - 1]!.value; | |
| 231 | ||
| 232 | const groups = permissionGroups(workspaceOwned); | |
| 233 | const given = groups.flatMap((group) => group.resources).filter(({ resource }) => levels[resource]).length; | |
| 234 | const note = one && !workspaceOwned ? policyNote(one, chosen?.policy, chosen?.owner ?? false) : null; | |
| 235 | const blocked = Boolean(one && !workspaceOwned && chosen?.policy && !chosen.policy.allowTokensForThisWorkspace); | |
| 236 | const keptOut = !workspaceOwned && reach === ALL_WORKSPACES ? keptOutOfAll(workspaces) : []; | |
| 237 | const choosePreset = (id: PresetId) => setLevels(permissionsOf(presetScopes(id))); | |
| 238 | ||
| 239 | return ( | |
| 240 | <div className="space-y-8"> | |
| 241 | <section className="space-y-4"> | |
| 242 | <Field label="Token name" hint="Name it after what will use it."> | |
| 243 | <Input name="name" maxLength={100} placeholder="release bot" defaultValue={editing?.name} required={!editing} /> | |
| 244 | </Field> | |
| 245 | <Field label="Description" hint="Optional. What it is for, for whoever reviews it."> | |
| 246 | <Textarea name="description" rows={2} maxLength={500} defaultValue={editing?.description ?? ""} /> | |
| 247 | </Field> | |
| 248 | {!editing && ( | |
| 249 | <div className="flex flex-col gap-1.5 sm:max-w-xs"> | |
| 250 | <label htmlFor="token-expires" className="text-sm font-medium text-muted"> | |
| 251 | Expiration | |
| 252 | </label> | |
| 253 | <SelectField id="token-expires" name="expires" value={expiresShown} onValueChange={setExpires} options={expiryOptions} /> | |
| 254 | {expiresShown === "never" ? ( | |
| 255 | <p className="flex items-start gap-1.5 text-xs text-warn"> | |
| 256 | <TriangleAlert size={13} className="mt-px shrink-0" /> | |
| 257 | It works until someone deletes it. Prefer an expiration. | |
| 258 | </p> | |
| 259 | ) : ( | |
| 260 | !choices.never && <p className="text-xs text-faint">The workspaces it reaches allow at most {describeDays(choices.days[choices.days.length - 1]!)}.</p> | |
| 261 | )} | |
| 262 | </div> | |
| 263 | )} | |
| 264 | </section> | |
| 265 | ||
| 266 | <section className="space-y-4"> | |
| 267 | <div> | |
| 268 | <h3 className="text-sm font-medium text-fg">Where it reaches</h3> | |
| 269 | <p className="mt-0.5 text-xs text-faint"> | |
| 270 | {workspaceOwned | |
| 271 | ? `It acts as ${slug}, in ${slug} only, and keeps working when the person who made it leaves.` | |
| 272 | : "It never reaches more than you can; this narrows it further."} | |
| 273 | </p> | |
| 274 | </div> | |
| 275 | {!workspaceOwned && | |
| 276 | (editing ? ( | |
| 277 | <p className="rounded-md border border-line px-3 py-2 text-sm"> | |
| 278 | {reach === ALL_WORKSPACES | |
| 279 | ? "All your workspaces" | |
| 280 | : reach === NO_WORKSPACE | |
| 281 | ? "No workspace: your account and public repositories" | |
| 282 | : <span className="font-mono">{reach}</span>} | |
| 283 | <input type="hidden" name="workspace" value={reach} /> | |
| 284 | </p> | |
| 285 | ) : ( | |
| 286 | <div className="flex flex-col gap-1.5"> | |
| 287 | <label htmlFor="token-workspace" className="text-sm font-medium text-muted"> | |
| 288 | Workspaces | |
| 289 | </label> | |
| 290 | <SelectField | |
| 291 | id="token-workspace" | |
| 292 | name="workspace" | |
| 293 | value={reach} | |
| 294 | onValueChange={setReach} | |
| 295 | options={[ | |
| 296 | { value: ALL_WORKSPACES, label: "All your workspaces", description: "Every workspace you belong to, now and later." }, | |
| 297 | ...workspaces.map((choice) => ({ | |
| 298 | value: choice.slug, | |
| 299 | label: choice.slug, | |
| 300 | disabled: choice.policy?.allowTokensForThisWorkspace === false, | |
| 301 | description: choice.policy?.allowTokensForThisWorkspace === false ? "Does not allow tokens made for it" : "This workspace only", | |
| 302 | })), | |
| 303 | { value: NO_WORKSPACE, label: "No workspace", description: "Your account and public repositories only." }, | |
| 304 | ]} | |
| 305 | /> | |
| 306 | </div> | |
| 307 | ))} | |
| 308 | {keptOut.length > 0 && ( | |
| 309 | <Note tone="warn"> | |
| 310 | {keptOut.join(", ")} {keptOut.length === 1 ? "keeps" : "keep"} out tokens made for all of a member's workspaces: make a token for{" "} | |
| 311 | {keptOut.length === 1 ? "it" : "each"} alone to reach {keptOut.length === 1 ? "it" : "them"}. | |
| 312 | </Note> | |
| 313 | )} | |
| 314 | {note && <Note tone={blocked ? "danger" : "warn"}>{note}</Note>} | |
| 315 | {one && ( | |
| 316 | <RepositoryChoice | |
| 317 | slug={one} | |
| 318 | repos={workspaceOwned ? repos : (chosen?.repos ?? editing?.repositories ?? [])} | |
| 319 | selection={selection} | |
| 320 | onSelection={setSelection} | |
| 321 | picked={picked} | |
| 322 | onPicked={setPicked} | |
| 323 | allowPublic={!workspaceOwned} | |
| 324 | /> | |
| 325 | )} | |
| 326 | </section> | |
| 327 | ||
| 328 | <section className="space-y-4"> | |
| 329 | <div className="flex flex-wrap items-center gap-1.5"> | |
| 330 | <h3 className="mr-1 text-sm font-medium text-fg">Permissions</h3> | |
| 331 | {PRESETS.filter((option) => option.id !== "full").map((option) => ( | |
| 332 | <Hint key={option.id} label={option.description}> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 333 | <Button |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 334 | type="button" |
| 335 | onClick={() => choosePreset(option.id)} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 336 | variant="outline" |
| 337 | size="inline" | |
| 338 | className="rounded-full px-2.5 py-0.5 text-xs text-muted" | |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 339 | > |
| 340 | {option.label} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 341 | </Button> |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 342 | </Hint> |
| 343 | ))} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 344 | <Button |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 345 | type="button" |
| 346 | onClick={() => setLevels({})} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 347 | variant="outline" |
| 348 | size="inline" | |
| 349 | className="rounded-full px-2.5 py-0.5 text-xs text-muted font-normal" | |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 350 | > |
| 351 | Clear | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 352 | </Button> |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 353 | <span className="ml-auto text-xs text-faint">{given === 1 ? "1 permission" : `${given} permissions`}</span> |
| 354 | </div> | |
| 355 | {groups.map((group) => ( | |
| 356 | <section key={group.group} aria-labelledby={`group-${group.group}`} className="rounded-md border border-line"> | |
| 357 | <header className="border-b border-line px-3 py-2 sm:px-4"> | |
| 358 | <h4 id={`group-${group.group}`} className="text-xs font-medium text-muted"> | |
| 359 | {group.label} | |
| 360 | </h4> | |
| 361 | <p className="text-xs text-faint">{group.about}</p> | |
| 362 | </header> | |
| 363 | <div className="divide-y divide-line px-3 sm:px-4"> | |
| 364 | {group.resources.map(({ resource, label }) => ( | |
| 365 | <PermissionRow | |
| 366 | key={resource} | |
| 367 | resource={resource} | |
| 368 | label={label} | |
| 369 | value={levels[resource] ?? "none"} | |
| 370 | onChange={(value) => | |
| 371 | setLevels((now) => { | |
| 372 | const next = { ...now }; | |
| 373 | if (value === "none") delete next[resource]; | |
| 374 | else next[resource] = value; | |
| 375 | return next; | |
| 376 | }) | |
| 377 | } | |
| 378 | /> | |
| 379 | ))} | |
| 380 | </div> | |
| 381 | </section> | |
| 382 | ))} | |
| 383 | {levels.workflow_files === "write" && ( | |
| 384 | <Note tone="warn"> | |
| 385 | Workflow files: write lets this token add and change workflow files, which run with the repository's secrets. | |
| 386 | </Note> | |
| 387 | )} | |
| 388 | {workspaceOwned && levels.repo === "admin" && ( | |
| 389 | <p className="flex items-start gap-2 rounded-md border border-danger/40 bg-danger/5 px-3 py-2 text-xs text-danger"> | |
| 390 | <ShieldAlert size={14} className="mt-px shrink-0" /> | |
| 391 | Repositories: admin makes this token an admin of {slug}'s repositories: it can rename, archive and delete them | |
| 392 | and change who has access, as far as its other permissions allow. Without it, it has Write, as a member does. | |
| 393 | </p> | |
| 394 | )} | |
| 395 | </section> | |
| Merge main into Artifacts Phase 2 | 396 | |
| 397 | {!workspaceOwned && ( | |
| 398 | <section className="space-y-3"> | |
| 399 | <div> | |
| 400 | <h3 className="text-sm font-medium text-fg">Website</h3> | |
| 401 | <p className="mt-0.5 text-xs text-faint">For automation that drives a browser, such as end-to-end tests.</p> | |
| 402 | </div> | |
| 403 | <CheckboxOption | |
| 404 | id="token-website" | |
| 405 | name="website" | |
| 406 | value="on" | |
| 407 | checked={website} | |
| 408 | onCheckedChange={(on) => setWebsite(on === true)} | |
| 409 | className="rounded-md border border-line px-3 py-2.5" | |
| 410 | labelClassName="font-medium" | |
| 411 | label="Use the website as you" | |
| 412 | description={ | |
| 413 | <> | |
| 414 | Sent as <code className="font-mono">Authorization: Bearer</code> on each request, it signs g1t.sh in as you | |
| 415 | without a password or a two-factor code. Tokens, two-factor authentication, your password, email addresses, | |
| 416 | keys, deleting your account or a workspace, giving a workspace away and payment methods still need you to sign | |
| 417 | in. | |
| 418 | </> | |
| 419 | } | |
| 420 | /> | |
| 421 | {website && ( | |
| 422 | <Note tone="warn"> | |
| 423 | The website does not hold this token to its permissions above: treat it as able to do anything you can in | |
| 424 | the workspaces it reaches. Keep it as safe as your password, and give it an expiration. | |
| 425 | </Note> | |
| 426 | )} | |
| 427 | </section> | |
| 428 | )} | |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 429 | </div> |
| 430 | ); | |
| 431 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.