Upgrade to Express v5, Dockerfile, and Health Checks (#29)

Co-authored-by: Aveline <g@xswan.net>
Co-authored-by: Adam Shiervani <adam.shiervani@gmail.com>
Co-authored-by: Marc Brooks <IDisposable@gmail.com>
This commit is contained in:
Noah Halstead
2025-10-13 09:29:40 -04:00
committed by GitHub
parent 2d20ce00e2
commit 17d01bb7eb
11 changed files with 630 additions and 330 deletions
+6
View File
@@ -0,0 +1,6 @@
.github/
.idea/
node_modules/
dist/
.DS_Store
.env
+31 -15
View File
@@ -1,23 +1,39 @@
PORT=3000
DATABASE_URL="postgresql://jetkvm:jetkvm@localhost:5432/jetkvm?schema=public"
GOOGLE_CLIENT_ID=XXX # Google OIDC Client ID
GOOGLE_CLIENT_SECRET=XXX # Google OIDC Client Secret
##
## Google Auth with OIDC Configuration
##
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
API_HOSTNAME=
APP_HOSTNAME=
API_HOSTNAME=XXX # Is needed for the OIDC Callback
APP_HOSTNAME=XXX # Is needed for the OIDC Callback
##
## Cloudflare TURN Service
##
CLOUDFLARE_TURN_ID=
CLOUDFLARE_TURN_TOKEN=
CLOUDFLARE_TURN_ID=XXX # Cloudflare TURN ID
CLOUDFLARE_TURN_TOKEN=XXX # Cloudflare TURN Token
##
## Session Cookie Secret
##
COOKIE_SECRET=
COOKIE_SECRET=XXX # Session Cookie Secret
###
### S3 Compatible Storage
###
R2_ENDPOINT=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET=
R2_CDN_URL=
R2_ENDPOINT=XXX # Any S3 compatible endpoint
R2_ACCESS_KEY_ID=XXX # Any S3 compatible access key
R2_SECRET_ACCESS_KEY=XXX # Any S3 compatible secret access key
R2_BUCKET=XXX # Any S3 compatible bucket
R2_CDN_URL=XXX # Any S3 compatible CDN URL
# Allowed CORS Origins, split by comma
CORS_ORIGINS=https://app.jetkvm.com,http://localhost:5173
CORS_ORIGINS=https://app.jetkvm.com,http://localhost:5173 # Allowed CORS Origins, split by comma
# Real IP Header for the reverse proxy (e.g. X-Real-IP), leave empty if not needed
REAL_IP_HEADER=
REAL_IP_HEADER=XXX # Real IP Header for the reverse proxy (e.g. X-Real-IP), leave empty if not needed
ICE_SERVERS=XXX # ICE Servers for WebRTC, split by comma (e.g. stun:stun.l.google.com:19302,stun:stun1.l.google.com:19302)
# ICE Servers for WebRTC, split by comma (e.g. stun:stun.l.google.com:19302,stun:stun1.l.google.com:19302)
ICE_SERVERS=
+1
View File
@@ -1,4 +1,5 @@
node_modules
.idea
dist/
.env
.env.development
+29
View File
@@ -0,0 +1,29 @@
FROM node:21.1.0-alpine AS packages
WORKDIR /usr/src/app
COPY LICENSE /usr/src/app/
COPY package.json /usr/src/app/
COPY package-lock.json /usr/src/app/
RUN npm install
FROM packages AS builder
WORKDIR /usr/src/app
COPY . /usr/src/app/
RUN npx prisma generate
RUN npm run build
FROM packages AS app
LABEL org.opencontainers.image.source="https://github.com/jetkvm/cloud-api"
WORKDIR /usr/src/app
COPY --from=builder /usr/src/app/prisma /usr/src/app/prisma
COPY --from=builder /usr/src/app/node_modules/.prisma /usr/src/app/node_modules/.prisma
COPY --from=builder /usr/src/app/dist /usr/src/app/dist
COPY .env.example ./.env
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["node", "./dist/index.js"]
+1 -1
View File
@@ -26,7 +26,7 @@ If you've found an issue and want to report it, please check our [Issues](https:
## Development
This project is built with Node.JS, Prisma and Express.
This project is built on Node.JS using Prisma and Express.
To start the development server, run:
+44
View File
@@ -0,0 +1,44 @@
name: jetkvm-cloud-api
networks:
jetkvm:
driver: bridge
services:
db:
image: postgres
restart: always
environment:
POSTGRES_PASSWORD: jetkvm
POSTGRES_USER: jetkvm
POSTGRES_DB: jetkvm
#ports:
# - "5432:5432"
networks:
- jetkvm
volumes:
- postgresql:/var/lib/postgresql/data
app: &app
build: .
environment:
PORT: 5172
DATABASE_URL: postgres://jetkvm:jetkvm@db:5432/jetkvm
depends_on:
- db
ports:
- "5172:5172"
networks:
- jetkvm
# Trigger prisma migration
# This can be done in the app container as well, but is generally discouraged.
app-migrate:
<<: *app
command: npm run prisma-migrate
ports: []
restart: no
volumes:
postgresql:
driver: local
+465 -274
View File
File diff suppressed because it is too large Load Diff
+14 -9
View File
@@ -1,19 +1,23 @@
{
"name": "jetkvm-cloud-api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"description": "JetKVM Cloud API and Websocket Server",
"main": "dist/src/index.js",
"scripts": {
"start": "NODE_ENV=production node -r ts-node/register ./src/index.ts",
"dev": "NODE_ENV=development node --watch --watch-path=./src --env-file=.env.development -r ts-node/register ./src/index.ts",
"dev:debug": "NODE_ENV=development node --watch --watch-path=./src --env-file=.env.development -r ts-node/register ./src/index.ts --inspect-brk"
"dev:debug": "NODE_ENV=development node --watch --watch-path=./src --env-file=.env.development -r ts-node/register ./src/index.ts --inspect-brk",
"prisma-dev": "prisma generate --watch",
"prisma-dev-migrate": "prisma migrate dev",
"prisma-migrate": "prisma migrate deploy",
"build": "tsc"
},
"engines": {
"node": "21.1.0"
},
"keywords": [],
"author": "",
"license": "ISC",
"author": "JetKVM",
"license": "GPL-2.0",
"dependencies": {
"@aws-sdk/client-s3": "^3.654.0",
"@prisma/client": "^5.13.0",
@@ -24,22 +28,23 @@
"@types/ws": "^8.5.10",
"cookie-session": "^2.1.0",
"cors": "^2.8.5",
"express": "^4.19.2",
"dotenv": "^16.4.7",
"express": "^5",
"helmet": "^7.1.0",
"http-proxy-middleware": "^3.0.0",
"http-proxy-middleware": "^3.0.3",
"jose": "^5.2.4",
"openid-client": "^5.6.5",
"prettier": "3.2.5",
"prisma": "^5.13.0",
"semver": "^7.6.3",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"ws": "^8.17.0"
"ws": "^8.17.1"
},
"optionalDependencies": {
"bufferutil": "^4.0.8"
},
"devDependencies": {
"prettier": "3.2.5",
"@types/semver": "^7.5.8"
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ declare global {
// This is needed because in development we don't want to restart
// the server with every change, but we want to make sure we don't
// create a new connectison to the DB with every change either.
// create a new connection to the DB with every change either.
if (process.env.NODE_ENV !== "development") {
prismaClient = new PrismaClient();
prismaClient.$connect();
+33 -29
View File
@@ -3,6 +3,7 @@ import cors from "cors";
import cookieSession from "cookie-session";
import * as jose from "jose";
import helmet from "helmet";
import 'dotenv/config';
import * as Devices from "./devices";
import * as OIDC from "./oidc";
@@ -18,6 +19,7 @@ declare global {
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: "development" | "production";
PORT: string;
API_HOSTNAME: string;
APP_HOSTNAME: string;
@@ -47,6 +49,8 @@ declare global {
}
}
const PORT = process.env.PORT || 3000;
const app = express();
app.use(helmet());
app.disable("x-powered-by");
@@ -74,25 +78,25 @@ export const cookieSessionMiddleware = cookieSession({
app.use(cookieSessionMiddleware);
function asyncHandler(fn: any) {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
return Promise.resolve(fn(req, res, next)).catch(next);
};
}
// express-session won't sent the cookie, as it's `secure` and `secureProxy` is set to true
// DO Apps doesn't send a X-Forwarded-Proto header, so we simply need to make a blanket trust
app.set("trust proxy", true);
const asyncAuthGuard = asyncHandler(authenticated);
app.get("/", (req, res) => {
return res.status(200).send("OK");
});
app.get("/healthz", (req, res) => {
return res.status(200).send({
ready: true,
time: new Date()
})
});
app.get(
"/me",
asyncAuthGuard,
asyncHandler(async (req: express.Request, res: express.Response) => {
authenticated,
async (req: express.Request, res: express.Response) => {
const idToken = req.session?.id_token;
const { sub, iss, exp, aud, iat, jti, nbf } = jose.decodeJwt(idToken);
@@ -105,32 +109,32 @@ app.get(
}
return res.json({ ...user, sub });
}),
},
);
app.get("/releases", asyncHandler(Releases.Retrieve));
app.get("/releases", Releases.Retrieve);
app.get(
"/releases/system_recovery/latest",
asyncHandler(Releases.RetrieveLatestSystemRecovery),
Releases.RetrieveLatestSystemRecovery,
);
app.get("/releases/app/latest", asyncHandler(Releases.RetrieveLatestApp));
app.get("/releases/app/latest", Releases.RetrieveLatestApp);
app.get("/devices", asyncAuthGuard, asyncHandler(Devices.List));
app.get("/devices/:id", asyncAuthGuard, asyncHandler(Devices.Retrieve));
app.post("/devices/token", asyncHandler(Devices.Token));
app.put("/devices/:id", asyncAuthGuard, asyncHandler(Devices.Update));
app.delete("/devices/:id", asyncHandler(Devices.Delete));
app.get("/devices", authenticated, Devices.List);
app.get("/devices/:id", authenticated, Devices.Retrieve);
app.post("/devices/token", Devices.Token);
app.put("/devices/:id", authenticated, Devices.Update);
app.delete("/devices/:id", Devices.Delete);
app.post("/webrtc/session", asyncAuthGuard, asyncHandler(Webrtc.CreateSession));
app.post("/webrtc/ice_config", asyncAuthGuard, asyncHandler(Webrtc.CreateIceCredentials));
app.post("/webrtc/session", authenticated, Webrtc.CreateSession);
app.post("/webrtc/ice_config", authenticated, Webrtc.CreateIceCredentials);
app.post(
"/webrtc/turn_activity",
asyncAuthGuard,
asyncHandler(Webrtc.CreateTurnActivity),
authenticated,
Webrtc.CreateTurnActivity,
);
app.post("/oidc/google", asyncHandler(OIDC.Google));
app.get("/oidc/callback_o", asyncHandler(OIDC.Callback));
app.post("/oidc/google", OIDC.Google);
app.get("/oidc/callback_o", OIDC.Callback);
app.get("/oidc/callback", (req, res) => {
/*
* We set the session cookie in the /oidc/google route as a part of 302 redirect to the OIDC login page
@@ -177,10 +181,10 @@ app.get("/oidc/callback", (req, res) => {
app.post(
"/logout",
asyncHandler((req: express.Request, res: express.Response) => {
(req: express.Request, res: express.Response) => {
req.session = null;
return res.json({ message: "Logged out" });
}),
}
);
// Error-handling middleware
@@ -190,7 +194,7 @@ app.use(
req: express.Request,
res: express.Response,
next: express.NextFunction,
) => {
): void => {
const isProduction = process.env.NODE_ENV === "production";
const statusCode = err instanceof HttpError ? err.status : 500;
@@ -207,8 +211,8 @@ app.use(
},
);
const server = app.listen(3000, () => {
console.log("Server started on port 3000");
const server = app.listen(PORT, () => {
console.log("Server started on port " + PORT);
});
initializeWebRTCSignaling(server);
+5 -1
View File
@@ -1,8 +1,12 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"target": "es5",
"module": "NodeNext",
"moduleResolution": "NodeNext"
"moduleResolution": "NodeNext",
"outDir": "dist",
"sourceMap": false,
},
"include": [
"src"