diff --git a/.env.exemple b/.env.exemple index 0496119..104888d 100644 --- a/.env.exemple +++ b/.env.exemple @@ -1,3 +1 @@ -DATABASE_URL="postgresql://username:password@ip_server:port/database?schema=public" -REDIS_URL="redis://username:password@ip_server:port" -SESSION_TTL_SECONDS=3600 \ No newline at end of file +DATABASE_URL="postgresql://username:password@ip_server:port/database?schema=public" \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..91c2b26 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,17 @@ +# name: Build + +# on: +# push: +# branches: [master] + +# jobs: +# build: +# name: Build +# runs-on: ubuntu-latest + +# steps: +# - uses: actions/checkout@v3 + +# - name: Call build in Dokploy +# if: success() +# run: curl -X GET ${{ secrets.BUILD_WEBHOOK_URL }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0df43f2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM node:22-alpine AS builder + +WORKDIR /usr/src/app + +RUN apk add --no-cache python3 make g++ + +COPY package.json ./ +COPY prisma ./prisma/ + +RUN yarn install --frozen-lockfile + +COPY . . + +RUN npx prisma generate + +RUN yarn build + +FROM node:22-alpine + +WORKDIR /usr/src/app + +COPY --from=builder /usr/src/app/package.json ./ +COPY --from=builder /usr/src/app/node_modules ./node_modules + +COPY --from=builder /usr/src/app/dist ./dist +COPY --from=builder /usr/src/app/prisma ./prisma + +EXPOSE 3000 + +CMD ["yarn", "start:prod"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7c099a4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: clipperia-api + ports: + - '3050:3000' + environment: + NODE_ENV: production + DATABASE_URL: postgresql://postgres:postgres@db:5432/clipperia?schema=public + volumes: + - .:/usr/src/app + - /usr/src/app/node_modules + networks: + - clipperia-network + # - dokploy-network + command: ['yarn', 'start:prod'] + +networks: + clipperia-network: + internal: true + dokploy-network: + external: true diff --git a/package.json b/package.json index e5cf9e0..8985083 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,9 @@ "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", - "start:dev": "NODE_ENV=development nest start --watch", - "start:debug": "NODE_ENV=development nest start --debug --watch", - "start:prod": "NODE_ENV=production node dist/main", + "start:dev": "cross-env NODE_ENV=development nest start --watch", + "start:debug": "cross-env NODE_ENV=development nest start --debug --watch", + "start:prod": "cross-env NODE_ENV=production node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", @@ -30,6 +30,7 @@ "bcrypt": "6.0.0", "class-transformer": "0.5.1", "class-validator": "0.14.2", + "cross-env": "10.0.0", "dayjs": "1.11.13", "jwks-rsa": "3.2.0", "passport": "0.7.0", diff --git a/src/app.module.ts b/src/app.module.ts index 504673d..04b2093 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,6 +1,5 @@ import { Module, MiddlewareConsumer } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { APP_GUARD } from '@nestjs/core'; import { PrismaModule } from './prisma/prisma.module'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @@ -20,7 +19,7 @@ import { RolesGuard } from './auth/roles.guard'; UsuariosModule, ], controllers: [AppController], - providers: [AppService, { provide: APP_GUARD, useClass: RolesGuard }], + providers: [AppService, RolesGuard], }) export class AppModule { configure(consumer: MiddlewareConsumer) { diff --git a/src/auth/roles.guard.ts b/src/auth/roles.guard.ts index 6962e3a..63e1050 100644 --- a/src/auth/roles.guard.ts +++ b/src/auth/roles.guard.ts @@ -13,27 +13,48 @@ import type { JwtPayload } from './keycloak.strategy'; export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} + private extractRoles(user: JwtPayload): string[] { + const roles: string[] = []; + + if (user.realm_access?.roles) { + roles.push(...user.realm_access.roles); + } + + if (user.resource_access) { + Object.values(user.resource_access).forEach((resource) => { + if (resource?.roles) { + roles.push(...resource.roles); + } + }); + } + + return [...new Set(roles)]; + } + canActivate(context: ExecutionContext): boolean { const requiredRoles = this.reflector.getAllAndOverride( ROLES_KEY, [context.getHandler(), context.getClass()], ); - if (!requiredRoles || requiredRoles.length === 0) { - return true; + + if (!requiredRoles || !requiredRoles.length) { + return false; } const request = context.switchToHttp().getRequest<{ user: JwtPayload }>(); const user = request.user; - const userRoles: string[] = [ - ...(user?.resource_access?.clipperia?.roles || []), - ]; + if (!user) { + throw new ForbiddenException('Usuário não autenticado'); + } + + const userRoles = this.extractRoles(user); - console.log(context); const hasRole = requiredRoles.some((role) => userRoles.includes(role)); + if (!hasRole) { throw new ForbiddenException( - 'O usuário não possui permissão para acessar esta rota', + 'Você não possui permissão para acessar este recurso', ); }