Skip to content

Commit 66fb184

Browse files
committed
feat(deployment): Add deployment management routes and enhance user session handling
1 parent 27d7232 commit 66fb184

File tree

5 files changed

+154
-11
lines changed

5 files changed

+154
-11
lines changed

.env.test

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@ APP_ENV=test
22
PORT=3021
33
GOOGLE_CLIENT_ID=...test.apps.googleusercontent.com
44
CLIENT_SECRET=GOC...test
5-
REDIRECT_URI=http://localhost:7737/api/auth/google
5+
REDIRECT_URI=http://localhost:7737/api/auth/google
6+
7+
CLICKHOUSE_HOST=http://localhost:8443
8+
CLICKHOUSE_USER=default
9+
CLICKHOUSE_PASSWORD=token_pass

api/click-house-client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ try {
5757
`,
5858
})
5959

60-
log.info('deployment_logs table is ready')
60+
log.info('logs table is ready')
6161
} catch (error) {
6262
log.error('Error creating ClickHouse table:', { error })
6363
Deno.exit(1)
@@ -85,7 +85,7 @@ async function insertLogs(
8585
})
8686
return respond.OK()
8787
} catch (error) {
88-
log.error("Erreur lors de l'insertion des logs dans ClickHouse:", { error })
88+
log.error('Error inserting logs into ClickHouse:', { error })
8989
throw respond.InternalServerError()
9090
}
9191
}

api/routes.ts

Lines changed: 145 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { makeRouter, route } from '/api/lib/router.ts'
22
import type { RequestContext } from '/api/lib/context.ts'
33
import { handleGoogleCallback, initiateGoogleAuth } from './auth.ts'
44
import {
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'
1416
import { respond } from './lib/response.ts'
1517
import { deleteCookie } from 'jsr:@std/http/cookie'
1618
import { getPicture } from '/api/picture.ts'
1719
import { insertLogs, LogsInputSchema } from './click-house-client.ts'
18-
import { decryptMessage } from './user.ts'
20+
import { decryptMessage, encryptMessage } from './user.ts'
1921

2022
const withUserSession = ({ user }: RequestContext) => {
2123
if (!user) throw Error('Missing user session')
@@ -28,11 +30,28 @@ const withAdminSession = ({ user }: RequestContext) => {
2830
const withDeploymentSession = async (ctx: RequestContext) => {
2931
const token = ctx.req.headers.get('Authorization')?.replace(/^Bearer /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+
3655
const 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

api/user.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const encoder = new TextEncoder()
77
const decoder = new TextDecoder()
88
const IV_SIZE = 12 // Initialization vector (12 bytes for AES-GCM)
99

10-
async function encryptMessage(message: string) {
10+
export async function encryptMessage(message: string) {
1111
const iv = crypto.getRandomValues(new Uint8Array(IV_SIZE))
1212
const encryptedMessage = await crypto.subtle.encrypt(
1313
{ name: 'AES-GCM', iv },

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"preact": "npm:preact@^10.26.9",
3535
"@preact/preset-vite": "npm:@preact/preset-vite@^2.10.2",
3636
"@preact/signals": "npm:@preact/signals",
37-
"@@clickhouse/client": "npm:@clickhouse/client",
37+
"@clickhouse/client": "npm:@clickhouse/client",
3838
"@tailwindcss/vite": "npm:@tailwindcss/vite@^4.1.11",
3939
"tailwindcss": "npm:tailwindcss@^4.1.11",
4040
"daisyui": "npm:daisyui@^5.0.46",

0 commit comments

Comments
 (0)