mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-06 00:15:32 +00:00
Merge branch 'feat/desktop-offline' into feat/do-policies
This commit is contained in:
+33
-13
@@ -273,21 +273,29 @@ export async function PUT(request: NextRequest) {
|
||||
// The refresh token may have been minted by the password+TOTP login route,
|
||||
// which works without a configured OAuth client by falling back to the
|
||||
// default client id - refreshing must fall back the same way (#873).
|
||||
// Discovery or the token endpoint out of reach is an outage: the client
|
||||
// waits it out. A 500 here would count toward signing the user out.
|
||||
let tokenResponse: Response;
|
||||
const upstream = IS_DESKTOP_BUILD ? await readCustomUpstream(cookieStore, slot) : null;
|
||||
const tokenEndpoint = await getTokenEndpoint(serverId, upstream ? { upstream } : { fallbackClientId: DEFAULT_CLIENT_ID });
|
||||
try {
|
||||
const tokenEndpoint = await getTokenEndpoint(serverId, upstream ? { upstream } : { fallbackClientId: DEFAULT_CLIENT_ID });
|
||||
|
||||
const params = buildOAuthParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
}, serverId, await defaultClientOptions(serverId, upstream));
|
||||
const params = buildOAuthParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
}, serverId, await defaultClientOptions(serverId, upstream));
|
||||
|
||||
const tokenResponse = await observeUpstream('oauth', () => fetch(tokenEndpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: params.toString(),
|
||||
// See revokeRefreshToken: the body holds the refresh token.
|
||||
redirect: 'error',
|
||||
}));
|
||||
tokenResponse = await observeUpstream('oauth', () => fetch(tokenEndpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: params.toString(),
|
||||
// See revokeRefreshToken: the body holds the refresh token.
|
||||
redirect: 'error',
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.warn('Token refresh: provider unreachable', { error: error instanceof Error ? error.message : 'Unknown error' });
|
||||
return NextResponse.json({ error: 'Token endpoint unavailable' }, { status: 503 });
|
||||
}
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const errorText = await tokenResponse.text();
|
||||
@@ -302,7 +310,10 @@ export async function PUT(request: NextRequest) {
|
||||
cookieStore.delete(accessTokenCookieName(slot));
|
||||
clearCustomUpstream(cookieStore, slot);
|
||||
clearIdToken(cookieStore, slot, request.nextUrl.basePath);
|
||||
return NextResponse.json({ error: 'Refresh failed' }, { status: 401 });
|
||||
// Only invalid_grant says the login itself is over. invalid_client, or
|
||||
// a provider refusing an early refresh (#552), says nothing about it,
|
||||
// and the client keeps the account's local data for those.
|
||||
return NextResponse.json({ error: oauthErrorCode(errorText) === 'invalid_grant' ? 'invalid_grant' : 'Refresh failed' }, { status: 401 });
|
||||
}
|
||||
return NextResponse.json({ error: 'Token endpoint unavailable' }, { status: 503 });
|
||||
}
|
||||
@@ -350,6 +361,15 @@ export async function PUT(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
function oauthErrorCode(body: string): string | null {
|
||||
try {
|
||||
const code = (JSON.parse(body) as { error?: unknown }).error;
|
||||
return typeof code === 'string' ? code : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke a slot's refresh token and clear its token cookies. With
|
||||
* `endSession`, also returns the URL that ends the provider's session, or null
|
||||
|
||||
@@ -7,8 +7,10 @@ export const dynamic = 'force-dynamic';
|
||||
const BASE_PATH = (process.env.NEXT_PUBLIC_BASE_PATH ?? '').replace(/\/+$/, '');
|
||||
/** Files a page can load: scripts, styles, fonts and the images they name. */
|
||||
const SHELL_FILE = /\.(js|css|woff2?|svg|png|webp)$/;
|
||||
/** The local store's engine in public/. */
|
||||
const ENGINE_FILE = /\.(mjs|wasm)$/;
|
||||
|
||||
async function listFiles(dir: string, prefix: string, out: string[]): Promise<void> {
|
||||
async function listFiles(dir: string, prefix: string, out: string[], wanted: RegExp = SHELL_FILE): Promise<void> {
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(dir, { withFileTypes: true });
|
||||
@@ -16,8 +18,8 @@ async function listFiles(dir: string, prefix: string, out: string[]): Promise<vo
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) await listFiles(join(dir, entry.name), `${prefix}${entry.name}/`, out);
|
||||
else if (SHELL_FILE.test(entry.name)) out.push(`${prefix}${entry.name}`);
|
||||
if (entry.isDirectory()) await listFiles(join(dir, entry.name), `${prefix}${entry.name}/`, out, wanted);
|
||||
else if (wanted.test(entry.name)) out.push(`${prefix}${entry.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,12 +28,18 @@ async function listFiles(dir: string, prefix: string, out: string[]): Promise<vo
|
||||
* /_next/static/, which is the same for every user and content-hashed. A
|
||||
* tab with offline mode on hands the list to the service worker, which keeps
|
||||
* the files in this build's cache so the app also starts without a network.
|
||||
* The local store's worker and its database engine come along: without them
|
||||
* the store can't start offline. The worker's address carries the build id, as
|
||||
* the page asks for it (lib/store/client/api.ts).
|
||||
*/
|
||||
export async function GET() {
|
||||
const files: string[] = [];
|
||||
await listFiles(join(process.cwd(), '.next', 'static'), `${BASE_PATH}/_next/static/`, files);
|
||||
await listFiles(join(process.cwd(), 'public', 'sqlite3mc'), `${BASE_PATH}/sqlite3mc/`, files, ENGINE_FILE);
|
||||
const buildId = process.env.NEXT_PUBLIC_BUILD_ID ?? '';
|
||||
files.push(`${BASE_PATH}/store-worker.js${buildId ? `?v=${encodeURIComponent(buildId)}` : ''}`);
|
||||
return NextResponse.json(
|
||||
{ buildId: process.env.NEXT_PUBLIC_BUILD_ID ?? '', files },
|
||||
{ buildId, files },
|
||||
{ headers: { 'Cache-Control': 'no-store' } },
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user