gitea: clear every cached config when the owner's row changes
the config cache is keyed by user, but a member's entry holds the owner's base — resolved at read time, since a member's row stores no url. dropping only the caller's entry was correct while each row was self-contained; once rows inherit, the owner moving or disconnecting the instance left every member cached against the previous host until the sidecar restarted. also returns the resolved instanceUrl from connectionState. members not being able to set it is the invariant; not seeing it never was and could not be — every avatar_url the instance hands back is on that origin. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,14 @@ import {
|
||||
getServiceInstanceUrl,
|
||||
getOwnerUser,
|
||||
} from 'officerdb';
|
||||
import { callGitea, getGiteaConfig, invalidateGiteaConfig, normalizeBase, probe } from './upstream';
|
||||
import {
|
||||
callGitea,
|
||||
getGiteaConfig,
|
||||
invalidateGiteaConfig,
|
||||
invalidateAllGiteaConfigs,
|
||||
normalizeBase,
|
||||
probe,
|
||||
} from './upstream';
|
||||
|
||||
// The officer-gitea sidecar. Owns the whole Gitea contract: the instance URL and the personal access
|
||||
// token. The platform side is a thin auth-gated forwarder holding no Gitea credentials.
|
||||
@@ -90,13 +97,20 @@ async function isOwner(userId: number): Promise<boolean> {
|
||||
// A member's row has no URL to return — it is genuinely not stored, not merely withheld — so this needs
|
||||
// no per-caller filtering. `instanceConfigured` is what the UI needs instead: it distinguishes "you have
|
||||
// not connected yet" from "there is nothing here to connect to", which are different screens.
|
||||
//
|
||||
// `instanceUrl` is the resolved base, returned to members as well as the owner. Not setting it is the
|
||||
// invariant (a member naming the URL would be a per-user SSRF hop); not SEEING it never was, and could
|
||||
// not be: every avatar_url and html_url the instance hands back is on that origin and gets rendered.
|
||||
// The UI needs it as a value rather than as a thousand strings because this instance's ROOT_URL disagrees
|
||||
// with its public host, so links have to be rebased client-side — see retargetUrls.
|
||||
async function connectionState(userId: number): Promise<Response> {
|
||||
const connection = await getServiceConnection(userId, 'gitea');
|
||||
const instanceConfigured = !!(await getServiceInstanceUrl('gitea'));
|
||||
const instanceUrl = await getServiceInstanceUrl('gitea');
|
||||
return Response.json({
|
||||
configured: !!connection,
|
||||
connection,
|
||||
instanceConfigured,
|
||||
instanceUrl,
|
||||
instanceConfigured: !!instanceUrl,
|
||||
isOwner: await isOwner(userId),
|
||||
});
|
||||
}
|
||||
@@ -105,8 +119,13 @@ async function handleConfig(req: Request, userId: number): Promise<Response> {
|
||||
if (req.method === 'GET') return connectionState(userId);
|
||||
|
||||
if (req.method === 'DELETE') {
|
||||
const owner = await isOwner(userId);
|
||||
await deleteServiceConnection(userId, 'gitea');
|
||||
invalidateGiteaConfig(userId);
|
||||
// The owner disconnecting removes the instance every member's row inherits, so every cached config
|
||||
// is now wrong — they must all resolve to null rather than keep talking to a server the platform no
|
||||
// longer claims. A member disconnecting only affects themselves.
|
||||
if (owner) invalidateAllGiteaConfigs();
|
||||
else invalidateGiteaConfig(userId);
|
||||
return connectionState(userId);
|
||||
}
|
||||
|
||||
@@ -154,7 +173,10 @@ async function handleConfig(req: Request, userId: number): Promise<Response> {
|
||||
secret: token ?? undefined,
|
||||
version: result.version,
|
||||
});
|
||||
invalidateGiteaConfig(userId);
|
||||
// An owner's save may have moved the instance, and every member's cached base was resolved from it.
|
||||
// Clearing only the caller would leave them pointed at the previous host until this process restarts.
|
||||
if (owner) invalidateAllGiteaConfigs();
|
||||
else invalidateGiteaConfig(userId);
|
||||
return Response.json({ connection, user: result.user, version: result.version });
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,18 @@ export function invalidateGiteaConfig(userId: number): void {
|
||||
cache.delete(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear every user's cached config.
|
||||
*
|
||||
* Needed because a member's entry holds the OWNER'S base, resolved at read time. Dropping only the
|
||||
* caller's entry was correct while each row was self-contained; once rows inherit, changing the instance
|
||||
* leaves every member cached against the previous host until this process restarts — pointing their
|
||||
* tokens at a server the owner has just moved away from. Call this whenever the owner's row changes.
|
||||
*/
|
||||
export function invalidateAllGiteaConfigs(): void {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
// One Gitea instance, one token per person. The base comes from whoever owns the platform; the token is
|
||||
// this caller's own, so what they see is their account — their repos, their notifications, their issues.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user