Replace the Schulcloud token without a restart
A token lasts 30 days and only a browser login yields one — the account is federated, so the server cannot mint it. Replacing it meant editing .env and recreating the container, every month. `schulcloud token set` (a hidden prompt, or piped input) and a /token page both send it to PUT /api/token. The server checks it with Schulcloud first — well-formed, unexpired, still logged in, the same account — then swaps it into the config every request reads, restarts the keepalive and saves it in STATE_DIR, a new volume, with mode 0600. At startup the newer of the saved token and TSC_JWT_COOKIE wins, unless they belong to different accounts. A refused paste changes nothing, and the token is never logged. The keepalive's pings carry a generation, so a 401 for the old token that arrives after a swap cannot stop the new cycle. `schulcloud token`, whoami and the log report the expiry and warn a week ahead. Found on the way: a host that is off for more than two hours loses the session however long the token has left — this machine lost it overnight — which is what the always-on Pi is for. 174 tests. Smoke 72/72 on the local instance, and a real swap verified end to end there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -133,12 +133,15 @@ export class SchulcloudClient {
|
||||
url: URL,
|
||||
accept: string,
|
||||
auth: 'bearer' | 'cookie' | 'none' = 'bearer',
|
||||
options: { idleTimeout?: boolean } = {},
|
||||
options: { idleTimeout?: boolean; token?: string } = {},
|
||||
): Promise<Response> {
|
||||
let lastError: unknown;
|
||||
const headers: Record<string, string> = { Accept: accept };
|
||||
if (auth === 'bearer') headers.Authorization = `Bearer ${this.config.jwt}`;
|
||||
if (auth === 'cookie') headers.Cookie = `jwt=${this.config.jwt}`;
|
||||
// Read at the moment of use, never earlier: the token can be replaced
|
||||
// while the server runs (core/session-token.ts).
|
||||
const jwt = options.token ?? this.config.jwt;
|
||||
if (auth === 'bearer') headers.Authorization = `Bearer ${jwt}`;
|
||||
if (auth === 'cookie') headers.Cookie = `jwt=${jwt}`;
|
||||
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
if (attempt > 0) await delay(backoffMs(attempt));
|
||||
@@ -253,6 +256,15 @@ export class SchulcloudClient {
|
||||
return this.getJson<MeResponse>('/api/v3/me');
|
||||
}
|
||||
|
||||
/**
|
||||
* `/me` as another token sees it, leaving the token in use untouched — how a
|
||||
* replacement is checked before it is swapped in.
|
||||
*/
|
||||
async meAs(token: string): Promise<MeResponse> {
|
||||
const response = await this.request(this.url('/api/v3/me'), 'application/json', 'bearer', { token });
|
||||
return (await response.json()) as MeResponse;
|
||||
}
|
||||
|
||||
// --- session ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user