diff --git a/api/lib/control/video-service.ts b/api/lib/control/video-service.ts index 7060bf15f..b69d3a511 100644 --- a/api/lib/control/video-service.ts +++ b/api/lib/control/video-service.ts @@ -3,6 +3,7 @@ import Config from '../config.js'; import { Type, Static } from '@sinclair/typebox'; import { VideoLeaseResponse } from '../types.js'; import fetch from '../fetch.js'; +import TAKAPI, { APIAuthCertificate } from '../tak-api.js'; export const Protocols = Type.Object({ rtmp: Type.Optional(Type.Object({ @@ -361,6 +362,29 @@ export default class VideoServiceControl { return lease; } + async from( + leaseid: string, + opts: { + username: string + admin: boolean + } + ): Promise<Static<typeof VideoLeaseResponse>> { + const lease = await this.config.models.VideoLease.from(leaseid); + + if (!opts.admin) { + const profile = await this.config.models.Profile.from(opts.username); + const api = await TAKAPI.init(new URL(String(this.config.server.api)), new APIAuthCertificate(profile.auth.cert, profile.auth.key)); + const groups = (await api.Group.list({ useCache: true })) + .data.map((group) => group.name); + + if (lease.username !== opts.username && (!lease.channel || !groups.includes(lease.channel))) { + throw new Err(400, null, 'You can only access a lease you created or that is assigned to a channel you are in'); + } + } + + return lease; + } + async commit( leaseid: string, body: { @@ -376,15 +400,7 @@ export default class VideoServiceControl { const video = await this.settings(); if (!video.configured) throw new Err(400, null, 'Media Integration is not configured'); - let lease = await this.config.models.VideoLease.from(leaseid); - - if (opts.admin) { - lease = await this.config.models.VideoLease.commit(leaseid, body); - } else if (lease.username === opts.username) { - lease = await this.config.models.VideoLease.commit(leaseid, body); - } else { - throw new Err(400, null, 'You can only update a lease you created'); - } + const lease = await this.from(leaseid, opts); try { await this.path(lease.path); @@ -435,14 +451,20 @@ export default class VideoServiceControl { } } - async delete(leaseid: string): Promise<void> { + async delete( + leaseid: string, + opts: { + username: string; + admin: boolean; + } + ): Promise<void> { const video = await this.settings(); if (!video.configured) throw new Err(400, null, 'Media Integration is not configured'); const headers = this.headers(video.username, video.password); - const lease = await this.config.models.VideoLease.from(leaseid); + const lease = await this.from(leaseid, opts); await this.config.models.VideoLease.delete(leaseid); diff --git a/api/routes/video-lease.ts b/api/routes/video-lease.ts index 56f46ecfe..19e55f11b 100644 --- a/api/routes/video-lease.ts +++ b/api/routes/video-lease.ts @@ -10,6 +10,7 @@ import { randomUUID } from 'node:crypto'; import { StandardResponse, VideoLeaseResponse } from '../lib/types.js'; import ECSVideoControl, { Protocols } from '../lib/control/video-service.js'; import * as Default from '../lib/limits.js'; +import TAKAPI, { APIAuthCertificate } from '../lib/tak-api.js'; export default async function router(schema: Schema, config: Config) { const videoControl = new ECSVideoControl(config); @@ -55,6 +56,12 @@ export default async function router(schema: Schema, config: Config) { } else { const user = await Auth.as_user(config, req); + const profile = await config.models.Profile.from(user.email); + const api = await TAKAPI.init(new URL(String(config.server.api)), new APIAuthCertificate(profile.auth.cert, profile.auth.key)); + + const groups = (await api.Group.list({ useCache: true })) + .data.map((group) => group.name); + res.json(await config.models.VideoLease.list({ limit: req.query.limit, page: req.query.page, @@ -62,7 +69,7 @@ export default async function router(schema: Schema, config: Config) { sort: req.query.sort, where: sql` name ~* ${req.query.filter} - AND username = ${user.email} + AND (username = ${user.email} OR channel IN ${groups}) AND ephemeral = ${req.query.ephemeral} ` })); @@ -87,16 +94,10 @@ export default async function router(schema: Schema, config: Config) { try { const user = await Auth.as_user(config, req); - let lease; - if (user.access === AuthUserAccess.ADMIN) { - lease = await config.models.VideoLease.from(req.params.lease); - } else { - lease = await config.models.VideoLease.from(req.params.lease); - - if (lease.username !== user.email) { - throw new Err(400, null, 'You can only delete a lease you created'); - } - } + const lease = await videoControl.from(req.params.lease, { + username: user.email, + admin: user.access === AuthUserAccess.ADMIN + }); res.json({ lease, @@ -233,17 +234,10 @@ export default async function router(schema: Schema, config: Config) { try { const user = await Auth.as_user(config, req); - if (user.access === AuthUserAccess.ADMIN) { - await videoControl.delete(req.params.lease); - } else { - const lease = await config.models.VideoLease.from(req.params.lease); - - if (lease.username === user.email) { - await videoControl.delete(req.params.lease); - } else { - throw new Err(400, null, 'You can only delete a lease you created'); - } - } + await videoControl.delete(req.params.lease, { + username: user.email, + admin: user.access === AuthUserAccess.ADMIN + }); res.json({ status: 200, diff --git a/api/web/src/components/Admin/Videos/AdminVideoLeases.vue b/api/web/src/components/Admin/Videos/AdminVideoLeases.vue index 52a1cc31d..efbe1b817 100644 --- a/api/web/src/components/Admin/Videos/AdminVideoLeases.vue +++ b/api/web/src/components/Admin/Videos/AdminVideoLeases.vue @@ -142,7 +142,7 @@ const list = ref<VideoLeaseList>({ items: [] }); -watch(paging, async () => { +watch(paging.value, async () => { await fetchList(); }); diff --git a/api/web/src/components/CloudTAK/Menu/Videos.vue b/api/web/src/components/CloudTAK/Menu/Videos.vue index f7c244735..d7082e4bd 100644 --- a/api/web/src/components/CloudTAK/Menu/Videos.vue +++ b/api/web/src/components/CloudTAK/Menu/Videos.vue @@ -86,6 +86,10 @@ label='Video Connections' :create='false' /> + <TablerAlert + v-else-if='error' + :err='error' + /> <template v-else> <div v-for='connection in connections.videoConnections' @@ -131,6 +135,10 @@ label='Video Leases' :create='false' /> + <TablerAlert + v-else-if='error' + :err='error' + /> <div v-for='l in leases.items' v-else @@ -199,6 +207,7 @@ import { useCOTStore } from '../../../stores/cots.ts'; import { useVideoStore } from '../../../stores/videos.ts'; import { TablerNone, + TablerAlert, TablerDelete, TablerIconButton } from '@tak-ps/vue-tabler'; @@ -215,6 +224,7 @@ const cotStore = useCOTStore(); const videoStore = useVideoStore(); const mode = ref('connections'); +const error = ref<Error | undefined>(); const loading = ref(true); const lease = ref(); const leases = ref<VideoLeaseList>({ total: 0, items: [] }); @@ -241,16 +251,28 @@ function expired(expiration: string | null): boolean { } async function fetchLeases(): Promise<void> { - lease.value = undefined; - loading.value = true; - leases.value = await std('/api/video/lease') as VideoLeaseList + try { + lease.value = undefined; + loading.value = true; + error.value = undefined; + leases.value = await std('/api/video/lease') as VideoLeaseList + } catch (err) { + error.value = err instanceof Error ? err : new Error(String(err)); + } + loading.value = false; } async function fetchConnections(): Promise<void> { - lease.value = undefined; - loading.value = true; - connections.value = await std('/api/marti/video') as VideoConnectionList; + try { + lease.value = undefined; + loading.value = true; + error.value = undefined; + connections.value = await std('/api/marti/video') as VideoConnectionList; + } catch (err) { + error.value = err instanceof Error ? err : new Error(String(err)); + } + loading.value = false; }