Add environment variable validation and ESLint configuration

This commit introduces validation for required environment variables, ensuring the application fails fast if any are missing. It also integrates ESLint with TypeScript support, adds new linting scripts, and updates development dependencies for improved code quality and consistency. Minor cleanup and type improvements are also included.
This commit is contained in:
Adam Shiervani 2025-02-10 19:26:05 +01:00
parent ae4bc804c2
commit 29c2294926
14 changed files with 1547 additions and 61 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
node_modules
.idea
.env
.env.development

View File

@ -23,7 +23,6 @@ The best place to search for answers is our [Documentation](https://jetkvm.com/d
If you've found an issue and want to report it, please check our [Issues](https://github.com/jetkvm/cloud-api/issues) page. Make sure the description contains information about the firmware version you're using, your platform, and a clear explanation of the steps to reproduce the issue.
## Development
This project is built with Node.JS, Prisma and Express.

10
eslint.config.mjs Normal file
View File

@ -0,0 +1,10 @@
// @ts-check
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
export default tseslint.config(eslint.configs.recommended, tseslint.configs.recommended, {
rules: {
"@typescript-eslint/no-unused-vars": ["warn", { caughtErrors: "none" }],
},
});

1488
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,11 @@
"main": "index.js",
"scripts": {
"start": "NODE_ENV=production node -r ts-node/register ./src/index.ts",
"dev": "NODE_ENV=development node --env-file=.env.development -r ts-node/register ./src/index.ts"
"dev": "NODE_ENV=development node --env-file=.env.development -r ts-node/register ./src/index.ts",
"format": "prettier --write --ignore-unknown .",
"lint": "eslint ./src",
"format:check": "prettier --check --ignore-unknown .",
"typecheck": "tsc --noEmit"
},
"engines": {
"node": "21.1.0"
@ -32,13 +36,17 @@
"prisma": "^5.13.0",
"semver": "^7.6.3",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"tsc": "^2.0.4",
"ws": "^8.17.0"
},
"optionalDependencies": {
"bufferutil": "^4.0.8"
},
"devDependencies": {
"@types/semver": "^7.5.8"
"@eslint/js": "^9.20.0",
"@types/semver": "^7.5.8",
"eslint": "^9.20.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.0"
}
}

View File

@ -2,7 +2,6 @@ import { type NextFunction, type Request, type Response } from "express";
import * as jose from "jose";
import { UnauthorizedError } from "./errors";
export const verifyToken = async (idToken: string) => {
const JWKS = jose.createRemoteJWKSet(
new URL("https://www.googleapis.com/oauth2/v3/certs"),
@ -15,7 +14,7 @@ export const verifyToken = async (idToken: string) => {
});
return payload;
} catch (e) {
} catch (e) {
console.error(e);
return null;
}

View File

@ -2,6 +2,7 @@ import { PrismaClient } from "@prisma/client";
let prismaClient: PrismaClient;
declare global {
// eslint-disable-next-line no-var
var __db: PrismaClient | undefined;
}
@ -19,7 +20,6 @@ if (process.env.NODE_ENV !== "development") {
prismaClient = global.__db;
}
// Have to cast it manually, because webstorm can't infer it for some reason
// https://github.com/prisma/prisma/issues/2359#issuecomment-963340538
export const prisma = prismaClient;

View File

@ -27,6 +27,7 @@ export class UnauthorizedError extends HttpError {
export class ForbiddenError extends HttpError {
constructor(message?: string, code?: string) {
super(403, message);
this.code = code;
this.name = "Forbidden";
}
}
@ -34,6 +35,7 @@ export class ForbiddenError extends HttpError {
export class NotFoundError extends HttpError {
constructor(message?: string, code?: string) {
super(404, message);
this.code = code;
this.name = "NotFoundError";
}
}

View File

@ -14,6 +14,7 @@ import { authenticated } from "./auth";
import { prisma } from "./db";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: "development" | "production";
@ -40,6 +41,32 @@ declare global {
}
}
const requiredEnvVars = [
"NODE_ENV",
"API_HOSTNAME",
"APP_HOSTNAME",
"COOKIE_SECRET",
"GOOGLE_CLIENT_ID",
"GOOGLE_CLIENT_SECRET",
"CLOUDFLARE_TURN_ID",
"CLOUDFLARE_TURN_TOKEN",
"R2_ENDPOINT",
"R2_ACCESS_KEY_ID",
"R2_SECRET_ACCESS_KEY",
"R2_BUCKET",
"R2_CDN_URL",
];
const missingEnvVars = requiredEnvVars.filter(envVar => !process.env[envVar]);
if (missingEnvVars.length > 0) {
console.error(
`The following required environment variables are missing: ${missingEnvVars.join(", ")}`,
);
throw Error(
`The following required environment variables are missing: ${missingEnvVars.join(", ")}`,
);
}
const app = express();
app.use(helmet());
app.disable("x-powered-by");
@ -64,6 +91,7 @@ app.use(
}),
);
// eslint-disable-next-line
function asyncHandler(fn: any) {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
return Promise.resolve(fn(req, res, next)).catch(next);
@ -84,7 +112,7 @@ app.get(
asyncAuthGuard,
asyncHandler(async (req: express.Request, res: express.Response) => {
const idToken = req.session?.id_token;
const { sub, iss, exp, aud, iat, jti, nbf } = jose.decodeJwt(idToken);
const { sub, iss } = jose.decodeJwt(idToken);
let user;
if (iss === "https://accounts.google.com") {
@ -174,28 +202,21 @@ app.post(
);
// Error-handling middleware
app.use(
(
err: HttpError | Error,
req: express.Request,
res: express.Response,
next: express.NextFunction,
) => {
const isProduction = process.env.NODE_ENV === "production";
const statusCode = err instanceof HttpError ? err.status : 500;
app.use((err: HttpError | Error, req: express.Request, res: express.Response) => {
const isProduction = process.env.NODE_ENV === "production";
const statusCode = err instanceof HttpError ? err.status : 500;
// Build the error response payload
const payload = {
name: err.name,
message: err.message,
...(isProduction ? {} : { stack: err.stack }),
};
// Build the error response payload
const payload = {
name: err.name,
message: err.message,
...(isProduction ? {} : { stack: err.stack }),
};
console.error(err);
console.error(err);
res.status(statusCode).json(payload);
},
);
res.status(statusCode).json(payload);
});
const server = app.listen(3000, () => {
console.log("Server started on port 3000");

View File

@ -111,7 +111,6 @@ export const Callback = async (req: express.Request, res: express.Response) => {
select: { user: { select: { googleId: true } } },
});
const isAdoptedByCurrentUser = deviceAdopted?.user.googleId === tokenClaims.sub;
const isAdoptedByOther = deviceAdopted && !isAdoptedByCurrentUser;
if (isAdoptedByOther) {

View File

@ -35,7 +35,7 @@ async function getLatestVersion(
}
// Extract version folder names
let versions = response.CommonPrefixes.map(cp => cp.Prefix!.split("/")[1])
const versions = response.CommonPrefixes.map(cp => cp.Prefix!.split("/")[1])
.filter(Boolean)
.filter(v => semver.valid(v));
@ -337,6 +337,7 @@ export async function RetrieveLatestApp(req: express.Request, res: express.Respo
}
// Helper function to convert stream to string
// eslint-disable-next-line
async function streamToString(stream: any): Promise<string> {
const chunks: Uint8Array[] = [];
@ -349,6 +350,7 @@ async function streamToString(stream: any): Promise<string> {
}
// Helper function to convert stream to buffer
// eslint-disable-next-line
async function streamToBuffer(stream: any): Promise<Buffer> {
const chunks = [];
for await (const chunk of stream) {

View File

@ -3,7 +3,7 @@ import express from "express";
import * as jose from "jose";
import { prisma } from "./db";
import { NotFoundError, UnprocessableEntityError } from "./errors";
import { IncomingMessage } from "http";
import http, { IncomingMessage } from "http";
import { Socket } from "node:net";
import { Device } from "@prisma/client";
@ -48,13 +48,13 @@ export const CreateSession = async (req: express.Request, res: express.Response)
try {
inFlight.add(id);
const resp: any = await new Promise((res, rej) => {
const resp = await new Promise<{ data: string }>((res, rej) => {
timeout = setTimeout(() => {
rej(new Error("Timeout waiting for response from ws"));
}, 5000);
// Hoist the res and rej functions to be used in the finally block for cleanup
wsRes = res;
wsRes = data => res(data as { data: string });
wsRej = rej;
ws.addEventListener("message", wsRes);
@ -142,7 +142,7 @@ async function updateDeviceLastSeen(id: string) {
}
}
export const registerWebsocketServer = (server: any) => {
export const registerWebsocketServer = (server: http.Server) => {
const wss = new WebSocketServer({ noServer: true });
server.on("upgrade", async (req: IncomingMessage, socket: Socket, head: Buffer) => {

View File

@ -4,10 +4,7 @@
"module": "NodeNext",
"moduleResolution": "NodeNext"
},
"include": [
"src"
],
"exclude": [
"node_modules"
]
"include": ["src"],
"exclude": ["node_modules"]
}