Merge remote-tracking branch 'origin/main' into pr/1131 and deno task fmt

pull/1131/head
Zef Hemel 2024-10-28 14:15:36 +01:00
commit ce5992719c
9 changed files with 113 additions and 75 deletions

View File

@ -5,7 +5,7 @@ import {
unregisterServiceWorkers, unregisterServiceWorkers,
} from "../sw_util.ts"; } from "../sw_util.ts";
import { encodePageURI } from "@silverbulletmd/silverbullet/lib/page_ref"; import { encodePageURI } from "@silverbulletmd/silverbullet/lib/page_ref";
import { urlPrefix, toRealUrl } from "../../lib/url_hack.ts"; import { toRealUrl, urlPrefix } from "../../lib/url_hack.ts";
const defaultFetchTimeout = 30000; // 30 seconds const defaultFetchTimeout = 30000; // 30 seconds
@ -69,7 +69,9 @@ export class HttpSpacePrimitives implements SpacePrimitives {
"Received an authentication redirect, redirecting to URL: " + "Received an authentication redirect, redirecting to URL: " +
redirectHeader, redirectHeader,
); );
location.href = redirectHeader.startsWith(urlPrefix) ? redirectHeader : toRealUrl(redirectHeader); location.href = redirectHeader.startsWith(urlPrefix)
? redirectHeader
: toRealUrl(redirectHeader);
throw new Error("Redirected"); throw new Error("Redirected");
} else { } else {
console.error("Got a redirect status but no location header", result); console.error("Got a redirect status but no location header", result);
@ -84,7 +86,9 @@ export class HttpSpacePrimitives implements SpacePrimitives {
result.url, result.url,
); );
alert("You are not authenticated, redirecting to: " + redirectHeader); alert("You are not authenticated, redirecting to: " + redirectHeader);
location.href = redirectHeader.startsWith(urlPrefix) ? redirectHeader : toRealUrl(redirectHeader); location.href = redirectHeader.startsWith(urlPrefix)
? redirectHeader
: toRealUrl(redirectHeader);
throw new Error("Not authenticated"); throw new Error("Not authenticated");
} else { } else {
// If not, let's reload // If not, let's reload

View File

@ -1,50 +1,48 @@
export const urlPrefix : string = Deno.env.get('SB_URL_PREFIX') ?? (globalThis.silverBulletConfig ? globalThis.silverBulletConfig.urlPrefix : null) ?? ''; export const urlPrefix: string = Deno.env.get("SB_URL_PREFIX") ??
(globalThis.silverBulletConfig
? globalThis.silverBulletConfig.urlPrefix
: null) ??
"";
export const toRealUrl = <T extends (string | URL)>(url : T) : T => { export const toRealUrl = <T extends (string | URL)>(url: T): T => {
if (typeof url === 'string') { if (typeof url === "string") {
const stringUrl = url as string; const stringUrl = url as string;
if (stringUrl.startsWith('http://') || stringUrl.startsWith('https://')) { if (stringUrl.startsWith("http://") || stringUrl.startsWith("https://")) {
const parsedUrl = new URL(stringUrl); const parsedUrl = new URL(stringUrl);
parsedUrl.pathname = urlPrefix + parsedUrl.pathname; parsedUrl.pathname = urlPrefix + parsedUrl.pathname;
//console.log("Converted ", url, parsedUrl.href) //console.log("Converted ", url, parsedUrl.href)
return String(parsedUrl.href) as T; return String(parsedUrl.href) as T;
} } else {
else { if (!stringUrl.startsWith("/")) {
if (!stringUrl.startsWith('/')) { console.log("Don't know how to deal with relative path: ", url);
console.log("Don't know how to deal with relative path: ", url); }
} //console.log("Converted ", url, urlPrefix + stringUrl)
//console.log("Converted ", url, urlPrefix + stringUrl) return (urlPrefix + stringUrl) as T;
return (urlPrefix + stringUrl) as T;
}
}
else if (url.protocol === 'http:' || url.protocol === 'https:') {
const parsedUrl = new URL(url as URL);
parsedUrl.pathname = urlPrefix + parsedUrl.pathname;
//console.log("Converted ", url, parsedUrl)
return parsedUrl as T;
}
else {
return url;
} }
} else if (url.protocol === "http:" || url.protocol === "https:") {
const parsedUrl = new URL(url as URL);
parsedUrl.pathname = urlPrefix + parsedUrl.pathname;
//console.log("Converted ", url, parsedUrl)
return parsedUrl as T;
} else {
return url;
}
}; };
export const toInternalUrl = (url : string) => { export const toInternalUrl = (url: string) => {
if (url.startsWith('http://') || url.startsWith('https://')) { if (url.startsWith("http://") || url.startsWith("https://")) {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
if (parsedUrl.pathname.startsWith(urlPrefix)) { if (parsedUrl.pathname.startsWith(urlPrefix)) {
parsedUrl.pathname = parsedUrl.pathname.substr(urlPrefix.length); parsedUrl.pathname = parsedUrl.pathname.substr(urlPrefix.length);
return parsedUrl.href; return parsedUrl.href;
} } else {
else { console.log("Don't know how to deal with non-prefix: ", url);
console.log("Don't know how to deal with non-prefix: ", url); return url;
return url;
}
} else if (url.startsWith(urlPrefix)) {
return url.substr(urlPrefix.length);
}
else {
console.log("Don't know how to deal with non-prefix: ", url);
return url;
} }
} else if (url.startsWith(urlPrefix)) {
return url.substr(urlPrefix.length);
} else {
console.log("Don't know how to deal with non-prefix: ", url);
return url;
}
}; };

View File

@ -60,12 +60,13 @@ export class JWTIssuer {
createJWT( createJWT(
payload: Record<string, unknown>, payload: Record<string, unknown>,
expirySeconds: number, expirySeconds?: number,
): Promise<string> { ): Promise<string> {
return create({ alg: "HS512", typ: "JWT" }, { const jwtPayload = { ...payload };
...payload, if (expirySeconds) {
exp: getNumericDate(expirySeconds), jwtPayload.exp = getNumericDate(expirySeconds);
}, this.key); }
return create({ alg: "HS512", typ: "JWT" }, jwtPayload, this.key);
} }
verifyAndDecodeJWT(jwt: string): Promise<Record<string, unknown>> { verifyAndDecodeJWT(jwt: string): Promise<Record<string, unknown>> {

View File

@ -20,7 +20,7 @@ import {
parsePageRef, parsePageRef,
} from "@silverbulletmd/silverbullet/lib/page_ref"; } from "@silverbulletmd/silverbullet/lib/page_ref";
import { base64Encode } from "$lib/crypto.ts"; import { base64Encode } from "$lib/crypto.ts";
import { urlPrefix, toRealUrl, toInternalUrl } from "$lib/url_hack.ts"; import { toInternalUrl, toRealUrl, urlPrefix } from "$lib/url_hack.ts";
const authenticationExpirySeconds = 60 * 60 * 24 * 7; // 1 week const authenticationExpirySeconds = 60 * 60 * 24 * 7; // 1 week
@ -105,7 +105,7 @@ export class HttpServer {
spaceServer.pagesPath.replaceAll("\\", "\\\\"), spaceServer.pagesPath.replaceAll("\\", "\\\\"),
).replaceAll( ).replaceAll(
"{{URL_PREFIX}}", "{{URL_PREFIX}}",
urlPrefix urlPrefix,
) )
.replace( .replace(
"{{DESCRIPTION}}", "{{DESCRIPTION}}",
@ -323,36 +323,39 @@ export class HttpServer {
this.app.get("/.logout", (c) => { this.app.get("/.logout", (c) => {
const url = new URL(toInternalUrl(c.req.url)); const url = new URL(toInternalUrl(c.req.url));
deleteCookie(c, authCookieName(url.host)); deleteCookie(c, authCookieName(url.host));
deleteCookie(c, "refreshLogin");
return c.redirect(toRealUrl("/.auth")); return c.redirect(toRealUrl("/.auth"));
}); });
this.app.get("/.auth", (c) => { this.app.get("/.auth", (c) => {
const html = this.clientAssetBundle.readTextFileSync(".client/auth.html") const html = this.clientAssetBundle.readTextFileSync(".client/auth.html")
.replaceAll( .replaceAll(
"{{URL_PREFIX}}", "{{URL_PREFIX}}",
urlPrefix urlPrefix,
); );
return c.html(html); return c.html(html);
}).post( }).post(
validator("form", (value, c) => { validator("form", (value, c) => {
const username = value["username"]; const username = value["username"];
const password = value["password"]; const password = value["password"];
const rememberMe = value["rememberMe"];
if ( if (
!username || typeof username !== "string" || !username || typeof username !== "string" ||
!password || typeof password !== "string" !password || typeof password !== "string" ||
(rememberMe && typeof rememberMe !== "string")
) { ) {
return c.redirect(toRealUrl("/.auth?error=0")); return c.redirect(toRealUrl("/.auth?error=0"));
} }
return { username, password }; return { username, password, rememberMe };
}), }),
async (c) => { async (c) => {
const req = c.req; const req = c.req;
const url = new URL(toInternalUrl(c.req.url)); const url = new URL(c.req.url);
const { username, password } = req.valid("form"); const { username, password, rememberMe } = req.valid("form");
const { const {
user: expectedUser, user: expectedUser,
@ -361,18 +364,24 @@ export class HttpServer {
if (username === expectedUser && password === expectedPassword) { if (username === expectedUser && password === expectedPassword) {
// Generate a JWT and set it as a cookie // Generate a JWT and set it as a cookie
const jwt = await this.spaceServer.jwtIssuer.createJWT( const jwt = rememberMe
{ username }, ? await this.spaceServer.jwtIssuer.createJWT({ username })
authenticationExpirySeconds, : await this.spaceServer.jwtIssuer.createJWT(
); { username },
authenticationExpirySeconds,
);
console.log("Successful auth"); console.log("Successful auth");
const inAWeek = new Date(
Date.now() + authenticationExpirySeconds * 1000,
);
setCookie(c, authCookieName(url.host), jwt, { setCookie(c, authCookieName(url.host), jwt, {
expires: new Date( expires: inAWeek,
Date.now() + authenticationExpirySeconds * 1000,
), // in a week
// sameSite: "Strict", // sameSite: "Strict",
// httpOnly: true, // httpOnly: true,
}); });
if (rememberMe) {
setCookie(c, "refreshLogin", "true", { expires: inAWeek });
}
const values = await c.req.parseBody(); const values = await c.req.parseBody();
const from = values["from"]; const from = values["from"];
const result = toRealUrl(typeof from === "string" ? from : "/"); const result = toRealUrl(typeof from === "string" ? from : "/");
@ -380,7 +389,7 @@ export class HttpServer {
return c.redirect(result); return c.redirect(result);
} else { } else {
console.error("Authentication failed, redirecting to auth page."); console.error("Authentication failed, redirecting to auth page.");
return c.redirect(toRealUrl("/.auth?error=1"), 401); return c.redirect("/.auth?error=1", 401);
} }
}, },
).all((c) => { ).all((c) => {
@ -414,6 +423,7 @@ export class HttpServer {
const authToken = authHeader.slice("Bearer ".length); const authToken = authHeader.slice("Bearer ".length);
if (authToken === this.spaceServer.authToken) { if (authToken === this.spaceServer.authToken) {
// All good, let's proceed // All good, let's proceed
this.refreshLogin(c, host);
return next(); return next();
} else { } else {
console.log( console.log(
@ -445,10 +455,28 @@ export class HttpServer {
return redirectToAuth(); return redirectToAuth();
} }
} }
this.refreshLogin(c, host);
return next(); return next();
}); });
} }
private refreshLogin(c: Context, host: string) {
if (getCookie(c, "refreshLogin")) {
const inAWeek = new Date(
Date.now() + authenticationExpirySeconds * 1000,
);
const jwt = getCookie(c, authCookieName(host));
if (jwt) {
setCookie(c, authCookieName(host), jwt, {
expires: inAWeek,
// sameSite: "Strict",
// httpOnly: true,
});
setCookie(c, "refreshLogin", "true", { expires: inAWeek });
}
}
}
private addFsRoutes() { private addFsRoutes() {
this.app.use( this.app.use(
"*", "*",

View File

@ -69,7 +69,10 @@
<body> <body>
<header> <header>
<h1> <h1>
Login to <img src="{{URL_PREFIX}}/.client/logo.png" style="height: 1ch" /> Login to <img
src="{{URL_PREFIX}}/.client/logo.png"
style="height: 1ch"
/>
SilverBullet SilverBullet
</h1> </h1>
</header> </header>
@ -95,6 +98,10 @@
placeholder="Password" placeholder="Password"
/> />
</div> </div>
<div>
<input type="checkbox" name="rememberMe" id="rememberMe" />
<label>Remember me</label>
</div>
<div> <div>
<input type="submit" value="Login" /> <input type="submit" value="Login" />
</div> </div>

View File

@ -88,7 +88,7 @@ declare global {
syncOnly: boolean; syncOnly: boolean;
readOnly: boolean; readOnly: boolean;
enableSpaceScript: boolean; enableSpaceScript: boolean;
urlPrefix : string; urlPrefix: string;
}; };
// deno-lint-ignore no-var // deno-lint-ignore no-var
var client: Client; var client: Client;

View File

@ -7,7 +7,7 @@ import type { Client } from "./client.ts";
import { cleanPageRef } from "@silverbulletmd/silverbullet/lib/resolve"; import { cleanPageRef } from "@silverbulletmd/silverbullet/lib/resolve";
import { renderTheTemplate } from "$common/syscalls/template.ts"; import { renderTheTemplate } from "$common/syscalls/template.ts";
import { safeRun } from "../lib/async.ts"; import { safeRun } from "../lib/async.ts";
import { toRealUrl, toInternalUrl } from "../lib/url_hack.ts"; import { toInternalUrl, toRealUrl } from "../lib/url_hack.ts";
export type PageState = PageRef & { export type PageState = PageRef & {
scrollTop?: number; scrollTop?: number;

View File

@ -16,7 +16,7 @@ import type { UploadFile } from "../../plug-api/types.ts";
import type { PageRef } from "@silverbulletmd/silverbullet/lib/page_ref"; import type { PageRef } from "@silverbulletmd/silverbullet/lib/page_ref";
import { openSearchPanel } from "@codemirror/search"; import { openSearchPanel } from "@codemirror/search";
import { diffAndPrepareChanges } from "../cm_util.ts"; import { diffAndPrepareChanges } from "../cm_util.ts";
import { toRealUrl, toInternalUrl } from "../../lib/url_hack.ts"; import { toInternalUrl, toRealUrl } from "../../lib/url_hack.ts";
export function editorSyscalls(client: Client): SysCallMapping { export function editorSyscalls(client: Client): SysCallMapping {
const syscalls: SysCallMapping = { const syscalls: SysCallMapping = {