1+ import {
2+ ServerRequestType as RequestEnum ,
3+ ServerRequest ,
4+ prepareNote55 ,
5+ } from '@vue-skuilder/common' ;
6+ import { CourseLookup } from '@vue-skuilder/db' ;
7+ import cookieParser from 'cookie-parser' ;
8+ import cors from 'cors' ;
9+ import type { Request , Response } from 'express' ;
10+ import express from 'express' ;
11+ import morgan from 'morgan' ;
12+ import Nano from 'nano' ;
13+ import PostProcess from './attachment-preprocessing/index.js' ;
14+ import {
15+ ClassroomCreationQueue ,
16+ ClassroomJoinQueue ,
17+ ClassroomLeaveQueue ,
18+ } from './client-requests/classroom-requests.js' ;
19+ import {
20+ CourseCreationQueue ,
21+ initCourseDBDesignDocInsert ,
22+ } from './client-requests/course-requests.js' ;
23+ import { packCourse } from './client-requests/pack-requests.js' ;
24+ import { requestIsAuthenticated } from './couchdb/authentication.js' ;
25+ import CouchDB , {
26+ useOrCreateCourseDB ,
27+ useOrCreateDB ,
28+ } from './couchdb/index.js' ;
29+ import { classroomDbDesignDoc } from './design-docs.js' ;
30+ import logger from './logger.js' ;
31+ import logsRouter from './routes/logs.js' ;
32+ import type { ExpressServerConfig , EnvironmentConfig } from './types.js' ;
33+
34+ export interface VueClientRequest extends express . Request {
35+ body : ServerRequest ;
36+ }
37+
38+ /**
39+ * Configuration options for creating an Express app.
40+ * Can be provided either as ExpressServerConfig (programmatic) or EnvironmentConfig (env vars).
41+ */
42+ export type AppConfig = ExpressServerConfig | EnvironmentConfig ;
43+
44+ /**
45+ * Type guard to determine if config is ExpressServerConfig (programmatic usage)
46+ */
47+ function isExpressServerConfig ( config : AppConfig ) : config is ExpressServerConfig {
48+ return 'couchdb' in config && typeof config . couchdb === 'object' ;
49+ }
50+
51+ /**
52+ * Convert ExpressServerConfig to environment-style config for internal usage
53+ */
54+ function convertToEnvConfig ( config : ExpressServerConfig ) : EnvironmentConfig {
55+ return {
56+ COUCHDB_SERVER : config . couchdb . server ,
57+ COUCHDB_PROTOCOL : config . couchdb . protocol ,
58+ COUCHDB_ADMIN : config . couchdb . username ,
59+ COUCHDB_PASSWORD : config . couchdb . password ,
60+ VERSION : config . version ,
61+ NODE_ENV : config . nodeEnv || 'development' ,
62+ } ;
63+ }
64+
65+ /**
66+ * Create and configure Express application with all routes and middleware.
67+ * This is the shared logic used by both standalone and programmatic modes.
68+ */
69+ export function createExpressApp ( config : AppConfig ) : express . Application {
70+ const app = express ( ) ;
71+
72+ // Normalize config to environment format for internal usage
73+ const envConfig = isExpressServerConfig ( config )
74+ ? convertToEnvConfig ( config )
75+ : config ;
76+
77+ // Configure CORS - use config if available, otherwise defaults
78+ const corsOptions = isExpressServerConfig ( config ) && config . cors
79+ ? config . cors
80+ : { credentials : true , origin : true } ;
81+
82+ // Middleware setup
83+ app . use ( cookieParser ( ) ) ;
84+ app . use ( express . json ( ) ) ;
85+ app . use ( cors ( corsOptions ) ) ;
86+ app . use (
87+ morgan ( 'combined' , {
88+ stream : { write : ( message : string ) => logger . info ( message . trim ( ) ) } ,
89+ } )
90+ ) ;
91+ app . use ( '/logs' , logsRouter ) ;
92+
93+ // Routes
94+ app . get ( '/courses' , ( _req : Request , res : Response ) => {
95+ void ( async ( ) => {
96+ try {
97+ const courses = await CourseLookup . allCourseWare ( ) ;
98+ res . send ( courses . map ( ( c ) => `${ c . _id } - ${ c . name } ` ) ) ;
99+ } catch ( error ) {
100+ logger . error ( 'Error fetching courses:' , error ) ;
101+ res . status ( 500 ) . send ( 'Failed to fetch courses' ) ;
102+ }
103+ } ) ( ) ;
104+ } ) ;
105+
106+ app . get ( '/course/:courseID/config' , ( req : Request , res : Response ) => {
107+ void ( async ( ) => {
108+ try {
109+ const courseDB = await useOrCreateCourseDB ( req . params . courseID ) ;
110+ const cfg = await courseDB . get ( 'CourseConfig' ) ; // [ ] pull courseConfig docName into global const
111+
112+ res . json ( cfg ) ;
113+ } catch ( error ) {
114+ logger . error ( 'Error fetching course config:' , error ) ;
115+ res . status ( 500 ) . send ( 'Failed to fetch course config' ) ;
116+ }
117+ } ) ( ) ;
118+ } ) ;
119+
120+ app . delete ( '/course/:courseID' , ( req : Request , res : Response ) => {
121+ void ( async ( ) => {
122+ try {
123+ logger . info ( `Delete request made on course ${ req . params . courseID } ...` ) ;
124+ const auth = await requestIsAuthenticated ( req ) ;
125+ if ( auth ) {
126+ logger . info ( `\tAuthenticated delete request made...` ) ;
127+ const dbResp = await CouchDB . db . destroy (
128+ `coursedb-${ req . params . courseID } `
129+ ) ;
130+ if ( ! dbResp . ok ) {
131+ res . json ( { success : false , error : dbResp } ) ;
132+ return ;
133+ }
134+ const delResp = await CourseLookup . delete ( req . params . courseID ) ;
135+
136+ if ( delResp . ok ) {
137+ res . json ( { success : true } ) ;
138+ } else {
139+ res . json ( { success : false , error : delResp } ) ;
140+ }
141+ } else {
142+ res . json ( { success : false , error : 'Not authenticated' } ) ;
143+ }
144+ } catch ( error ) {
145+ logger . error ( 'Error deleting course:' , error ) ;
146+ res . status ( 500 ) . json ( { success : false , error : 'Failed to delete course' } ) ;
147+ }
148+ } ) ( ) ;
149+ } ) ;
150+
151+ async function postHandler (
152+ req : VueClientRequest ,
153+ res : express . Response
154+ ) : Promise < void > {
155+ const auth = await requestIsAuthenticated ( req ) ;
156+ if ( auth ) {
157+ const body = req . body ;
158+ logger . info (
159+ `Authorized ${
160+ body . type ? body . type : '[unspecified request type]'
161+ } request made...`
162+ ) ;
163+
164+ if ( body . type === RequestEnum . CREATE_CLASSROOM ) {
165+ const id : number = ClassroomCreationQueue . addRequest ( body . data ) ;
166+ body . response = await ClassroomCreationQueue . getResult ( id ) ;
167+ res . json ( body . response ) ;
168+ } else if ( body . type === RequestEnum . DELETE_CLASSROOM ) {
169+ // [ ] add delete classroom request
170+ } else if ( body . type === RequestEnum . JOIN_CLASSROOM ) {
171+ const id : number = ClassroomJoinQueue . addRequest ( body . data ) ;
172+ body . response = await ClassroomJoinQueue . getResult ( id ) ;
173+ res . json ( body . response ) ;
174+ } else if ( body . type === RequestEnum . LEAVE_CLASSROOM ) {
175+ const id : number = ClassroomLeaveQueue . addRequest ( {
176+ username : req . body . user ,
177+ ...body . data ,
178+ } ) ;
179+ body . response = await ClassroomLeaveQueue . getResult ( id ) ;
180+ res . json ( body . response ) ;
181+ } else if ( body . type === RequestEnum . CREATE_COURSE ) {
182+ const id : number = CourseCreationQueue . addRequest ( body . data ) ;
183+ body . response = await CourseCreationQueue . getResult ( id ) ;
184+ res . json ( body . response ) ;
185+ } else if ( body . type === RequestEnum . ADD_COURSE_DATA ) {
186+ const payload = prepareNote55 (
187+ body . data . courseID ,
188+ body . data . codeCourse ,
189+ body . data . shape ,
190+ body . data . data ,
191+ body . data . author ,
192+ body . data . tags ,
193+ body . data . uploads
194+ ) ;
195+ CouchDB . use ( `coursedb-${ body . data . courseID } ` )
196+ . insert ( payload as Nano . MaybeDocument )
197+ . then ( ( r ) => {
198+ logger . info ( `\t\t\tCouchDB insert result: ${ JSON . stringify ( r ) } ` ) ;
199+ res . json ( r ) ;
200+ } )
201+ . catch ( ( e ) => {
202+ logger . info ( `\t\t\tCouchDB insert error: ${ JSON . stringify ( e ) } ` ) ;
203+ res . json ( e ) ;
204+ } ) ;
205+ } else if ( body . type === RequestEnum . PACK_COURSE ) {
206+ if ( envConfig . NODE_ENV !== 'studio' ) {
207+ logger . info (
208+ `\tPACK_COURSE request received in production mode, but this is not supported!`
209+ ) ;
210+ res . status ( 400 ) ;
211+ res . statusMessage = 'Packing courses is not supported in production mode.' ;
212+ res . send ( ) ;
213+ return ;
214+ }
215+
216+ body . response = await packCourse ( {
217+ courseId : body . courseId ,
218+ outputPath : body . outputPath
219+ } ) ;
220+ res . json ( body . response ) ;
221+ }
222+ } else {
223+ logger . info ( `\tREQUEST UNAUTHORIZED!` ) ;
224+ res . status ( 401 ) ;
225+ res . statusMessage = 'Unauthorized' ;
226+ res . send ( ) ;
227+ }
228+ }
229+
230+ app . post ( '/' , ( req : Request , res : Response ) => {
231+ void postHandler ( req , res ) ;
232+ } ) ;
233+
234+ app . get ( '/version' , ( _req : Request , res : Response ) => {
235+ res . send ( envConfig . VERSION ) ;
236+ } ) ;
237+
238+ app . get ( '/' , ( _req : Request , res : Response ) => {
239+ let status = `Express service is running.\nVersion: ${ envConfig . VERSION } \n` ;
240+
241+ CouchDB . session ( )
242+ . then ( ( s ) => {
243+ if ( s . ok ) {
244+ status += 'Couchdb is running.\n' ;
245+ } else {
246+ status += 'Couchdb session is NOT ok.\n' ;
247+ }
248+ } )
249+ . catch ( ( e ) => {
250+ status += `Problems in the couch session! ${ JSON . stringify ( e ) } ` ;
251+ } )
252+ . finally ( ( ) => {
253+ res . send ( status ) ;
254+ } ) ;
255+ } ) ;
256+
257+ return app ;
258+ }
259+
260+ /**
261+ * Initialize background services and database connections.
262+ * This should be called after the server starts listening.
263+ */
264+ export async function initializeServices ( ) : Promise < void > {
265+ try {
266+ // start the change-listener that does post-processing on user
267+ // media uploads
268+ void PostProcess ( ) ;
269+
270+ void initCourseDBDesignDocInsert ( ) ;
271+
272+ void useOrCreateDB ( 'classdb-lookup' ) ;
273+ try {
274+ await (
275+ await useOrCreateDB ( 'coursedb' )
276+ ) . insert (
277+ {
278+ validate_doc_update : classroomDbDesignDoc ,
279+ } as Nano . MaybeDocument ,
280+ '_design/_auth'
281+ ) ;
282+ } catch ( e ) {
283+ logger . info ( `Error: ${ e } ` ) ;
284+ }
285+ } catch ( e ) {
286+ logger . info ( `Error: ${ JSON . stringify ( e ) } ` ) ;
287+ }
288+ }
0 commit comments