feat: safer environment variables, updated packages

This commit is contained in:
Hackntosh 2024-09-28 00:05:00 +01:00
parent 2eaa2294da
commit 47b50415ea
11 changed files with 3565 additions and 2215 deletions

View file

@ -29,6 +29,7 @@ $ npm run docker:db
``` ```
This will start the following services: This will start the following services:
- **PostgreSQL** - **PostgreSQL**
- **Redis** - **Redis**
- **MinIO** - **MinIO**
@ -58,11 +59,10 @@ This will start all the previous services and the back-end image.
## 🗄️ Stack ## 🗄️ Stack
This back-end uses the following stack: This back-end uses the following stack:
- **Docker**
- **Fastify** - **Fastify**
- **MinIO** - **MinIO**
- **NestJS** - **NestJS**
- **Passport**
- **PostgreSQL** - **PostgreSQL**
- **Prisma** - **Prisma**
- **Redis** - **Redis**

5619
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -43,6 +43,8 @@
"@nestjs/throttler": "^5.1.1", "@nestjs/throttler": "^5.1.1",
"@prisma/client": "^5.9.1", "@prisma/client": "^5.9.1",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"dotenv": "^16.4.5",
"dotenv-expand": "^11.0.6",
"file-type": "^19.0.0", "file-type": "^19.0.0",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"nestjs-s3": "^2.0.1", "nestjs-s3": "^2.0.1",
@ -53,7 +55,8 @@
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"sharp": "^0.33.2" "sharp": "^0.33.2",
"tstl": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.5.3", "@biomejs/biome": "1.5.3",

View file

@ -8,6 +8,7 @@ import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis";
import { ZodValidationPipe } from "nestjs-zod"; import { ZodValidationPipe } from "nestjs-zod";
import { AuthModule } from "./auth/auth.module"; import { AuthModule } from "./auth/auth.module";
import { JwtAuthGuard } from "./auth/jwt-auth.guard"; import { JwtAuthGuard } from "./auth/jwt-auth.guard";
import { Configuration } from "./configuration";
import { KweeksModule } from "./kweeks/kweeks.module"; import { KweeksModule } from "./kweeks/kweeks.module";
import { UserModule } from "./users/users.module"; import { UserModule } from "./users/users.module";
@ -20,20 +21,18 @@ import { UserModule } from "./users/users.module";
}), }),
ThrottlerModule.forRoot({ ThrottlerModule.forRoot({
throttlers: [{ limit: 10, ttl: 60000 }], throttlers: [{ limit: 10, ttl: 60000 }],
storage: new ThrottlerStorageRedisService( storage: new ThrottlerStorageRedisService(Configuration.REDIS_URL()),
`redis://:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}/0`,
),
}), }),
KweeksModule, KweeksModule,
FastifyMulterModule, FastifyMulterModule,
S3Module.forRoot({ S3Module.forRoot({
config: { config: {
credentials: { credentials: {
accessKeyId: process.env.MINIO_ROOT_USER, // CHANGE WHEN PRODUCTION TO S3 accessKeyId: Configuration.MINIO_ROOT_USER(),
secretAccessKey: process.env.MINIO_ROOT_PASSWORD, secretAccessKey: Configuration.MINIO_ROOT_PASSWORD(),
}, },
region: "us-east-1", region: "us-east-1",
endpoint: process.env.MINIO_ENDPOINT, endpoint: Configuration.MINIO_ENDPOINT(),
forcePathStyle: true, forcePathStyle: true,
}, },
}), }),

View file

@ -1,6 +1,7 @@
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt"; import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport"; import { PassportModule } from "@nestjs/passport";
import { Configuration } from "src/configuration";
import { UserModule } from "src/users/users.module"; import { UserModule } from "src/users/users.module";
import { AuthController } from "./auth.controller"; import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service"; import { AuthService } from "./auth.service";
@ -13,7 +14,7 @@ import { LocalStrategy } from "./local.strategy";
UserModule, UserModule,
PassportModule, PassportModule,
JwtModule.register({ JwtModule.register({
secret: process.env.JWT_ACCESS_SECRET, secret: Configuration.JWT_ACCESS_SECRET(),
signOptions: { expiresIn: "1d" }, // TODO: add refresh tokens signOptions: { expiresIn: "1d" }, // TODO: add refresh tokens
}), }),
], ],

View file

@ -1,6 +1,7 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport"; import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt"; import { ExtractJwt, Strategy } from "passport-jwt";
import { Configuration } from "src/configuration";
type Payload = { type Payload = {
displayName: string; displayName: string;
@ -14,7 +15,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
super({ super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false, ignoreExpiration: false,
secretOrKey: process.env.JWT_ACCESS_SECRET, secretOrKey: Configuration.JWT_ACCESS_SECRET(),
}); });
} }

14
src/configuration.ts Normal file
View file

@ -0,0 +1,14 @@
import { Environment } from "./environment";
export namespace Configuration {
export const NODE_ENV = () => Environment.env.NODE_ENV;
export const SERVER_HOST = () => Environment.env.SERVER_HOST;
export const SERVER_PORT = () => Environment.env.SERVER_PORT;
export const JWT_ACCESS_SECRET = () => Environment.env.JWT_ACCESS_SECRET;
export const REDIS_URL = () => Environment.env.REDIS_URL;
export const MINIO_ROOT_USER = () => Environment.env.MINIO_ROOT_USER;
export const MINIO_ROOT_PASSWORD = () => Environment.env.MINIO_ROOT_PASSWORD;
export const MINIO_DEFAULT_BUCKETS = () =>
Environment.env.MINIO_DEFAULT_BUCKETS;
export const MINIO_ENDPOINT = () => Environment.env.MINIO_ENDPOINT;
}

78
src/environment.ts Normal file
View file

@ -0,0 +1,78 @@
import dotenv from "dotenv";
import dotEnvExpand from "dotenv-expand";
import { Singleton } from "tstl";
import { z } from "nestjs-zod/z";
/**
* Global variables of the server.
*/
export class Environment {
public static get env(): IEnvironment {
return environments.get();
}
public static get node_env(): NodeEnv {
if (nodeEnvWrapper.value === undefined || nodeEnvWrapper.value === null) {
nodeEnvWrapper.value = environments.get().NODE_ENV;
}
return nodeEnvWrapper.value;
}
public setMode(mode: NodeEnv): void {
if (!["dev", "prod"].includes(mode)) {
throw new Error("Invalid NODE_ENV value, expected 'dev' or 'prod'");
}
nodeEnvWrapper.value = mode;
}
}
const EnvironmentSchema = z.object({
NODE_ENV: z.enum(["dev", "prod"]),
POSTGRES_HOST: z.string(),
POSTGRES_DB: z.string(),
POSTGRES_USER: z.string(),
POSTGRES_PASSWORD: z.string(),
POSTGRES_PORT: z.string().regex(/^[0-9]+$/),
DATABASE_URL: z.string(),
REDIS_HOST: z.string(),
REDIS_PORT: z.string().regex(/^[0-9]+$/),
REDIS_PASSWORD: z.string(),
REDIS_URL: z.string(),
SERVER_PORT: z.string().regex(/^[0-9]+$/),
SERVER_HOST: z.string(),
JWT_ACCESS_SECRET: z.string(),
MINIO_ROOT_USER: z.string(),
MINIO_ROOT_PASSWORD: z.string(),
MINIO_DEFAULT_BUCKETS: z.string(),
MINIO_ENDPOINT: z.string(),
});
type IEnvironment = z.infer<typeof EnvironmentSchema>;
type NodeEnv = "dev" | "prod";
interface INodeEnv {
value?: NodeEnv;
}
const nodeEnvWrapper: INodeEnv = {};
const environments = new Singleton(() => {
const env = dotenv.config();
dotEnvExpand.expand(env);
const parsedEnv = EnvironmentSchema.safeParse(process.env);
if (!parsedEnv.success) {
const errors = parsedEnv.error.format();
throw new Error(`Environment validation failed: ${JSON.stringify(errors)}`);
}
return parsedEnv.data;
});

View file

@ -1,4 +1,4 @@
import * as helmet from "@fastify/helmet"; import helmet from "@fastify/helmet";
import { NestFactory } from "@nestjs/core"; import { NestFactory } from "@nestjs/core";
import { import {
FastifyAdapter, FastifyAdapter,
@ -7,6 +7,7 @@ import {
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import { patchNestJsSwagger } from "nestjs-zod"; import { patchNestJsSwagger } from "nestjs-zod";
import { AppModule } from "./app.module"; import { AppModule } from "./app.module";
import { Configuration } from "./configuration";
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>( const app = await NestFactory.create<NestFastifyApplication>(
@ -44,6 +45,6 @@ async function bootstrap() {
await app.register(helmet); await app.register(helmet);
await app.listen(process.env.SERVER_PORT, process.env.SERVER_HOST); await app.listen(Configuration.SERVER_PORT(), Configuration.SERVER_HOST);
} }
bootstrap(); bootstrap();

View file

@ -3,6 +3,7 @@ import { File } from "@nest-lab/fastify-multer";
import { Injectable, InternalServerErrorException } from "@nestjs/common"; import { Injectable, InternalServerErrorException } from "@nestjs/common";
import { InjectS3, S3 } from "nestjs-s3"; import { InjectS3, S3 } from "nestjs-s3";
import sharp from "sharp"; import sharp from "sharp";
import { Configuration } from "src/configuration";
@Injectable() @Injectable()
export class S3Service { export class S3Service {
@ -18,7 +19,7 @@ export class S3Service {
.toBuffer(); .toBuffer();
const params: PutObjectCommandInput = { const params: PutObjectCommandInput = {
Bucket: process.env.MINIO_DEFAULT_BUCKETS, Bucket: Configuration.MINIO_DEFAULT_BUCKETS(),
Key: `profile_images/${userID}.webp`, Key: `profile_images/${userID}.webp`,
Body: compressedBuffer, Body: compressedBuffer,
ContentType: "image/webp", ContentType: "image/webp",
@ -29,7 +30,7 @@ export class S3Service {
const { ETag } = await this.s3.send(new PutObjectCommand(params)); const { ETag } = await this.s3.send(new PutObjectCommand(params));
if (ETag !== null) { if (ETag !== null) {
return `${process.env.MINIO_ENDPOINT}/${process.env.MINIO_DEFAULT_BUCKETS}/profile_images/${userID}.webp`; return `${Configuration.MINIO_ENDPOINT}/${Configuration.MINIO_DEFAULT_BUCKETS}/profile_images/${userID}.webp`;
} }
throw new InternalServerErrorException( throw new InternalServerErrorException(
@ -64,13 +65,13 @@ export class S3Service {
const Key = `posts/${id}/${index}.webp`; const Key = `posts/${id}/${index}.webp`;
const params: PutObjectCommandInput = { const params: PutObjectCommandInput = {
Bucket: process.env.MINIO_DEFAULT_BUCKETS, Bucket: Configuration.MINIO_DEFAULT_BUCKETS(),
Key, Key,
Body: buffer, Body: buffer,
ContentType: "image/webp", ContentType: "image/webp",
}; };
await this.s3.send(new PutObjectCommand(params)); await this.s3.send(new PutObjectCommand(params));
return `${process.env.MINIO_ENDPOINT}/${process.env.MINIO_DEFAULT_BUCKETS}/${Key}`; return `${Configuration.MINIO_ENDPOINT}/${Configuration.MINIO_DEFAULT_BUCKETS}/${Key}`;
} }
} }

View file

@ -2,6 +2,7 @@
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"declaration": true, "declaration": true,
"esModuleInterop": true,
"removeComments": true, "removeComments": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,