ask for the URL first, and offer what tea already knows

The form asked for a URL and a token together, on a fresh instance, while the
machine in front of it often had both on disk already — somebody had run `tea
login` by hand.

Two changes.

One thing at a time: until the platform knows where Gitea is, a token cannot be
checked, because there is nothing to check it against. So the owner names the
instance first, and only then does the page ask for — or discover — a token.
That step now probes /api/v1/version unauthenticated, since demanding a token
to validate a URL refuses a good URL for the wrong reason and sends whoever
typed it looking for a credential problem.

And it reads tea's own config when the platform has no instance yet, offering
the URL it finds. A machine that already has a working login should not be
asked to retype what it can see.

Members skip the first step entirely — the instance is the owner's.
This commit is contained in:
officer
2026-08-19 10:25:22 +00:00
parent 862512d83a
commit d4efc52447
4 changed files with 102 additions and 23 deletions
+15 -4
View File
@@ -16,9 +16,10 @@ import {
invalidateAllGiteaConfigs,
normalizeBase,
probe,
reachable,
UnreadableCredentials,
} from './upstream';
import { readTeaLogin, writeTeaLogin, osUserFor, testTeaLogin } from './tea-config';
import { readTeaLogin, readAnyTeaLogin, writeTeaLogin, osUserFor, testTeaLogin } from './tea-config';
import { API_URL } from '@@/officer-url.mjs';
// The officer-gitea sidecar. Owns the whole Gitea contract: the instance URL and the personal access
@@ -136,7 +137,10 @@ async function connectionState(userId: number): Promise<Response> {
//
// Read rather than asked for: on a machine where the owner and their members already ran `tea login`
// by hand, the settings page should say "connected" instead of demanding something they have.
const tea = instanceUrl ? await readTeaLogin(userId, instanceUrl) : null;
// With an instance known, ask about THAT one. Without, offer whatever tea already has — a machine
// where somebody ran `tea login` by hand knows the URL and holds a working token, and asking them to
// retype it is asking for something the box can already see.
const tea = instanceUrl ? await readTeaLogin(userId, instanceUrl) : await readAnyTeaLogin(userId);
const osUser = await osUserFor(userId);
return Response.json({
@@ -146,7 +150,8 @@ async function connectionState(userId: number): Promise<Response> {
instanceConfigured: !!instanceUrl,
isOwner: await isOwner(userId),
// What tea already knows, so the page can say who you are without a round trip to Gitea.
tea: tea ? { name: tea.name, user: tea.user, hasToken: tea.hasToken } : null,
// `url` included so the form can offer it when the platform has no instance yet.
tea: tea ? { name: tea.name, url: tea.url, user: tea.user, hasToken: tea.hasToken } : null,
// Null means never provisioned an OS account, so there is nowhere to write a config. The page says
// so rather than failing on save.
canStoreToken: !!osUser || (await isOwner(userId)),
@@ -199,8 +204,14 @@ async function handleConfig(req: Request, userId: number): Promise<Response> {
// fails with the reason. Storing first and discovering later makes every downstream screen fail
// mysteriously instead. For a member this also proves their token is valid against the shared
// instance, which is the only thing that can be wrong on their side.
// Setting the INSTANCE is its own step, and it must not demand a token: the owner names where Gitea
// is first, and only then does the page ask for — or discover — a token for it. Probing with one
// when there is none would refuse a perfectly good URL for the wrong reason.
const current = token ? null : await getGiteaConfig(userId);
const result = await probe({ id: existing?.id ?? 0, base, token: token ?? current?.token ?? null });
const candidate = token ?? current?.token ?? null;
const result = candidate
? await probe({ id: existing?.id ?? 0, base, token: candidate })
: await reachable(base);
if (!result.ok) return bad(result.error ?? 'could not reach the instance');
// THE TOKEN GOES TO `tea`, NOT THE DATABASE.