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

@ -6,16 +6,16 @@
</picture> </picture>
</p> </p>
A simple RESTful API made with **NestJS** and **Fastify**. A simple RESTful API made with **NestJS** and **Fastify**.
### 🚀 Preparing the environment ### 🚀 Preparing the environment
Make sure that you have Node, NPM, Docker and Docker Compose installed on your computer. Make sure that you have Node, NPM, Docker and Docker Compose installed on your computer.
First, install the necessary packages with the following commands: First, install the necessary packages with the following commands:
```bash ```bash
$ npm i $ npm i
``` ```
After that, you can update the `.env` and the `docker.env` files. The `.env` file is for development environment and the `docker.env` is for production. After that, you can update the `.env` and the `docker.env` files. The `.env` file is for development environment and the `docker.env` is for production.
@ -29,9 +29,10 @@ $ npm run docker:db
``` ```
This will start the following services: This will start the following services:
- **PostgreSQL**
- **Redis** - **PostgreSQL**
- **MinIO** - **Redis**
- **MinIO**
Apply the migrations to the database with the following command: Apply the migrations to the database with the following command:
@ -58,16 +59,15 @@ 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** - **Swagger**
- **Swagger** - **Typescript**
- **Typescript**
## License ## License

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,