Skip to content

Commit 0d457a0

Browse files
committed
Upgrade packages
Update dockerfile for correct node version. Update package for openid-client This essentially required reconstructing the oidc.go and auth.go files following the 6.x examples. While doing that, resolved some performance problems by only validating and parsing the session-stored JWT token once in the authenticated path and inserting the resulting sub (subject) into the request object for access in the next endpoint. This greatly simplifies the parts that expect an authenticated endpoint. Added indexes for database performance. Can't get the migration to generate!
1 parent 888fd63 commit 0d457a0

File tree

14 files changed

+2070
-3316
lines changed

14 files changed

+2070
-3316
lines changed

.github/workflows/pull-request.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
- name: Use Node.js
3232
uses: actions/setup-node@v4
3333
with:
34-
node-version: v22.21.0
34+
node-version: v22.22.0
3535
cache: 'npm'
3636
cache-dependency-path: '**/package-lock.json'
3737

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM node:21.1.0-alpine AS packages
1+
FROM node:22.22.0-alpine AS packages
22
WORKDIR /usr/src/app
33

44
COPY LICENSE /usr/src/app/

package-lock.json

Lines changed: 1870 additions & 3110 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "jetkvm-cloud-api",
3-
"version": "1.0.0",
3+
"version": "2026.01.29.0845",
44
"description": "JetKVM Cloud API and Websocket Server",
55
"main": "dist/src/index.js",
66
"scripts": {
@@ -16,45 +16,42 @@
1616
"test:coverage": "vitest run --coverage"
1717
},
1818
"engines": {
19-
"node": "22.x"
19+
"node": "^22.22.0"
2020
},
2121
"keywords": [],
2222
"author": "JetKVM",
2323
"license": "GPL-2.0",
2424
"dependencies": {
25-
"@aws-sdk/client-s3": "^3.654.0",
26-
"@prisma/client": "^5.13.0",
27-
"@tsconfig/node22": "^22.0.0",
25+
"@aws-sdk/client-s3": "^3.978.0",
26+
"@prisma/client": "^6.19.2",
27+
"@tsconfig/node22": "^22.0.5",
2828
"@types/cookie-session": "^2.0.49",
29-
"@types/cors": "^2.8.17",
30-
"@types/node": "^20.12.10",
31-
"@types/ws": "^8.5.10",
32-
"cookie-session": "^2.1.0",
33-
"cors": "^2.8.5",
34-
"dotenv": "^16.4.7",
35-
"express": "^5",
36-
"helmet": "^7.1.0",
37-
"http-proxy-middleware": "^3.0.3",
38-
"jose": "^5.2.4",
39-
"lru-cache": "^11.2.2",
40-
"openid-client": "^5.6.5",
41-
"prisma": "^5.13.0",
42-
"semver": "^7.6.3",
29+
"@types/cors": "^2.8.19",
30+
"@types/node": "^25.1.0",
31+
"@types/ws": "^8.18.1",
32+
"cookie-session": "^2.1.1",
33+
"cors": "^2.8.6",
34+
"dotenv": "^17.2.3",
35+
"express": "^5.2.1",
36+
"helmet": "^8.1.0",
37+
"http-proxy-middleware": "^3.0.5",
38+
"jose": "^6.1.3",
39+
"lru-cache": "^11.2.5",
40+
"openid-client": "^6.8.1",
41+
"prisma": "^6.19.2",
42+
"semver": "^7.7.3",
4343
"ts-node": "^10.9.2",
44-
"typescript": "^5.4.5",
45-
"ws": "^8.17.1",
44+
"typescript": "^5.9.3",
45+
"ws": "^8.19.0",
4646
"zod": "^4.3.6"
4747
},
4848
"optionalDependencies": {
4949
"bufferutil": "^4.0.8"
5050
},
5151
"devDependencies": {
5252
"@types/express": "^5.0.6",
53-
"@types/lru-cache": "^7.10.9",
54-
"@types/semver": "^7.5.8",
55-
"@vitest/coverage-v8": "^4.0.18",
56-
"aws-sdk-client-mock": "^4.1.0",
57-
"prettier": "3.2.5",
53+
"@types/semver": "^7.7.1",
54+
"prettier": "3.8.1",
5855
"vitest": "^4.0.18"
5956
}
6057
}

prisma/schema.prisma

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ model User {
2020
picture String?
2121
device Device[]
2222
Activity TurnActivity[]
23+
24+
@@index([googleId], type: Hash)
2325
}
2426

2527
model Device {
@@ -31,6 +33,10 @@ model Device {
3133
tempToken String?
3234
tempTokenExpiresAt DateTime?
3335
secretToken String? @unique
36+
37+
@@index([userId], type: Hash)
38+
@@index([tempToken], type: Hash)
39+
@@index([secretToken], type: Hash)
3440
}
3541

3642
model TurnActivity {
@@ -40,6 +46,8 @@ model TurnActivity {
4046
createdAt DateTime? @default(now()) @db.Timestamp(6)
4147
bytesSent Int
4248
bytesReceived Int
49+
50+
@@index([userId], type: Hash)
4351
}
4452

4553
model Release {
@@ -53,4 +61,5 @@ model Release {
5361
hash String
5462
5563
@@unique([version, type])
64+
@@index([type, rolloutPercentage])
5665
}

src/auth.ts

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,50 @@
11
import { type NextFunction, type Request, type Response } from "express";
22
import * as jose from "jose";
3-
import { UnauthorizedError } from "./errors";
4-
3+
import { BadRequestError, UnauthorizedError } from "./errors";
4+
5+
const JWKS_URL = new URL("https://www.googleapis.com/oauth2/v3/certs")
6+
const JWKS = jose.createRemoteJWKSet(JWKS_URL);
7+
const verificationOptions = {
8+
//algorithms: ['RS256'],
9+
issuer: "https://accounts.google.com",
10+
audience: process.env.GOOGLE_CLIENT_ID,
11+
};
512

613
export const verifyToken = async (idToken: string) => {
7-
const JWKS = jose.createRemoteJWKSet(
8-
new URL("https://www.googleapis.com/oauth2/v3/certs"),
9-
);
10-
1114
try {
12-
const { payload } = await jose.jwtVerify(idToken, JWKS, {
13-
issuer: "https://accounts.google.com",
14-
audience: process.env.GOOGLE_CLIENT_ID,
15-
});
16-
15+
const { payload } = await jose.jwtVerify(idToken, JWKS, verificationOptions);
16+
console.log('JWT Payload:', payload);
1717
return payload;
18-
} catch (e) {
19-
console.error(e);
18+
} catch (error: any) {
19+
console.error('JWT Verification Failed:', error.message);
2020
return null;
2121
}
2222
};
2323

2424
export const authenticated = async (req: Request, res: Response, next: NextFunction) => {
25-
const idToken = req.session?.id_token;
26-
if (!idToken) throw new UnauthorizedError();
25+
const session = req.session;
26+
if (!session) throw new UnauthorizedError("No session found");
27+
28+
const idToken = session.id_token;
29+
if (!idToken) throw new UnauthorizedError("No ID token found in session");
2730

2831
const payload = await verifyToken(idToken);
29-
if (!payload) throw new UnauthorizedError();
30-
if (!payload.exp) throw new UnauthorizedError();
32+
if (!payload) throw new UnauthorizedError("Invalid ID token");
3133

32-
if (new Date(payload.exp * 1000) < new Date()) {
33-
throw new UnauthorizedError();
34+
const { sub, iss, exp } = payload;
35+
if (!sub) throw new UnauthorizedError("Missing sub (subject) in token");
36+
if (!iss) throw new UnauthorizedError("Missing iss (issuer) in token");
37+
if (!exp) throw new UnauthorizedError("Missing exp (expiration) in token");
38+
39+
if (new Date(payload.exp! * 1000) < new Date()) {
40+
throw new UnauthorizedError("ID token has expired");
3441
}
3542

43+
const isGoogle = iss === "https://accounts.google.com";
44+
if (!isGoogle) throw new BadRequestError("Token is not from Google");
45+
46+
req.subject = sub;
47+
req.issuer = iss;
48+
3649
next();
37-
};
50+
};

src/db.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ if (process.env.NODE_ENV !== "development") {
1919
prismaClient = global.__db;
2020
}
2121

22-
2322
// Have to cast it manually, because webstorm can't infer it for some reason
2423
// https://github.com/prisma/prisma/issues/2359#issuecomment-963340538
2524
export const prisma = prismaClient;

src/devices.ts

Lines changed: 30 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as jose from "jose";
21
import { prisma } from "./db";
32
import express from "express";
43
import {
@@ -12,45 +11,40 @@ import { authenticated } from "./auth";
1211
import { activeConnections } from "./webrtc-signaling";
1312

1413
export const List = async (req: express.Request, res: express.Response) => {
15-
const idToken = req.session?.id_token;
16-
const { iss, sub } = jose.decodeJwt(idToken);
17-
18-
// Authorization server’s identifier for the user
19-
const isGoogle = iss === "https://accounts.google.com";
20-
if (isGoogle) {
21-
const devices = await prisma.device.findMany({
22-
where: { user: { googleId: sub } },
23-
select: { id: true, name: true, lastSeen: true },
24-
});
14+
const { subject } = req;
15+
if (!subject) throw new UnauthorizedError("Missing subject in token");
2516

26-
return res.json({
27-
devices: devices.map(device => {
28-
const activeDevice = activeConnections.get(device.id);
29-
const version = activeDevice?.[2] || null;
30-
31-
return {
32-
...device,
33-
online: !!activeDevice,
34-
version,
35-
};
36-
}),
37-
});
38-
} else {
39-
throw new BadRequestError("Token is not from Google");
40-
}
17+
const devices = await prisma.device.findMany({
18+
where: { user: { googleId: subject } },
19+
select: { id: true, name: true, lastSeen: true },
20+
});
21+
22+
return res.json({
23+
devices: devices.map(device => {
24+
const activeDevice = activeConnections.get(device.id);
25+
const version = activeDevice?.[2] || null;
26+
27+
return {
28+
...device,
29+
online: !!activeDevice,
30+
version,
31+
};
32+
}),
33+
});
4134
};
4235

4336
export const Retrieve = async (
4437
req: express.Request<{ id: string }>,
4538
res: express.Response
4639
) => {
47-
const idToken = req.session?.id_token;
48-
const { sub } = jose.decodeJwt(idToken);
40+
const { subject } = req;
41+
if (!subject) throw new UnauthorizedError("Missing subject in token");
42+
4943
const { id } = req.params;
5044
if (!id) throw new UnprocessableEntityError("Missing device id in params");
5145

5246
const device = await prisma.device.findUnique({
53-
where: { id, user: { googleId: sub } },
47+
where: { id, user: { googleId: subject } },
5448
select: { id: true, name: true, user: { select: { googleId: true } } },
5549
});
5650

@@ -62,18 +56,17 @@ export const Update = async (
6256
req: express.Request<{ id: string }>,
6357
res: express.Response
6458
) => {
65-
const idToken = req.session?.id_token;
66-
const { sub } = jose.decodeJwt(idToken);
67-
if (!sub) throw new UnauthorizedError("Missing sub in token");
59+
const { subject } = req;
60+
if (!subject) throw new UnauthorizedError("Missing subject in token");
6861

6962
const { id } = req.params;
7063
if (!id) throw new UnprocessableEntityError("Missing device id in params");
7164

7265
const { name } = req.body as { name: string };
73-
if (!name) throw new UnprocessableEntityError("Missing name in body");
66+
if (!name) throw new UnprocessableEntityError("Missing device name in body");
7467

7568
const device = await prisma.device.update({
76-
where: { id, user: { googleId: sub } },
69+
where: { id, user: { googleId: subject } },
7770
data: { name },
7871
select: { id: true },
7972
});
@@ -125,14 +118,13 @@ export const Delete = async (
125118
throw new BadRequestError("Unauthorized");
126119
}
127120

128-
const idToken = req.session?.id_token;
129-
const { sub } = jose.decodeJwt(idToken);
130-
if (!sub) throw new UnauthorizedError("Missing sub in token");
121+
const { subject } = req;
122+
if (!subject) throw new UnauthorizedError("Missing subject in token");
131123

132124
const { id } = req.params;
133125
if (!id) throw new UnprocessableEntityError("Missing device id in params");
134126

135-
await prisma.device.delete({ where: { id, user: { googleId: sub } } });
127+
await prisma.device.delete({ where: { id, user: { googleId: subject } } });
136128

137129
// We just removed the device, so we should close any running open socket connections
138130
const conn = activeConnections.get(id);

src/index.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import express from "express";
22
import cors from "cors";
33
import cookieSession from "cookie-session";
4-
import * as jose from "jose";
54
import helmet from "helmet";
65
import 'dotenv/config';
76

@@ -97,26 +96,25 @@ app.get(
9796
"/me",
9897
authenticated,
9998
async (req: express.Request, res: express.Response) => {
100-
const idToken = req.session?.id_token;
101-
const { sub, iss, exp, aud, iat, jti, nbf } = jose.decodeJwt(idToken);
99+
const { subject, issuer } = req;
100+
if (!subject || !issuer) {
101+
return res.status(401).json({ message: "Unauthorized" });
102+
}
102103

103104
let user;
104-
if (iss === "https://accounts.google.com") {
105+
if (issuer === "https://accounts.google.com") {
105106
user = await prisma.user.findUnique({
106-
where: { googleId: sub },
107+
where: { googleId: subject },
107108
select: { picture: true, email: true },
108109
});
109110
}
110111

111-
return res.json({ ...user, sub });
112+
return res.json({ ...user, subject });
112113
},
113114
);
114115

115116
app.get("/releases", Releases.Retrieve);
116-
app.get(
117-
"/releases/system_recovery/latest",
118-
Releases.RetrieveLatestSystemRecovery,
119-
);
117+
app.get("/releases/system_recovery/latest", Releases.RetrieveLatestSystemRecovery);
120118
app.get("/releases/app/latest", Releases.RetrieveLatestApp);
121119

122120
app.get("/devices", authenticated, Devices.List);
@@ -127,11 +125,7 @@ app.delete("/devices/:id", Devices.Delete);
127125

128126
app.post("/webrtc/session", authenticated, Webrtc.CreateSession);
129127
app.post("/webrtc/ice_config", authenticated, Webrtc.CreateIceCredentials);
130-
app.post(
131-
"/webrtc/turn_activity",
132-
authenticated,
133-
Webrtc.CreateTurnActivity,
134-
);
128+
app.post("/webrtc/turn_activity", authenticated, Webrtc.CreateTurnActivity);
135129

136130
app.post("/oidc/google", OIDC.Google);
137131
app.get("/oidc/callback_o", OIDC.Callback);

0 commit comments

Comments
 (0)