diff --git a/README.md b/README.md index 98c1df7..6a1bc91 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,13 @@ The API will be available at `http://localhost:3000`. Push to `main` → GitHub Actions builds the Docker image, pushes to GHCR, then triggers Render to pull and deploy. +### Live Demo + +- **API**: [taskforge-api-9i9h.onrender.com](https://taskforge-api-9i9h.onrender.com) +- **Swagger Docs**: [taskforge-api-9i9h.onrender.com/docs](https://taskforge-api-9i9h.onrender.com/docs) + +> **Note:** Hosted on Render's free tier — the service sleeps after 15 minutes of inactivity. If the first request hangs, it's waking up (~30 seconds). Just wait and retry. + ## API documentation Swagger UI is available at `/docs` once the server is running. diff --git a/src/app.controller.ts b/src/app.controller.ts index 6a35fa2..ad65348 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,10 +1,12 @@ import { Controller, Get } from "@nestjs/common"; import { AllowAnonymous } from "@thallesp/nestjs-better-auth"; +import { SkipArcjet } from "./common/guards/arcjet-optional.guard.js"; @Controller() export class AppController { @Get() @AllowAnonymous() + @SkipArcjet() getRoot() { return { message: "Welcome to the Taskforge API", diff --git a/src/common/guards/arcjet-optional.guard.ts b/src/common/guards/arcjet-optional.guard.ts new file mode 100644 index 0000000..6482135 --- /dev/null +++ b/src/common/guards/arcjet-optional.guard.ts @@ -0,0 +1,35 @@ +import type { ArcjetNest } from "@arcjet/nest"; +import { ARCJET, ArcjetGuard } from "@arcjet/nest"; +import { + type ExecutionContext, + Inject, + Injectable, + SetMetadata, +} from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; + +export const SKIP_ARCJET = "skip_arcjet"; +export const SkipArcjet = () => SetMetadata(SKIP_ARCJET, true); + +@Injectable() +export class ArcjetOptionalGuard extends ArcjetGuard { + constructor( + @Inject(ARCJET) aj: ArcjetNest, + private readonly reflector: Reflector, + ) { + super(aj); + } + + override async canActivate(context: ExecutionContext): Promise { + const skipArcjet = this.reflector.getAllAndOverride(SKIP_ARCJET, [ + context.getHandler(), + context.getClass(), + ]); + + if (skipArcjet) { + return true; + } + + return super.canActivate(context); + } +} diff --git a/src/lib/security/arcjet.module.ts b/src/lib/security/arcjet.module.ts index 88c1766..8246b43 100644 --- a/src/lib/security/arcjet.module.ts +++ b/src/lib/security/arcjet.module.ts @@ -1,13 +1,16 @@ +import type { ArcjetNest } from "@arcjet/nest"; import { - ArcjetGuard, + ARCJET, ArcjetModule, + cloudflare, detectBot, shield, slidingWindow, } from "@arcjet/nest"; import { Module } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; -import { APP_GUARD } from "@nestjs/core"; +import { APP_GUARD, Reflector } from "@nestjs/core"; +import { ArcjetOptionalGuard } from "../../common/guards/arcjet-optional.guard.js"; @Module({ imports: [ @@ -15,6 +18,7 @@ import { APP_GUARD } from "@nestjs/core"; isGlobal: true, useFactory: (configService: ConfigService) => ({ key: configService.getOrThrow("ARCJET_KEY"), + proxies: [cloudflare()], rules: [ detectBot({ mode: "LIVE", @@ -31,6 +35,13 @@ import { APP_GUARD } from "@nestjs/core"; inject: [ConfigService], }), ], - providers: [{ provide: APP_GUARD, useClass: ArcjetGuard }], + providers: [ + { + provide: APP_GUARD, + useFactory: (aj: ArcjetNest, reflector: Reflector) => + new ArcjetOptionalGuard(aj, reflector), + inject: [ARCJET, Reflector], + }, + ], }) export class ArcjetSecurityModule {} diff --git a/src/main.ts b/src/main.ts index 0ac75a2..abbb16e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,17 @@ import { BadRequestException, ValidationPipe } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { NestFactory, Reflector } from "@nestjs/core"; +import type { NestExpressApplication } from "@nestjs/platform-express"; import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; import { AppModule } from "./app.module.js"; import { AllExceptionsFilter } from "./common/filters/all-exceptions.filter.js"; import { TransformInterceptor } from "./common/interceptors/transform.interceptor.js"; async function bootstrap() { - const app = await NestFactory.create(AppModule, { bodyParser: false }); + const app = await NestFactory.create(AppModule, { + bodyParser: false, + }); + app.set("trust proxy", true); const configService = app.get(ConfigService);