Skip to content

Commit bec762a

Browse files
committed
refactor: add fn to register houcuspocus and swagger-ui
- access houcuspocus through request.app.locals - move auth module in core - define types for express request / local - update eslint config for consistent type import
1 parent 86f47aa commit bec762a

18 files changed

+238
-164
lines changed
Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import express from 'express';
1+
import { type Request as ExpressRequest } from 'express';
22

3-
import { verifyJwt } from './utils/jwt.ts';
4-
import { AuthError } from './utils/error.ts';
5-
import env from './utils/env.ts';
3+
import { verifyJwt, verifyServiceToken } from '../utils/jwt.ts';
4+
import { AuthError } from '../utils/error.ts';
5+
import env from '../utils/env.ts';
66

77
export async function expressAuthentication(
8-
request: express.Request,
8+
request: ExpressRequest,
99
securityName: string,
1010
// scopes?: string[]
11+
// FIXME: node-tsc does not consider this valid
12+
// ): Promise<ExpressRequest['user']> {
1113
) {
1214
if (securityName !== 'userAuthJwt' && securityName !== 'backendAuthToken') {
1315
// NOTE: Express error handler handles AuthError
@@ -46,7 +48,8 @@ export async function expressAuthentication(
4648
};
4749
}
4850
else {
49-
if (token !== env.SERVICE_TOKEN) {
51+
const isValid = verifyServiceToken(token, env.SERVICE_TOKEN);
52+
if (!isValid) {
5053
// NOTE: Express error handler handles AuthError
5154
throw new AuthError('Service token is not correct', 401);
5255
}

app/core/express.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import express from 'express';
22
import { ValidateError } from 'tsoa';
33
import expressWebsockets from 'express-ws';
4-
import fs from 'fs';
5-
import yaml from 'yaml';
6-
import swaggerUi from 'swagger-ui-express';
74
import morgan from 'morgan';
85
import compression from 'compression';
96
import cors from 'cors';
@@ -45,18 +42,6 @@ export function initWsApp(
4542
origin: config.allowedOrigins,
4643
}));
4744

48-
// Support swagger ui
49-
expressWsApp.use('/docs', swaggerUi.serve, async (_req: express.Request, res: express.Response) => {
50-
// NOTE: Using YAML because JSON giving error
51-
// https://gitlab.com/gitlab-org/gitlab/-/issues/379097
52-
53-
const file = fs.readFileSync('./generated/swagger.yaml', 'utf8');
54-
const swaggerDocument = yaml.parse(file);
55-
56-
const swaggerHtml = swaggerUi.generateHTML(swaggerDocument);
57-
return res.send(swaggerHtml);
58-
});
59-
6045
preRoutesRegistrationHook(expressWsApp);
6146

6247
// Register other routes from tsoa
@@ -94,16 +79,18 @@ export function initWsApp(
9479
});
9580
}
9681
if (err instanceof Error) {
82+
console.error(err);
9783
return res.status(500).json({
9884
message: 'Internal server error',
9985
details: err.message,
10086
});
10187
}
102-
console.error('Uncaught error:', err);
88+
console.error(err);
10389
return res.status(500).json({
10490
message: 'Internal server error',
10591
});
10692
},
10793
);
94+
10895
return expressWsApp;
10996
}

app/core/hocuspocus.ts

Lines changed: 138 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import * as Y from 'yjs';
2-
import { Connection, Hocuspocus, type Extension } from '@hocuspocus/server';
2+
import { type Configuration, type Connection, Hocuspocus, type Extension } from '@hocuspocus/server';
33
import { Logger } from '@hocuspocus/extension-logger';
44
import { S3 } from '@hocuspocus/extension-s3';
5+
import { type Application } from 'express-ws';
56

67
import {
78
fetchReport,
@@ -23,123 +24,8 @@ export const s3Extension = new S3({
2324

2425
const loggerExtention = new Logger();
2526

26-
// Configure hocuspocus server
27-
export const hocuspocusServer = new Hocuspocus({
28-
extensions: [
29-
s3Extension satisfies Extension,
30-
loggerExtention satisfies Extension,
31-
],
32-
onConnect: async (data) => {
33-
const { documentName, context } = data;
34-
const docInfo = validateDocName(documentName);
35-
36-
if (docInfo instanceof Error) {
37-
// NOTE: Throwing exception so that connection is not established
38-
throw docInfo;
39-
}
40-
41-
return {
42-
...context,
43-
doc: docInfo,
44-
};
45-
},
46-
onAuthenticate: async (data) => {
47-
// NOTE: onAuthenticate will only be called if user supplied a "token"
48-
// TODO: check what happens if no token is sent?
49-
const { token, context } = data;
50-
51-
const tokenData = await verifyJwt(
52-
token,
53-
env.WEB_COGNITO_USER_POOL_ID,
54-
env.WEB_COGNITO_USER_POOL_CLIENT_ID,
55-
env.COGNITO_ISSUER,
56-
'id',
57-
);
58-
59-
if (tokenData instanceof Error) {
60-
// NOTE: Throwing exception so that authenitcation fails
61-
throw Error('Token must be valid!');
62-
}
63-
64-
// TODO: Update permissions from user group and pass permission function
65-
const id = tokenData['cognito:username'];
66-
const groups = tokenData['cognito:groups'];
67-
if (!groups || groups.length <= 0) {
68-
// NOTE: Throwing exception so that authenitcation fails
69-
throw Error('User should be in a group to edit documents');
70-
}
71-
72-
return {
73-
...context,
74-
user: {
75-
id: id as string,
76-
username: tokenData.preferred_username as string,
77-
email: tokenData.email as string,
78-
groups: groups as string[],
79-
token,
80-
},
81-
};
82-
},
83-
onLoadDocument: async (data) => {
84-
// NOTE: We can load the data from extension directly if it exists in s3
85-
const updateFromS3 = await s3Extension.configuration.fetch(data);
86-
if (updateFromS3 !== null) {
87-
Y.applyUpdate(data.document, updateFromS3);
88-
console.debug(`Loaded document "${data.documentName}" from s3`);
89-
return data.document;
90-
}
91-
92-
// NOTE: This means we are creating a direct connection.
93-
// In this case, we should not load data from API
94-
if (!data.context || !data.context.user) {
95-
// NOTE: Throwing exception so that empty document is not created
96-
throw Error(`Could not load document "${data.documentName}" from s3`);
97-
}
98-
99-
// NOTE: If document not in S3, get from server
100-
const { token } = data.context.user;
101-
const { id } = data.context.doc;
102-
const reportV1 = await fetchReport(
103-
env.BACKEND_HOST,
104-
id,
105-
token,
106-
);
107-
108-
if (reportV1 instanceof Error) {
109-
// NOTE: Throwing exception so that empty document is not created
110-
throw reportV1;
111-
}
112-
113-
slateReportToDoc(reportV1, data.document);
114-
console.debug(`Loaded document "${data.documentName}" from API`);
115-
return data.document;
116-
},
117-
onChange: async (data) => {
118-
const {
119-
document,
120-
transactionOrigin,
121-
} = data;
122-
123-
// NOTE: We only want to update these counts when changes are from the client
124-
const connection: Connection | null | undefined = transactionOrigin;
125-
if (connection === null || connection === undefined) {
126-
return;
127-
}
128-
129-
document.transact(() => {
130-
changeReportUpdateStates(
131-
document,
132-
oldValue => ({
133-
no_of_updates: (oldValue?.no_of_updates ?? 0) + 1,
134-
last_updated: new Date().getTime(),
135-
}),
136-
);
137-
});
138-
},
139-
});
140-
14127
// Get a document from hocuspocus or s3
142-
export async function getDoc(name: string) {
28+
export async function getDoc(name: string, hocuspocusServer: Hocuspocus) {
14329
const doc = hocuspocusServer.documents.get(name);
14430
if (doc) {
14531
return doc;
@@ -155,3 +41,138 @@ export async function getDoc(name: string) {
15541
Y.applyUpdate(s3Doc, fetched);
15642
return s3Doc;
15743
}
44+
45+
export function registerHocuspocus(app: Application, otherConfig?: Partial<Configuration>) {
46+
// Configure hocuspocus server
47+
const hocuspocusServer = new Hocuspocus({
48+
extensions: [
49+
s3Extension satisfies Extension,
50+
loggerExtention satisfies Extension,
51+
],
52+
onConnect: async (data) => {
53+
const { documentName, context } = data;
54+
const docInfo = validateDocName(documentName);
55+
56+
if (docInfo instanceof Error) {
57+
// NOTE: Throwing exception so that connection is not established
58+
throw docInfo;
59+
}
60+
61+
return {
62+
...context,
63+
doc: docInfo,
64+
};
65+
},
66+
onAuthenticate: async (data) => {
67+
// NOTE: onAuthenticate will only be called if user supplied a "token"
68+
// TODO: check what happens if no token is sent?
69+
const { token, context } = data;
70+
71+
const tokenData = await verifyJwt(
72+
token,
73+
env.WEB_COGNITO_USER_POOL_ID,
74+
env.WEB_COGNITO_USER_POOL_CLIENT_ID,
75+
env.COGNITO_ISSUER,
76+
'id',
77+
);
78+
79+
if (tokenData instanceof Error) {
80+
// NOTE: Throwing exception so that authenitcation fails
81+
throw Error('Token must be valid!');
82+
}
83+
84+
// TODO: Update permissions from user group and pass permission function
85+
const id = tokenData['cognito:username'];
86+
const groups = tokenData['cognito:groups'];
87+
if (!groups || groups.length <= 0) {
88+
// NOTE: Throwing exception so that authenitcation fails
89+
throw Error('User should be in a group to edit documents');
90+
}
91+
92+
return {
93+
...context,
94+
user: {
95+
id: id as string,
96+
username: tokenData.preferred_username as string,
97+
email: tokenData.email as string,
98+
groups: groups as string[],
99+
token,
100+
},
101+
};
102+
},
103+
onLoadDocument: async (data) => {
104+
// NOTE: We can load the data from extension directly if it exists in s3
105+
const updateFromS3 = await s3Extension.configuration.fetch(data);
106+
if (updateFromS3 !== null) {
107+
Y.applyUpdate(data.document, updateFromS3);
108+
console.debug(`Loaded document "${data.documentName}" from s3`);
109+
return data.document;
110+
}
111+
112+
// NOTE: This means we are creating a direct connection.
113+
// In this case, we should not load data from API
114+
if (!data.context || !data.context.user) {
115+
// NOTE: Throwing exception so that empty document is not created
116+
throw Error(`Could not load document "${data.documentName}" from s3`);
117+
}
118+
119+
// NOTE: If document not in S3, get from server
120+
const { token } = data.context.user;
121+
const { id } = data.context.doc;
122+
const reportV1 = await fetchReport(
123+
env.BACKEND_HOST,
124+
id,
125+
token,
126+
);
127+
128+
if (reportV1 instanceof Error) {
129+
// FIXME: Convert exception to CloseEvent
130+
// NOTE: Throwing exception so that empty document is not created
131+
throw {
132+
code: 9000,
133+
reason: reportV1.message,
134+
};
135+
}
136+
137+
slateReportToDoc(reportV1, data.document);
138+
console.debug(`Loaded document "${data.documentName}" from API`);
139+
return data.document;
140+
},
141+
onChange: async (data) => {
142+
const {
143+
document,
144+
transactionOrigin,
145+
} = data;
146+
147+
// NOTE: We only want to update these counts when changes are from the client
148+
const connection: Connection | null | undefined = transactionOrigin;
149+
if (connection === null || connection === undefined) {
150+
return;
151+
}
152+
153+
document.transact(() => {
154+
changeReportUpdateStates(
155+
document,
156+
oldValue => ({
157+
no_of_updates: (oldValue?.no_of_updates ?? 0) + 1,
158+
last_updated: new Date().getTime(),
159+
}),
160+
);
161+
});
162+
},
163+
...otherConfig,
164+
});
165+
166+
// NOTE: We are attaching hocuspocus so that we can access this later
167+
app.locals.hocuspocus = hocuspocusServer;
168+
169+
app.ws('/connect/', (websocket, request) => {
170+
hocuspocusServer.handleConnection(websocket, request);
171+
});
172+
173+
app.ws('/collab/', (websocket, request) => {
174+
hocuspocusServer.handleConnection(websocket, request);
175+
});
176+
177+
return hocuspocusServer;
178+
}

app/core/swagger.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { type Application } from 'express-ws';
2+
import { type Request as ExpressRequest, type Response as ExpressResponse } from 'express';
3+
import swaggerUi from 'swagger-ui-express';
4+
import fs from 'fs';
5+
import yaml from 'yaml';
6+
7+
export function registerSwaggerUi(app: Application) {
8+
app.use('/docs', swaggerUi.serve, async (_req: ExpressRequest, res: ExpressResponse) => {
9+
// NOTE: Using YAML because JSON giving error
10+
// https://gitlab.com/gitlab-org/gitlab/-/issues/379097
11+
12+
const file = fs.readFileSync('./generated/swagger.yaml', 'utf8');
13+
const swaggerDocument = yaml.parse(file);
14+
15+
const swaggerHtml = swaggerUi.generateHTML(swaggerDocument);
16+
return res.send(swaggerHtml);
17+
});
18+
}

app/main.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { hocuspocusServer } from './core/hocuspocus.ts';
21
import { initWsApp } from './core/express.ts';
2+
import { registerSwaggerUi } from './core/swagger.ts';
3+
import { registerHocuspocus } from './core/hocuspocus.ts';
34

45
import env from './utils/env.ts';
56

@@ -11,10 +12,8 @@ const expressWsApp = initWsApp(
1112
],
1213
},
1314
(app) => {
14-
// Register collaboration endpoint to upgrade to websocket
15-
app.ws('/collaboration/', (websocket, request) => {
16-
hocuspocusServer.handleConnection(websocket, request);
17-
});
15+
registerSwaggerUi(app);
16+
registerHocuspocus(app);
1817
},
1918
);
2019

0 commit comments

Comments
 (0)