diff --git a/backend/package-lock.json b/backend/package-lock.json index 6a6466c..59360ff 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -24,6 +24,7 @@ "@nestjs/graphql": "^12.2.2", "@nestjs/jwt": "^11.0.2", "@nestjs/mapped-types": "*", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.4.22", "@nestjs/schedule": "^4.1.2", "@nestjs/swagger": "^7.4.0", @@ -54,7 +55,11 @@ "ioredis": "^5.10.1", "ipfs-http-client": "^60.0.1", "node-cron": "^4.2.1", + feature/role-based-auth + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "p-limit": "^7.3.0", +main "pg": "^8.18.0", "prisma": "^5.0.0", "qrcode": "^1.5.4", @@ -75,6 +80,8 @@ "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", +feature/role-based-auth + "@types/passport-jwt": "^4.0.1", main "@types/qrcode": "^1.5.5", "@types/speakeasy": "^2.0.10", "@types/supertest": "^2.0.12", @@ -1525,6 +1532,161 @@ "yallist": "^3.0.2" } }, +feature/role-based-auth + "node_modules/@discordjs/builders": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz", + "integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.2", + "@discordjs/util": "^1.2.0", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.40", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz", + "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.1.tgz", + "integrity": "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.2.0", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.5", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.40", + "magic-bytes.js": "^1.13.0", + "tslib": "^2.6.3", + "undici": "6.24.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@sapphire/snowflake": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz", + "integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@discordjs/rest/node_modules/undici": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/@discordjs/util": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz", + "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.38.1", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@dnsquery/dns-packet": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@dnsquery/dns-packet/-/dns-packet-6.1.1.tgz", + "integrity": "sha512-WXTuFvL3G+74SchFAtz3FgIYVOe196ycvGsMgvSH/8Goptb1qpIQtIuM4SOK9G9lhMWYpHxnXyy544ZhluFOew==", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1540,6 +1702,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, + main "license": "MIT", "engines": { "node": ">=6.9.0" @@ -5075,11 +5238,29 @@ "@opentelemetry/api": "^1.3.0" } }, +feature/role-based-auth + "node_modules/@nestjs/passport": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz", + "integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "passport": "^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, + "node_modules/@nestjs/platform-express": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.22.tgz", + "integrity": "sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==", + "license": "MIT", + "node_modules/@opentelemetry/instrumentation-http": { "version": "0.57.1", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.57.1.tgz", "integrity": "sha512-ThLmzAQDs7b/tdKI3BV2+yawuF09jF111OFsovqT1Qj3D8vjwKBwhi/rDE5xethwn4tSXtZcJ9hBsVAlWFQZ7g==", "license": "Apache-2.0", + main "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/instrumentation": "0.57.1", @@ -6371,11 +6552,52 @@ "node": ">=18.0.0" } }, + feature/role-based-auth + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sendgrid/client": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.6.tgz", + "integrity": "sha512-/BHu0hqwXNHr2aLhcXU7RmmlVqrdfrbY9KpaNj00KZHlVOVoRxRVrpOCabIB+91ISXJ6+mLM9vpaVUhK6TwBWA==", + "license": "MIT", + "node_modules/@smithy/util-body-length-node": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", "license": "Apache-2.0", + main "dependencies": { "tslib": "^2.6.2" }, @@ -6934,6 +7156,38 @@ "form-data": "^4.0.4" } }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, "node_modules/@types/pg": { "version": "8.6.1", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", @@ -7093,6 +7347,15 @@ "@types/node": "*" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -14827,6 +15090,42 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -14914,6 +15213,11 @@ "node": ">=8" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/pg": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", @@ -16433,6 +16737,21 @@ "node": ">= 0.10.0" } }, +feature/role-based-auth + "node_modules/speakeasy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/speakeasy/-/speakeasy-2.0.0.tgz", + "integrity": "sha512-lW2A2s5LKi8rwu77ewisuUOtlCydF/hmQSOJjpTqTj1gZLkNgTaYnyvfxy2WBr4T/h+9c4g8HIITfj83OkFQFw==", + "license": "MIT", + "dependencies": { + "base32.js": "0.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, +======= +main "node_modules/speakeasy/node_modules/base32.js": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index c1865ec..fb661fb 100644 --- a/backend/package.json +++ b/backend/package.json @@ -40,6 +40,7 @@ "@nestjs/core": "^10.4.22", "@nestjs/graphql": "^12.2.2", "@nestjs/jwt": "^11.0.2", + "@nestjs/passport": "^10.0.3", "@nestjs/mapped-types": "*", "@nestjs/platform-express": "^10.4.22", "@nestjs/schedule": "^4.1.2", @@ -73,6 +74,8 @@ "node-cron": "^4.2.1", "p-limit": "^7.3.0", "pg": "^8.18.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "prisma": "^5.0.0", "qrcode": "^1.5.4", "reflect-metadata": "^0.2.2", @@ -96,6 +99,12 @@ "@types/speakeasy": "^2.0.10", "@types/supertest": "^2.0.12", "@types/web-push": "^3.6.4", + feature/role-based-auth + "@types/speakeasy": "^2.0.10", + "@types/qrcode": "^1.5.5", + "@types/passport-jwt": "^4.0.1", + + "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.42.0", diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 1cfa5f7..d0894a9 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -16,6 +16,7 @@ model User { profileData Json? @map("profile_data") reputationScore Int @default(0) @map("reputation_score") trustScore Int @default(500) @map("trust_score") + role UserRole @default(INVESTOR) @map("role") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -151,6 +152,12 @@ enum KycStatus { EXPIRED } +enum UserRole { + INVESTOR + CREATOR + ADMIN +} + enum EmailDigestMode { INSTANT WEEKLY diff --git a/backend/src/admin/controllers/audit.controller.ts b/backend/src/admin/controllers/audit.controller.ts index 2ce0b92..066090a 100644 --- a/backend/src/admin/controllers/audit.controller.ts +++ b/backend/src/admin/controllers/audit.controller.ts @@ -15,9 +15,14 @@ import { Throttle } from '@nestjs/throttler'; import { AuditExporterService } from '../services/audit-exporter.service'; import { GenerateAuditPackageDto, AuditPackageResponseDto } from '../dto/audit-package.dto'; import { AdminGuard } from '../../guards/admin.guard'; +import { JwtAuthGuard } from '../../guards/jwt-auth.guard'; +import { RolesGuard } from '../../guards/roles.guard'; +import { Roles } from '../../decorators/roles.decorator'; +import { UserRole } from '@prisma/client'; @Controller('admin/audit') -@UseGuards(AdminGuard) +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles(UserRole.ADMIN) export class AuditController { constructor(private readonly auditService: AuditExporterService) {} diff --git a/backend/src/api/institutional.controller.ts b/backend/src/api/institutional.controller.ts index 807d0a8..bfe3de6 100644 --- a/backend/src/api/institutional.controller.ts +++ b/backend/src/api/institutional.controller.ts @@ -9,6 +9,10 @@ import { } from '@nestjs/common'; import { Response } from 'express'; import { AdminGuard } from '../guards/admin.guard'; +import { JwtAuthGuard } from '../guards/jwt-auth.guard'; +import { RolesGuard } from '../guards/roles.guard'; +import { Roles } from '../decorators/roles.decorator'; +import { UserRole } from '@prisma/client'; import { PrismaService } from '../prisma.service'; type ReportFormat = 'json' | 'csv' | 'xml'; diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 45c4a7e..6d154b9 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -32,6 +32,7 @@ import { AdminModule } from './admin/admin.module'; import { SupportModule } from './support/support.module'; import { GovernanceModule } from './governance/governance.module'; import { ApiModule } from './api/api.module'; +import { AuthModule } from './auth/auth.module'; import { APP_GUARD } from '@nestjs/core'; import { MaintenanceGuard } from './guards/maintenance.guard'; @@ -82,6 +83,7 @@ import { MaintenanceGuard } from './guards/maintenance.guard'; AdminModule, GovernanceModule, ApiModule, + AuthModule, ], controllers: [AppController], providers: [ diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts new file mode 100644 index 0000000..e00670d --- /dev/null +++ b/backend/src/auth/auth.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { JwtStrategy } from '../guards/jwt.strategy'; +import { AuthService } from '../services/auth.service'; +import { DatabaseModule } from '../database.module'; + +@Module({ + imports: [ + DatabaseModule, + PassportModule, + JwtModule.register({ + secret: process.env.JWT_SECRET, + signOptions: { expiresIn: process.env.JWT_EXPIRATION }, + }), + ], + providers: [AuthService, JwtStrategy], + exports: [AuthService, JwtModule], +}) +export class AuthModule {} \ No newline at end of file diff --git a/backend/src/decorators/roles.decorator.ts b/backend/src/decorators/roles.decorator.ts new file mode 100644 index 0000000..c5b5018 --- /dev/null +++ b/backend/src/decorators/roles.decorator.ts @@ -0,0 +1,5 @@ +import { SetMetadata } from '@nestjs/common'; +import { UserRole } from '@prisma/client'; + +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles); \ No newline at end of file diff --git a/backend/src/guards/jwt-auth.guard.ts b/backend/src/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..18588a5 --- /dev/null +++ b/backend/src/guards/jwt-auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') {} \ No newline at end of file diff --git a/backend/src/guards/jwt.strategy.ts b/backend/src/guards/jwt.strategy.ts new file mode 100644 index 0000000..693dd7c --- /dev/null +++ b/backend/src/guards/jwt.strategy.ts @@ -0,0 +1,33 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PrismaService } from '../prisma.service'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor(private prisma: PrismaService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: process.env.JWT_SECRET, + }); + } + + async validate(payload: any) { + const user = await this.prisma.user.findUnique({ + where: { id: payload.sub }, + }); + + if (!user) { + throw new UnauthorizedException(); + } + + return { + id: user.id, + walletAddress: user.walletAddress, + role: user.role, + reputationScore: user.reputationScore, + trustScore: user.trustScore, + }; + } +} \ No newline at end of file diff --git a/backend/src/guards/roles.guard.ts b/backend/src/guards/roles.guard.ts new file mode 100644 index 0000000..de8d203 --- /dev/null +++ b/backend/src/guards/roles.guard.ts @@ -0,0 +1,23 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { UserRole } from '@prisma/client'; +import { ROLES_KEY } from '../decorators/roles.decorator'; + +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + if (!requiredRoles) { + return true; + } + + const { user } = context.switchToHttp().getRequest(); + return requiredRoles.some((role) => user?.role === role); + } +} \ No newline at end of file diff --git a/backend/src/user.controller.ts b/backend/src/user.controller.ts index b398c4e..9f5778a 100644 --- a/backend/src/user.controller.ts +++ b/backend/src/user.controller.ts @@ -44,7 +44,8 @@ export class UserController { } @Put('freeze-request/:requestId/review') - @UseGuards(AdminGuard) + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) async reviewFreezeRequest( @Param('requestId') requestId: string, @Body() body: { adminId: string; approved: boolean; adminNotes?: string }, diff --git a/backend/src/verification/controllers/kyc-admin.controller.ts b/backend/src/verification/controllers/kyc-admin.controller.ts index de438f5..20dc0d5 100644 --- a/backend/src/verification/controllers/kyc-admin.controller.ts +++ b/backend/src/verification/controllers/kyc-admin.controller.ts @@ -9,9 +9,14 @@ import { import { KycAdminService } from '../services/kyc-admin.service'; import { KycOverrideDto } from '../dto/kyc-override.dto'; import { AdminGuard } from '../../guards/admin.guard'; +import { JwtAuthGuard } from '../../guards/jwt-auth.guard'; +import { RolesGuard } from '../../guards/roles.guard'; +import { Roles } from '../../decorators/roles.decorator'; +import { UserRole } from '@prisma/client'; @Controller('admin/kyc') -@UseGuards(AdminGuard) +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles(UserRole.ADMIN) export class KycAdminController { constructor(private readonly kycService: KycAdminService) {}