commit 5f8b53de26012cc3e3ae1c69b9050a56d8f41cb7
parent 750c0f9231576c846255bdc8b87e4cf865c92d7e
Author: finwo <finwo@pm.me>
Date: Sun, 27 Sep 2026 18:48:42 +0200
Redirect only private-only screens to the login page
Diffstat:
4 files changed, 37 insertions(+), 37 deletions(-)
diff --git a/web/src/component/layout.tsx b/web/src/component/layout.tsx
@@ -5,13 +5,11 @@ import * as auth from '../util/auth';
import { ApiError } from '../util/api';
import { ago, stamp } from '../util/format';
-export const session = {
- user: null as { id: string; username: string; role: string } | null,
-};
+export { session } from '../util/auth';
// Mirrors canManageProject on the server.
export function manages(project: { owner_id?: string | null } | null): boolean {
- const user = session.user;
+ const user = auth.session.user;
if (!user || !project) return false;
if (user.role === 'admin') return true;
return project.owner_id != null && project.owner_id === user.id;
@@ -30,14 +28,14 @@ export const Chrome: m.Component = {
<span class="brand">conductor</span>
{Link('/jobs', 'jobs')}
{Link('/projects', 'projects')}
- {session.user ? Link('/workers', 'workers') : null}
- {session.user?.role === 'admin' ? Link('/users', 'users') : null}
- {session.user ? Link('/sessions', 'sessions') : null}
+ {auth.session.user ? Link('/workers', 'workers') : null}
+ {auth.session.user?.role === 'admin' ? Link('/users', 'users') : null}
+ {auth.session.user ? Link('/sessions', 'sessions') : null}
</nav>
<div class="who">
- {session.user
+ {auth.session.user
? [
- <span>{session.user.username}</span>,
+ <span>{auth.session.user.username}</span>,
<button class="link" onclick={() => auth.signOut()}>sign out</button>,
]
: <button class="link" onclick={() => auth.signIn()}>sign in</button>}
diff --git a/web/src/main.tsx b/web/src/main.tsx
@@ -14,8 +14,11 @@ import { Users, Sessions } from './screen/users';
const root = document.body;
-function wrap(screen: m.Component): m.RouteResolver {
- return { render: () => m(Chrome, m(screen)) };
+function wrap(screen: m.Component, needsUser = false): m.RouteResolver {
+ return {
+ onmatch: () => (needsUser && !session.user ? auth.signIn() : undefined),
+ render: () => m(Chrome, m(screen)),
+ };
}
const routes: m.RouteDefs = {
@@ -25,10 +28,10 @@ const routes: m.RouteDefs = {
'/projects': wrap(Projects),
'/projects/:project': wrap(Project),
'/projects/:project/jobs': wrap(Jobs),
- '/workers': wrap(Workers),
- '/workers/:worker': wrap(Worker),
- '/users': wrap(Users),
- '/sessions': wrap(Sessions),
+ '/workers': wrap(Workers, true),
+ '/workers/:worker': wrap(Worker, true),
+ '/users': wrap(Users, true),
+ '/sessions': wrap(Sessions, true),
};
function fail(error: unknown) {
@@ -60,6 +63,8 @@ async function start() {
m.route.prefix = '#!';
m.route(root, '/jobs', routes);
if (resume) m.route.set(resume.replace(/^#!/, ''));
+
+ setInterval(() => { if (!document.hidden) m.redraw(); }, 1000);
}
start();
diff --git a/web/src/util/api.ts b/web/src/util/api.ts
@@ -43,20 +43,21 @@ async function send(url: string, options: Options, token: string | null): Promis
});
}
+async function authorized(url: string, options: Options): Promise<Response> {
+ const res = await send(url, options, await bearer());
+ if (res.status !== 401 || !signedIn()) return res;
+
+ try {
+ await refresh();
+ } catch {
+ forget();
+ }
+ return send(url, options, await bearer());
+}
+
export async function request<T>(path: string, options: Options = {}): Promise<T> {
const url = path.startsWith('http') ? path : urlFor(path, options.query);
- let res = await send(url, options, await bearer());
-
- // One retry: the token may have been accepted a moment ago and expired
- // since, and that is not worth showing anyone.
- if (res.status === 401 && signedIn()) {
- try {
- await refresh();
- res = await send(url, options, await bearer());
- } catch {
- forget();
- }
- }
+ const res = await authorized(url, options);
if (res.status === 204) return undefined as T;
@@ -74,16 +75,7 @@ export async function request<T>(path: string, options: Options = {}): Promise<T
// The envelope's pagination, flattened onto the rows it describes.
export async function page<T>(path: string, options: Options = {}): Promise<Page<T>> {
const url = path.startsWith('http') ? path : urlFor(path, options.query);
- let res = await send(url, options, await bearer());
-
- if (res.status === 401 && signedIn()) {
- try {
- await refresh();
- res = await send(url, options, await bearer());
- } catch {
- forget();
- }
- }
+ const res = await authorized(url, options);
const body = await res.json().catch(() => ({}));
if (!res.ok || body.ok === false) throw new ApiError(res.status, body.error ?? {});
diff --git a/web/src/util/auth.ts b/web/src/util/auth.ts
@@ -17,6 +17,10 @@ export type Discovery = {
scopes?: string;
};
+export const session = {
+ user: null as { id: string; username: string; role: string } | null,
+};
+
let discovery: Discovery | null = null;
let accessToken: string | null = null;
let expiresAt = 0;
@@ -175,6 +179,7 @@ export async function bearer(): Promise<string | null> {
export function forget(): void {
accessToken = null;
expiresAt = 0;
+ session.user = null;
localStorage.removeItem(STORE);
localStorage.removeItem(IDENTITY);
}