@@ -4,13 +4,37 @@ import { zValidator } from "@hono/zod-validator";
44import { randomBytes } from "node:crypto" ;
55import { db } from "../db/index.js" ;
66import * as schema from "../db/schema.js" ;
7- import { eq , desc , count , and , isNotNull , isNull , type SQL } from "drizzle-orm" ;
7+ import {
8+ eq ,
9+ desc ,
10+ count ,
11+ and ,
12+ isNotNull ,
13+ isNull ,
14+ sql ,
15+ getTableColumns ,
16+ type SQL ,
17+ } from "drizzle-orm" ;
818import { getDockerClient } from "../lib/docker.js" ;
919import type { AuthEnv } from "../middleware/auth.js" ;
1020import { authMiddleware } from "../middleware/auth.js" ;
1121import { paginationQuery } from "../lib/pagination.js" ;
22+ import { requireSessionAccess , handleSessionAccessError } from "../lib/session.js" ;
23+
24+ /** Session columns without the potentially-large resultHtml blob. */
25+ const { resultHtml : _resultHtml , ...sessionColumns } = getTableColumns ( schema . codingSessions ) ;
26+ const sessionSummary = {
27+ ...sessionColumns ,
28+ hasResult : sql < boolean > `${ schema . codingSessions . resultHtml } is not null` . as ( "has_result" ) ,
29+ } ;
30+
31+ /** Strip resultHtml from a raw DB row and add hasResult boolean. */
32+ function toSessionSummary < T extends { resultHtml ?: string | null } > ( { resultHtml, ...rest } : T ) {
33+ return { ...rest , hasResult : resultHtml != null } ;
34+ }
1235
1336const app = new Hono < AuthEnv > ( )
37+ . onError ( handleSessionAccessError )
1438 // ---------------------------------------------------------------------------
1539 // GET /api/sessions — list sessions
1640 // ---------------------------------------------------------------------------
@@ -55,7 +79,7 @@ const app = new Hono<AuthEnv>()
5579
5680 const rows = await db
5781 . select ( {
58- session : schema . codingSessions ,
82+ ... sessionSummary ,
5983 userName : schema . user . name ,
6084 userEmail : schema . user . email ,
6185 } )
@@ -67,9 +91,9 @@ const app = new Hono<AuthEnv>()
6791 . offset ( offset ) ;
6892
6993 return c . json ( {
70- data : rows . map ( ( r ) => ( {
71- ...r . session ,
72- user : { name : r . userName , email : r . userEmail } ,
94+ data : rows . map ( ( { userName , userEmail , ... s } ) => ( {
95+ ...s ,
96+ user : { name : userName , email : userEmail } ,
7397 } ) ) ,
7498 total,
7599 page,
@@ -86,7 +110,7 @@ const app = new Hono<AuthEnv>()
86110 . where ( where ) ;
87111
88112 const rows = await db
89- . select ( )
113+ . select ( sessionSummary )
90114 . from ( schema . codingSessions )
91115 . where ( where )
92116 . orderBy ( desc ( schema . codingSessions . createdAt ) )
@@ -105,7 +129,7 @@ const app = new Hono<AuthEnv>()
105129 const id = c . req . param ( "id" ) ;
106130
107131 const [ codingSession ] = await db
108- . select ( )
132+ . select ( sessionSummary )
109133 . from ( schema . codingSessions )
110134 . where ( eq ( schema . codingSessions . id , id ) )
111135 . limit ( 1 ) ;
@@ -325,7 +349,7 @@ const app = new Hono<AuthEnv>()
325349 . where ( eq ( schema . codingSessions . id , codingSession . id ) )
326350 . returning ( ) ;
327351
328- return c . json ( updated [ 0 ] ) ;
352+ return c . json ( toSessionSummary ( updated [ 0 ] ) ) ;
329353 } catch ( err ) {
330354 await db
331355 . update ( schema . codingSessions )
@@ -349,19 +373,8 @@ const app = new Hono<AuthEnv>()
349373 // PUT /api/sessions/:id/stop
350374 // ---------------------------------------------------------------------------
351375 . put ( "/:id/stop" , authMiddleware , async ( c ) => {
352- const session = c . get ( "session" ) ;
353376 const id = c . req . param ( "id" ) ;
354-
355- const [ codingSession ] = await db
356- . select ( )
357- . from ( schema . codingSessions )
358- . where ( eq ( schema . codingSessions . id , id ) )
359- . limit ( 1 ) ;
360-
361- if ( ! codingSession ) return c . json ( { error : "Session not found" } , 404 ) ;
362- if ( codingSession . userId !== session . user . id && session . user . role !== "admin" ) {
363- return c . json ( { error : "Forbidden" } , 403 ) ;
364- }
377+ const codingSession = await requireSessionAccess ( id , c . get ( "session" ) . user ) ;
365378
366379 if ( ! codingSession . containerId ) {
367380 return c . json ( { error : "No container associated with this session" } , 400 ) ;
@@ -384,26 +397,15 @@ const app = new Hono<AuthEnv>()
384397 . where ( eq ( schema . codingSessions . id , id ) )
385398 . returning ( ) ;
386399
387- return c . json ( updated [ 0 ] ) ;
400+ return c . json ( toSessionSummary ( updated [ 0 ] ) ) ;
388401 } )
389402
390403 // ---------------------------------------------------------------------------
391404 // DELETE /api/sessions/:id — destroy session + container
392405 // ---------------------------------------------------------------------------
393406 . delete ( "/:id" , authMiddleware , async ( c ) => {
394- const session = c . get ( "session" ) ;
395407 const id = c . req . param ( "id" ) ;
396-
397- const [ codingSession ] = await db
398- . select ( )
399- . from ( schema . codingSessions )
400- . where ( eq ( schema . codingSessions . id , id ) )
401- . limit ( 1 ) ;
402-
403- if ( ! codingSession ) return c . json ( { error : "Session not found" } , 404 ) ;
404- if ( codingSession . userId !== session . user . id && session . user . role !== "admin" ) {
405- return c . json ( { error : "Forbidden" } , 403 ) ;
406- }
408+ const codingSession = await requireSessionAccess ( id , c . get ( "session" ) . user ) ;
407409
408410 if ( codingSession . containerId ) {
409411 try {
@@ -427,19 +429,8 @@ const app = new Hono<AuthEnv>()
427429 // PUT /api/sessions/:id/restart
428430 // ---------------------------------------------------------------------------
429431 . put ( "/:id/restart" , authMiddleware , async ( c ) => {
430- const session = c . get ( "session" ) ;
431432 const id = c . req . param ( "id" ) ;
432-
433- const [ codingSession ] = await db
434- . select ( )
435- . from ( schema . codingSessions )
436- . where ( eq ( schema . codingSessions . id , id ) )
437- . limit ( 1 ) ;
438-
439- if ( ! codingSession ) return c . json ( { error : "Session not found" } , 404 ) ;
440- if ( codingSession . userId !== session . user . id && session . user . role !== "admin" ) {
441- return c . json ( { error : "Forbidden" } , 403 ) ;
442- }
433+ const codingSession = await requireSessionAccess ( id , c . get ( "session" ) . user ) ;
443434
444435 if ( ! codingSession . containerId ) {
445436 return c . json ( { error : "No container associated with this session" } , 400 ) ;
@@ -515,26 +506,15 @@ const app = new Hono<AuthEnv>()
515506 . where ( eq ( schema . codingSessions . id , id ) )
516507 . returning ( ) ;
517508
518- return c . json ( updated [ 0 ] ) ;
509+ return c . json ( toSessionSummary ( updated [ 0 ] ) ) ;
519510 } )
520511
521512 // ---------------------------------------------------------------------------
522513 // GET /api/sessions/:id/recreate-params
523514 // ---------------------------------------------------------------------------
524515 . get ( "/:id/recreate-params" , authMiddleware , async ( c ) => {
525- const session = c . get ( "session" ) ;
526516 const id = c . req . param ( "id" ) ;
527-
528- const [ original ] = await db
529- . select ( )
530- . from ( schema . codingSessions )
531- . where ( eq ( schema . codingSessions . id , id ) )
532- . limit ( 1 ) ;
533-
534- if ( ! original ) return c . json ( { error : "Session not found" } , 404 ) ;
535- if ( original . userId !== session . user . id && session . user . role !== "admin" ) {
536- return c . json ( { error : "Forbidden" } , 403 ) ;
537- }
517+ const original = await requireSessionAccess ( id , c . get ( "session" ) . user ) ;
538518
539519 return c . json ( {
540520 name : original . name ,
@@ -546,23 +526,28 @@ const app = new Hono<AuthEnv>()
546526 } ) ;
547527 } )
548528
529+ // ---------------------------------------------------------------------------
530+ // GET /api/sessions/:id/results/latest — serve raw result HTML with CSP
531+ // ---------------------------------------------------------------------------
532+ . get ( "/:id/results/latest" , authMiddleware , async ( c ) => {
533+ const id = c . req . param ( "id" ) ;
534+ const codingSession = await requireSessionAccess ( id , c . get ( "session" ) . user ) ;
535+ if ( ! codingSession . resultHtml ) return c . text ( "No result" , 404 ) ;
536+
537+ c . header (
538+ "Content-Security-Policy" ,
539+ "default-src 'none'; style-src 'unsafe-inline' *; img-src * data: blob:; font-src * data:; script-src 'unsafe-inline' * blob:; connect-src 'none'; form-action 'none'; frame-src 'none'" ,
540+ ) ;
541+ c . header ( "X-Content-Type-Options" , "nosniff" ) ;
542+ return c . html ( codingSession . resultHtml ) ;
543+ } )
544+
549545 // ---------------------------------------------------------------------------
550546 // DELETE /api/sessions/:id/result
551547 // ---------------------------------------------------------------------------
552548 . delete ( "/:id/result" , authMiddleware , async ( c ) => {
553- const session = c . get ( "session" ) ;
554549 const id = c . req . param ( "id" ) ;
555-
556- const [ codingSession ] = await db
557- . select ( )
558- . from ( schema . codingSessions )
559- . where ( eq ( schema . codingSessions . id , id ) )
560- . limit ( 1 ) ;
561-
562- if ( ! codingSession ) return c . json ( { error : "Session not found" } , 404 ) ;
563- if ( codingSession . userId !== session . user . id && session . user . role !== "admin" ) {
564- return c . json ( { error : "Forbidden" } , 403 ) ;
565- }
550+ await requireSessionAccess ( id , c . get ( "session" ) . user ) ;
566551
567552 await db
568553 . update ( schema . codingSessions )
0 commit comments