Compare commits
29 Commits
feature/au
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
7b43387a0c | ||
|
8f51c04cad | ||
|
41a9bfad98 | ||
|
e52c9acf2d | ||
|
bc4dcc26ef | ||
|
28e42b2248 | ||
|
80ce7d7f16 | ||
|
0ae3da74a0 | ||
|
006fe8cb74 | ||
|
161b01d8cb | ||
|
238b01f5f3 | ||
3d131193b6 | |||
38e491dc13 | |||
dd486229c4 | |||
|
76093fb5e8 | ||
|
63176d1863 | ||
|
d9cc0db0d2 | ||
|
8245f4ddfc | ||
|
c2028d9309 | ||
|
2e22e0bb4d | ||
|
a6d24cee9d | ||
|
ccf496a5d9 | ||
|
ab51873493 | ||
|
6521da705b | ||
|
f3c5673c75 | ||
|
b07b6082e4 | ||
|
3301b365f9 | ||
|
1988673f80 | ||
|
a95ed47302 |
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.log
|
||||||
|
*.md
|
||||||
|
.git
|
@ -1 +1,6 @@
|
|||||||
JWT_SECRET=
|
DATABASE_URL="mysql://USER:PASS@IP:PORT/DB"
|
||||||
|
|
||||||
|
AUTH_JWKS_URI=
|
||||||
|
AUTH_USERNAME_FIELD="name"
|
||||||
|
|
||||||
|
PORT=3000
|
@ -2,5 +2,9 @@
|
|||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"useTabs": true,
|
"useTabs": true,
|
||||||
"tabWidth": 4
|
"tabWidth": 4,
|
||||||
|
"semi": true,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"endOfLine": "lf"
|
||||||
}
|
}
|
||||||
|
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Use the official Node.js image as the base image
|
||||||
|
FROM node:20
|
||||||
|
|
||||||
|
# Set the working directory inside the container
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Copy package.json and package-lock.json to the working directory
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install the application dependencies
|
||||||
|
RUN yarn install
|
||||||
|
|
||||||
|
# Copy the rest of the application files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Generate Prisma client
|
||||||
|
RUN yarn prisma generate
|
||||||
|
|
||||||
|
# Build the NestJS application
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
# Expose the application port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Command to run the application
|
||||||
|
CMD ["node", "dist/main"]
|
5
docker-compose.yml
Normal file
5
docker-compose.yml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
services:
|
||||||
|
api:
|
||||||
|
image: toogether/api
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
110
package.json
110
package.json
@ -1,53 +1,61 @@
|
|||||||
{
|
{
|
||||||
"name": "@toogether/server",
|
"name": "@toogether/server",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "nest start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/main",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"preinstall": "node -e \"if(process.env.npm_execpath.indexOf('yarn') === -1) throw new Error('You must use Yarn to install, not NPM')\""
|
"preinstall": "node -e \"if(process.env.npm_execpath.indexOf('yarn') === -1) throw new Error('You must use Yarn to install, not NPM')\"",
|
||||||
},
|
"migrate:dev": "npx prisma migrate dev",
|
||||||
"dependencies": {
|
"migrate:dev:create": "npx prisma migrate dev --create-only",
|
||||||
"@nestjs/common": "^10.0.0",
|
"migrate:deploy": "npx prisma migrate deploy",
|
||||||
"@nestjs/config": "^3.3.0",
|
"prisma:generate": "npx prisma generate",
|
||||||
"@nestjs/core": "^10.0.0",
|
"prisma:studio": "npx prisma studio",
|
||||||
"@nestjs/jwt": "^10.2.0",
|
"prisma:seed": "npx prisma db seed"
|
||||||
"@nestjs/mapped-types": "^2.0.6",
|
},
|
||||||
"@nestjs/passport": "^10.0.3",
|
"dependencies": {
|
||||||
"@nestjs/platform-express": "^10.4.11",
|
"@nestjs/common": "^10.0.0",
|
||||||
"@prisma/client": "5.22.0",
|
"@nestjs/config": "^3.3.0",
|
||||||
"axios": "^1.7.7",
|
"@nestjs/core": "^10.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"@nestjs/mapped-types": "^2.0.6",
|
||||||
"class-validator": "^0.14.1",
|
"@nestjs/platform-express": "^10.4.11",
|
||||||
"passport": "^0.7.0",
|
"@nestjs/platform-socket.io": "^10.4.12",
|
||||||
"passport-discord": "^0.1.4",
|
"@nestjs/swagger": "^8.0.7",
|
||||||
"passport-jwt": "^4.0.1",
|
"@nestjs/websockets": "^10.4.12",
|
||||||
"prisma": "^5.22.0",
|
"@prisma/client": "^6.0.1",
|
||||||
"reflect-metadata": "^0.2.0",
|
"axios": "^1.7.7",
|
||||||
"rxjs": "^7.8.1"
|
"class-transformer": "^0.5.1",
|
||||||
},
|
"class-validator": "^0.14.1",
|
||||||
"devDependencies": {
|
"joi": "^17.13.3",
|
||||||
"@nestjs/cli": "^10.0.0",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"jwks-rsa": "^3.1.0",
|
||||||
"@types/express": "^5.0.0",
|
"nestjs-prisma": "^0.23.0",
|
||||||
"@types/node": "^20.3.1",
|
"prisma": "^6.0.1",
|
||||||
"@types/passport-discord": "^0.1.14",
|
"reflect-metadata": "^0.2.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
"rxjs": "^7.8.1",
|
||||||
"@typescript-eslint/parser": "^8.0.0",
|
"socket.io": "^4.8.1"
|
||||||
"eslint": "^9.0.0",
|
},
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"devDependencies": {
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"prettier": "^3.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"source-map-support": "^0.5.21",
|
"@types/express": "^5.0.0",
|
||||||
"ts-loader": "^9.4.3",
|
"@types/node": "^20.3.1",
|
||||||
"ts-node": "^10.9.1",
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"@typescript-eslint/parser": "^8.0.0",
|
||||||
"typescript": "^5.1.3"
|
"eslint": "^9.0.0",
|
||||||
}
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"ts-loader": "^9.4.3",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.1.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,98 @@
|
|||||||
// This is your Prisma schema file,
|
|
||||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
||||||
|
|
||||||
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
|
||||||
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "mysql"
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id
|
||||||
username String?
|
username String
|
||||||
isAdmin Boolean @default(false)
|
role Role @default(STUDENT)
|
||||||
password String?
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
Class Class[]
|
||||||
|
SentMessages UserMessage[] @relation("SentMessages")
|
||||||
|
ReceivedMessages UserMessage[] @relation("ReceivedMessages")
|
||||||
|
RoomSurveyAnswerUser RoomSurveyAnswerUser[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserMessage {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
content String
|
||||||
|
|
||||||
|
sender User @relation(fields: [sender_user_Id], references: [id], name: "SentMessages")
|
||||||
|
sender_user_Id String
|
||||||
|
|
||||||
|
receiver User @relation(fields: [receiver_user_Id], references: [id], name: "ReceivedMessages")
|
||||||
|
receiver_user_Id String
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
model Class {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String @unique
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
ClassRoom Room[]
|
||||||
|
Students User[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Room {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
Class Class[]
|
||||||
|
Messages RoomMessage[]
|
||||||
|
Documents RoomDocument[]
|
||||||
|
Surveys RoomSurvey[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model RoomMessage {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
content String
|
||||||
|
Room Room @relation(fields: [roomId], references: [id])
|
||||||
|
roomId String
|
||||||
|
}
|
||||||
|
|
||||||
|
model RoomDocument {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
content String
|
||||||
|
Room Room @relation(fields: [roomId], references: [id])
|
||||||
|
roomId String
|
||||||
|
}
|
||||||
|
|
||||||
|
model RoomSurvey {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
content String
|
||||||
|
Room Room @relation(fields: [roomId], references: [id])
|
||||||
|
roomId String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
endAt DateTime?
|
||||||
|
|
||||||
|
Answers RoomSurveyAnswer[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model RoomSurveyAnswer {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
content String
|
||||||
|
Survey RoomSurvey @relation(fields: [surveyId], references: [id])
|
||||||
|
surveyId Int
|
||||||
|
isRight Boolean
|
||||||
|
Users RoomSurveyAnswerUser[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model RoomSurveyAnswerUser {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
User User @relation(fields: [userId], references: [id])
|
||||||
|
userId String
|
||||||
|
Answer RoomSurveyAnswer @relation(fields: [answerId], references: [id])
|
||||||
|
answerId Int
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Role {
|
||||||
|
STUDENT
|
||||||
|
ADMIN
|
||||||
}
|
}
|
||||||
|
9
src/ApiResponses/UnauthorizedResponse.ts
Normal file
9
src/ApiResponses/UnauthorizedResponse.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { ApiResponseNoStatusOptions } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
export const UnauthorizedResponse = {
|
||||||
|
description: "Unauthorized",
|
||||||
|
example: {
|
||||||
|
message: "Unauthorized",
|
||||||
|
statusCode: 401,
|
||||||
|
},
|
||||||
|
} as ApiResponseNoStatusOptions;
|
15
src/app.controller.ts
Normal file
15
src/app.controller.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Controller, Get, UseGuards } from "@nestjs/common";
|
||||||
|
import { ApiBearerAuth } from "@nestjs/swagger";
|
||||||
|
import { JwtAuthGuard } from "./modules/auth/guards/jwt.guard";
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
export class AppController {
|
||||||
|
@Get("ping")
|
||||||
|
pong() {
|
||||||
|
return {
|
||||||
|
message: new Date().toLocaleString("fr"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,24 +1,28 @@
|
|||||||
|
import env from "@Config/env";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { ConfigModule } from "@nestjs/config";
|
import { ConfigModule } from "@nestjs/config";
|
||||||
import { JwtModule } from "@nestjs/jwt";
|
import { envValidation } from "@Validations/env.validation";
|
||||||
|
|
||||||
import { AppService } from "./app.service";
|
import { AuthModule } from "@Modules/auth/auth.module";
|
||||||
import { UserModule } from "./user/user.module";
|
import { UserModule } from "@Modules/user/user.module";
|
||||||
|
import { PrismaModule } from "nestjs-prisma";
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AppController } from "./app.controller";
|
||||||
|
import { ClassModule } from "./modules/class/class.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
|
load: [env],
|
||||||
|
validationSchema: envValidation,
|
||||||
}),
|
}),
|
||||||
JwtModule.register({
|
PrismaModule.forRoot({
|
||||||
global: true,
|
isGlobal: true,
|
||||||
secret: process.env.JWT_SECRET,
|
|
||||||
}),
|
}),
|
||||||
UserModule,
|
UserModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
|
ClassModule,
|
||||||
],
|
],
|
||||||
providers: [AppService],
|
controllers: [AppController]
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AppService {
|
|
||||||
getHello() {
|
|
||||||
return {
|
|
||||||
message: "Hello World!",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
|
||||||
import { AuthGuard } from "@nestjs/passport";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DiscordAuthGuard extends AuthGuard("discord") {
|
|
||||||
handleRequest(err, user, info, context) {
|
|
||||||
if (err || !user) {
|
|
||||||
const errorMessage = info?.message || "Authentication failed";
|
|
||||||
throw new UnauthorizedException(errorMessage);
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import { AuthGuard } from "@nestjs/passport";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class JwtAuthGuard extends AuthGuard("jwt") {}
|
|
@ -1,33 +0,0 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import { JwtService } from "@nestjs/jwt";
|
|
||||||
import { PassportStrategy } from "@nestjs/passport";
|
|
||||||
import { Profile, Strategy } from "passport-discord";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DiscordStrategy extends PassportStrategy(Strategy, "discord") {
|
|
||||||
constructor(private jwtService: JwtService) {
|
|
||||||
super({
|
|
||||||
clientID: process.env.DISCORD_CLIENT_ID,
|
|
||||||
clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
|
||||||
callbackURL: process.env.DISCORD_CALLBACK_URL,
|
|
||||||
scope: ["identify", "email"]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async validate(
|
|
||||||
_accessToken: string,
|
|
||||||
_refreshToken: string,
|
|
||||||
profile: Profile,
|
|
||||||
done: Function,
|
|
||||||
) {
|
|
||||||
const jwtPayload = {
|
|
||||||
id: profile.id,
|
|
||||||
username: profile.username,
|
|
||||||
email: profile.email,
|
|
||||||
};
|
|
||||||
|
|
||||||
const jwt = this.jwtService.sign(jwtPayload);
|
|
||||||
|
|
||||||
done(null, { jwt });
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import { PassportStrategy } from "@nestjs/passport";
|
|
||||||
import { Profile } from "passport-discord";
|
|
||||||
import { Strategy, ExtractJwt } from "passport-jwt";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class JWTStrategy extends PassportStrategy(Strategy) {
|
|
||||||
constructor() {
|
|
||||||
super({
|
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
||||||
ignoreExipration: false,
|
|
||||||
secretOrKey: process.env.JWT_SECRET,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async validate(profile: Profile): Promise<any> {
|
|
||||||
return profile;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
import { Controller, Get, Query, Req, Res, UseGuards } from "@nestjs/common";
|
|
||||||
import { URLSearchParams } from "node:url";
|
|
||||||
import { JwtAuthGuard } from "./Guards/jwt.guard";
|
|
||||||
import { DiscordAuthGuard } from "./Guards/discord.guard";
|
|
||||||
|
|
||||||
@Controller("auth")
|
|
||||||
export class AuthController {
|
|
||||||
@Get("providers")
|
|
||||||
Providers() {
|
|
||||||
const discordOauth2Params = new URLSearchParams({
|
|
||||||
client_id: process.env.DISCORD_CLIENT_ID,
|
|
||||||
response_type: "code",
|
|
||||||
redirect_uri: process.env.DISCORD_CALLBACK_URL,
|
|
||||||
scope: "identify email",
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
discord: {
|
|
||||||
url: `https://discord.com/oauth2/authorize?${discordOauth2Params.toString()}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get("discord/callback")
|
|
||||||
@UseGuards(DiscordAuthGuard)
|
|
||||||
CallbackDiscord(@Req() req, @Res() res) {
|
|
||||||
const { user } = req;
|
|
||||||
|
|
||||||
res.send(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get("profile")
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
Profile(@Req() req) {
|
|
||||||
return req.user;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
import { Module } from "@nestjs/common";
|
|
||||||
import { AuthController } from "./auth.controller";
|
|
||||||
import { AuthService } from "./auth.service";
|
|
||||||
|
|
||||||
import { DiscordStrategy } from "./Strategy/discord.strategy";
|
|
||||||
import { JWTStrategy } from "./Strategy/jwt.strategy";
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
controllers: [AuthController],
|
|
||||||
providers: [AuthService, DiscordStrategy, JWTStrategy],
|
|
||||||
})
|
|
||||||
export class AuthModule {}
|
|
@ -1,4 +0,0 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AuthService {}
|
|
6
src/config/env.ts
Normal file
6
src/config/env.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default () => ({
|
||||||
|
auth: {
|
||||||
|
jwksURL: process.env.AUTH_JWKS_URI,
|
||||||
|
usernameField: process.env.AUTH_USERNAME_FIELD
|
||||||
|
},
|
||||||
|
});
|
3
src/interfaces/jwtPayload.ts
Normal file
3
src/interfaces/jwtPayload.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface JwtPayload {
|
||||||
|
id: string;
|
||||||
|
}
|
6
src/interfaces/oauth2.ts
Normal file
6
src/interfaces/oauth2.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface Oauth2Profile {
|
||||||
|
id?: string;
|
||||||
|
sub?: string;
|
||||||
|
|
||||||
|
email: string;
|
||||||
|
}
|
47
src/main.ts
47
src/main.ts
@ -1,6 +1,12 @@
|
|||||||
import { NestFactory } from "@nestjs/core";
|
import {
|
||||||
|
ClassSerializerInterceptor,
|
||||||
|
HttpStatus,
|
||||||
|
ValidationPipe,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { HttpAdapterHost, NestFactory, Reflector } from "@nestjs/core";
|
||||||
|
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
|
||||||
|
import { PrismaClientExceptionFilter } from "nestjs-prisma";
|
||||||
import { AppModule } from "./app.module";
|
import { AppModule } from "./app.module";
|
||||||
import { ValidationPipe } from "@nestjs/common";
|
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
@ -9,8 +15,45 @@ async function bootstrap() {
|
|||||||
origin: "*",
|
origin: "*",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { httpAdapter } = app.get(HttpAdapterHost);
|
||||||
|
app.useGlobalFilters(
|
||||||
|
new PrismaClientExceptionFilter(httpAdapter, {
|
||||||
|
P2000: {
|
||||||
|
errorMessage: "A required field is missing",
|
||||||
|
statusCode: HttpStatus.BAD_REQUEST,
|
||||||
|
},
|
||||||
|
P2002: {
|
||||||
|
errorMessage:
|
||||||
|
"A ressource with the same unique fields already exists",
|
||||||
|
statusCode: HttpStatus.CONFLICT,
|
||||||
|
},
|
||||||
|
P2025: {
|
||||||
|
errorMessage: "The requested ressource does not exist",
|
||||||
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
app.useGlobalPipes(new ValidationPipe());
|
app.useGlobalPipes(new ValidationPipe());
|
||||||
|
|
||||||
|
app.useGlobalInterceptors(
|
||||||
|
new ClassSerializerInterceptor(app.get(Reflector), {
|
||||||
|
excludeExtraneousValues: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const config = new DocumentBuilder()
|
||||||
|
.setTitle("Toogether API")
|
||||||
|
.addBearerAuth()
|
||||||
|
.build();
|
||||||
|
const documentFactory = () => SwaggerModule.createDocument(app, config);
|
||||||
|
SwaggerModule.setup("documentation", app, documentFactory, {
|
||||||
|
swaggerOptions: {
|
||||||
|
tryItOutEnabled: true,
|
||||||
|
persistAuthorization: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await app.listen(process.env.PORT ?? 3000, "0.0.0.0");
|
await app.listen(process.env.PORT ?? 3000, "0.0.0.0");
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
8
src/modules/auth/auth.module.ts
Normal file
8
src/modules/auth/auth.module.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { AuthService } from "./auth.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [AuthService],
|
||||||
|
exports: [AuthService],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
49
src/modules/auth/auth.service.ts
Normal file
49
src/modules/auth/auth.service.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import * as jwt from "jsonwebtoken";
|
||||||
|
import JwksRsa, * as jwksRsa from "jwks-rsa";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
private jwksClient: JwksRsa.JwksClient;
|
||||||
|
|
||||||
|
constructor(configService: ConfigService) {
|
||||||
|
this.jwksClient = jwksRsa({
|
||||||
|
jwksUri: configService.get<string>("AUTH_JWKS_URI"),
|
||||||
|
cache: true,
|
||||||
|
rateLimit: true,
|
||||||
|
jwksRequestsPerMinute: 10,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSigningKey(kid: string): Promise<string> {
|
||||||
|
const key = await this.jwksClient.getSigningKey(kid);
|
||||||
|
return key.getPublicKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeJwt(token: string) {
|
||||||
|
return jwt.decode(token, { complete: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyJwt(token: string, key: string) {
|
||||||
|
return jwt.verify(token, key, {
|
||||||
|
algorithms: ["RS256"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkToken(token: string): Promise<jwt.JwtPayload> {
|
||||||
|
const decodedHeader = this.decodeJwt(token);
|
||||||
|
const kid = decodedHeader?.header?.kid;
|
||||||
|
|
||||||
|
if (!kid) throw "Token kid not found";
|
||||||
|
|
||||||
|
const key = await this.getSigningKey(kid);
|
||||||
|
|
||||||
|
const jwtPayload = this.verifyJwt(token, key);
|
||||||
|
|
||||||
|
if (typeof jwtPayload == "string")
|
||||||
|
throw new UnauthorizedException("Invalid token");
|
||||||
|
|
||||||
|
return jwtPayload;
|
||||||
|
}
|
||||||
|
}
|
4
src/modules/auth/decorators/roles.decorator.ts
Normal file
4
src/modules/auth/decorators/roles.decorator.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { SetMetadata } from "@nestjs/common";
|
||||||
|
import { $Enums } from "@prisma/client";
|
||||||
|
|
||||||
|
export const Roles = (roles: $Enums.Role[]) => SetMetadata("roles", roles);
|
55
src/modules/auth/guards/jwt.guard.ts
Normal file
55
src/modules/auth/guards/jwt.guard.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { UserService } from "@/modules/user/user.service";
|
||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { AuthService } from "../auth.service";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private readonly userService: UserService,
|
||||||
|
private readonly authService: AuthService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const token = this.extractTokenFromHeader(request);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new UnauthorizedException("No token provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jwtPayload = await this.authService.checkToken(token);
|
||||||
|
|
||||||
|
let user = await this.userService.findOrCreate({
|
||||||
|
id: jwtPayload.sub.toString(),
|
||||||
|
username:
|
||||||
|
jwtPayload[this.configService.get("auth.usernameField")],
|
||||||
|
});
|
||||||
|
|
||||||
|
request.user = user;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
throw new UnauthorizedException(`Invalid token: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractTokenFromHeader(request: any): string | null {
|
||||||
|
const authHeader = request.headers["authorization"];
|
||||||
|
if (!authHeader) return null;
|
||||||
|
|
||||||
|
const parts = authHeader.split(" ");
|
||||||
|
if (parts.length !== 2 || parts[0] !== "Bearer") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts[1];
|
||||||
|
}
|
||||||
|
}
|
45
src/modules/auth/guards/role.guard.ts
Normal file
45
src/modules/auth/guards/role.guard.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
ForbiddenException,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { Reflector } from "@nestjs/core";
|
||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RolesGuard implements CanActivate {
|
||||||
|
constructor(private readonly reflector: Reflector) {}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const RolesHandler = this.reflector.get<string[]>(
|
||||||
|
"roles",
|
||||||
|
context.getHandler(),
|
||||||
|
);
|
||||||
|
const RolesClass = this.reflector.get<string[]>(
|
||||||
|
"roles",
|
||||||
|
context.getClass(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!RolesHandler && !RolesClass) return true;
|
||||||
|
|
||||||
|
const request = context.switchToHttp().getRequest() as Request;
|
||||||
|
const user = request.user;
|
||||||
|
|
||||||
|
if (!user) throw new ForbiddenException("User not authenticated");
|
||||||
|
|
||||||
|
const hasRoleHandler =
|
||||||
|
RolesHandler?.some((role) => user.role?.includes(role)) ??
|
||||||
|
false,
|
||||||
|
hasRoleClass =
|
||||||
|
RolesClass?.some((role) => user.role?.includes(role)) ?? false;
|
||||||
|
|
||||||
|
if (hasRoleHandler) return true;
|
||||||
|
else if (hasRoleClass) return true;
|
||||||
|
else
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
`User doesn't have the right role, expected: ${RolesHandler ?? RolesClass}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
40
src/modules/class/ApiResponses/ClassResponse.ts
Normal file
40
src/modules/class/ApiResponses/ClassResponse.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { ApiResponseNoStatusOptions } from "@nestjs/swagger";
|
||||||
|
import { ClassEntity } from "../entities/class.entity";
|
||||||
|
|
||||||
|
export const ClassResponse = {
|
||||||
|
type: ClassEntity,
|
||||||
|
examples: {
|
||||||
|
example: {
|
||||||
|
summary: "A class",
|
||||||
|
value: {
|
||||||
|
id: "1",
|
||||||
|
name: "Sigyn",
|
||||||
|
createdAt: new Date(),
|
||||||
|
} as ClassEntity,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ApiResponseNoStatusOptions;
|
||||||
|
|
||||||
|
export const ClassesResponse = {
|
||||||
|
type: ClassEntity,
|
||||||
|
isArray: true,
|
||||||
|
examples: {
|
||||||
|
example: {
|
||||||
|
summary: "A list of classes",
|
||||||
|
value: [
|
||||||
|
{ id: "1", name: "Sigyn", createdAt: new Date() },
|
||||||
|
{ id: "2", name: "Loki", createdAt: new Date() },
|
||||||
|
] as ClassEntity[],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ApiResponseNoStatusOptions;
|
||||||
|
|
||||||
|
export const ClassCountResponse = {
|
||||||
|
description: "The class count",
|
||||||
|
examples: {
|
||||||
|
example: {
|
||||||
|
summary: "A count of classes",
|
||||||
|
value: { count: 2 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ApiResponseNoStatusOptions;
|
102
src/modules/class/class.controller.ts
Normal file
102
src/modules/class/class.controller.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { UnauthorizedResponse } from "@/ApiResponses/UnauthorizedResponse";
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import {
|
||||||
|
ClassCountResponse,
|
||||||
|
ClassesResponse,
|
||||||
|
ClassResponse,
|
||||||
|
} from "./ApiResponses/ClassResponse";
|
||||||
|
import { ClassService } from "./class.service";
|
||||||
|
|
||||||
|
import { CreateClassDto } from "./dto/create-class.dto";
|
||||||
|
import { UpdateClassDto } from "./dto/update-class.dto";
|
||||||
|
|
||||||
|
import { Roles } from "@/modules/auth/decorators/roles.decorator";
|
||||||
|
import { JwtAuthGuard } from "@/modules/auth/guards/jwt.guard";
|
||||||
|
import { RolesGuard } from "@/modules/auth/guards/role.guard";
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
|
ApiQuery,
|
||||||
|
ApiUnauthorizedResponse,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
import { ClassEntity } from "./entities/class.entity";
|
||||||
|
|
||||||
|
@Controller("class")
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Roles(["ADMIN"])
|
||||||
|
@ApiUnauthorizedResponse(UnauthorizedResponse)
|
||||||
|
export class ClassController {
|
||||||
|
constructor(private readonly classService: ClassService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOkResponse(ClassResponse)
|
||||||
|
@ApiOperation({ summary: "Create a new class" })
|
||||||
|
async create(@Body() createClassDto: CreateClassDto) {
|
||||||
|
return await this.classService.create(createClassDto).then((class_) => {
|
||||||
|
return new ClassEntity(class_);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOkResponse(ClassesResponse)
|
||||||
|
@ApiOperation({ summary: "Get all classes" })
|
||||||
|
async findAll() {
|
||||||
|
return await this.classService
|
||||||
|
.findAll({})
|
||||||
|
.then((classes) =>
|
||||||
|
classes.map((class_) => new ClassEntity(class_)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(":id")
|
||||||
|
@ApiOkResponse(ClassResponse)
|
||||||
|
@ApiOperation({ summary: "Get a class by id" })
|
||||||
|
async findOne(@Param("id") id: string) {
|
||||||
|
return await this.classService
|
||||||
|
.findOne(id)
|
||||||
|
.then((class_) => new ClassEntity(class_));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(":id")
|
||||||
|
@ApiOkResponse(ClassResponse)
|
||||||
|
@ApiOperation({ summary: "Update a class by id" })
|
||||||
|
async update(
|
||||||
|
@Param("id") id: string,
|
||||||
|
@Body() updateClassDto: UpdateClassDto,
|
||||||
|
) {
|
||||||
|
return await this.classService
|
||||||
|
.update(id, updateClassDto)
|
||||||
|
.then((class_) => new ClassEntity(class_));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(":id")
|
||||||
|
@ApiOkResponse(ClassResponse)
|
||||||
|
@ApiOperation({ summary: "Remove a class by id" })
|
||||||
|
async remove(@Param("id") id: string) {
|
||||||
|
return await this.classService
|
||||||
|
.remove(id)
|
||||||
|
.then((class_) => new ClassEntity(class_));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete()
|
||||||
|
@ApiOkResponse(ClassCountResponse)
|
||||||
|
@ApiOperation({ summary: "Remove multiple classes by ids" })
|
||||||
|
@ApiQuery({ name: "ids", required: true, type: [String] })
|
||||||
|
async bulkRemove(@Query("ids") ids: string | string[]) {
|
||||||
|
if (typeof ids === "string") ids = [ids];
|
||||||
|
return await this.classService.bulkRemove(ids);
|
||||||
|
}
|
||||||
|
}
|
11
src/modules/class/class.module.ts
Normal file
11
src/modules/class/class.module.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { ClassService } from "./class.service";
|
||||||
|
import { ClassController } from "./class.controller";
|
||||||
|
import { UserModule } from "../user/user.module";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [UserModule],
|
||||||
|
controllers: [ClassController],
|
||||||
|
providers: [ClassService],
|
||||||
|
})
|
||||||
|
export class ClassModule {}
|
57
src/modules/class/class.service.ts
Normal file
57
src/modules/class/class.service.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { CreateClassDto } from "./dto/create-class.dto";
|
||||||
|
import { UpdateClassDto } from "./dto/update-class.dto";
|
||||||
|
import { PrismaService } from "nestjs-prisma";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ClassService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async create(createClassDto: CreateClassDto) {
|
||||||
|
return await this.prisma.class.create({ data: createClassDto });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll({
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
cursor,
|
||||||
|
where,
|
||||||
|
orderBy,
|
||||||
|
}: {
|
||||||
|
skip?: number;
|
||||||
|
take?: number;
|
||||||
|
cursor?: Prisma.ClassWhereUniqueInput;
|
||||||
|
where?: Prisma.ClassWhereInput;
|
||||||
|
orderBy?: Record<string, unknown>;
|
||||||
|
}) {
|
||||||
|
return await this.prisma.class.findMany({
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
cursor,
|
||||||
|
where,
|
||||||
|
orderBy,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string) {
|
||||||
|
return await this.prisma.class.findUniqueOrThrow({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, updateClassDto: UpdateClassDto) {
|
||||||
|
return await this.prisma.class.update({
|
||||||
|
where: { id },
|
||||||
|
data: updateClassDto,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string) {
|
||||||
|
return await this.prisma.class.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async bulkRemove(ids: string[]) {
|
||||||
|
return await this.prisma.class.deleteMany({
|
||||||
|
where: { id: { in: ids } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
8
src/modules/class/dto/create-class.dto.ts
Normal file
8
src/modules/class/dto/create-class.dto.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class CreateClassDto {
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty()
|
||||||
|
name: string;
|
||||||
|
}
|
4
src/modules/class/dto/update-class.dto.ts
Normal file
4
src/modules/class/dto/update-class.dto.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from "@nestjs/swagger";
|
||||||
|
import { CreateClassDto } from "./create-class.dto";
|
||||||
|
|
||||||
|
export class UpdateClassDto extends PartialType(CreateClassDto) {}
|
21
src/modules/class/entities/class.entity.ts
Normal file
21
src/modules/class/entities/class.entity.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { ApiProperty, ApiSchema } from "@nestjs/swagger";
|
||||||
|
import { Expose } from "class-transformer";
|
||||||
|
|
||||||
|
@ApiSchema({ name: "Class" })
|
||||||
|
export class ClassEntity {
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
constructor(partial: Partial<ClassEntity>) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
41
src/modules/user/ApiResponses/UserReponse.ts
Normal file
41
src/modules/user/ApiResponses/UserReponse.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { ApiResponseNoStatusOptions } from "@nestjs/swagger";
|
||||||
|
import { UserEntity } from "../entities/user.entity";
|
||||||
|
|
||||||
|
export const UserResponse = {
|
||||||
|
type: UserEntity,
|
||||||
|
description: "The user has been successfully found.",
|
||||||
|
examples: {
|
||||||
|
example: {
|
||||||
|
summary: "A user example",
|
||||||
|
value: {
|
||||||
|
id: "1",
|
||||||
|
role: "ADMIN",
|
||||||
|
username: "admin",
|
||||||
|
} as UserEntity,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ApiResponseNoStatusOptions;
|
||||||
|
|
||||||
|
export const UsersResponse = {
|
||||||
|
type: UserEntity,
|
||||||
|
isArray: true,
|
||||||
|
examples: {
|
||||||
|
example: {
|
||||||
|
summary: "A list of users",
|
||||||
|
value: [
|
||||||
|
{ id: "1", role: "ADMIN", username: "admin" },
|
||||||
|
{ id: "2", role: "STUDENT", username: "student" },
|
||||||
|
] as UserEntity[],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ApiResponseNoStatusOptions;
|
||||||
|
|
||||||
|
export const UserCountResponse = {
|
||||||
|
description: "The users count",
|
||||||
|
examples: {
|
||||||
|
example: {
|
||||||
|
summary: "A count of users",
|
||||||
|
value: { count: 2 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ApiResponseNoStatusOptions;
|
17
src/modules/user/dto/create-user.dto.ts
Normal file
17
src/modules/user/dto/create-user.dto.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Role } from "@prisma/client";
|
||||||
|
import { IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class CreateUserDTO {
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty()
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty()
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty()
|
||||||
|
role: Role;
|
||||||
|
}
|
5
src/modules/user/dto/update-user.dto.ts
Normal file
5
src/modules/user/dto/update-user.dto.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { PartialType } from "@nestjs/mapped-types";
|
||||||
|
|
||||||
|
import { CreateUserDTO } from "./create-user.dto";
|
||||||
|
|
||||||
|
export class UpdateUserDTO extends PartialType(CreateUserDTO) {}
|
22
src/modules/user/entities/user.entity.ts
Normal file
22
src/modules/user/entities/user.entity.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ApiProperty, ApiSchema } from "@nestjs/swagger";
|
||||||
|
import { $Enums } from "@prisma/client";
|
||||||
|
import { Expose } from "class-transformer";
|
||||||
|
|
||||||
|
@ApiSchema({ name: "User" })
|
||||||
|
export class UserEntity {
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
role: $Enums.Role;
|
||||||
|
|
||||||
|
constructor(partial: Partial<UserEntity>) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
94
src/modules/user/user.controller.ts
Normal file
94
src/modules/user/user.controller.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { UnauthorizedResponse } from "@/ApiResponses/UnauthorizedResponse";
|
||||||
|
import { Roles } from "@/modules/auth/decorators/roles.decorator";
|
||||||
|
import { JwtAuthGuard } from "@/modules/auth/guards/jwt.guard";
|
||||||
|
import { RolesGuard } from "@/modules/auth/guards/role.guard";
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Query,
|
||||||
|
UseGuards
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
ApiQuery,
|
||||||
|
ApiUnauthorizedResponse
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
import {
|
||||||
|
UserCountResponse,
|
||||||
|
UserResponse,
|
||||||
|
UsersResponse,
|
||||||
|
} from "./ApiResponses/UserReponse";
|
||||||
|
import { UpdateUserDTO } from "./dto/update-user.dto";
|
||||||
|
import { UserEntity } from "./entities/user.entity";
|
||||||
|
import { UserService } from "./user.service";
|
||||||
|
|
||||||
|
@Controller("user")
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Roles(["ADMIN"])
|
||||||
|
@ApiUnauthorizedResponse(UnauthorizedResponse)
|
||||||
|
export class UserController {
|
||||||
|
constructor(private readonly userService: UserService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOkResponse(UsersResponse)
|
||||||
|
@ApiOperation({ summary: "Get all users" })
|
||||||
|
async findAll(): Promise<UserEntity[]> {
|
||||||
|
return await this.userService
|
||||||
|
.findAll()
|
||||||
|
.then((users) => users.map((user) => new UserEntity(user)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(":id")
|
||||||
|
@ApiOkResponse(UserResponse)
|
||||||
|
@ApiOperation({ summary: "Get user by id" })
|
||||||
|
async findOne(@Param("id") id: string): Promise<UserEntity> {
|
||||||
|
return this.userService
|
||||||
|
.findOne(id)
|
||||||
|
.then((user) => new UserEntity(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(":id")
|
||||||
|
@ApiOkResponse(UserResponse)
|
||||||
|
@ApiOperation({ summary: "Update user by id" })
|
||||||
|
async update(
|
||||||
|
@Param("id") id: string,
|
||||||
|
@Body() updateUserDto: UpdateUserDTO,
|
||||||
|
): Promise<UserEntity> {
|
||||||
|
return this.userService
|
||||||
|
.update(id, updateUserDto)
|
||||||
|
.then((user) => new UserEntity(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(":id")
|
||||||
|
@ApiOkResponse(UserResponse)
|
||||||
|
@ApiOperation({ summary: "Delete user by id" })
|
||||||
|
@ApiParam({
|
||||||
|
name: "id",
|
||||||
|
type: String,
|
||||||
|
description: "The user id",
|
||||||
|
example: "1",
|
||||||
|
})
|
||||||
|
async remove(@Param("id") id: string): Promise<UserEntity> {
|
||||||
|
return this.userService.remove(id).then((user) => new UserEntity(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete()
|
||||||
|
@ApiOkResponse(UserCountResponse)
|
||||||
|
@ApiOperation({ summary: "Delete users by ids" })
|
||||||
|
@ApiQuery({ name: "ids", required: true, type: [String] })
|
||||||
|
bulkRemove(@Query("ids") ids: string | string[]): Promise<{
|
||||||
|
count: number;
|
||||||
|
}> {
|
||||||
|
if (typeof ids === "string") ids = [ids];
|
||||||
|
return this.userService.bulkRemove(ids);
|
||||||
|
}
|
||||||
|
}
|
62
src/modules/user/user.gateway.ts
Normal file
62
src/modules/user/user.gateway.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import {
|
||||||
|
OnGatewayConnection,
|
||||||
|
OnGatewayDisconnect,
|
||||||
|
SubscribeMessage,
|
||||||
|
WebSocketGateway,
|
||||||
|
WebSocketServer,
|
||||||
|
WsResponse,
|
||||||
|
} from "@nestjs/websockets";
|
||||||
|
import { Server, Socket } from "socket.io";
|
||||||
|
import { AuthService } from "../auth/auth.service";
|
||||||
|
import { UserService } from "./user.service";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
|
@WebSocketGateway()
|
||||||
|
export class UserGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||||
|
constructor(
|
||||||
|
private readonly authService: AuthService,
|
||||||
|
private readonly userService: UserService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@WebSocketServer() io: Server;
|
||||||
|
|
||||||
|
public clients: Socket[] = [];
|
||||||
|
|
||||||
|
async handleConnection(client: Socket) {
|
||||||
|
const token = client.handshake.headers.authorization;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var jwtPayload = await this.authService.checkToken(token);
|
||||||
|
|
||||||
|
if (!jwtPayload) throw "Invalid token";
|
||||||
|
} catch (error) {
|
||||||
|
client.emit("auth", error);
|
||||||
|
return client.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.userService.findOrCreate({
|
||||||
|
id: jwtPayload.sub.toString(),
|
||||||
|
username: jwtPayload[this.configService.get("auth.usernameField")],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
client.emit("auth", "User not found");
|
||||||
|
return client.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
client.request.user = user;
|
||||||
|
|
||||||
|
this.clients.push(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDisconnect(client: Socket) {
|
||||||
|
this.clients = this.clients.filter((c) => c.id !== client.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeMessage("message")
|
||||||
|
sendMessage(_client: Socket, message: string): WsResponse<unknown> {
|
||||||
|
this.io.emit("message", message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
14
src/modules/user/user.module.ts
Normal file
14
src/modules/user/user.module.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { PrismaService } from "nestjs-prisma";
|
||||||
|
|
||||||
|
import { AuthService } from "@Modules/auth/auth.service";
|
||||||
|
import { UserController } from "./user.controller";
|
||||||
|
import { UserGateway } from "./user.gateway";
|
||||||
|
import { UserService } from "./user.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [UserService, AuthService, PrismaService, UserGateway],
|
||||||
|
controllers: [UserController],
|
||||||
|
exports: [UserService, AuthService],
|
||||||
|
})
|
||||||
|
export class UserModule {}
|
96
src/modules/user/user.service.ts
Normal file
96
src/modules/user/user.service.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { PrismaService } from "nestjs-prisma";
|
||||||
|
|
||||||
|
import { CreateUserDTO } from "./dto/create-user.dto";
|
||||||
|
import { UpdateUserDTO } from "./dto/update-user.dto";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findAll({
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
cursor,
|
||||||
|
where,
|
||||||
|
orderBy,
|
||||||
|
}: {
|
||||||
|
skip?: number;
|
||||||
|
take?: number;
|
||||||
|
cursor?: Prisma.UserWhereUniqueInput;
|
||||||
|
where?: Prisma.UserWhereInput;
|
||||||
|
orderBy?: Record<string, unknown>;
|
||||||
|
} = {}) {
|
||||||
|
return await this.prisma.user.findMany({
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
cursor,
|
||||||
|
where,
|
||||||
|
orderBy,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string) {
|
||||||
|
return await this.prisma.user.findUniqueOrThrow({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOrCreate({ id, username }: { id: string; username: string }) {
|
||||||
|
let user = await this.prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
const isFirstUser = (await this.prisma.user.count()) === 0;
|
||||||
|
|
||||||
|
user = await this.prisma.user.create({
|
||||||
|
data: {
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
role: isFirstUser ? "ADMIN" : "STUDENT",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(createUserDto: CreateUserDTO) {
|
||||||
|
return await this.prisma.user.create({
|
||||||
|
data: {
|
||||||
|
id: createUserDto.id,
|
||||||
|
username: createUserDto.username,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, updateUserInput: UpdateUserDTO) {
|
||||||
|
return await this.prisma.user.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
username: updateUserInput.username,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string) {
|
||||||
|
return await this.prisma.user.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async bulkRemove(ids: string[]) {
|
||||||
|
return await this.prisma.user.deleteMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: ids,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
import { Injectable, OnModuleInit } from "@nestjs/common";
|
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PrismaService extends PrismaClient implements OnModuleInit {
|
|
||||||
async onModuleInit() {
|
|
||||||
await this.$connect();
|
|
||||||
}
|
|
||||||
}
|
|
9
src/types/http.d.ts
vendored
Normal file
9
src/types/http.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { UserEntity } from "@/modules/user/entities/user.entity";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
import { IncomingMessage } from "http";
|
||||||
|
|
||||||
|
declare module "http" {
|
||||||
|
interface IncomingMessage {
|
||||||
|
user?: UserEntity;
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
import { IsBoolean, IsString } from "class-validator";
|
|
||||||
|
|
||||||
export class CreateUserInput {
|
|
||||||
@IsString()
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@IsBoolean()
|
|
||||||
isAdmin: boolean;
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
import { IsString } from "class-validator";
|
|
||||||
|
|
||||||
export class SetUserPasswordInput {
|
|
||||||
@IsString()
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
password: string;
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
import { PartialType } from "@nestjs/mapped-types";
|
|
||||||
import { IsString } from "class-validator";
|
|
||||||
|
|
||||||
import { CreateUserInput } from "./create-user.input";
|
|
||||||
|
|
||||||
export class UpdateUserInput extends PartialType(CreateUserInput) {
|
|
||||||
@IsString()
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
username: string;
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
import {
|
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
Delete,
|
|
||||||
Get,
|
|
||||||
Param,
|
|
||||||
Post,
|
|
||||||
UseGuards,
|
|
||||||
} from "@nestjs/common";
|
|
||||||
import { UserService } from "./user.service";
|
|
||||||
|
|
||||||
import { JwtAuthGuard } from "src/auth/Guards/jwt.guard";
|
|
||||||
import { CreateUserInput } from "./dto/create-user.input";
|
|
||||||
|
|
||||||
@Controller("user")
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
export class UserController {
|
|
||||||
constructor(private readonly userService: UserService) {}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
create(@Body() createUserInput: CreateUserInput) {
|
|
||||||
return this.userService.create(createUserInput);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
findAll() {
|
|
||||||
return this.userService.findAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete(":id")
|
|
||||||
removeUser(@Param("id") id: string) {
|
|
||||||
return this.userService.remove(id);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
import { Exclude } from "class-transformer";
|
|
||||||
|
|
||||||
export class UserEntity {
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
isAdmin: boolean;
|
|
||||||
|
|
||||||
@Exclude()
|
|
||||||
password: string;
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
import { Module } from "@nestjs/common";
|
|
||||||
|
|
||||||
import { UserService } from "./user.service";
|
|
||||||
|
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
|
||||||
import { UserController } from './user.controller';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [UserService, PrismaService],
|
|
||||||
controllers: [UserController]
|
|
||||||
})
|
|
||||||
export class UserModule {}
|
|
@ -1,77 +0,0 @@
|
|||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
|
||||||
import { plainToClass } from "class-transformer";
|
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
|
||||||
|
|
||||||
import { CreateUserInput } from "./dto/create-user.input";
|
|
||||||
import { UpdateUserInput } from "./dto/update-user.input";
|
|
||||||
import { SetUserPasswordInput } from "./dto/setpassword-user.input";
|
|
||||||
import { UserEntity } from "./user.entity";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class UserService {
|
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
|
||||||
|
|
||||||
async create(createUserInput: CreateUserInput) {
|
|
||||||
const user = await this.prisma.user.create({
|
|
||||||
data: {
|
|
||||||
username: createUserInput.username,
|
|
||||||
isAdmin: createUserInput.isAdmin,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return plainToClass(UserEntity, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(updateUserInput: UpdateUserInput) {
|
|
||||||
const user = await this.prisma.user.update({
|
|
||||||
where: { id: updateUserInput.id },
|
|
||||||
data: {
|
|
||||||
username: updateUserInput.username,
|
|
||||||
isAdmin: updateUserInput.isAdmin,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return plainToClass(UserEntity, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setPassword(setUserPasswordInput: SetUserPasswordInput) {
|
|
||||||
const exist = await this.prisma.user.findUnique({
|
|
||||||
where: { id: setUserPasswordInput.id },
|
|
||||||
});
|
|
||||||
if (!exist) throw new NotFoundException("User not found");
|
|
||||||
|
|
||||||
const user = await this.prisma.user.update({
|
|
||||||
where: { id: setUserPasswordInput.id },
|
|
||||||
data: {
|
|
||||||
password: setUserPasswordInput.password,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return plainToClass(UserEntity, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findAll() {
|
|
||||||
const users = await this.prisma.user.findMany();
|
|
||||||
|
|
||||||
return users.map((user) => plainToClass(UserEntity, user));
|
|
||||||
}
|
|
||||||
|
|
||||||
async findOne(id: string) {
|
|
||||||
const user = await this.prisma.user.findUnique({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
return plainToClass(UserEntity, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
async remove(id: string) {
|
|
||||||
const exist = await this.prisma.user.findUnique({ where: { id } });
|
|
||||||
if (!exist) throw new NotFoundException("User not found");
|
|
||||||
|
|
||||||
const user = await this.prisma.user.delete({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
return plainToClass(UserEntity, user);
|
|
||||||
}
|
|
||||||
}
|
|
10
src/validations/env.validation.ts
Normal file
10
src/validations/env.validation.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import * as Joi from "joi";
|
||||||
|
|
||||||
|
export const envValidation = Joi.object({
|
||||||
|
DATABASE_URL: Joi.string().required(),
|
||||||
|
|
||||||
|
AUTH_JWKS_URI: Joi.string().uri().required(),
|
||||||
|
AUTH_USERNAME_FIELD: Joi.string().required(),
|
||||||
|
|
||||||
|
PORT: Joi.number().optional(),
|
||||||
|
});
|
@ -1,4 +1,10 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
"exclude": [
|
||||||
}
|
"node_modules",
|
||||||
|
"test",
|
||||||
|
"dist",
|
||||||
|
"**/*spec.ts",
|
||||||
|
"prisma"
|
||||||
|
]
|
||||||
|
}
|
@ -9,7 +9,15 @@
|
|||||||
"target": "ES2021",
|
"target": "ES2021",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./src",
|
||||||
|
"typeRoots": ["./src/types/*.d.ts"],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["*"],
|
||||||
|
"@Modules/*": ["modules/*"],
|
||||||
|
"@Interfaces/*": ["interfaces/*"],
|
||||||
|
"@Config/*": ["config/*"],
|
||||||
|
"@Validations/*": ["validations/*"]
|
||||||
|
},
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strictNullChecks": false,
|
"strictNullChecks": false,
|
||||||
|
Loading…
Reference in New Issue
Block a user