mirror of
https://github.com/jetkvm/cloud-api.git
synced 2025-09-16 08:38:15 +00:00
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:
parent
ae4bc804c2
commit
29c2294926
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
.idea
|
||||
.env
|
||||
.env.development
|
||||
@ -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
10
eslint.config.mjs
Normal 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
1488
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
61
src/index.ts
61
src/index.ts
@ -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");
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -4,10 +4,7 @@
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user