Rewritten frontend with Vue
This commit is contained in:
83
frontend/src/utils/awaitable-websocket.js
Normal file
83
frontend/src/utils/awaitable-websocket.js
Normal file
@@ -0,0 +1,83 @@
|
||||
class AwaitableWebSocket extends WebSocket {
|
||||
#received = []
|
||||
#waiting = []
|
||||
#err = null
|
||||
#opened = false
|
||||
|
||||
constructor(resolve, reject, url, protocols, binaryType) {
|
||||
super(url, protocols)
|
||||
this.binaryType = binaryType || 'blob'
|
||||
this.onopen = () => {
|
||||
this.#opened = true
|
||||
resolve(this)
|
||||
}
|
||||
this.onmessage = e => {
|
||||
if (this.#waiting.length) this.#waiting.shift().resolve(e.data)
|
||||
else this.#received.push(e.data)
|
||||
}
|
||||
this.onclose = e => {
|
||||
if (!this.#opened) {
|
||||
reject(new Error(`WebSocket ${this.url} failed to connect, code ${e.code}`))
|
||||
return
|
||||
}
|
||||
this.#err = e.wasClean
|
||||
? new Error(`Websocket ${this.url} closed ${e.code}`)
|
||||
: new Error(`WebSocket ${this.url} closed with error ${e.code}`)
|
||||
this.#waiting.splice(0).forEach(p => p.reject(this.#err))
|
||||
}
|
||||
}
|
||||
|
||||
receive() {
|
||||
// If we have a message already received, return it immediately
|
||||
if (this.#received.length) return Promise.resolve(this.#received.shift())
|
||||
// Wait for incoming messages, if we have an error, reject immediately
|
||||
if (this.#err) return Promise.reject(this.#err)
|
||||
return new Promise((resolve, reject) => this.#waiting.push({ resolve, reject }))
|
||||
}
|
||||
|
||||
async receive_bytes() {
|
||||
const data = await this.receive()
|
||||
if (typeof data === 'string') {
|
||||
console.error("WebSocket received text data, expected a binary message", data)
|
||||
throw new Error("WebSocket received text data, expected a binary message")
|
||||
}
|
||||
return data instanceof Blob ? data.bytes() : new Uint8Array(data)
|
||||
}
|
||||
|
||||
async receive_json() {
|
||||
const data = await this.receive()
|
||||
if (typeof data !== 'string') {
|
||||
console.error("WebSocket received binary data, expected JSON string", data)
|
||||
throw new Error("WebSocket received binary data, expected JSON string")
|
||||
}
|
||||
let parsed
|
||||
try {
|
||||
parsed = JSON.parse(data)
|
||||
} catch (err) {
|
||||
console.error("Failed to parse JSON from WebSocket message", data, err)
|
||||
throw new Error("Failed to parse JSON from WebSocket message")
|
||||
}
|
||||
if (parsed.error) {
|
||||
throw new Error(`Server: ${parsed.error}`)
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
send_json(data) {
|
||||
let jsonData
|
||||
try {
|
||||
jsonData = JSON.stringify(data)
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to stringify data for WebSocket: ${err.message}`)
|
||||
}
|
||||
this.send(jsonData)
|
||||
}
|
||||
}
|
||||
|
||||
// Construct an async WebSocket with await aWebSocket(url)
|
||||
export default function aWebSocket(url, options = {}) {
|
||||
const { protocols, binaryType } = options
|
||||
return new Promise((resolve, reject) => {
|
||||
new AwaitableWebSocket(resolve, reject, url, protocols, binaryType)
|
||||
})
|
||||
}
|
||||
24
frontend/src/utils/helpers.js
Normal file
24
frontend/src/utils/helpers.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// Utility functions
|
||||
|
||||
export function formatDate(dateString) {
|
||||
if (!dateString) return 'Never'
|
||||
|
||||
const date = new Date(dateString)
|
||||
const now = new Date()
|
||||
const diffMs = now - date
|
||||
const diffMinutes = Math.floor(diffMs / (1000 * 60))
|
||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (diffMs < 0 || diffDays > 7) return date.toLocaleDateString()
|
||||
if (diffMinutes === 0) return 'Just now'
|
||||
if (diffMinutes < 60) return diffMinutes === 1 ? 'a minute ago' : `${diffMinutes} minutes ago`
|
||||
if (diffHours < 24) return diffHours === 1 ? 'an hour ago' : `${diffHours} hours ago`
|
||||
return diffDays === 1 ? 'a day ago' : `${diffDays} days ago`
|
||||
}
|
||||
|
||||
export function getCookie(name) {
|
||||
const value = `; ${document.cookie}`
|
||||
const parts = value.split(`; ${name}=`)
|
||||
if (parts.length === 2) return parts.pop().split(';').shift()
|
||||
}
|
||||
38
frontend/src/utils/passkey.js
Normal file
38
frontend/src/utils/passkey.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { startRegistration, startAuthentication } from '@simplewebauthn/browser'
|
||||
import aWebSocket from '@/utils/awaitable-websocket'
|
||||
|
||||
export async function register(url, options) {
|
||||
if (options) url += `?${new URLSearchParams(options).toString()}`
|
||||
const ws = await aWebSocket(url)
|
||||
|
||||
const optionsJSON = await ws.receive_json()
|
||||
const registrationResponse = await startRegistration({ optionsJSON })
|
||||
ws.send_json(registrationResponse)
|
||||
|
||||
const result = await ws.receive_json()
|
||||
ws.close()
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function registerUser(user_name) {
|
||||
return register('/auth/ws/new_user_registration', { user_name })
|
||||
}
|
||||
|
||||
export async function registerCredential() {
|
||||
return register('/auth/ws/add_credential')
|
||||
}
|
||||
export async function registerWithToken(token) {
|
||||
return register('/auth/ws/add_device_credential', {token})
|
||||
}
|
||||
|
||||
export async function authenticateUser() {
|
||||
const ws = await aWebSocket('/auth/ws/authenticate')
|
||||
|
||||
const optionsJSON = await ws.receive_json()
|
||||
const authResponse = await startAuthentication({ optionsJSON })
|
||||
ws.send_json(authResponse)
|
||||
|
||||
const result = await ws.receive_json()
|
||||
ws.close()
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user