mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 01:26:23 +00:00
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:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user