All checks were successful
ci/woodpecker/push/push Pipeline was successful
## Added - CoT (Cursor on Target) server on port 8089 enabling ATAK/iTAK device connectivity - Support for TAK stream protocol and traditional XML CoT messages - TLS/SSL support with automatic fallback to plain TCP - Username/password authentication for CoT connections - Real-time device position tracking with TTL-based expiration (90s default) - API endpoints: `/api/cot/config`, `/api/cot/server-package`, `/api/cot/truststore`, `/api/me/cot-password` - TAK Server section in Settings with QR code for iTAK setup - ATAK password management in Account page for OIDC users - CoT device markers on map showing real-time positions - Comprehensive documentation in `docs/` directory - Environment variables: `COT_PORT`, `COT_TTL_MS`, `COT_REQUIRE_AUTH`, `COT_SSL_CERT`, `COT_SSL_KEY`, `COT_DEBUG` - Dependencies: `fast-xml-parser`, `jszip`, `qrcode` ## Changed - Authentication system supports CoT password management for OIDC users - Database schema includes `cot_password_hash` field - Test suite refactored to follow functional design principles ## Removed - Consolidated utility modules: `authConfig.js`, `authSkipPaths.js`, `bootstrap.js`, `poiConstants.js`, `session.js` ## Security - XML entity expansion protection in CoT parser - Enhanced input validation and SQL injection prevention - Authentication timeout to prevent hanging connections ## Breaking Changes - Port 8089 must be exposed for CoT server. Update firewall rules and Docker/Kubernetes configurations. ## Migration Notes - OIDC users must set ATAK password via Account settings before connecting - Docker: expose port 8089 (`-p 8089:8089`) - Kubernetes: update Helm values to expose port 8089 Co-authored-by: Madison Grubb <madison@elastiflow.com> Reviewed-on: #6
127 lines
3.5 KiB
JavaScript
127 lines
3.5 KiB
JavaScript
import { closeRouter, getProducer, getTransport } from './mediasoup.js'
|
|
import { acquire } from './asyncLock.js'
|
|
import { LIVE_SESSION_TTL_MS } from './constants.js'
|
|
|
|
const sessions = new Map()
|
|
|
|
export const createSession = async (userId, label = '') => {
|
|
return acquire(`session-create-${userId}`, async () => {
|
|
const id = crypto.randomUUID()
|
|
const session = {
|
|
id,
|
|
userId,
|
|
label: (label || 'Live').trim() || 'Live',
|
|
lat: 0,
|
|
lng: 0,
|
|
updatedAt: Date.now(),
|
|
routerId: null,
|
|
producerId: null,
|
|
transportId: null,
|
|
}
|
|
sessions.set(id, session)
|
|
return session
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Atomically get existing active session or create new one for user.
|
|
* @param {string} userId - User ID
|
|
* @param {string} label - Session label
|
|
* @returns {Promise<object>} Session object
|
|
*/
|
|
export const getOrCreateSession = async (userId, label = '') => {
|
|
return acquire(`session-get-or-create-${userId}`, async () => {
|
|
const now = Date.now()
|
|
for (const s of sessions.values()) {
|
|
if (s.userId === userId && now - s.updatedAt <= LIVE_SESSION_TTL_MS) {
|
|
return s
|
|
}
|
|
}
|
|
return await createSession(userId, label)
|
|
})
|
|
}
|
|
|
|
export const getLiveSession = id => sessions.get(id)
|
|
|
|
export const getActiveSessionByUserId = async (userId) => {
|
|
return acquire(`session-get-${userId}`, async () => {
|
|
const now = Date.now()
|
|
for (const s of sessions.values()) {
|
|
if (s.userId === userId && now - s.updatedAt <= LIVE_SESSION_TTL_MS) return s
|
|
}
|
|
})
|
|
}
|
|
|
|
export const updateLiveSession = async (id, updates) => {
|
|
return acquire(`session-update-${id}`, async () => {
|
|
const session = sessions.get(id)
|
|
if (!session) {
|
|
throw new Error('Session not found')
|
|
}
|
|
const now = Date.now()
|
|
if (Number.isFinite(updates.lat)) session.lat = updates.lat
|
|
if (Number.isFinite(updates.lng)) session.lng = updates.lng
|
|
if (updates.routerId !== undefined) session.routerId = updates.routerId
|
|
if (updates.producerId !== undefined) session.producerId = updates.producerId
|
|
if (updates.transportId !== undefined) session.transportId = updates.transportId
|
|
session.updatedAt = now
|
|
return session
|
|
})
|
|
}
|
|
|
|
export const deleteLiveSession = async (id) => {
|
|
await acquire(`session-delete-${id}`, async () => {
|
|
sessions.delete(id)
|
|
})
|
|
}
|
|
|
|
export const clearSessions = () => sessions.clear()
|
|
|
|
const cleanupSession = async (session) => {
|
|
if (session.producerId) {
|
|
const producer = getProducer(session.producerId)
|
|
producer?.close()
|
|
}
|
|
if (session.transportId) {
|
|
const transport = getTransport(session.transportId)
|
|
transport?.close()
|
|
}
|
|
if (session.routerId) {
|
|
await closeRouter(session.id).catch((err) => {
|
|
console.error(`[liveSessions] Error closing router for expired session ${session.id}:`, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
export const getActiveSessions = async () => {
|
|
return acquire('get-active-sessions', async () => {
|
|
const now = Date.now()
|
|
const active = []
|
|
const expired = []
|
|
|
|
for (const session of sessions.values()) {
|
|
if (now - session.updatedAt <= LIVE_SESSION_TTL_MS) {
|
|
active.push({
|
|
id: session.id,
|
|
userId: session.userId,
|
|
label: session.label,
|
|
lat: session.lat,
|
|
lng: session.lng,
|
|
updatedAt: session.updatedAt,
|
|
hasStream: Boolean(session.producerId),
|
|
})
|
|
}
|
|
else {
|
|
expired.push(session)
|
|
}
|
|
}
|
|
|
|
for (const session of expired) {
|
|
await cleanupSession(session)
|
|
sessions.delete(session.id)
|
|
}
|
|
|
|
return active
|
|
})
|
|
}
|