feat: hosting access tab (#5995)

* feat: implement access tab with dummy data

* fix: spacing

* feat: qa

* feat: implement backend

* qa: qa pass

* feat: fix user "search"

* fix: lint

* feat: change to bitfield

* feat: fix fields

* fix: lint

* fix: lint

* feat: hook up api

* feat: fix permissions

* feat: audit log table event start

* feat: better mobile mode for audit log table

* feat: i18n

* feat: qa

* feat: enforce permissions

* feat: email template start

* feat: qa

* fix: tooltip bug

* feat: qa

* impl: sse support in api-client

* feat: sse impl

* fix: desync path

* feat: time frame picker from analytics

* feat: QA

* fix: spacing

* fix: permisison audit log entries

* fix: hosting manage page shared server detection

* fix: lint

* feat: qa + lint

* feat: audit log table sort by time

* feat: finish frontend panel stuff

* fix: lint

* fix: backend alignment

* fix: lint

* fix: supress friend errors

* feat: qa

* fix: qa

* fix: lint

* fix: utils barrel

* fix: safari cookies in dev

* fix: pin nuxt

* feat: fixes + notif fix

* fix: notifications

* feat: qa

* fix: notification sync not happening immediately

* fix: qa

* fix: qa

* feat: qa

* blog + prepr

* feat: toast shit

* blog images

* thumbnail update one last time

* prepr

* feat: use reinvite route

* update images

* fix: reinvite stuff

* fix: lint

* fix: alignment of save bar

* fix: notif sizing

* fix: split up access

* fix: lint

* fix: lint

* fix: link

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Calum H.
2026-06-04 15:58:01 +00:00
committed by GitHub
co-authored by Prospector
parent 58ad58f958
commit bd97ace974
227 changed files with 15578 additions and 2153 deletions
+55
View File
@@ -0,0 +1,55 @@
import type { RequestOptions } from '../types/request'
export function appendRequestParams(url: string, params?: RequestOptions['params']): string {
if (!params) return url
const filteredParams: Record<string, string> = {}
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
filteredParams[key] = String(value)
}
}
const queryString = new URLSearchParams(filteredParams).toString()
if (!queryString) return url
return `${url}${url.includes('?') ? '&' : '?'}${queryString}`
}
export function toFetchBody(body: unknown): BodyInit | null | undefined {
if (!body) return undefined
if (
typeof body === 'object' &&
!(body instanceof FormData) &&
!(body instanceof URLSearchParams) &&
!(body instanceof Blob) &&
!(body instanceof ArrayBuffer) &&
!ArrayBuffer.isView(body as ArrayBufferView)
) {
return JSON.stringify(body)
}
return body as BodyInit
}
export async function parseResponseErrorData(response: Response): Promise<unknown> {
const contentType = response.headers.get('content-type')?.toLowerCase() ?? ''
try {
if (contentType.includes('application/json') || contentType.includes('+json')) {
return await response.json()
}
const text = await response.text()
if (!text) return undefined
try {
return JSON.parse(text)
} catch {
return text
}
} catch {
return undefined
}
}
+139
View File
@@ -0,0 +1,139 @@
import type { Archon } from '../modules/archon/types'
export type ParsedSseEvent = {
kind: 'event'
id?: string
event?: string
data: string
}
export type ParsedSseRetry = {
kind: 'retry'
retry: number
}
export type ParsedSseItem = ParsedSseEvent | ParsedSseRetry
export class SseParser {
private buffer = ''
private eventName = ''
private data = ''
private id: string | undefined
feed(chunk: string): ParsedSseItem[] {
this.buffer += chunk
const items: ParsedSseItem[] = []
while (true) {
const lineEnd = this.findLineEnd()
if (!lineEnd) break
const { line, length } = lineEnd
this.buffer = this.buffer.slice(length)
this.processLine(line, items)
}
return items
}
end(): ParsedSseItem[] {
const items: ParsedSseItem[] = []
if (this.buffer.length > 0) {
this.processLine(this.buffer.endsWith('\r') ? this.buffer.slice(0, -1) : this.buffer, items)
this.buffer = ''
}
this.dispatch(items)
return items
}
private findLineEnd(): { line: string; length: number } | null {
const lf = this.buffer.indexOf('\n')
const cr = this.buffer.indexOf('\r')
if (lf === -1 && cr === -1) return null
if (cr !== -1 && (lf === -1 || cr < lf)) {
if (cr === this.buffer.length - 1) return null
const length = this.buffer[cr + 1] === '\n' ? cr + 2 : cr + 1
return {
line: this.buffer.slice(0, cr),
length,
}
}
return {
line: this.buffer.slice(0, lf),
length: lf + 1,
}
}
private processLine(line: string, items: ParsedSseItem[]): void {
if (line === '') {
this.dispatch(items)
return
}
if (line.startsWith(':')) return
const colon = line.indexOf(':')
const field = colon === -1 ? line : line.slice(0, colon)
let value = colon === -1 ? '' : line.slice(colon + 1)
if (value.startsWith(' ')) value = value.slice(1)
switch (field) {
case 'event':
this.eventName = value
break
case 'data':
this.data += `${value}\n`
break
case 'id':
this.id = value
break
case 'retry': {
const retry = Number(value)
if (Number.isInteger(retry) && retry >= 0) {
items.push({ kind: 'retry', retry })
}
break
}
}
}
private dispatch(items: ParsedSseItem[]): void {
if (!this.data) {
this.eventName = ''
this.id = undefined
return
}
items.push({
kind: 'event',
id: this.id,
event: this.eventName || undefined,
data: this.data.endsWith('\n') ? this.data.slice(0, -1) : this.data,
})
this.eventName = ''
this.data = ''
this.id = undefined
}
}
export function parseSyncEventData(data: string): Archon.Sync.v1.SyncEvent | null {
let parsed: unknown
try {
parsed = JSON.parse(data)
} catch {
return null
}
if (!parsed || typeof parsed !== 'object') return null
const event = parsed as { type?: unknown }
if (typeof event.type !== 'string') return null
return parsed as Archon.Sync.v1.SyncEvent
}