mirror of
https://github.com/narrrl/narl-workspace.git
synced 2026-11-04 00:47:01 +01:00
sessions: every signed-in browser is a row, and one can be signed out
A session cookie now carries the id of a row in the new sessions table, and is refused once that row is gone. :sessions lists the browsers signed in, newest first, and :sessions rm <n> signs one out without touching the rest; it locks the vault too, since the vault is open for the whole workspace. Logging out deletes the row, :logout! and a login password change delete them all, and the sweep drops expired rows. last_seen is kept in memory and written at most every five minutes. Cookies issued before this carry no id and are refused: every browser logs in once more after the upgrade.
This commit is contained in:
1 parent
6c82264f57
commit
165561f6a2
10 files changed
+540
-56
No files matched your search
@@ -57,6 +57,16 @@ export interface SpaceOverview {
|
||||
recent: NodeSummary[]
|
||||
}
|
||||
|
||||
/** A browser that is signed in. */
|
||||
export interface SignedIn {
|
||||
id: string
|
||||
created_at: number
|
||||
last_seen: number
|
||||
user_agent: string
|
||||
/** The session asking. */
|
||||
current: boolean
|
||||
}
|
||||
|
||||
/** A public key allowed to mount the workspace over SFTP. */
|
||||
export interface SshKey {
|
||||
id: number
|
||||
@@ -460,6 +470,9 @@ export const api = {
|
||||
readShared: (token: string, key: string) =>
|
||||
request<SharedDocument>(`/share/${encodeURIComponent(token)}/${encodeURIComponent(key)}`),
|
||||
|
||||
sessions: () => request<SignedIn[]>('/sessions'),
|
||||
revokeSession: (id: string) =>
|
||||
request<{ ok: true }>(`/sessions/${encodeURIComponent(id)}`, { method: 'DELETE' }),
|
||||
listSshKeys: () => request<SshKey[]>('/ssh-keys'),
|
||||
|
||||
addSshKey: (name: string, public_key: string) =>
|
||||
|
||||
+54
-1
@@ -1,6 +1,6 @@
|
||||
import { api, isContainer, type BackupStatus } from './api'
|
||||
import { workspace } from './store.svelte'
|
||||
import { formatDay, parseDay, parseDue, sealedNote, type ScrollAmount } from './util'
|
||||
import { formatDay, parseDay, parseDue, relative, sealedNote, type ScrollAmount } from './util'
|
||||
|
||||
export type { ScrollAmount }
|
||||
|
||||
@@ -102,6 +102,33 @@ const ago = (at: number | null) => {
|
||||
return `${Math.round(seconds / 86400)}d ago`
|
||||
}
|
||||
|
||||
/** "firefox on linux", out of a user-agent string, as far as it says. */
|
||||
function browserOf(agent: string): string {
|
||||
const browser = /Edg\//.test(agent)
|
||||
? 'edge'
|
||||
: /Firefox\//.test(agent)
|
||||
? 'firefox'
|
||||
: /Chrome\//.test(agent)
|
||||
? 'chrome'
|
||||
: /Safari\//.test(agent)
|
||||
? 'safari'
|
||||
: /curl\//.test(agent)
|
||||
? 'curl'
|
||||
: 'a browser'
|
||||
const system = /Android/.test(agent)
|
||||
? 'android'
|
||||
: /iPhone|iPad/.test(agent)
|
||||
? 'ios'
|
||||
: /Mac OS X/.test(agent)
|
||||
? 'macos'
|
||||
: /Windows/.test(agent)
|
||||
? 'windows'
|
||||
: /Linux/.test(agent)
|
||||
? 'linux'
|
||||
: ''
|
||||
return system ? `${browser} on ${system}` : browser
|
||||
}
|
||||
|
||||
const describeBackup = (status: BackupStatus) => {
|
||||
if (!status.configured) {
|
||||
return workspace.say('proton drive backups are off — run `narl-workspace proton-login` on the server', 'error')
|
||||
@@ -782,6 +809,32 @@ export const commands: CommandSpec[] = [
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'sessions',
|
||||
args: '[rm <n>]',
|
||||
group: 'view',
|
||||
help: 'every browser signed in, numbered — rm signs one of them out, and locks the vault',
|
||||
run: (arg) =>
|
||||
workspace.guard(async () => {
|
||||
const trimmed = arg.trim()
|
||||
const sessions = await api.sessions()
|
||||
if (trimmed === '' || trimmed === 'ls' || trimmed === 'list') {
|
||||
return workspace.say(
|
||||
sessions
|
||||
.map((s, i) => `${i + 1} ${s.current ? 'this one' : browserOf(s.user_agent)}, seen ${relative(s.last_seen)}`)
|
||||
.join(' · '),
|
||||
)
|
||||
}
|
||||
const rmMatch = /^rm\s+(\d+)$/.exec(trimmed)
|
||||
if (!rmMatch) return workspace.say('usage: :sessions [rm <n>]', 'error')
|
||||
const target = sessions[Number(rmMatch[1]) - 1]
|
||||
if (!target) return workspace.say(`no session ${rmMatch[1]} — :sessions lists them`, 'error')
|
||||
if (target.current) return workspace.say('that is this browser — :logout ends it', 'error')
|
||||
await api.revokeSession(target.id)
|
||||
await workspace.loadVault()
|
||||
workspace.say(`signed out ${browserOf(target.user_agent)}, seen ${relative(target.last_seen)} — the vault is locked`)
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'sshkey',
|
||||
args: '[add <name> <key> | rm <name> | vault <name> on|off]',
|
||||
|
||||
Reference in new issue
Block a user