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:
@@ -1,6 +1,8 @@
|
||||
import type { Config } from './config.ts';
|
||||
import { SchulcloudClient } from './core/client.ts';
|
||||
import type { SessionKeepalive } from './core/keepalive.ts';
|
||||
import { FileManager } from './core/legacy-files.ts';
|
||||
import { SessionToken } from './core/session-token.ts';
|
||||
import { Indexer } from './indexer/indexer.ts';
|
||||
import { Store } from './store/store.ts';
|
||||
|
||||
@@ -23,10 +25,22 @@ export interface Services {
|
||||
files: FileManager;
|
||||
store: Store | undefined;
|
||||
indexer: Indexer | undefined;
|
||||
/** The Schulcloud token, which `/api/token` can replace without a restart. */
|
||||
session: SessionToken;
|
||||
/**
|
||||
* Set by the entry point that runs one, for status reports. Created there
|
||||
* rather than here because each entry point logs to a different stream.
|
||||
*/
|
||||
keepalive?: SessionKeepalive;
|
||||
}
|
||||
|
||||
export async function createServices(config: Config): Promise<Services> {
|
||||
const client = new SchulcloudClient(config);
|
||||
// Before anything else reads config.jwt: a token replaced at runtime and
|
||||
// saved may be newer than the one in the environment.
|
||||
const session = new SessionToken(config, client, config.stateDir);
|
||||
await session.load();
|
||||
|
||||
const files = new FileManager(client);
|
||||
const store = await Store.open(config.databaseUrl);
|
||||
const indexer = store ? new Indexer(client, store, config) : undefined;
|
||||
@@ -37,7 +51,7 @@ export async function createServices(config: Config): Promise<Services> {
|
||||
'/files, /manifest and refresh_index are unavailable. Set DATABASE_URL to enable them.',
|
||||
);
|
||||
}
|
||||
return { config, client, files, store, indexer };
|
||||
return { config, client, files, store, indexer, session };
|
||||
}
|
||||
|
||||
export async function closeServices(services: Services): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user