minor: heavily simplify server and app content. unify styling (#4)
All checks were successful
ci/woodpecker/push/push Pipeline was successful

Co-authored-by: Madison Grubb <madison@elastiflow.com>
Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
2026-02-14 04:52:18 +00:00
parent 1a143d2f8e
commit 17f28401ba
40 changed files with 595 additions and 933 deletions

View File

@@ -1,17 +1,5 @@
/**
* Read auth config from env. Returns only non-secret data for client.
* Auth always allows local (password) sign-in and OIDC when configured.
* @returns {{ oidc: { enabled: boolean, label: string } }} Public auth config (oidc.enabled, oidc.label).
*/
export function getAuthConfig() {
const hasOidcEnv
= process.env.OIDC_ISSUER && process.env.OIDC_CLIENT_ID && process.env.OIDC_CLIENT_SECRET
const envLabel = process.env.OIDC_LABEL ?? ''
const label = envLabel || (hasOidcEnv ? 'Sign in with OIDC' : '')
return {
oidc: {
enabled: !!hasOidcEnv,
label,
},
}
const hasOidc = !!(process.env.OIDC_ISSUER && process.env.OIDC_CLIENT_ID && process.env.OIDC_CLIENT_SECRET)
const label = process.env.OIDC_LABEL?.trim() || (hasOidc ? 'Sign in with OIDC' : '')
return Object.freeze({ oidc: { enabled: hasOidc, label } })
}

View File

@@ -1,20 +1,10 @@
/**
* Require authenticated user. Optionally require role. Throws 401 if none, 403 if role insufficient.
* @param {import('h3').H3Event} event
* @param {{ role?: 'admin' | 'adminOrLeader' }} [opts] - role: 'admin' = admin only; 'adminOrLeader' = admin or leader
* @returns {{ id: string, identifier: string, role: string }} The current user.
*/
const ROLES_ADMIN_OR_LEADER = Object.freeze(['admin', 'leader'])
export function requireAuth(event, opts = {}) {
const user = event.context.user
if (!user) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
if (!user) throw createError({ statusCode: 401, message: 'Unauthorized' })
const { role } = opts
if (role === 'admin' && user.role !== 'admin') {
throw createError({ statusCode: 403, message: 'Forbidden' })
}
if (role === 'adminOrLeader' && user.role !== 'admin' && user.role !== 'leader') {
throw createError({ statusCode: 403, message: 'Forbidden' })
}
if (role === 'admin' && user.role !== 'admin') throw createError({ statusCode: 403, message: 'Forbidden' })
if (role === 'adminOrLeader' && !ROLES_ADMIN_OR_LEADER.includes(user.role)) throw createError({ statusCode: 403, message: 'Forbidden' })
return user
}

View File

@@ -1,30 +1,21 @@
/**
* Paths that skip auth middleware (no session required).
* Do not add a path here if any handler under it uses requireAuth (with or without role).
* When adding a new API route that requires auth, add its path prefix to PROTECTED_PATH_PREFIXES below
* so tests can assert it is never skipped.
*/
export const SKIP_PATHS = [
/** Paths that skip auth (no session required). Do not add if any handler uses requireAuth. */
export const SKIP_PATHS = Object.freeze([
'/api/auth/login',
'/api/auth/logout',
'/api/auth/config',
'/api/auth/oidc/authorize',
'/api/auth/oidc/callback',
]
])
/**
* Path prefixes for API routes that require an authenticated user (or role).
* Every path in this list must NOT be skipped (skipAuth must return false).
* Used by tests to prevent protected routes from being added to SKIP_PATHS.
*/
export const PROTECTED_PATH_PREFIXES = [
/** Path prefixes for protected routes. Used by tests to ensure they're never in SKIP_PATHS. */
export const PROTECTED_PATH_PREFIXES = Object.freeze([
'/api/cameras',
'/api/devices',
'/api/live',
'/api/me',
'/api/pois',
'/api/users',
]
])
export function skipAuth(path) {
if (path.startsWith('/api/health') || path === '/health') return true

View File

@@ -1,13 +1,10 @@
import { randomBytes } from 'node:crypto'
import { hashPassword } from './password.js'
const DEFAULT_ADMIN_IDENTIFIER = 'admin'
const PASSWORD_CHARS = 'abcdefghjkmnopqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789'
const PASSWORD_CHARS = Object.freeze('abcdefghjkmnopqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789')
const generateRandomPassword = () => {
const bytes = randomBytes(14)
return Array.from(bytes, b => PASSWORD_CHARS[b % PASSWORD_CHARS.length]).join('')
}
const generateRandomPassword = () =>
Array.from(randomBytes(14), b => PASSWORD_CHARS[b % PASSWORD_CHARS.length]).join('')
export async function bootstrapAdmin(run, get) {
const row = await get('SELECT COUNT(*) as n FROM users')
@@ -15,7 +12,7 @@ export async function bootstrapAdmin(run, get) {
const email = process.env.BOOTSTRAP_EMAIL?.trim()
const password = process.env.BOOTSTRAP_PASSWORD
const identifier = (email && password) ? email : DEFAULT_ADMIN_IDENTIFIER
const identifier = (email && password) ? email : 'admin'
const plainPassword = (email && password) ? password : generateRandomPassword()
await run(

View File

@@ -0,0 +1 @@
export const POI_ICON_TYPES = Object.freeze(['pin', 'flag', 'waypoint'])

View File

@@ -1,15 +1,6 @@
const DEFAULT_DAYS = 7
const MIN_DAYS = 1
const MAX_DAYS = 365
const [MIN_DAYS, MAX_DAYS, DEFAULT_DAYS] = [1, 365, 7]
/**
* Session lifetime in days (for cookie and DB expires_at). Uses SESSION_MAX_AGE_DAYS.
* Clamped to 1365 days.
*/
export function getSessionMaxAgeDays() {
const raw = process.env.SESSION_MAX_AGE_DAYS != null
? Number.parseInt(process.env.SESSION_MAX_AGE_DAYS, 10)
: Number.NaN
if (Number.isFinite(raw)) return Math.max(MIN_DAYS, Math.min(MAX_DAYS, raw))
return DEFAULT_DAYS
const raw = Number.parseInt(process.env.SESSION_MAX_AGE_DAYS ?? '', 10)
return Number.isFinite(raw) ? Math.max(MIN_DAYS, Math.min(MAX_DAYS, raw)) : DEFAULT_DAYS
}

View File

@@ -1,19 +1,6 @@
/**
* WebRTC signaling message handlers.
* Processes WebSocket messages for WebRTC operations.
*/
import { getLiveSession, updateLiveSession } from './liveSessions.js'
import { getRouter, createTransport, getTransport } from './mediasoup.js'
/**
* Handle WebSocket message for WebRTC signaling.
* @param {string} userId
* @param {string} sessionId
* @param {string} type
* @param {object} data
* @returns {Promise<object | null>} Response message or null
*/
export async function handleWebSocketMessage(userId, sessionId, type, data) {
const session = getLiveSession(sessionId)
if (!session) {