feat: allow to enable secure websocket for server channel (#1014)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
autofix-ci[bot]
parent
9b1b2420e7
commit
e4add70e13
@@ -201,6 +201,14 @@ CAPACITOR_DEV_SERVER_URL=https://<your-ip-address>:5273 pnpm open:ios
|
||||
|
||||
Then Xcode will open and you can click the "Run" button to run the app on your iPhone.
|
||||
|
||||
If you need to connect server channel on pocket in wireless mode, you need to start tamagotchi as root:
|
||||
|
||||
```shell
|
||||
sudo pnpm dev:tamagotchi
|
||||
```
|
||||
|
||||
Then enable secure websocket in tamagotchi `settings/system/general`.
|
||||
|
||||
### Documentation Site
|
||||
|
||||
```shell
|
||||
|
||||
@@ -130,8 +130,14 @@ export default defineConfig({
|
||||
VueRouter({
|
||||
dts: resolve(import.meta.dirname, 'src/renderer/typed-router.d.ts'),
|
||||
routesFolder: [
|
||||
{
|
||||
src: resolve(import.meta.dirname, '..', '..', 'packages', 'stage-pages', 'src', 'pages'),
|
||||
exclude: base => [
|
||||
...base,
|
||||
'**/settings/system/general.vue',
|
||||
],
|
||||
},
|
||||
resolve(import.meta.dirname, 'src', 'renderer', 'pages'),
|
||||
resolve(import.meta.dirname, '..', '..', 'packages', 'stage-pages', 'src', 'pages'),
|
||||
],
|
||||
exclude: ['**/components/**'],
|
||||
}),
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
"jszip": "^3.10.1",
|
||||
"localforage": "^1.10.0",
|
||||
"mediabunny": "^1.29.0",
|
||||
"mkcert": "catalog:",
|
||||
"node-vibrant": "^4.0.3",
|
||||
"nprogress": "^0.2.0",
|
||||
"onnxruntime-web": "^1.23.2",
|
||||
|
||||
@@ -13,7 +13,7 @@ import icon from '../../resources/icon.png?asset'
|
||||
import { openDebugger, setupDebugger } from './app/debugger'
|
||||
import { emitAppBeforeQuit, emitAppReady, emitAppWindowAllClosed } from './libs/bootkit/lifecycle'
|
||||
import { setElectronMainDirname } from './libs/electron/location'
|
||||
import { setupServerChannel } from './services/airi/channel-server'
|
||||
import { setupServerChannelHandlers } from './services/airi/channel-server'
|
||||
import { setupAutoUpdater } from './services/electron/auto-updater'
|
||||
import { setupTray } from './tray'
|
||||
import { setupAboutWindowReusable } from './windows/about'
|
||||
@@ -72,7 +72,7 @@ initScreenCaptureForMain()
|
||||
app.whenReady().then(async () => {
|
||||
injeca.setLogger(createLoggLogger(useLogg('injeca').useGlobalConfig()))
|
||||
|
||||
const serverChannel = injeca.provide('modules:channel-server', () => setupServerChannel())
|
||||
const serverChannel = injeca.provide('modules:channel-server', () => setupServerChannelHandlers())
|
||||
const autoUpdater = injeca.provide('services:auto-updater', () => setupAutoUpdater())
|
||||
const widgetsManager = injeca.provide('windows:widgets', () => setupWidgetsWindowManager())
|
||||
const noticeWindow = injeca.provide('windows:notice', () => setupNoticeWindowManager())
|
||||
|
||||
@@ -1,26 +1,139 @@
|
||||
import { env } from 'node:process'
|
||||
import { execSync } from 'node:child_process'
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { env, platform } from 'node:process'
|
||||
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
import { defineInvokeHandler } from '@moeru/eventa'
|
||||
import { createContext } from '@moeru/eventa/adapters/electron/main'
|
||||
import { app, ipcMain } from 'electron'
|
||||
import { createCA, createCert } from 'mkcert'
|
||||
|
||||
import { electronRestartWebSocketServer, electronStartWebSocketServer } from '../../../../shared/eventa'
|
||||
import { onAppBeforeQuit } from '../../../libs/bootkit/lifecycle'
|
||||
|
||||
export async function setupServerChannel() {
|
||||
let serverInstance: { close: (closeActiveConnections?: boolean) => Promise<void> } | null = null
|
||||
|
||||
async function installCACertificate(caCert: string) {
|
||||
const userDataPath = app.getPath('userData')
|
||||
const caCertPath = join(userDataPath, 'websocket-ca-cert.pem')
|
||||
writeFileSync(caCertPath, caCert)
|
||||
|
||||
try {
|
||||
if (platform === 'darwin') {
|
||||
execSync(`security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${caCertPath}"`, { stdio: 'ignore' })
|
||||
}
|
||||
else if (platform === 'win32') {
|
||||
execSync(`certutil -addstore -f "Root" "${caCertPath}"`, { stdio: 'ignore' })
|
||||
}
|
||||
else if (platform === 'linux') {
|
||||
const caDir = '/usr/local/share/ca-certificates'
|
||||
const caFileName = 'airi-websocket-ca.crt'
|
||||
try {
|
||||
writeFileSync(join(caDir, caFileName), caCert)
|
||||
execSync('update-ca-certificates', { stdio: 'ignore' })
|
||||
}
|
||||
catch {
|
||||
const userCaDir = join(env.HOME || '', '.local/share/ca-certificates')
|
||||
try {
|
||||
if (!existsSync(userCaDir)) {
|
||||
execSync(`mkdir -p "${userCaDir}"`, { stdio: 'ignore' })
|
||||
}
|
||||
writeFileSync(join(userCaDir, caFileName), caCert)
|
||||
}
|
||||
catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Ignore installation errors
|
||||
}
|
||||
}
|
||||
|
||||
async function generateCertificate() {
|
||||
const userDataPath = app.getPath('userData')
|
||||
const caCertPath = join(userDataPath, 'websocket-ca-cert.pem')
|
||||
const caKeyPath = join(userDataPath, 'websocket-ca-key.pem')
|
||||
|
||||
let ca: { key: string, cert: string }
|
||||
|
||||
if (existsSync(caCertPath) && existsSync(caKeyPath)) {
|
||||
ca = {
|
||||
cert: readFileSync(caCertPath, 'utf-8'),
|
||||
key: readFileSync(caKeyPath, 'utf-8'),
|
||||
}
|
||||
}
|
||||
else {
|
||||
ca = await createCA({
|
||||
organization: 'AIRI',
|
||||
countryCode: 'US',
|
||||
state: 'Development',
|
||||
locality: 'Local',
|
||||
validity: 365,
|
||||
})
|
||||
writeFileSync(caCertPath, ca.cert)
|
||||
writeFileSync(caKeyPath, ca.key)
|
||||
|
||||
await installCACertificate(ca.cert)
|
||||
}
|
||||
|
||||
const cert = await createCert({
|
||||
ca: { key: ca.key, cert: ca.cert },
|
||||
domains: ['localhost', '127.0.0.1', env.SERVER_RUNTIME_HOSTNAME || 'localhost'],
|
||||
validity: 365,
|
||||
})
|
||||
|
||||
return {
|
||||
cert: cert.cert,
|
||||
key: cert.key,
|
||||
}
|
||||
}
|
||||
|
||||
async function getOrCreateCertificate() {
|
||||
const userDataPath = app.getPath('userData')
|
||||
const certPath = join(userDataPath, 'websocket-cert.pem')
|
||||
const keyPath = join(userDataPath, 'websocket-key.pem')
|
||||
|
||||
if (existsSync(certPath) && existsSync(keyPath)) {
|
||||
return {
|
||||
cert: readFileSync(certPath, 'utf-8'),
|
||||
key: readFileSync(keyPath, 'utf-8'),
|
||||
}
|
||||
}
|
||||
|
||||
const { cert, key } = await generateCertificate()
|
||||
writeFileSync(certPath, cert)
|
||||
writeFileSync(keyPath, key)
|
||||
|
||||
return { cert, key }
|
||||
}
|
||||
|
||||
export async function setupServerChannel(options?: { websocketSecureEnabled?: boolean }) {
|
||||
const log = useLogg('main/server-runtime').useGlobalConfig()
|
||||
|
||||
// Start the server-runtime server with WebSocket support
|
||||
const secureEnabled = options?.websocketSecureEnabled ?? false
|
||||
|
||||
try {
|
||||
// Dynamically import the server-runtime and listhen
|
||||
const serverRuntime = await import('@proj-airi/server-runtime')
|
||||
const { serve } = await import('h3')
|
||||
const { plugin: ws } = await import('crossws/server')
|
||||
const { serve } = await import('h3')
|
||||
|
||||
const app = serverRuntime.setupApp()
|
||||
const h3App = serverRuntime.setupApp()
|
||||
|
||||
const serverInstance = serve(app, {
|
||||
// TODO: add proper crossws typing upstream
|
||||
plugins: [ws({ resolve: async req => (await app.fetch(req) as any).crossws })],
|
||||
port: env.PORT ? Number(env.PORT) : 6121,
|
||||
hostname: env.SERVER_RUNTIME_HOSTNAME || 'localhost',
|
||||
const port = env.PORT ? Number(env.PORT) : 6121
|
||||
const hostname = env.SERVER_RUNTIME_HOSTNAME || 'localhost'
|
||||
|
||||
// FIXME: should prompt user to grant permission to save certificate files on macOS
|
||||
const tls = secureEnabled ? await getOrCreateCertificate() : undefined
|
||||
|
||||
const instance = serve(h3App.app, {
|
||||
// @ts-expect-error - the .crossws property wasn't extended in types
|
||||
plugins: [ws({ resolve: async req => (await h3App.app.fetch(req)).crossws })],
|
||||
port,
|
||||
hostname,
|
||||
tls,
|
||||
reusePort: true,
|
||||
silent: true,
|
||||
manual: true,
|
||||
@@ -30,7 +143,17 @@ export async function setupServerChannel() {
|
||||
},
|
||||
})
|
||||
|
||||
const servePromise = serverInstance.serve()
|
||||
serverInstance = {
|
||||
close: async (closeActiveConnections = false) => {
|
||||
log.log('closing all peers')
|
||||
h3App.closeAllPeers()
|
||||
log.log('closing server instance')
|
||||
await instance.close(closeActiveConnections)
|
||||
log.log('server instance closed')
|
||||
},
|
||||
}
|
||||
|
||||
const servePromise = instance.serve()
|
||||
if (servePromise instanceof Promise) {
|
||||
servePromise.catch((error) => {
|
||||
const nodejsError = error as NodeJS.ErrnoException
|
||||
@@ -43,6 +166,9 @@ export async function setupServerChannel() {
|
||||
})
|
||||
}
|
||||
|
||||
const protocol = secureEnabled ? 'wss' : 'ws'
|
||||
log.log(`@proj-airi/server-runtime started on ${protocol}://${hostname}:${port}`)
|
||||
|
||||
onAppBeforeQuit(async () => {
|
||||
if (serverInstance && typeof serverInstance.close === 'function') {
|
||||
try {
|
||||
@@ -59,10 +185,38 @@ export async function setupServerChannel() {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
log.log('@proj-airi/server-runtime started on ws://localhost:6121')
|
||||
}
|
||||
catch (error) {
|
||||
log.withError(error).error('failed to start WebSocket server')
|
||||
}
|
||||
}
|
||||
|
||||
export async function restartServerChannel(options?: { websocketSecureEnabled?: boolean }) {
|
||||
const log = useLogg('main/server-runtime').useGlobalConfig()
|
||||
log.log('restarting server channel', { options })
|
||||
|
||||
if (serverInstance && typeof serverInstance.close === 'function') {
|
||||
try {
|
||||
log.log('closing existing server instance')
|
||||
await serverInstance.close(true)
|
||||
log.log('existing server instance closed')
|
||||
}
|
||||
catch {
|
||||
// Ignore errors when closing
|
||||
}
|
||||
}
|
||||
serverInstance = null
|
||||
await setupServerChannel(options)
|
||||
}
|
||||
|
||||
export function setupServerChannelHandlers() {
|
||||
const { context } = createContext(ipcMain)
|
||||
|
||||
defineInvokeHandler(context, electronStartWebSocketServer, async (req) => {
|
||||
await setupServerChannel({ websocketSecureEnabled: req?.websocketSecureEnabled })
|
||||
})
|
||||
|
||||
defineInvokeHandler(context, electronRestartWebSocketServer, async (req) => {
|
||||
await restartServerChannel({ websocketSecureEnabled: req?.websocketSecureEnabled })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import { toast, Toaster } from 'vue-sonner'
|
||||
|
||||
import ResizeHandler from './components/ResizeHandler.vue'
|
||||
|
||||
import { electronOpenSettings, electronStartTrackMousePosition } from '../shared/eventa'
|
||||
import { useElectronEventaContext } from './composables/electron-vueuse'
|
||||
import { electronOpenSettings, electronStartTrackMousePosition, electronStartWebSocketServer } from '../shared/eventa'
|
||||
import { useElectronEventaContext, useElectronEventaInvoke } from './composables/electron-vueuse'
|
||||
|
||||
const { isDark: dark } = useTheme()
|
||||
const i18n = useI18n()
|
||||
@@ -49,7 +49,8 @@ watch(dark, () => updateThemeColor(), { immediate: true })
|
||||
watch(route, () => updateThemeColor(), { immediate: true })
|
||||
onMounted(() => updateThemeColor())
|
||||
|
||||
// FIXME: store settings to file
|
||||
const startWebSocketServer = useElectronEventaInvoke(electronStartWebSocketServer)
|
||||
|
||||
onMounted(async () => {
|
||||
analyticsStore.initialize()
|
||||
cardStore.initialize()
|
||||
@@ -58,6 +59,7 @@ onMounted(async () => {
|
||||
await chatSessionStore.initialize()
|
||||
await displayModelsStore.loadDisplayModelsFromIndexedDB()
|
||||
await settingsStore.initializeStageModel()
|
||||
await startWebSocketServer({ websocketSecureEnabled: settingsStore.websocketSecureEnabled })
|
||||
|
||||
await serverChannelStore.initialize({ possibleEvents: ['ui:configure'] }).catch(err => console.error('Failed to initialize Mods Server Channel in App.vue:', err))
|
||||
await contextBridgeStore.initialize()
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import SettingsGeneralFields from '@proj-airi/stage-pages/components/settings-general-fields.vue'
|
||||
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { FieldCheckbox } from '@proj-airi/ui'
|
||||
import { watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { electronRestartWebSocketServer } from '../../../../shared/eventa'
|
||||
import { useElectronEventaInvoke } from '../../../composables/electron-vueuse'
|
||||
|
||||
const settings = useSettings()
|
||||
const { t } = useI18n()
|
||||
|
||||
const restartServer = useElectronEventaInvoke(electronRestartWebSocketServer)
|
||||
|
||||
watch(() => settings.websocketSecureEnabled, async (newValue) => {
|
||||
await restartServer({ websocketSecureEnabled: newValue })
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsGeneralFields>
|
||||
<template #additional-fields>
|
||||
<FieldCheckbox
|
||||
v-model="settings.websocketSecureEnabled"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (5 * 10)"
|
||||
:delay="5 * 50"
|
||||
:label="t('settings.websocket-secure-enabled.title')"
|
||||
:description="t('settings.websocket-secure-enabled.description')"
|
||||
/>
|
||||
</template>
|
||||
</SettingsGeneralFields>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
titleKey: settings.pages.system.general.title
|
||||
subtitleKey: settings.title
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -7,6 +7,8 @@ export const electronOpenSettings = defineInvokeEventa('eventa:invoke:electron:w
|
||||
export const electronOpenChat = defineInvokeEventa('eventa:invoke:electron:windows:chat:open')
|
||||
export const electronOpenSettingsDevtools = defineInvokeEventa('eventa:invoke:electron:windows:settings:devtools:open')
|
||||
export const electronOpenDevtoolsWindow = defineInvokeEventa<void, { route?: string }>('eventa:invoke:electron:windows:devtools:open')
|
||||
export const electronStartWebSocketServer = defineInvokeEventa<void, { websocketSecureEnabled: boolean }>('eventa:invoke:electron:start-websocket-server')
|
||||
export const electronRestartWebSocketServer = defineInvokeEventa<void, { websocketSecureEnabled: boolean }>('eventa:invoke:electron:restart-websocket-server')
|
||||
export const captionIsFollowingWindowChanged = defineEventa<boolean>('eventa:event:electron:windows:caption-overlay:is-following-window-changed')
|
||||
export const captionGetIsFollowingWindow = defineInvokeEventa<boolean>('eventa:invoke:electron:windows:caption-overlay:get-is-following-window')
|
||||
|
||||
|
||||
@@ -930,6 +930,10 @@ vrm:
|
||||
skybox:
|
||||
skybox-intensity: SkyBox Intensity
|
||||
skybox-specular-mix: Specular Mix
|
||||
websocket-secure-enabled:
|
||||
title: Enable Secure WebSocket (WSS)
|
||||
description: >-
|
||||
Enable WSS (WebSocket Secure) to allow HTTPS connections for mobile (pocket) in development mode. This will automatically generate a self-signed certificate.
|
||||
wip:
|
||||
title: Work in Progress
|
||||
description: >-
|
||||
|
||||
@@ -903,6 +903,10 @@ vrm:
|
||||
skybox:
|
||||
skybox-intensity: SkyBox Intensity
|
||||
skybox-specular-mix: Specular Mix
|
||||
websocket-secure-enabled:
|
||||
title: Habilitar WebSocket Seguro (WSS)
|
||||
description: >-
|
||||
Habilite WSS (WebSocket Secure) para permitir conexiones HTTPS para móvil (pocket) en modo de desarrollo. Esto generará automáticamente un certificado autofirmado.
|
||||
wip:
|
||||
title: Work in Progress
|
||||
description: >-
|
||||
|
||||
@@ -903,6 +903,10 @@ vrm:
|
||||
skybox:
|
||||
skybox-intensity: Intensité de la SkyBox
|
||||
skybox-specular-mix: Mélange spéculaire
|
||||
websocket-secure-enabled:
|
||||
title: Activer WebSocket Sécurisé (WSS)
|
||||
description: >-
|
||||
Activez WSS (WebSocket Secure) pour permettre les connexions HTTPS pour mobile (pocket) en mode développement. Cela générera automatiquement un certificat auto-signé.
|
||||
wip:
|
||||
title: Work in Progress
|
||||
description: >-
|
||||
|
||||
@@ -903,6 +903,10 @@ vrm:
|
||||
skybox:
|
||||
skybox-intensity: スカイボックスの強度
|
||||
skybox-specular-mix: 鏡面反射の混合
|
||||
websocket-secure-enabled:
|
||||
title: セキュア WebSocket (WSS) を有効にする
|
||||
description: >-
|
||||
開発モードでモバイル(pocket)のHTTPS接続を許可するために、WSS(WebSocket Secure)を有効にします。これにより自己署名証明書が自動的に生成されます。
|
||||
wip:
|
||||
title: 開発中
|
||||
description: >-
|
||||
|
||||
@@ -903,6 +903,10 @@ vrm:
|
||||
skybox:
|
||||
skybox-intensity: SkyBox Intensity
|
||||
skybox-specular-mix: Specular Mix
|
||||
websocket-secure-enabled:
|
||||
title: 보안 WebSocket (WSS) 활성화
|
||||
description: >-
|
||||
개발 모드에서 모바일(pocket)의 HTTPS 연결을 허용하기 위해 WSS(WebSocket Secure)를 활성화합니다. 자체 서명 인증서가 자동으로 생성됩니다.
|
||||
wip:
|
||||
title: Work in Progress
|
||||
description: >-
|
||||
|
||||
@@ -903,6 +903,10 @@ vrm:
|
||||
skybox:
|
||||
skybox-intensity: SkyBox Intensity
|
||||
skybox-specular-mix: Specular Mix
|
||||
websocket-secure-enabled:
|
||||
title: Включить безопасный WebSocket (WSS)
|
||||
description: >-
|
||||
Включите WSS (WebSocket Secure), чтобы разрешить HTTPS-подключения для мобильных устройств (pocket) в режиме разработки. Это автоматически сгенерирует самоподписанный сертификат.
|
||||
wip:
|
||||
title: Work in Progress
|
||||
description: >-
|
||||
|
||||
@@ -903,6 +903,10 @@ vrm:
|
||||
skybox:
|
||||
skybox-intensity: Cường Độ SkyBox
|
||||
skybox-specular-mix: Độ Trộn Phản Chiếu
|
||||
websocket-secure-enabled:
|
||||
title: Bật WebSocket Bảo Mật (WSS)
|
||||
description: >-
|
||||
Bật WSS (WebSocket Secure) để cho phép kết nối HTTPS cho di động (pocket) trong chế độ phát triển. Điều này sẽ tự động tạo chứng chỉ tự ký.
|
||||
wip:
|
||||
title: Work in Progress
|
||||
description: >-
|
||||
|
||||
@@ -903,6 +903,10 @@ vrm:
|
||||
skybox:
|
||||
skybox-intensity: 天空盒光照强度
|
||||
skybox-specular-mix: 漫反射/镜面反射混合系数
|
||||
websocket-secure-enabled:
|
||||
title: 启用安全 WebSocket (WSS)
|
||||
description: >-
|
||||
启用 WSS(WebSocket Secure)以允许开发模式下移动端(pocket)的 HTTPS 连接。这将自动生成自签名证书。
|
||||
wip:
|
||||
title: 正在开发中
|
||||
description: >-
|
||||
|
||||
@@ -903,6 +903,10 @@ vrm:
|
||||
skybox:
|
||||
skybox-intensity: 天空盒光照強度
|
||||
skybox-specular-mix: 漫反射/鏡面反射混合係數
|
||||
websocket-secure-enabled:
|
||||
title: 啟用安全 WebSocket (WSS)
|
||||
description: >-
|
||||
啟用 WSS(WebSocket Secure)以允許開發模式下行動裝置(pocket)的 HTTPS 連線。這將自動產生自簽名憑證。
|
||||
wip:
|
||||
title: Work in Progress
|
||||
description: >-
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"crossws": "^0.4.3",
|
||||
"h3": "^2.0.1-rc.11",
|
||||
"listhen": "^1.9.0",
|
||||
"nanoid": "catalog:"
|
||||
"nanoid": "catalog:",
|
||||
"srvx": "^0.10.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export function setupApp(options?: {
|
||||
readTimeout?: number
|
||||
message?: MessageHeartbeat | string
|
||||
}
|
||||
}): H3 {
|
||||
}): { app: H3, closeAllPeers: () => void } {
|
||||
const instanceId = options?.instanceId || optionOrEnv(undefined, 'SERVER_INSTANCE_ID', nanoid())
|
||||
const authToken = optionOrEnv(options?.auth?.token, 'AUTHENTICATION_TOKEN', '')
|
||||
|
||||
@@ -121,7 +121,7 @@ export function setupApp(options?: {
|
||||
if (now - peerInfo.lastHeartbeatAt > heartbeatTtlMs) {
|
||||
logger.withFields({ peer: id, peerName: peerInfo.name }).debug('heartbeat expired, dropping peer')
|
||||
try {
|
||||
(peerInfo.peer as Peer & { close?: () => void }).close?.()
|
||||
peerInfo.peer.close?.()
|
||||
}
|
||||
catch (error) {
|
||||
logger.withFields({ peer: id, peerName: peerInfo.name }).withError(error as Error).debug('failed to close expired peer')
|
||||
@@ -382,7 +382,18 @@ export function setupApp(options?: {
|
||||
},
|
||||
}))
|
||||
|
||||
return app
|
||||
function closeAllPeers() {
|
||||
console.log('closing all peers', peers.size)
|
||||
for (const peer of peers.values()) {
|
||||
console.log('closing peer', peer.peer.id)
|
||||
peer.peer.close?.()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
app,
|
||||
closeAllPeers,
|
||||
}
|
||||
}
|
||||
|
||||
export const app = setupApp() as H3
|
||||
export const { app, closeAllPeers: _ } = setupApp()
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface Peer {
|
||||
send: (data: unknown, options?: {
|
||||
compress?: boolean
|
||||
}) => number | void | undefined
|
||||
close?: () => void
|
||||
/**
|
||||
* WebSocket lifecycle state (mirrors WebSocket.readyState)
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import { all } from '@proj-airi/i18n'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { FieldCheckbox, FieldSelect, useTheme } from '@proj-airi/ui'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const settings = useSettings()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { isDark: dark } = useTheme()
|
||||
|
||||
const languages = computed(() => {
|
||||
return Object.entries(all).map(([value, label]) => ({ value, label }))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 rounded-lg bg-neutral-50 p-4 dark:bg-neutral-800">
|
||||
<FieldCheckbox
|
||||
v-model="dark"
|
||||
v-motion
|
||||
:class="['mb-2']"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (2 * 10)"
|
||||
:delay="2 * 50"
|
||||
:label="t('settings.theme.title')"
|
||||
:description="t('settings.theme.description')"
|
||||
/>
|
||||
|
||||
<FieldSelect
|
||||
v-model="settings.language"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (3 * 10)"
|
||||
:delay="3 * 50"
|
||||
:class="['transition-all', 'ease-in-out', 'duration-250']"
|
||||
:label="t('settings.language.title')"
|
||||
:description="t('settings.language.description')"
|
||||
:options="languages"
|
||||
/>
|
||||
|
||||
<slot name="additional-fields" />
|
||||
|
||||
<div
|
||||
v-motion
|
||||
:class="['text-neutral-200/50', 'dark:text-neutral-600/20', 'pointer-events-none', 'fixed', 'top-[65dvh]', 'right--15', 'z--1', 'flex', 'items-center', 'justify-center']"
|
||||
:initial="{ scale: 0.9, opacity: 0, rotate: 30 }"
|
||||
:enter="{ scale: 1, opacity: 1, rotate: 0 }"
|
||||
:duration="250"
|
||||
>
|
||||
<div :class="['text-60', 'i-solar:emoji-funny-square-bold-duotone']" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -31,6 +31,8 @@ export const useSettingsGeneral = defineStore('settings-general', () => {
|
||||
const disableTransitions = useLocalStorageManualReset<boolean>('settings/disable-transitions', true)
|
||||
const usePageSpecificTransitions = useLocalStorageManualReset<boolean>('settings/use-page-specific-transitions', true)
|
||||
|
||||
const websocketSecureEnabled = useLocalStorageManualReset<boolean>('settings/websocket/secure-enabled', false)
|
||||
|
||||
function getLanguage() {
|
||||
let language = localStorage.getItem('settings/language')
|
||||
|
||||
@@ -53,6 +55,7 @@ export const useSettingsGeneral = defineStore('settings-general', () => {
|
||||
language.reset()
|
||||
disableTransitions.reset()
|
||||
usePageSpecificTransitions.reset()
|
||||
websocketSecureEnabled.reset()
|
||||
}
|
||||
|
||||
onMounted(() => language.value = getLanguage())
|
||||
@@ -61,6 +64,7 @@ export const useSettingsGeneral = defineStore('settings-general', () => {
|
||||
language,
|
||||
disableTransitions,
|
||||
usePageSpecificTransitions,
|
||||
websocketSecureEnabled,
|
||||
getLanguage,
|
||||
resetState,
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ export const useSettings = defineStore('settings', () => {
|
||||
disableTransitions: generalRefs.disableTransitions,
|
||||
usePageSpecificTransitions: generalRefs.usePageSpecificTransitions,
|
||||
language: generalRefs.language,
|
||||
websocketSecureEnabled: generalRefs.websocketSecureEnabled,
|
||||
|
||||
// Stage model settings
|
||||
stageModelRenderer: stageModelRefs.stageModelRenderer,
|
||||
|
||||
Generated
+19
@@ -144,6 +144,9 @@ catalogs:
|
||||
knip:
|
||||
specifier: ^5.82.1
|
||||
version: 5.82.1
|
||||
mkcert:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
nano-staged:
|
||||
specifier: ^0.9.0
|
||||
version: 0.9.0
|
||||
@@ -1025,6 +1028,9 @@ importers:
|
||||
mediabunny:
|
||||
specifier: ^1.29.0
|
||||
version: 1.29.0
|
||||
mkcert:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.0
|
||||
node-vibrant:
|
||||
specifier: ^4.0.3
|
||||
version: 4.0.3(encoding@0.1.13)
|
||||
@@ -1945,6 +1951,9 @@ importers:
|
||||
nanoid:
|
||||
specifier: 'catalog:'
|
||||
version: 5.1.6
|
||||
srvx:
|
||||
specifier: ^0.10.1
|
||||
version: 0.10.1
|
||||
|
||||
packages/server-sdk:
|
||||
dependencies:
|
||||
@@ -13235,6 +13244,11 @@ packages:
|
||||
mitt@3.0.1:
|
||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||
|
||||
mkcert@3.2.0:
|
||||
resolution: {integrity: sha512-026Eivq9RoOjOuLJGzbhGwXUAjBxRX11Z7Jbm4/7lqT/Av+XNy9SPrJte6+UpEt7i+W3e/HZYxQqlQcqXZWSzg==}
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
mkdirp-classic@0.5.3:
|
||||
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||
|
||||
@@ -27981,6 +27995,11 @@ snapshots:
|
||||
|
||||
mitt@3.0.1: {}
|
||||
|
||||
mkcert@3.2.0:
|
||||
dependencies:
|
||||
commander: 11.1.0
|
||||
node-forge: 1.3.3
|
||||
|
||||
mkdirp-classic@0.5.3: {}
|
||||
|
||||
mkdirp@0.5.6:
|
||||
|
||||
@@ -70,6 +70,7 @@ catalog:
|
||||
injeca: ^0.1.7
|
||||
is-network-error: ^1.3.0
|
||||
knip: ^5.82.1
|
||||
mkcert: ^3.2.0
|
||||
nano-staged: ^0.9.0
|
||||
nanoid: 5.1.6
|
||||
ofetch: ^1.5.1
|
||||
|
||||
Reference in New Issue
Block a user