From dc6a68967abec9e3db8a50b7871cb4f7006bc533 Mon Sep 17 00:00:00 2001 From: ingalls Date: Wed, 6 Mar 2024 08:36:15 -0700 Subject: [PATCH 01/17] Latest CoTs and Changes --- api/lib/api/mission.ts | 35 +++++++++++++++++++++++++++++++++-- api/routes/data-asset.ts | 4 +++- api/routes/marti-mission.ts | 4 +++- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/api/lib/api/mission.ts b/api/lib/api/mission.ts index cae88ee4f..83c9dc32b 100644 --- a/api/lib/api/mission.ts +++ b/api/lib/api/mission.ts @@ -57,6 +57,11 @@ export const AttachContentsInput = Type.Object({ uids: Type.Optional(Type.Array(Type.String())), }); +export const DetachContentsInput = Type.Object({ + hash: Type.Optional(Type.String()), + uid: Type.Optional(Type.String()) +}); + /** * @class */ @@ -81,6 +86,32 @@ export default class { } } + changes(name: string, query: { + secago: number; + start: string; + end: string; + squashed: boolean; + + [key: string]: unknown; + }, opts?: Static) { + const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/changes`, this.api.url); + + for (const q in query) url.searchParams.append(q, String(query[q])); + return await this.api.fetch(url, { + method: 'GET', + headers: this.#headers(opts), + }); + } + + latestCots(name: string, opts?: Static): Promise { + const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/cot`, this.api.url); + + return await this.api.fetch(url, { + method: 'GET', + headers: this.#headers(opts) + }); + } + /** * Return users associated with this mission */ @@ -96,9 +127,9 @@ export default class { /** * Remove a file from the mission */ - async detachContents(name: string, hash: string, opts?: Static) { + async detachContents(name: string, body: Static, opts?: Static) { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/contents`, this.api.url); - url.searchParams.append('hash', hash); + if (body.hash) url.searchParams.append('hash', hash); return await this.api.fetch(url, { method: 'DELETE', diff --git a/api/routes/data-asset.ts b/api/routes/data-asset.ts index 61d891121..59c97fa8c 100644 --- a/api/routes/data-asset.ts +++ b/api/routes/data-asset.ts @@ -252,7 +252,9 @@ export default async function router(schema: Schema, config: Config) { for (const content of mission.contents) { if (content.data.name === file) { - await api.Mission.detachContents(data.name, content.data.hash, { + await api.Mission.detachContents(data.name, { + hashes: [content.data.hash] + }, { token: data.mission_token }); } diff --git a/api/routes/marti-mission.ts b/api/routes/marti-mission.ts index 567aacf58..978914dc3 100644 --- a/api/routes/marti-mission.ts +++ b/api/routes/marti-mission.ts @@ -316,7 +316,9 @@ export default async function router(schema: Schema, config: Config) { const api = await TAKAPI.init(new URL(String(config.server.api)), new APIAuthCertificate(auth.cert, auth.key)); - const missionContent = await api.Mission.detachContents(req.params.name, req.params.hash); + const missionContent = await api.Mission.detachContents(req.params.name, { + hashes [req.params.hash] + }); return res.json(missionContent); } catch (err) { From a9822a11dee535d3842a512cf487ff019a19af20 Mon Sep 17 00:00:00 2001 From: ingalls Date: Wed, 6 Mar 2024 08:52:49 -0700 Subject: [PATCH 02/17] Add Profile Missions --- api/lib/api/mission.ts | 7 ++++--- api/lib/schema.ts | 9 +++++++++ api/routes/data-asset.ts | 2 +- api/routes/marti-mission.ts | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/api/lib/api/mission.ts b/api/lib/api/mission.ts index 83c9dc32b..70a837895 100644 --- a/api/lib/api/mission.ts +++ b/api/lib/api/mission.ts @@ -86,7 +86,7 @@ export default class { } } - changes(name: string, query: { + async changes(name: string, query: { secago: number; start: string; end: string; @@ -103,7 +103,7 @@ export default class { }); } - latestCots(name: string, opts?: Static): Promise { + async latestCots(name: string, opts?: Static): Promise { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/cot`, this.api.url); return await this.api.fetch(url, { @@ -129,7 +129,8 @@ export default class { */ async detachContents(name: string, body: Static, opts?: Static) { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/contents`, this.api.url); - if (body.hash) url.searchParams.append('hash', hash); + if (body.hash) url.searchParams.append('hash', body.hash); + if (body.uid) url.searchParams.append('uid', body.uid); return await this.api.fetch(url, { method: 'DELETE', diff --git a/api/lib/schema.ts b/api/lib/schema.ts index a7e4aaeae..168ff142a 100644 --- a/api/lib/schema.ts +++ b/api/lib/schema.ts @@ -204,6 +204,15 @@ export const ConnectionToken = pgTable('connection_tokens', { updated: timestamp('updated', { withTimezone: true, mode: 'string' }).notNull().default(sql`Now()`), }); +export const ProfileMissions = pgTable('profile_missions', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + guid: text('guid').notNull(), + token: text('token').notNull(), + created: timestamp('created', { withTimezone: true, mode: 'string' }).notNull().default(sql`Now()`), + updated: timestamp('updated', { withTimezone: true, mode: 'string' }).notNull().default(sql`Now()`), +}); + export const ProfileOverlay = pgTable('profile_overlays', { id: serial('id').primaryKey(), name: text('name').notNull(), diff --git a/api/routes/data-asset.ts b/api/routes/data-asset.ts index 59c97fa8c..5b0bc2edd 100644 --- a/api/routes/data-asset.ts +++ b/api/routes/data-asset.ts @@ -253,7 +253,7 @@ export default async function router(schema: Schema, config: Config) { for (const content of mission.contents) { if (content.data.name === file) { await api.Mission.detachContents(data.name, { - hashes: [content.data.hash] + hash: content.data.hash }, { token: data.mission_token }); diff --git a/api/routes/marti-mission.ts b/api/routes/marti-mission.ts index 978914dc3..0d5d7fd8a 100644 --- a/api/routes/marti-mission.ts +++ b/api/routes/marti-mission.ts @@ -317,7 +317,7 @@ export default async function router(schema: Schema, config: Config) { const api = await TAKAPI.init(new URL(String(config.server.api)), new APIAuthCertificate(auth.cert, auth.key)); const missionContent = await api.Mission.detachContents(req.params.name, { - hashes [req.params.hash] + hash: req.params.hash }); return res.json(missionContent); From bef6719162c98e75c8689b0735bfc270bd2d5a20 Mon Sep 17 00:00:00 2001 From: ingalls Date: Wed, 6 Mar 2024 09:09:47 -0700 Subject: [PATCH 03/17] Add Migration Files --- api/migrations/0018_first_bucky.sql | 8 + api/migrations/meta/0018_snapshot.json | 1319 ++++++++++++++++++++++++ api/migrations/meta/_journal.json | 7 + 3 files changed, 1334 insertions(+) create mode 100644 api/migrations/0018_first_bucky.sql create mode 100644 api/migrations/meta/0018_snapshot.json diff --git a/api/migrations/0018_first_bucky.sql b/api/migrations/0018_first_bucky.sql new file mode 100644 index 000000000..789c04dce --- /dev/null +++ b/api/migrations/0018_first_bucky.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS "profile_missions" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "guid" text NOT NULL, + "token" text NOT NULL, + "created" timestamp with time zone DEFAULT Now() NOT NULL, + "updated" timestamp with time zone DEFAULT Now() NOT NULL +); diff --git a/api/migrations/meta/0018_snapshot.json b/api/migrations/meta/0018_snapshot.json new file mode 100644 index 000000000..5338ad455 --- /dev/null +++ b/api/migrations/meta/0018_snapshot.json @@ -0,0 +1,1319 @@ +{ + "id": "5827bcce-0220-4af9-8e40-6d2408aa6bd4", + "prevId": "7b758713-70ed-464f-8502-4c0dca92c9bf", + "version": "5", + "dialect": "pg", + "tables": { + "basemaps": { + "name": "basemaps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bounds": { + "name": "bounds", + "type": "GEOMETRY(POLYGON, 4326)", + "primaryKey": false, + "notNull": false + }, + "center": { + "name": "center", + "type": "GEOMETRY(POINT, 4326)", + "primaryKey": false, + "notNull": false + }, + "minzoom": { + "name": "minzoom", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "maxzoom": { + "name": "maxzoom", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 16 + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'png'" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'raster'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "connections": { + "name": "connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth": { + "name": "auth", + "type": "json", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "connection_sinks": { + "name": "connection_sinks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "connection": { + "name": "connection", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "logging": { + "name": "logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "connection_sinks_connection_connections_id_fk": { + "name": "connection_sinks_connection_connections_id_fk", + "tableFrom": "connection_sinks", + "tableTo": "connections", + "columnsFrom": [ + "connection" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "connection_tokens": { + "name": "connection_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "connection": { + "name": "connection", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + } + }, + "indexes": {}, + "foreignKeys": { + "connection_tokens_connection_connections_id_fk": { + "name": "connection_tokens_connection_connections_id_fk", + "tableFrom": "connection_tokens", + "tableTo": "connections", + "columnsFrom": [ + "connection" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "data": { + "name": "data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "auto_transform": { + "name": "auto_transform", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "mission_sync": { + "name": "mission_sync", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "mission_role": { + "name": "mission_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'MISSION_SUBSCRIBER'" + }, + "mission_token": { + "name": "mission_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mission_groups": { + "name": "mission_groups", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": [] + }, + "assets": { + "name": "assets", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[\"*\"]'::json" + }, + "connection": { + "name": "connection", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "data_connection_connections_id_fk": { + "name": "data_connection_connections_id_fk", + "tableFrom": "data", + "tableTo": "connections", + "columnsFrom": [ + "connection" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "icons": { + "name": "icons", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "iconset": { + "name": "iconset", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type2525b": { + "name": "type2525b", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "icons_iconset_iconsets_uid_fk": { + "name": "icons_iconset_iconsets_uid_fk", + "tableFrom": "icons", + "tableTo": "iconsets", + "columnsFrom": [ + "iconset" + ], + "columnsTo": [ + "uid" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "iconsets": { + "name": "iconsets", + "schema": "", + "columns": { + "uid": { + "name": "uid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_group": { + "name": "default_group", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_friendly": { + "name": "default_friendly", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_hostile": { + "name": "default_hostile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_neutral": { + "name": "default_neutral", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_unknown": { + "name": "default_unknown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "skip_resize": { + "name": "skip_resize", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "imports": { + "name": "imports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Pending'" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Unknown'" + }, + "mode_id": { + "name": "mode_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + } + }, + "indexes": {}, + "foreignKeys": { + "imports_username_profile_username_fk": { + "name": "imports_username_profile_username_fk", + "tableFrom": "imports", + "tableTo": "profile", + "columnsFrom": [ + "username" + ], + "columnsTo": [ + "username" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "layers": { + "name": "layers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enabled_styles": { + "name": "enabled_styles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "styles": { + "name": "styles", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "logging": { + "name": "logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "stale": { + "name": "stale", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20000 + }, + "task": { + "name": "task", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection": { + "name": "connection", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment": { + "name": "environment", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "memory": { + "name": "memory", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 128 + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 128 + }, + "data": { + "name": "data", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + } + }, + "indexes": {}, + "foreignKeys": { + "layers_connection_connections_id_fk": { + "name": "layers_connection_connections_id_fk", + "tableFrom": "layers", + "tableTo": "connections", + "columnsFrom": [ + "connection" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "layers_data_data_id_fk": { + "name": "layers_data_data_id_fk", + "tableFrom": "layers", + "tableTo": "data", + "columnsFrom": [ + "data" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "layer_alerts": { + "name": "layer_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "layer": { + "name": "layer", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'alert-circle'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'yellow'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Details Unknown'" + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "layer_alerts_layer_layers_id_fk": { + "name": "layer_alerts_layer_layers_id_fk", + "tableFrom": "layer_alerts", + "tableTo": "layers", + "columnsFrom": [ + "layer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "profile": { + "name": "profile", + "schema": "", + "columns": { + "username": { + "name": "username", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "auth": { + "name": "auth", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "tak_callsign": { + "name": "tak_callsign", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'CloudTAK User'" + }, + "tak_group": { + "name": "tak_group", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Orange'" + }, + "tak_role": { + "name": "tak_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Team Member'" + }, + "tak_loc": { + "name": "tak_loc", + "type": "GEOMETRY(POINT, 4326)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "profile_chats": { + "name": "profile_chats", + "schema": "", + "columns": { + "username": { + "name": "username", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "chatroom": { + "name": "chatroom", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sender_callsign": { + "name": "sender_callsign", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sender_uid": { + "name": "sender_uid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "profile_missions": { + "name": "profile_missions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "guid": { + "name": "guid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "profile_overlays": { + "name": "profile_overlays", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "pos": { + "name": "pos", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'vector'" + }, + "opacity": { + "name": "opacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "visible": { + "name": "visible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode_id": { + "name": "mode_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "profile_overlays_username_profile_username_fk": { + "name": "profile_overlays_username_profile_username_fk", + "tableFrom": "profile_overlays", + "tableTo": "profile", + "columnsFrom": [ + "username" + ], + "columnsTo": [ + "username" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "profile_overlays_username_url_unique": { + "name": "profile_overlays_username_url_unique", + "nullsNotDistinct": false, + "columns": [ + "username", + "url" + ] + } + } + }, + "server": { + "name": "server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Default'" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth": { + "name": "auth", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "api": { + "name": "api", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "spatial_ref_sys": { + "name": "spatial_ref_sys", + "schema": "", + "columns": { + "srid": { + "name": "srid", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "auth_name": { + "name": "auth_name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "auth_srid": { + "name": "auth_srid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "srtext": { + "name": "srtext", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": false + }, + "proj4text": { + "name": "proj4text", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "tokens": { + "name": "tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "Now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/api/migrations/meta/_journal.json b/api/migrations/meta/_journal.json index cdd6bd89f..a8cc2f7e3 100644 --- a/api/migrations/meta/_journal.json +++ b/api/migrations/meta/_journal.json @@ -127,6 +127,13 @@ "when": 1709588304086, "tag": "0017_fantastic_robbie_robertson", "breakpoints": true + }, + { + "idx": 18, + "version": "5", + "when": 1709741317451, + "tag": "0018_first_bucky", + "breakpoints": true } ] } \ No newline at end of file From 7836463470adb34599206970a3f36ac28906138c Mon Sep 17 00:00:00 2001 From: ingalls Date: Wed, 6 Mar 2024 09:13:36 -0700 Subject: [PATCH 04/17] Add ProfileMission --- api/lib/models.ts | 2 ++ api/lib/schema.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/api/lib/models.ts b/api/lib/models.ts index 776685315..a10243623 100644 --- a/api/lib/models.ts +++ b/api/lib/models.ts @@ -16,6 +16,7 @@ export default class Models { Profile: Modeler; ProfileChat: ProfileChat; ProfileOverlay: Modeler; + ProfileMission: Modeler; Iconset: Modeler; Icon: Modeler; @@ -30,6 +31,7 @@ export default class Models { this.Server = new Modeler(pg, pgtypes.Server); this.Profile = new Modeler(pg, pgtypes.Profile); this.ProfileOverlay = new Modeler(pg, pgtypes.ProfileOverlay); + this.ProfileMission = new Modeler(pg, pgtypes.ProfileMission); this.Basemap = new Modeler(pg, pgtypes.Basemap); this.Import = new Modeler(pg, pgtypes.Import); this.Connection = new Modeler(pg, pgtypes.Connection); diff --git a/api/lib/schema.ts b/api/lib/schema.ts index 168ff142a..fefee0e76 100644 --- a/api/lib/schema.ts +++ b/api/lib/schema.ts @@ -204,7 +204,7 @@ export const ConnectionToken = pgTable('connection_tokens', { updated: timestamp('updated', { withTimezone: true, mode: 'string' }).notNull().default(sql`Now()`), }); -export const ProfileMissions = pgTable('profile_missions', { +export const ProfileMission = pgTable('profile_missions', { id: serial('id').primaryKey(), name: text('name').notNull(), guid: text('guid').notNull(), From e16f103aeb8dfe413742fcbde845424aeb48dd51 Mon Sep 17 00:00:00 2001 From: ingalls Date: Wed, 6 Mar 2024 10:29:56 -0700 Subject: [PATCH 05/17] Only call Auth once & add changes api --- api/routes/marti-mission.ts | 51 +++++++++++-------- .../components/CloudTAK/Mission/Mission.vue | 2 +- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/api/routes/marti-mission.ts b/api/routes/marti-mission.ts index 0d5d7fd8a..6fc18cbdb 100644 --- a/api/routes/marti-mission.ts +++ b/api/routes/marti-mission.ts @@ -32,8 +32,6 @@ export default async function router(schema: Schema, config: Config) { res: Type.Any() }, async (req, res) => { try { - await Auth.is_auth(config, req); - const user = await Auth.as_user(config, req); const auth = (await config.models.Profile.from(user.email)).auth; const api = await TAKAPI.init(new URL(String(config.server.api)), new APIAuthCertificate(auth.cert, auth.key)); @@ -47,8 +45,37 @@ export default async function router(schema: Schema, config: Config) { } }); + await schema.get('/marti/missions/:name/changes', { + name: 'Mission Changes', + group: 'MartiMissions', + params: Type.Object({ + name: Type.String(), + }), + description: 'Helper API to get mission changes', + query: Type.Object({ + secago: Type.Optional(Type.String()), + start: Type.Optional(Type.String()), + end: Type.Optional(Type.String()), + squashed: Type.Optional(Type.Boolean()) + }), + res: Type.Any() + }, async (req, res) => { + try { + const user = await Auth.as_user(config, req); + const auth = (await config.models.Profile.from(user.email)).auth; + const api = await TAKAPI.init(new URL(String(config.server.api)), new APIAuthCertificate(auth.cert, auth.key)); + + const query: Record = {}; + for (const q in req.query) query[q] = String(req.query[q]); + const changes = await api.Mission.changes(req.params.name, query); + return res.json(changes); + } catch (err) { + return Err.respond(err, res); + } + }); + await schema.delete('/marti/missions/:name', { - name: 'Delete Mission', + name: 'Mission Delete', group: 'MartiMissions', params: Type.Object({ name: Type.String(), @@ -61,8 +88,6 @@ export default async function router(schema: Schema, config: Config) { res: GenericMartiResponse }, async (req, res) => { try { - await Auth.is_auth(config, req); - const user = await Auth.as_user(config, req); const auth = (await config.models.Profile.from(user.email)).auth; const api = await TAKAPI.init(new URL(String(config.server.api)), new APIAuthCertificate(auth.cert, auth.key)); @@ -103,8 +128,6 @@ export default async function router(schema: Schema, config: Config) { res: GenericMartiResponse }, async (req, res) => { try { - await Auth.is_auth(config, req); - const user = await Auth.as_user(config, req); const auth = (await config.models.Profile.from(user.email)).auth; const api = await TAKAPI.init(new URL(String(config.server.api)), new APIAuthCertificate(auth.cert, auth.key)); @@ -131,8 +154,6 @@ export default async function router(schema: Schema, config: Config) { res: GenericMartiResponse }, async (req, res) => { try { - await Auth.is_auth(config, req); - const user = await Auth.as_user(config, req); const auth = (await config.models.Profile.from(user.email)).auth; const api = await TAKAPI.init(new URL(String(config.server.api)), new APIAuthCertificate(auth.cert, auth.key)); @@ -156,8 +177,6 @@ export default async function router(schema: Schema, config: Config) { res: MissionSubscriber }, async (req, res) => { try { - await Auth.is_auth(config, req); - const user = await Auth.as_user(config, req); const auth = (await config.models.Profile.from(user.email)).auth; const api = await TAKAPI.init(new URL(String(config.server.api)), new APIAuthCertificate(auth.cert, auth.key)); @@ -180,8 +199,6 @@ export default async function router(schema: Schema, config: Config) { res: GenericMartiResponse }, async (req, res) => { try { - await Auth.is_auth(config, req); - const user = await Auth.as_user(config, req); const auth = (await config.models.Profile.from(user.email)).auth; const api = await TAKAPI.init(new URL(String(config.server.api)), new APIAuthCertificate(auth.cert, auth.key)); @@ -204,8 +221,6 @@ export default async function router(schema: Schema, config: Config) { res: GenericMartiResponse }, async (req, res) => { try { - await Auth.is_auth(config, req); - const user = await Auth.as_user(config, req); const auth = (await config.models.Profile.from(user.email)).auth; const api = await TAKAPI.init(new URL(String(config.server.api)), new APIAuthCertificate(auth.cert, auth.key)); @@ -236,8 +251,6 @@ export default async function router(schema: Schema, config: Config) { })), }, async (req, res) => { try { - await Auth.is_auth(config, req); - const user = await Auth.as_user(config, req); const auth = (await config.models.Profile.from(user.email)).auth; const api = await TAKAPI.init(new URL(String(config.server.api)), new APIAuthCertificate(auth.cert, auth.key)); @@ -263,8 +276,6 @@ export default async function router(schema: Schema, config: Config) { res: GenericMartiResponse }, async (req, res) => { try { - await Auth.is_auth(config, req); - const user = await Auth.as_user(config, req); const profile = await config.models.Profile.from(user.email); const auth = profile.auth; @@ -308,8 +319,6 @@ export default async function router(schema: Schema, config: Config) { res: GenericMartiResponse }, async (req, res) => { try { - await Auth.is_auth(config, req); - const user = await Auth.as_user(config, req); const profile = await config.models.Profile.from(user.email); const auth = profile.auth; diff --git a/api/web/src/components/CloudTAK/Mission/Mission.vue b/api/web/src/components/CloudTAK/Mission/Mission.vue index 45870a72b..e338b2442 100644 --- a/api/web/src/components/CloudTAK/Mission/Mission.vue +++ b/api/web/src/components/CloudTAK/Mission/Mission.vue @@ -407,7 +407,7 @@ export default { try { this.loading.mission = true; const url = window.stdurl(`/api/marti/missions/${this.mission.name}`); - url.searchParams.append('changes', 'true'); + url.searchParams.append('changes', 'false'); url.searchParams.append('logs', 'true'); this.mission = await window.std(url); } catch (err) { From 808ea90f9c509dd2ae827fbeb72af909a9d09e07 Mon Sep 17 00:00:00 2001 From: ingalls Date: Wed, 6 Mar 2024 11:24:50 -0700 Subject: [PATCH 06/17] Migrate Query into Typebox Types --- api/lib/api/mission.ts | 220 +++++++++++------- api/routes/marti-mission.ts | 16 +- .../components/CloudTAK/Mission/Mission.vue | 12 + 3 files changed, 155 insertions(+), 93 deletions(-) diff --git a/api/lib/api/mission.ts b/api/lib/api/mission.ts index 70a837895..d2d4bfff3 100644 --- a/api/lib/api/mission.ts +++ b/api/lib/api/mission.ts @@ -62,6 +62,69 @@ export const DetachContentsInput = Type.Object({ uid: Type.Optional(Type.String()) }); +export const ChangesInput = Type.Object({ + secago: Type.Optional(Type.Integer()), + start: Type.Optional(Type.String()), + end: Type.Optional(Type.String()), + squashed: Type.Optional(Type.Boolean()) +}) + +export const SubscribedInput = Type.Object({ + uid: Type.String(), +}) + +export const UnsubscribeInput = Type.Object({ + uid: Type.String(), + disconnectOnly: Type.Optional(Type.Boolean()) +}) + +export const SubscribeInput = Type.Object({ + uid: Type.String(), + password: Type.Optional(Type.String()), + secago: Type.Optional(Type.Integer()), + start: Type.Optional(Type.String()), + end: Type.Optional(Type.String()) +}) + +export const DeleteInput = Type.Object({ + creatorUid: Type.Optional(Type.String()), + deepDelete: Type.Optional(Type.Boolean()) +}) + +export const GetInput = Type.Object({ + password: Type.Optional(Type.String()), + changes: Type.Optional(Type.Boolean()), + logs: Type.Optional(Type.Boolean()), + secago: Type.Optional(Type.Integer()), + start: Type.Optional(Type.String()), + end: Type.Optional(Type.String()) + +}); + +export const ListInput = Type.Object({ + passwordProtected: Type.Optional(Type.Boolean()), + defaultRole: Type.Optional(Type.Boolean()), + tool: Type.Optional(Type.String()) +}); + +export const CreateInput = Type.Object({ + group: Type.Union([Type.Array(Type.String()), Type.String()]), + creatorUid: Type.String(), + description: Type.Optional(Type.String({ default: '' })), + chatRoom: Type.Optional(Type.String()), + baseLayer: Type.Optional(Type.String()), + bbox: Type.Optional(Type.String()), + boundingPolygon: Type.Optional(Type.Array(Type.String())), + path: Type.Optional(Type.String()), + classification: Type.Optional(Type.String()), + tool: Type.Optional(Type.String({ default: 'public' })), + password: Type.Optional(Type.String()), + defaultRole: Type.Optional(Type.String()), + expiration: Type.Optional(Type.Integer()), + inviteOnly: Type.Optional(Type.Boolean({ default: false })), + allowDupe: Type.Optional(Type.Boolean({ default: false })), +}); + /** * @class */ @@ -86,14 +149,11 @@ export default class { } } - async changes(name: string, query: { - secago: number; - start: string; - end: string; - squashed: boolean; - - [key: string]: unknown; - }, opts?: Static) { + async changes( + name: string, + query: Static, + opts?: Static + ) { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/changes`, this.api.url); for (const q in query) url.searchParams.append(q, String(query[q])); @@ -103,7 +163,10 @@ export default class { }); } - async latestCots(name: string, opts?: Static): Promise { + async latestCots( + name: string, + opts?: Static + ): Promise { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/cot`, this.api.url); return await this.api.fetch(url, { @@ -115,7 +178,10 @@ export default class { /** * Return users associated with this mission */ - async contacts(name: string, opts?: Static) { + async contacts( + name: string, + opts?: Static + ) { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/contacts`, this.api.url); return await this.api.fetch(url, { @@ -127,7 +193,11 @@ export default class { /** * Remove a file from the mission */ - async detachContents(name: string, body: Static, opts?: Static) { + async detachContents( + name: string, + body: Static, + opts?: Static + ) { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/contents`, this.api.url); if (body.hash) url.searchParams.append('hash', body.hash); if (body.uid) url.searchParams.append('uid', body.uid); @@ -141,7 +211,11 @@ export default class { /** * Attach a file resource by hash from the TAK Server file manager */ - async attachContents(name: string, body: Static, opts?: Static) { + async attachContents( + name: string, + body: Static, + opts?: Static + ) { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/contents`, this.api.url); return await this.api.fetch(url, { @@ -154,7 +228,12 @@ export default class { /** * Upload a Mission Package */ - async upload(name: string, creatorUid: string, body: Readable, opts?: Static) { + async upload( + name: string, + creatorUid: string, + body: Readable, + opts?: Static + ) { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/contents/missionpackage`, this.api.url); url.searchParams.append('creatorUid', creatorUid); @@ -168,7 +247,10 @@ export default class { /** * Return UIDs associated with any subscribed users */ - async subscriptions(name: string, opts?: Static): Promise>> { + async subscriptions( + name: string, + opts?: Static + ): Promise>> { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/subscriptions`, this.api.url); return await this.api.fetch(url, { method: 'GET', @@ -179,7 +261,10 @@ export default class { /** * Return permissions associated with any subscribed users */ - async subscriptionRoles(name: string, opts?: Static): Promise> { + async subscriptionRoles( + name: string, + opts?: Static + ): Promise> { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/subscriptions/roles`, this.api.url); return await this.api.fetch(url, { method: 'GET', @@ -190,7 +275,10 @@ export default class { /** * Return permissions associated with a given mission if subscribed */ - async subscription(name: string, opts?: Static): Promise> { + async subscription( + name: string, + opts?: Static + ): Promise> { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/subscription`, this.api.url); const res = await this.api.fetch(url, { method: 'GET', @@ -203,15 +291,11 @@ export default class { /** * Subscribe to a mission */ - async subscribe(name: string, query: { - uid: string; - password?: string; - secago?: number; - start?: string; - end?: string; - - [key: string]: unknown; - }, opts?: Static) { + async subscribe( + name: string, + query: Static, + opts?: Static + ) { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/subscription`, this.api.url); for (const q in query) url.searchParams.append(q, String(query[q])); @@ -224,11 +308,11 @@ export default class { /** * Get current subscription status */ - async subscribed(name: string, query: { - uid: string; - - [key: string]: unknown; - }, opts?: Static) { + async subscribed( + name: string, + query: Static, + opts?: Static + ) { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/subscription`, this.api.url); for (const q in query) url.searchParams.append(q, String(query[q])); @@ -241,12 +325,11 @@ export default class { /** * Unsubscribe from a mission */ - async unsubscribe(name: string, query: { - uid: string; - disconnectOnly?: string; - - [key: string]: unknown; - }, opts?: Static) { + async unsubscribe( + name: string, + query: Static, + opts?: Static + ) { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/subscription`, this.api.url); for (const q in query) url.searchParams.append(q, String(query[q])); @@ -277,16 +360,11 @@ export default class { /** * Get mission by its GUID */ - async getGuid(guid: string, query: { - password?: string; - changes?: string; - logs?: string; - secago?: string; - start?: string; - end?: string; - - [key: string]: unknown; - }, opts?: Static): Promise> { + async getGuid( + guid: string, + query: Static, + opts?: Static + ): Promise> { const url = new URL(`/Marti/api/missions/guid/${encodeURIComponent(guid)}`, this.api.url); for (const q in query) url.searchParams.append(q, String(query[q])); @@ -302,17 +380,11 @@ export default class { /** * Get mission by its Name */ - async get(name: string, query: { - password?: string; - changes?: string; - logs?: string; - secago?: string; - start?: string; - end?: string; - - [key: string]: unknown; - }, opts?: Static): Promise> { - name = name.trim(); + async get( + name: string, + query: Static, + opts?: Static + ): Promise> { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}`, this.api.url); for (const q in query) url.searchParams.append(q, String(query[q])); @@ -329,25 +401,10 @@ export default class { /** * Create a new mission */ - async create(name: string, query: { - group: Array | string; - creatorUid: string; - description?: string; - chatRoom?: string; - baseLayer?: string; - bbox?: string; - boundingPolygon?: string; - path?: string; - classification?: string; - tool?: string; - password?: string; - defaultRole?: string; - expiration?: string; - inviteOnly?: string; - allowDupe?: string; - - [key: string]: unknown; - }): Promise>> { + async create( + name: string, + query: Static + ): Promise>> { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}`, this.api.url); if (query.group && Array.isArray(query.group)) query.group = query.group.join(','); @@ -360,12 +417,11 @@ export default class { /** * Delete a mission */ - async delete(name: string, query: { - creatorUid?: string; - deepDelete?: string; - - [key: string]: unknown; - }, opts?: Static) { + async delete( + name: string, + query: Static, + opts?: Static + ) { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}`, this.api.url); for (const q in query) url.searchParams.append(q, String(query[q])); diff --git a/api/routes/marti-mission.ts b/api/routes/marti-mission.ts index 6fc18cbdb..cce6ce827 100644 --- a/api/routes/marti-mission.ts +++ b/api/routes/marti-mission.ts @@ -4,7 +4,7 @@ import Err from '@openaddresses/batch-error'; import Auth from '../lib/auth.js'; import Config from '../lib/config.js'; import { GenericMartiResponse } from '../lib/types.js'; -import { MissionSubscriber } from '../lib/api/mission.js'; +import { MissionSubscriber, Mission, ChangesInput } from '../lib/api/mission.js'; import { Profile } from '../lib/schema.js'; import S3 from '../lib/aws/s3.js'; import TAKAPI, { @@ -29,7 +29,7 @@ export default async function router(schema: Schema, config: Config) { start: Type.Optional(Type.String()), end: Type.Optional(Type.String()) }), - res: Type.Any() + res: Mission }, async (req, res) => { try { const user = await Auth.as_user(config, req); @@ -52,12 +52,7 @@ export default async function router(schema: Schema, config: Config) { name: Type.String(), }), description: 'Helper API to get mission changes', - query: Type.Object({ - secago: Type.Optional(Type.String()), - start: Type.Optional(Type.String()), - end: Type.Optional(Type.String()), - squashed: Type.Optional(Type.Boolean()) - }), + query: ChangesInput, res: Type.Any() }, async (req, res) => { try { @@ -65,9 +60,8 @@ export default async function router(schema: Schema, config: Config) { const auth = (await config.models.Profile.from(user.email)).auth; const api = await TAKAPI.init(new URL(String(config.server.api)), new APIAuthCertificate(auth.cert, auth.key)); - const query: Record = {}; - for (const q in req.query) query[q] = String(req.query[q]); - const changes = await api.Mission.changes(req.params.name, query); + const changes = await api.Mission.changes(req.params.name, req.query); + return res.json(changes); } catch (err) { return Err.respond(err, res); diff --git a/api/web/src/components/CloudTAK/Mission/Mission.vue b/api/web/src/components/CloudTAK/Mission/Mission.vue index e338b2442..118db7de0 100644 --- a/api/web/src/components/CloudTAK/Mission/Mission.vue +++ b/api/web/src/components/CloudTAK/Mission/Mission.vue @@ -274,6 +274,7 @@ export default { initial: !this.initial.passwordProtected, mission: !this.initial.passwordProtected, logs: false, + changes: true, users: true, delete: false }, @@ -325,6 +326,7 @@ export default { await Promise.all([ this.fetchSubscriptions(), + this.fetchChanges(), this.fetchImports() ]); }, @@ -380,6 +382,16 @@ export default { } this.loading.users = false; }, + fetchChanges: async function() { + this.loading.changes = true; + try { + const url = await window.stdurl(`/api/marti/missions/${this.mission.name}/changes`); + this.changes = (await window.std(url)).data; + } catch (err) { + this.err = err; + } + this.loading.changes = false; + }, fetchSubscriptions: async function() { try { const url = await window.stdurl(`/api/marti/missions/${this.mission.name}/subscriptions/roles`); From 8e80cfcc7fb4220a8419814cbe744872b19709ef Mon Sep 17 00:00:00 2001 From: ingalls Date: Wed, 6 Mar 2024 11:27:08 -0700 Subject: [PATCH 07/17] Remove unused getGuid --- api/routes/profile-overlays.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/routes/profile-overlays.ts b/api/routes/profile-overlays.ts index 35db52ca6..d246142b5 100644 --- a/api/routes/profile-overlays.ts +++ b/api/routes/profile-overlays.ts @@ -94,7 +94,7 @@ export default async function router(schema: Schema, config: Config) { 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 mission = await api.Mission.getGuid(overlay.mode_id, { uid: user.email }); + const mission = await api.Mission.getGuid(overlay.mode_id, {}); await api.Mission.subscribe(mission.name, { uid: user.email }); } @@ -127,7 +127,7 @@ export default async function router(schema: Schema, config: Config) { if (overlay.mode === 'mission') { 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 mission = await api.Mission.getGuid(overlay.mode_id, { uid: user.email }); + const mission = await api.Mission.getGuid(overlay.mode_id, {}); await api.Mission.unsubscribe(mission.name, { uid: user.email }); } From ee02928565cb6bcfe9c75fda6d3cfe9819f6d201 Mon Sep 17 00:00:00 2001 From: ingalls Date: Wed, 6 Mar 2024 11:38:05 -0700 Subject: [PATCH 08/17] Add Mission Change Type --- api/lib/api/mission.ts | 14 +++++++++++++- api/routes/marti-mission.ts | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/api/lib/api/mission.ts b/api/lib/api/mission.ts index d2d4bfff3..be171f5e5 100644 --- a/api/lib/api/mission.ts +++ b/api/lib/api/mission.ts @@ -36,6 +36,18 @@ export const Mission = Type.Object({ missionChanges: Type.Optional(Type.Array(Type.Unknown())) // Only present on Mission.get() }); +export const MissionChange = Type.Object({ + isFederatedChange: Type.Boolean(), + type: Type.String(), + missionName: Type.String(), + timestamp: Type.String(), + serverTime: Type.String(), + creatorUid: Type.String(), + contentUid: Type.Optional(Type.String()), + details: Type.Optional(Type.Any()), + contentResource: Type.Optional(Type.Any()) +}); + export const MissionSubscriber = Type.Object({ token: Type.Optional(Type.String()), clientUid: Type.String(), @@ -153,7 +165,7 @@ export default class { name: string, query: Static, opts?: Static - ) { + ): Promise>> { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/changes`, this.api.url); for (const q in query) url.searchParams.append(q, String(query[q])); diff --git a/api/routes/marti-mission.ts b/api/routes/marti-mission.ts index cce6ce827..90a42baa5 100644 --- a/api/routes/marti-mission.ts +++ b/api/routes/marti-mission.ts @@ -53,7 +53,7 @@ export default async function router(schema: Schema, config: Config) { }), description: 'Helper API to get mission changes', query: ChangesInput, - res: Type.Any() + res: GenericMartiResponse }, async (req, res) => { try { const user = await Auth.as_user(config, req); From 1e03e9783b6535fbf4cac5120e2f8fc72b816771 Mon Sep 17 00:00:00 2001 From: ingalls Date: Wed, 6 Mar 2024 11:40:11 -0700 Subject: [PATCH 09/17] Call Changes API --- api/web/src/components/CloudTAK/Mission/Mission.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/web/src/components/CloudTAK/Mission/Mission.vue b/api/web/src/components/CloudTAK/Mission/Mission.vue index 118db7de0..429469107 100644 --- a/api/web/src/components/CloudTAK/Mission/Mission.vue +++ b/api/web/src/components/CloudTAK/Mission/Mission.vue @@ -156,9 +156,9 @@