@@ -2,6 +2,8 @@ import { makeRouter, route } from '/api/lib/router.ts'
22import type { RequestContext } from '/api/lib/context.ts'
33import { handleGoogleCallback , initiateGoogleAuth } from './auth.ts'
44import {
5+ DeploymentDef ,
6+ DeploymentsCollection ,
57 ProjectDef ,
68 ProjectsCollection ,
79 TeamDef ,
@@ -10,12 +12,12 @@ import {
1012 UserDef ,
1113 UsersCollection ,
1214} from './schema.ts'
13- import { ARR , BOOL , OBJ , optional , STR } from './lib/validator.ts'
15+ import { ARR , BOOL , NUM , OBJ , optional , STR } from './lib/validator.ts'
1416import { respond } from './lib/response.ts'
1517import { deleteCookie } from 'jsr:@std/http/cookie'
1618import { getPicture } from '/api/picture.ts'
1719import { insertLogs , LogsInputSchema } from './click-house-client.ts'
18- import { decryptMessage } from './user.ts'
20+ import { decryptMessage , encryptMessage } from './user.ts'
1921
2022const withUserSession = ( { user } : RequestContext ) => {
2123 if ( ! user ) throw Error ( 'Missing user session' )
@@ -28,11 +30,28 @@ const withAdminSession = ({ user }: RequestContext) => {
2830const withDeploymentSession = async ( ctx : RequestContext ) => {
2931 const token = ctx . req . headers . get ( 'Authorization' ) ?. replace ( / ^ B e a r e r / i, '' )
3032 if ( ! token ) throw respond . Unauthorized ( { message : 'Missing token' } )
31- const data = await decryptMessage ( token )
32- if ( ! data ) throw respond . Unauthorized ( { message : 'Invalid token' } )
33- ctx . resource = 'default'
33+ const message = await decryptMessage ( token )
34+ if ( ! message ) throw respond . Unauthorized ( { message : 'Invalid token' } )
35+ const data = JSON . parse ( message )
36+ const dep = DeploymentsCollection . get ( data ?. url )
37+ if ( ! dep || dep . tokenSalt !== data ?. tokenSalt ) {
38+ throw respond . Unauthorized ( { message : 'Invalid token' } )
39+ }
40+ ctx . resource = dep ?. url
3441}
3542
43+ const deploymentOutput = OBJ ( {
44+ projectId : STR ( 'The ID of the project' ) ,
45+ url : STR ( 'The URL of the deployment' ) ,
46+ logsEnabled : BOOL ( 'Whether logging is enabled' ) ,
47+ databaseEnabled : BOOL ( 'Whether the database is enabled' ) ,
48+ sqlEndpoint : optional ( STR ( 'The SQL endpoint' ) ) ,
49+ sqlToken : optional ( STR ( 'The SQL token' ) ) ,
50+ createdAt : optional ( NUM ( 'The creation date of the deployment' ) ) ,
51+ updatedAt : optional ( NUM ( 'The last update date of the deployment' ) ) ,
52+ token : optional ( STR ( 'The deployment token' ) ) ,
53+ } )
54+
3655const defs = {
3756 'GET/api/health' : route ( {
3857 fn : ( ) => new Response ( 'OK' ) ,
@@ -193,14 +212,134 @@ const defs = {
193212 output : BOOL ( 'Indicates if the project was deleted' ) ,
194213 description : 'Delete a project by ID' ,
195214 } ) ,
215+ 'GET/api/project/deployments' : route ( {
216+ authorize : withUserSession ,
217+ fn : ( _ctx , { project } ) => {
218+ const deployments = DeploymentsCollection . filter ( ( d ) =>
219+ d . projectId === project
220+ )
221+ if ( ! deployments . length ) {
222+ throw respond . NotFound ( { message : 'Deployments not found' } )
223+ }
224+ return deployments . map ( ( { tokenSalt : _ , ...d } ) => {
225+ return {
226+ ...d ,
227+ createdAt : d . createdAt ,
228+ updatedAt : d . updatedAt ,
229+ token : undefined ,
230+ sqlToken : undefined ,
231+ sqlEndpoint : undefined ,
232+ }
233+ } )
234+ } ,
235+ input : OBJ ( { project : STR ( 'The ID of the project' ) } ) ,
236+ output : ARR ( deploymentOutput , 'List of deployments' ) ,
237+ description : 'Get deployments by project ID' ,
238+ } ) ,
239+ 'GET/api/deployment' : route ( {
240+ authorize : withAdminSession ,
241+ fn : async ( _ctx , url ) => {
242+ const dep = DeploymentsCollection . get ( url )
243+ if ( ! dep ) throw respond . NotFound ( )
244+ const { tokenSalt, ...deployment } = dep
245+ const token = await encryptMessage (
246+ JSON . stringify ( { url : deployment . url , tokenSalt } ) ,
247+ )
248+ return {
249+ ...deployment ,
250+ createdAt : deployment . createdAt ,
251+ updatedAt : deployment . updatedAt ,
252+ token,
253+ }
254+ } ,
255+ input : STR ( ) ,
256+ output : deploymentOutput ,
257+ description : 'Get a deployment by ID' ,
258+ } ) ,
259+ 'POST/api/deployment' : route ( {
260+ authorize : withAdminSession ,
261+ fn : async ( _ctx , input ) => {
262+ const tokenSalt = performance . now ( ) . toString ( )
263+ const { tokenSalt : _ , ...deployment } = await DeploymentsCollection
264+ . insert ( {
265+ ...input ,
266+ tokenSalt,
267+ } )
268+ const token = await encryptMessage (
269+ JSON . stringify ( { url : deployment . url , tokenSalt } ) ,
270+ )
271+ return {
272+ ...deployment ,
273+ createdAt : deployment . createdAt ,
274+ updatedAt : deployment . updatedAt ,
275+ token,
276+ }
277+ } ,
278+ input : DeploymentDef ,
279+ output : deploymentOutput ,
280+ description : 'Create a new deployment' ,
281+ } ) ,
282+ 'PUT/api/deployment' : route ( {
283+ authorize : withAdminSession ,
284+ fn : async ( _ctx , input ) => {
285+ const { tokenSalt, ...deployment } = await DeploymentsCollection
286+ . update ( input . url , input )
287+ const token = await encryptMessage (
288+ JSON . stringify ( { url : deployment . url , tokenSalt } ) ,
289+ )
290+ return {
291+ ...deployment ,
292+ createdAt : deployment . createdAt ,
293+ updatedAt : deployment . updatedAt ,
294+ token,
295+ }
296+ } ,
297+ input : DeploymentDef ,
298+ output : deploymentOutput ,
299+ description : 'Update a deployment by ID' ,
300+ } ) ,
301+ 'GET/api/deployment/token/regenerate' : route ( {
302+ authorize : withAdminSession ,
303+ fn : async ( _ctx , input ) => {
304+ const dep = DeploymentsCollection . get ( input )
305+ if ( ! dep ) throw respond . NotFound ( )
306+ const tokenSalt = performance . now ( ) . toString ( )
307+
308+ const { tokenSalt : _ , ...deployment } = await DeploymentsCollection
309+ . update ( input , { ...dep , tokenSalt } )
310+ const token = await encryptMessage (
311+ JSON . stringify ( { url : deployment . url , tokenSalt } ) ,
312+ )
313+ return {
314+ ...deployment ,
315+ createdAt : deployment . createdAt ,
316+ updatedAt : deployment . updatedAt ,
317+ token,
318+ }
319+ } ,
320+ input : STR ( ) ,
321+ output : deploymentOutput ,
322+ description : 'Regenerate a deployment token' ,
323+ } ) ,
324+ 'DELETE/api/deployment' : route ( {
325+ authorize : withAdminSession ,
326+ fn : async ( _ctx , input ) => {
327+ const dep = DeploymentsCollection . get ( input )
328+ if ( ! dep ) throw respond . NotFound ( )
329+ await DeploymentsCollection . delete ( input )
330+ return respond . NoContent ( )
331+ } ,
332+ input : STR ( ) ,
333+ description : 'Delete a deployment' ,
334+ } ) ,
196335 'POST/api/logs' : route ( {
197336 authorize : withDeploymentSession ,
198337 fn : ( ctx , logs ) => {
199338 if ( ! ctx . resource ) throw respond . InternalServerError ( )
200339 return insertLogs ( ctx . resource , logs )
201340 } ,
202341 input : LogsInputSchema ,
203- description : 'Insert logs into ClickHouse' ,
342+ description : 'Insert logs into ClickHouse NB: a Bearer token is required ' ,
204343 } ) ,
205344} as const
206345
0 commit comments