Vibecoded CI/CD
This commit is contained in:
@@ -3,7 +3,10 @@
|
||||
A session is a bearer token the API handed us, kept in a cookie rather than
|
||||
localStorage so the server can see it too: the sidebar then knows who is
|
||||
signed in on the very first draw, before any script runs. The token itself is
|
||||
never decoded here — the API is asked who it belongs to (see restoreSession). */
|
||||
never decoded here — the API is asked who it belongs to (see restoreSession).
|
||||
|
||||
In the desktop build the token is kept twice — see keepTokenRemembered, at
|
||||
the foot of this file. */
|
||||
|
||||
export interface AuthUser {
|
||||
id: string
|
||||
@@ -34,6 +37,11 @@ export function useAuth() {
|
||||
maxAge: WEEK_IN_SECONDS,
|
||||
sameSite: 'lax',
|
||||
})
|
||||
/* A window is not owed a cookie jar: its own address is one a webview is not
|
||||
everywhere obliged to store cookies against, and a session that could not
|
||||
outlive the window would ask the person to sign in every time they opened
|
||||
the app. A no-op anywhere else. */
|
||||
keepTokenRemembered(token, TOKEN_COOKIE)
|
||||
const user = useState<AuthUser | null>('auth:user', () => null)
|
||||
|
||||
const loggedIn = computed(() => user.value !== null)
|
||||
|
||||
65
frontend/app/plugins/desktop.ts
Normal file
65
frontend/app/plugins/desktop.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/* The window's own habits.
|
||||
|
||||
These are the two things the wiki does differently once it is a desktop app
|
||||
rather than a page of the web — see utils/desktop.ts for the asking, and
|
||||
src-tauri/ for the shell. The web build takes the first line and returns:
|
||||
nothing here changes how a browser sees the wiki.
|
||||
|
||||
Runs after the auth plugin (plugins come in name order), which matters: by
|
||||
now a signed-in person's account has already had its say about the interface
|
||||
language, and an account outranks this device. */
|
||||
|
||||
import { isDesktop, leadsOutward, openExternal, storeGet, storeSet } from '~/utils/desktop'
|
||||
|
||||
/** Where a window keeps the interface language it was last shown. Also the
|
||||
* name of the cookie the web build writes it to — the same choice, remembered
|
||||
* the way this kind of host can be sure of remembering it. */
|
||||
const LOCALE_KEY = 'mathew-locale'
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
if (!import.meta.client || !isDesktop()) return
|
||||
|
||||
/* Links. The wiki's own addresses are relative and the router turns them as
|
||||
it always does; an absolute one is about something outside this window —
|
||||
and a window has one page, no tab strip, no back button a reader would
|
||||
trust after the wiki had been replaced by some other site's document. So
|
||||
those go out to the system's browser instead, and the window keeps
|
||||
showing the wiki it was asked to show.
|
||||
|
||||
Captured on the way in, so it also catches the links drawn from article
|
||||
Markdown — the rendered text belongs to the article, which has no reason
|
||||
to know it is not in a browser. */
|
||||
document.addEventListener(
|
||||
'click',
|
||||
(event) => {
|
||||
if (event.defaultPrevented || event.button !== 0) return
|
||||
// a modified click is the reader asking for the browser's own gesture
|
||||
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
|
||||
const target = event.target as Element | null
|
||||
const href = target?.closest?.('a')?.getAttribute('href')
|
||||
if (!href || !leadsOutward(href)) return
|
||||
event.preventDefault()
|
||||
void openExternal(href)
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
/* The interface language, kept where a window can be certain of keeping it.
|
||||
On the web the choice rides on the `mathew-locale` cookie, which the server
|
||||
reads so the first draw already speaks it. A window draws nothing on a
|
||||
server, and its own address — `tauri://localhost` — is one a webview is
|
||||
not everywhere obliged to keep cookies for, so the choice is mirrored into
|
||||
localStorage: a lost cookie costs a language switch at the next start, and
|
||||
that is all.
|
||||
|
||||
An account's own choice outranks the mirror, as it outranks the cookie —
|
||||
hence the guard, the auth plugin having run first. Whatever the UI ends up
|
||||
speaking is then written back, so the mirror always says what this window
|
||||
last showed. */
|
||||
const { locale, choose } = useAppLocale()
|
||||
const { user } = useAuth()
|
||||
const remembered = storeGet(LOCALE_KEY)
|
||||
if (remembered && !user.value?.locale) void choose(remembered)
|
||||
|
||||
watch(locale, (code) => storeSet(LOCALE_KEY, code))
|
||||
})
|
||||
116
frontend/app/utils/desktop.ts
Normal file
116
frontend/app/utils/desktop.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/* Running as a desktop app.
|
||||
|
||||
One bundle answers both a browser and an app window (Tauri — see
|
||||
src-tauri/), so nothing in `app/` is desktop code: it is the wiki asking at
|
||||
run time whether it happens to be sitting in a window. The answer comes from
|
||||
the shell, which marks the page it loads and marks no other — so the same
|
||||
dist-desktop/ files opened in an ordinary browser behave as a web page, and
|
||||
a web page that somehow met Tauri would only gain the shell's habits.
|
||||
|
||||
What a window does differently, then, is small and all about being a window
|
||||
with one page and no browser chrome: a link leads out to the system's own
|
||||
browser rather than replacing the wiki in the only view there is, and the
|
||||
few bytes the app remembers of a person live where a window can be certain
|
||||
of keeping them. */
|
||||
|
||||
import type { Ref } from 'vue'
|
||||
import { watch } from 'vue'
|
||||
|
||||
/** The handle every Tauri webview injects into its page — present before the
|
||||
* npm API is imported, and without it. Nothing else sets this name. */
|
||||
export function isDesktop(): boolean {
|
||||
if (!import.meta.client) return false
|
||||
return (window as Window & { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__ != null
|
||||
}
|
||||
|
||||
/* A window is expected to have localStorage. A private browser may throw on
|
||||
being asked for it at all, and a setting that cannot be remembered is worth
|
||||
far less than a page that still draws — so the three doors below are the
|
||||
only place any of it is touched, and none of them lets a throw through. */
|
||||
|
||||
export function storeGet(key: string): string | null {
|
||||
try {
|
||||
return localStorage.getItem(key)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function storeSet(key: string, value: string): void {
|
||||
try {
|
||||
localStorage.setItem(key, value)
|
||||
} catch {
|
||||
/* nothing remembered; nothing else depends on it */
|
||||
}
|
||||
}
|
||||
|
||||
export function storeClear(key: string): void {
|
||||
try {
|
||||
localStorage.removeItem(key)
|
||||
} catch {
|
||||
/* as good as cleared, for as long as this page lives */
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether an address leads outside this app — to a document on some other
|
||||
* site, or to a mail client, or to a phone dialler. The app's own pages are
|
||||
* the relative `/wiki/…` the router already knows; an absolute address that
|
||||
* happens to name the wiki's public site counts as outward too, and opens
|
||||
* there in the browser, which is the honest reading of a link written out in
|
||||
* full. */
|
||||
export function leadsOutward(href: string): boolean {
|
||||
if (!import.meta.client || !href) return false
|
||||
let url: URL
|
||||
try {
|
||||
url = new URL(href, window.location.href)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
if (url.protocol === 'mailto:' || url.protocol === 'tel:') return true
|
||||
return (url.protocol === 'http:' || url.protocol === 'https:') && url.origin !== window.location.origin
|
||||
}
|
||||
|
||||
/** Hand a document to whatever machine-level application owns it: the browser
|
||||
* for a page, the mail client for `mailto:`. In a browser a link already does
|
||||
* this, so the fallback is the browser's own new tab; in a window there is no
|
||||
* other tab, and following the link in place would leave the wiki's page
|
||||
* gone from the only view there is. */
|
||||
export async function openExternal(url: string): Promise<void> {
|
||||
if (!import.meta.client || !url) return
|
||||
if (isDesktop()) {
|
||||
try {
|
||||
const { openUrl } = await import('@tauri-apps/plugin-opener')
|
||||
await openUrl(url)
|
||||
return
|
||||
} catch {
|
||||
/* no shell to hand it to, or this window is not allowed this one — the
|
||||
least worse thing left is the browser's way out */
|
||||
}
|
||||
}
|
||||
window.open(url, '_blank', 'noopener')
|
||||
}
|
||||
|
||||
/** Keep a session token where a window cannot lose it.
|
||||
|
||||
On the web a cookie is the whole of it: the server reads it, and the browser
|
||||
keeps it as long as the API said the token lasts. A window's own address
|
||||
(`tauri://localhost`) is one a webview is not everywhere obliged to keep
|
||||
cookies for, and a session dying with the window would mean signing in again
|
||||
every time the app was opened — for a token the API hands out for a week. So
|
||||
here it is mirrored into localStorage and taken back from there whenever the
|
||||
cookie comes up empty, whichever of the two this webview chose to keep.
|
||||
|
||||
The cookie still leads: it is the one already at hand, the one a web build
|
||||
would have read, and the one the sign-out line clears. A browser asked this
|
||||
question of the same files ignores all of it — there is nothing to mirror
|
||||
where cookies work — so the wiki's behavior there is untouched. */
|
||||
export function keepTokenRemembered(token: Ref<string | null>, key: string): void {
|
||||
if (!import.meta.client || !isDesktop()) return
|
||||
|
||||
if (!token.value) {
|
||||
const kept = storeGet(key)
|
||||
if (kept) token.value = kept
|
||||
}
|
||||
// sync, so signing out clears the mirror in the same breath, not on a tick
|
||||
watch(token, (value) => (value ? storeSet(key, value) : storeClear(key)), { flush: 'sync' })
|
||||
}
|
||||
Reference in New Issue
Block a user