Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# --- APP ---
PORT=3000
NODE_ENV=development
COOKIE_SECRET=same-serious-secret
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000

# --- POSTGRES ---
Expand Down
37 changes: 33 additions & 4 deletions libs/bootstrap/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,17 @@ import type { BootstrapOptions } from './interfaces/options.interface';
import fastifyCookie from '@fastify/cookie';
import fastifyCompress from '@fastify/compress';
import fastifyMultipart from '@fastify/multipart';
import fastifyCsrf from '@fastify/csrf-protection';
import { createId } from '@paralleldrive/cuid2';

export async function bootstrapApp(options: BootstrapOptions) {
const adapter = new FastifyAdapter();
const startTime = performance.now();
const adapter = new FastifyAdapter({
requestIdHeader: 'x-request-id',
genReqId: (req) => {
return (req.headers['x-request-id'] as string) || createId();
},
});

const {
appModule,
Expand All @@ -28,7 +36,7 @@ export async function bootstrapApp(options: BootstrapOptions) {

let rootModule = appModule;

// TODO: Improve merging modules (in case of multiple features needed)
// TODO: Improve merging modules (in case of multiple features needed) or migrate to fastify throttle
if (throttlerOptions) {
rootModule = setupThrottler(rootModule, throttlerOptions);
}
Expand Down Expand Up @@ -74,15 +82,36 @@ export async function bootstrapApp(options: BootstrapOptions) {

await setupSwagger(app, fullOptions);
}
if (useCookieParser) app.register(fastifyCookie, { secret: 'SAME-SECRET' });
if (useCookieParser) {
const secret = configService.getOrThrow('COOKIE_SECRET');
await app.register(fastifyCookie, { secret });
await app.register(fastifyCsrf, {
cookieOpts: {
signed: true,
httpOnly: true,
sameSite: 'strict',
secure: configService.getOrThrow('NODE_ENV') === 'production',
},
});
}
if (setupApp) setupApp(app);

await app.listen(port, '0.0.0.0', (_err, address) => {
const baseUrl = `${address}${apiPrefix ? '/' + apiPrefix : ''}`;

if (_err) {
logger.error(_err);
process.exit(1);
}

logger.verbose(`Application is running on: ${address}${apiPrefix ? '/' + apiPrefix : ''}`);
const startupTime = (performance.now() - startTime).toFixed(2);
logger.verbose(`Environment: ${process.env.NODE_ENV || 'development'}`);
logger.verbose(`API Endpoint: ${baseUrl}`);
logger.verbose(`Health Check: ${baseUrl}/health`);
logger.verbose(`Swagger UI: ${baseUrl}/${swaggerOptions?.path ?? 'docs'}`);
logger.verbose(
`OpenAPI (Specs): ${baseUrl}/${swaggerOptions?.path ?? 'docs'}/s/{json,yaml}`,
);
logger.verbose(`Boot Time: ${startupTime}ms`);
});
}
1 change: 1 addition & 0 deletions libs/config/src/config.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const timeStringSchema = z.string().regex(/^[0-9]+[smhdw]$/, {
export const ConfigSchema = z.object({
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
COOKIE_SECRET: z.string({ error: 'COOKIE_SECRET is missing' }),
DB_USERNAME: z.string({ error: 'DB_USERNAME is missing' }),
DB_PASSWORD: z.string({ error: 'DB_PASSWORD is missing' }),
DB_DATABASE: z.string({ error: 'DB_DATABASE is missing' }),
Expand Down
8 changes: 2 additions & 6 deletions libs/s3/src/s3.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import type { S3ModuleOptions, S3ModuleAsyncOptions } from './interfaces';
import { S3Service } from './s3.service';
import { S3_OPTIONS } from './s3.constants';

@Module({
providers: [S3Service],
exports: [S3Service],
})
@Module({})
export class S3Module {
static register(options: S3ModuleOptions): DynamicModule {
const { global, ...config } = options;
Expand All @@ -20,10 +17,9 @@ export class S3Module {
}

static registerAsync(options: S3ModuleAsyncOptions): DynamicModule {
const { global, imports } = options;
const { imports } = options;

return {
global,
module: S3Module,
imports: imports || [],
providers: [this.createAsyncOptionsProvider(options), S3Service],
Expand Down
24 changes: 21 additions & 3 deletions libs/s3/src/s3.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { S3Client } from '@aws-sdk/client-s3';
import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { S3_OPTIONS } from './s3.constants';
import { S3ModuleOptions } from './interfaces';
import { PutObjectCommand } from '@aws-sdk/client-s3';
Expand Down Expand Up @@ -28,13 +28,31 @@ export class S3Service {
});
}

async uploadPublicFile(
async deleteFile(fileUrl: string): Promise<void> {
try {
const url = new URL(fileUrl);
const pathParts = url.pathname.split('/');
const key = pathParts.slice(2).join('/');

await this.s3Client.send(
new DeleteObjectCommand({
Bucket: this.bucket,
Key: key,
}),
);
} catch (error) {
console.error('S3 Rollback failed:', error);
}
}

async uploadFile(
fileBuffer: Buffer,
originalName: string,
mimetype: string,
folder: string,
): Promise<string> {
const extension = extname(originalName);
const fileName = `${randomUUID()}${extension}`;
const fileName = `${folder}/${randomUUID()}${extension}`;

const command = new PutObjectCommand({
Bucket: this.bucket,
Expand Down
56 changes: 56 additions & 0 deletions migrations/0002_pink_krista_starr.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
CREATE TYPE "base"."team_role" AS ENUM ('admin', 'moderator', 'member');

CREATE TYPE "base"."member_status" AS ENUM ('pending', 'active', 'declined', 'banned');

CREATE TABLE
"base"."tags" (
"id" text PRIMARY KEY NOT NULL,
"name" varchar(50) NOT NULL,
CONSTRAINT "tags_name_unique" UNIQUE ("name")
);

CREATE TABLE
"base"."team_members" (
"team_id" text NOT NULL,
"user_id" text NOT NULL,
"role" "base"."team_role" DEFAULT 'member' NOT NULL,
"status" "base"."member_status" DEFAULT 'pending' NOT NULL,
"joined_at" timestamp,
"created_at" timestamp DEFAULT now () NOT NULL,
CONSTRAINT "team_members_team_id_user_id_pk" PRIMARY KEY ("team_id", "user_id")
);

CREATE TABLE
"base"."teams" (
"id" text PRIMARY KEY NOT NULL,
"slug" varchar(120) NOT NULL,
"name" varchar(100) NOT NULL,
"description" text,
"avatar_url" text,
"cover_url" text,
"owner_id" text,
"created_at" timestamp DEFAULT now () NOT NULL,
"updated_at" timestamp DEFAULT now () NOT NULL,
CONSTRAINT "teams_slug_unique" UNIQUE ("slug")
);

CREATE TABLE
"base"."teams_to_tags" (
"team_id" text NOT NULL,
"tag_id" text NOT NULL,
CONSTRAINT "teams_to_tags_team_id_tag_id_pk" PRIMARY KEY ("team_id", "tag_id")
);

ALTER TABLE "base"."team_members" ADD CONSTRAINT "team_members_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "base"."teams" ("id") ON DELETE cascade ON UPDATE no action;

ALTER TABLE "base"."team_members" ADD CONSTRAINT "team_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "base"."users" ("id") ON DELETE cascade ON UPDATE no action;

ALTER TABLE "base"."teams" ADD CONSTRAINT "teams_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "base"."users" ("id") ON DELETE no action ON UPDATE no action;

ALTER TABLE "base"."teams_to_tags" ADD CONSTRAINT "teams_to_tags_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "base"."teams" ("id") ON DELETE cascade ON UPDATE no action;

ALTER TABLE "base"."teams_to_tags" ADD CONSTRAINT "teams_to_tags_tag_id_tags_id_fk" FOREIGN KEY ("tag_id") REFERENCES "base"."tags" ("id") ON DELETE cascade ON UPDATE no action;

CREATE INDEX "member_status_idx" ON "base"."team_members" USING btree ("status");

CREATE INDEX "team_slug_idx" ON "base"."teams" USING btree ("slug");
18 changes: 18 additions & 0 deletions migrations/0003_open_oracle.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
ALTER TYPE "base"."team_role" ADD VALUE 'owner' BEFORE 'admin';
ALTER TYPE "base"."team_role" ADD VALUE 'lead' BEFORE 'moderator';
ALTER TYPE "base"."team_role" ADD VALUE 'viewer';
ALTER TABLE "base"."teams" DROP CONSTRAINT "teams_owner_id_users_id_fk";

ALTER TABLE "base"."team_members" ALTER COLUMN "status" SET DATA TYPE text;
ALTER TABLE "base"."team_members" ALTER COLUMN "status" SET DEFAULT 'inactive'::text;
DROP TYPE "base"."member_status";
CREATE TYPE "base"."member_status" AS ENUM('active', 'banned', 'inactive');
ALTER TABLE "base"."team_members" ALTER COLUMN "status" SET DEFAULT 'inactive'::"base"."member_status";
ALTER TABLE "base"."team_members" ALTER COLUMN "status" SET DATA TYPE "base"."member_status" USING "status"::"base"."member_status";
ALTER TABLE "base"."teams" ADD COLUMN "deleted_at" timestamp;
ALTER TABLE "base"."teams" ADD CONSTRAINT "teams_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "base"."users"("id") ON DELETE set null ON UPDATE no action;
CREATE INDEX "member_role_idx" ON "base"."team_members" USING btree ("user_id","role");
CREATE UNIQUE INDEX "team_active_slug_idx" ON "base"."teams" USING btree ("slug") WHERE "base"."teams"."deleted_at" is null;
CREATE INDEX "team_owner_idx" ON "base"."teams" USING btree ("owner_id");
CREATE INDEX "team_deleted_at_idx" ON "base"."teams" USING btree ("deleted_at");
CREATE INDEX "teams_to_tags_tag_id_idx" ON "base"."teams_to_tags" USING btree ("tag_id");
Loading
Loading