From 82117339461f4a43f2ce188ac60f6d795febcb77 Mon Sep 17 00:00:00 2001 From: Negin Moshki <133236446+NeginMoshki@users.noreply.github.com> Date: Fri, 4 Aug 2023 17:04:11 +0200 Subject: [PATCH] Add js doc comments (#43) * Add comments for auth and browser package * DocUpdate user.controller.spec.ts * DocUpdate user.controller.ts * DocUpdate user.entity.ts * DocUpdate user.controller.spec.ts * DocUpdate user.controller.spec.ts * DocUpdate user.controller.ts * DocUpdate user.entity.ts * DocUpdate user.module.ts * DocUpdate user.service.spec.ts * DocUpdate user.service.ts * DocUpdate mikro-orm.config.ts * Add comments for the backend classes * Format code * Remove @fileoverview comments --------- Co-authored-by: neda_moshki <uzijb@student.kit.edu> Co-authored-by: Neda Moshki <133236496+NedaMoshki@users.noreply.github.com> Co-authored-by: Florian Raith <florianraith00@gmail.com> --- src/app.module.ts | 3 + src/auth/auth.controller.spec.ts | 11 +++ src/auth/auth.controller.ts | 34 +++++++++ src/auth/auth.dto.ts | 15 ++++ src/auth/auth.guard.ts | 12 ++++ src/auth/auth.module.ts | 3 + src/auth/auth.service.spec.ts | 11 +++ src/auth/auth.service.ts | 38 ++++++++++ src/auth/jwt.strategy.ts | 18 +++++ src/browser/browser.module.ts | 3 + src/browser/browser.service.ts | 87 ++++++++++++++++++++++ src/browser/browser.ts | 63 ++++++++++++++++ src/category/category.controller.spec.ts | 4 ++ src/category/category.controller.ts | 28 ++++++++ src/category/category.dto.ts | 10 +++ src/category/category.entity.ts | 30 ++++++++ src/category/category.module.ts | 4 ++ src/category/category.service.spec.ts | 10 +++ src/category/category.service.ts | 44 ++++++++++++ src/channel/browser.gateway.ts | 75 +++++++++++++++++++ src/channel/channel.gateway.spec.ts | 3 + src/channel/channel.gateway.ts | 69 ++++++++++++++++++ src/channel/channel.module.ts | 3 + src/channel/channel.service.ts | 65 +++++++++++++++++ src/channel/channel.ts | 92 ++++++++++++++++++++++++ src/channel/notes.gateway.ts | 27 +++++++ src/channel/whiteboard.gateway.ts | 14 ++++ src/common/constraints/exists.ts | 13 +++- src/common/constraints/match.ts | 14 ++++ src/common/constraints/unique.ts | 18 +++++ src/common/guards/admin.guard.ts | 13 ++++ src/common/not-found.interceptor.ts | 10 ++- src/main.ts | 15 +++- src/mikro-orm.config.ts | 12 ++++ src/note/note.entity.ts | 8 +++ src/note/note.module.ts | 3 + src/note/note.service.ts | 27 +++++++ src/room/room.controller.spec.ts | 9 +++ src/room/room.controller.ts | 32 +++++++++ src/room/room.dto.ts | 6 ++ src/room/room.entity.ts | 12 ++++ src/room/room.module.ts | 3 + src/room/room.service.spec.ts | 9 +++ src/room/room.service.ts | 44 ++++++++++++ src/user/user.controller.spec.ts | 14 ++++ src/user/user.controller.ts | 30 ++++++++ src/user/user.entity.ts | 14 ++++ src/user/user.module.ts | 3 + src/user/user.service.spec.ts | 13 ++++ src/user/user.service.ts | 46 ++++++++++++ 50 files changed, 1141 insertions(+), 3 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 3aafd79..76bd509 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -10,6 +10,9 @@ import { ExistsConstraint } from './common/constraints/exists'; import { ChannelModule } from './channel/channel.module'; import { BrowserModule } from './browser/browser.module'; +/** + * Main application module where other modules are imported and configured. + */ @Module({ imports: [ MikroOrmModule.forRoot(), diff --git a/src/auth/auth.controller.spec.ts b/src/auth/auth.controller.spec.ts index 27a31e6..a2234ed 100644 --- a/src/auth/auth.controller.spec.ts +++ b/src/auth/auth.controller.spec.ts @@ -1,9 +1,17 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AuthController } from './auth.controller'; +/** + * Test suite for the AuthController class. + */ describe('AuthController', () => { let controller: AuthController; + /** + * Executes before each individual test case. + * Creates a TestingModule containing the AuthController. + * Retrieves an instance of AuthController from the module. + */ beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [AuthController], @@ -12,6 +20,9 @@ describe('AuthController', () => { controller = module.get<AuthController>(AuthController); }); + /** + * Single test case: Verifies if the AuthController is defined. + */ it('should be defined', () => { expect(controller).toBeDefined(); }); diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 5de5800..e0ef93a 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -14,10 +14,20 @@ import { AuthGuard } from './auth.guard'; import { Response } from 'express'; import { RegisterUser, LoginUser } from './auth.dto'; +/** + * Controller handling authentication-related operations. + */ @Controller('auth') export class AuthController { constructor(private readonly auth: AuthService) {} + /** + * Registers a new user. + * + * @param data - Registration data for the new user. + * @param response - Express response object to set cookies. + * @returns The registration payload. + */ @HttpCode(HttpStatus.OK) @Post('register') public async register( @@ -31,6 +41,13 @@ export class AuthController { return payload; } + /** + * Logs in a user. + * + * @param data - Login credentials of the user. + * @param response - Express response object to set cookies. + * @returns The login payload. + */ @HttpCode(HttpStatus.OK) @Post('login') public async login( @@ -44,6 +61,11 @@ export class AuthController { return payload; } + /** + * Retrieves the user's profile information. + * + * @returns The user's profile and token expiration. + */ @UseGuards(AuthGuard) @Get('profile') public async profile() { @@ -53,6 +75,12 @@ export class AuthController { return { user, exp: token.exp }; } + /** + * Logs out the currently authenticated user. + * + * @param response - Express response object to clear cookies. + * @returns A success message. + */ @UseGuards(AuthGuard) @Post('logout') public async logout(@Res({ passthrough: true }) response: Response) { @@ -61,6 +89,12 @@ export class AuthController { return { message: 'success' }; } + /** + * Deletes the account of the currently authenticated user. + * + * @param response - Express response object to clear cookies. + * @returns A success message. + */ @UseGuards(AuthGuard) @Delete('delete') public async delete(@Res({ passthrough: true }) response: Response) { diff --git a/src/auth/auth.dto.ts b/src/auth/auth.dto.ts index cc46691..3505179 100644 --- a/src/auth/auth.dto.ts +++ b/src/auth/auth.dto.ts @@ -4,6 +4,9 @@ import { User } from '../user/user.entity'; import { IsUnique } from '../common/constraints/unique'; import { Match } from '../common/constraints/match'; +/** + * Data structure for registering a new user. + */ export class RegisterUser { @IsNotEmpty({ message: 'Schule / Universität oder Organisation darf nicht leer sein', @@ -43,8 +46,14 @@ export class RegisterUser { confirmPassword: string; } +/** + * Data structure for creating a new user, based on the RegisterUser class but without confirmPassword field. + */ export class CreateUser extends OmitType(RegisterUser, ['confirmPassword']) {} +/** + * Data structure for user login. + */ export class LoginUser { @IsEmail( {}, @@ -60,6 +69,9 @@ export class LoginUser { password: string; } +/** + * Data structure for changing user password. + */ export class ChangePassword { @IsNotEmpty({ message: 'Bitte gib dein aktuelles Passwort ein', @@ -83,6 +95,9 @@ export class ChangePassword { confirmNewPassword: string; } +/** + * Payload structure returned after successful authentication. + */ export interface AuthPayload { token: string; user: User; diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts index b10c95e..12c79e4 100644 --- a/src/auth/auth.guard.ts +++ b/src/auth/auth.guard.ts @@ -3,6 +3,9 @@ import { AuthGuard as PassportAuthGuard } from '@nestjs/passport'; import { AuthService } from './auth.service'; import { JwtService } from '@nestjs/jwt'; +/** + * Custom authentication guard extending PassportAuthGuard for JWT-based authentication. + */ @Injectable() export class AuthGuard extends PassportAuthGuard('jwt') { constructor( @@ -11,6 +14,14 @@ export class AuthGuard extends PassportAuthGuard('jwt') { ) { super(); } + + /** + * Determines if the request is authorized. + * Extends the default canActivate method of PassportAuthGuard. + * + * @param context - ExecutionContext containing the request and response objects. + * @returns A boolean indicating if the request is authorized. + */ async canActivate(context) { const canActivateResult = (await super.canActivate(context)) as boolean; @@ -20,6 +31,7 @@ export class AuthGuard extends PassportAuthGuard('jwt') { const payload = { sub: user.id }; const token = this.jwtService.sign(payload); + response.cookie('jwt', token, { httpOnly: true }); } diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index d85de9a..76c89df 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -5,6 +5,9 @@ import { UserModule } from '../user/user.module'; import { JwtModule } from '@nestjs/jwt'; import { JwtStrategy } from './jwt.strategy'; +/** + * Module handling authentication-related components. + */ @Global() @Module({ imports: [ diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts index 800ab66..55c24e1 100644 --- a/src/auth/auth.service.spec.ts +++ b/src/auth/auth.service.spec.ts @@ -1,9 +1,17 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AuthService } from './auth.service'; +/** + * Test suite for the AuthService class. + */ describe('AuthService', () => { let service: AuthService; + /** + * Executes before each individual test case. + * Creates a TestingModule containing the AuthService. + * Retrieves an instance of AuthService from the module. + */ beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [AuthService], @@ -12,6 +20,9 @@ describe('AuthService', () => { service = module.get<AuthService>(AuthService); }); + /** + * Single test case: Verifies if the AuthService is defined. + */ it('should be defined', () => { expect(service).toBeDefined(); }); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 0d085dd..5d9bbab 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -8,6 +8,9 @@ import { REQUEST } from '@nestjs/core'; import { JwtToken } from './jwt.strategy'; import * as bcrypt from 'bcrypt'; +/** + * Service responsible for authentication-related operations. + */ @Injectable() export class AuthService { constructor( @@ -16,6 +19,13 @@ export class AuthService { @Inject(REQUEST) private readonly request: Request, ) {} + /** + * Handles user login. + * + * @param loginUser - Login credentials of the user. + * @returns Authentication payload containing token, user, and expiration. + * @throws UnauthorizedException if credentials are invalid. + */ public async login({ email, password }: LoginUser): Promise<AuthPayload> { const user = await this.users.findByEmail(email); @@ -26,6 +36,12 @@ export class AuthService { return this.createToken(user); } + /** + * Handles user registration. + * + * @param data - Registration data for the new user. + * @returns Authentication payload containing token, user, and expiration. + */ public async register(data: CreateUser): Promise<AuthPayload> { // TODO: email verification @@ -34,10 +50,22 @@ export class AuthService { return this.createToken(user); } + /** + * Deletes a user by their ID. + * + * @param id - ID of the user to be deleted. + * @returns Resolves when deletion is successful. + */ public async delete(id: number): Promise<void> { return await this.users.delete(id); } + /** + * Creates an authentication payload (token, user, expiration). + * + * @param user - User entity for which to create the payload. + * @returns Authentication payload containing token, user, and expiration. + */ private createToken(user: User): AuthPayload { const payload = { sub: user.id }; const token = this.jwtService.sign(payload); @@ -46,10 +74,20 @@ export class AuthService { return { token, user, exp }; } + /** + * Retrieves the currently authenticated user. + * + * @returns A Promise that resolves to the authenticated user entity. + */ public async user(): Promise<User> { return await this.users.findOne(this.token().sub); } + /** + * Retrieves the JWT token from the request. + * + * @returns The JWT token. + */ public token(): JwtToken { return this.request.user as JwtToken; } diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts index 868a2d4..fbe247e 100644 --- a/src/auth/jwt.strategy.ts +++ b/src/auth/jwt.strategy.ts @@ -4,12 +4,18 @@ import { Injectable } from '@nestjs/common'; import { UserService } from '../user/user.service'; import { Request } from 'express'; +/** + * Interface representing the JWT token structure. + */ export interface JwtToken { sub: number; iat: number; exp: number; } +/** + * JWT authentication strategy using PassportStrategy. + */ @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor(private readonly userService: UserService) { @@ -20,6 +26,12 @@ export class JwtStrategy extends PassportStrategy(Strategy) { }); } + /** + * Retrieves the JWT token from the request cookie. + * + * @param req - Express request object. + * @returns The JWT token from the cookie. + */ public static fromCookie(req: Request): string { if (req && req.cookies) { return req.cookies['jwt']; @@ -28,6 +40,12 @@ export class JwtStrategy extends PassportStrategy(Strategy) { return null; } + /** + * Validates the JWT token payload. + * + * @param payload - Decoded JWT token payload. + * @returns The validated payload. + */ public async validate(payload: JwtToken) { return payload; } diff --git a/src/browser/browser.module.ts b/src/browser/browser.module.ts index 6b383e5..fabedce 100644 --- a/src/browser/browser.module.ts +++ b/src/browser/browser.module.ts @@ -1,6 +1,9 @@ import { Global, Module } from '@nestjs/common'; import { BrowserService } from './browser.service'; +/** + * Global module providing services related to browser functionality. + */ @Global() @Module({ providers: [BrowserService], diff --git a/src/browser/browser.service.ts b/src/browser/browser.service.ts index e4d8831..f341046 100644 --- a/src/browser/browser.service.ts +++ b/src/browser/browser.service.ts @@ -11,10 +11,18 @@ const LOGGER = new Logger('BrowserService'); const PATH_TO_EXTENSION = path.join(process.cwd(), 'browser-extension'); const EXTENSION_ID = 'jjndjgheafjngoipoacpjgeicjeomjli'; +/** + * Service responsible for managing browser instances and interactions. + */ @Injectable() export class BrowserService implements OnModuleDestroy { private browserContexts: Map<string, Browser> = new Map(); + /** + * Opens a new Puppeteer browser instance. + * + * @returns The Puppeteer browser instance. + */ private async openBrowser() { const args = []; @@ -37,6 +45,13 @@ export class BrowserService implements OnModuleDestroy { }); } + /** + * Opens a website in a browser context. + * + * @param channelId - Identifier for the channel. + * @param url - URL of the website to open. + * @returns Peer ID associated with the browser context. + */ public async openWebsite( channelId: string, url: string, @@ -62,14 +77,32 @@ export class BrowserService implements OnModuleDestroy { return browser.peerId; } + /** + * Retrieves the Peer ID associated with a channel. + * + * @param channelId - Identifier for the channel. + * @returns The Peer ID associated with the browser context. + */ public getPeerId(channelId: string): string { return this.browserContexts.get(channelId)?.peerId; } + /** + * Retrieve the browser context associated with a specific channel. + * @param channel The channel for which to retrieve the browser context. + * @returns The browser context associated with the channel, or null if not found. + */ public getFromChannel(channel: Channel): Browser | null { return this.browserContexts.get(channel.id) ?? null; } + /** + * Moves the mouse cursor to a specific position in a browser context. + * + * @param channelId - Identifier for the channel. + * @param x - The X-coordinate of the mouse cursor. + * @param y - The Y-coordinate of the mouse cursor. + */ public async moveMouse( channelId: string, x: number, @@ -78,49 +111,103 @@ export class BrowserService implements OnModuleDestroy { await this.browserContexts.get(channelId)?.moveMouse(x, y); } + /** + * Initiates a mouse down event in a browser context. + * + * @param channelId - Identifier for the channel. + */ public async mouseDown(channelId: string): Promise<void> { await this.browserContexts.get(channelId)?.mouseDown(); } + /** + * Initiates a mouse up event in a browser context. + * + * @param channelId - Identifier for the channel. + */ public async mouseUp(channelId: string): Promise<void> { await this.browserContexts.get(channelId)?.mouseUp(); } + /** + * Initiates a key down event in a browser context. + * + * @param channelId - Identifier for the channel. + * @param key - The key to be pressed. + */ public async keyDown(channelId: string, key: string): Promise<void> { await this.browserContexts.get(channelId)?.keyDown(key); } + /** + * Initiates a key up event in a browser context. + * + * @param channelId - Identifier for the channel. + * @param key - The key to be released. + */ public async keyUp(channelId: string, key: string): Promise<void> { await this.browserContexts.get(channelId)?.keyUp(key); } + /** + * Initiates a scroll event in a browser context. + * + * @param channelId - Identifier for the channel. + * @param deltaY - The amount to scroll along the Y-axis. + */ public async scroll(channelId: string, deltaY: number): Promise<void> { await this.browserContexts.get(channelId)?.scroll(deltaY); } + /** + * Reloads the current page in a browser context. + * + * @param channelId - Identifier for the channel. + */ public async reload(channelId: string): Promise<void> { await this.browserContexts.get(channelId)?.reload(); } + /** + * Navigates back to the previous page in a browser context. + * + * @param channelId - Identifier for the channel. + */ public async navigateBack(channelId: string): Promise<void> { await this.browserContexts.get(channelId)?.navigateBack(); } + /** + * Navigates forward to the next page in a browser context. + * + * @param channelId - Identifier for the channel. + */ public async navigateForward(channelId: string): Promise<void> { await this.browserContexts.get(channelId)?.navigateForward(); } + /** + * Closes a specific browser context. + * + * @param channelId - Identifier for the channel. + */ public async closeBrowserContext(channelId: string): Promise<void> { await this.browserContexts.get(channelId)?.close(); this.browserContexts.delete(channelId); } + /** + * Closes the service, disposing of browser instances and contexts. + */ public async close(): Promise<void> { for (const browser of this.browserContexts.values()) { await browser.close(); } } + /** + * Handles cleanup on module destruction. + */ public async onModuleDestroy(): Promise<void> { await this.close(); } diff --git a/src/browser/browser.ts b/src/browser/browser.ts index 10c7fa0..05a9702 100644 --- a/src/browser/browser.ts +++ b/src/browser/browser.ts @@ -9,10 +9,19 @@ const LOGGER = new Logger('Browser'); const PATH_TO_EXTENSION = path.join(process.cwd(), 'browser-extension'); const EXTENSION_ID = 'jjndjgheafjngoipoacpjgeicjeomjli'; +/** + * Represents a Puppeteer-based browser instance. + */ export class Browser { public peerId: string; private page: Page; + /** + * Creates an instance of the Browser class. + * + * @param browser - The Puppeteer browser instance. + * @param url - The initial URL to open. + */ constructor( private browser: PuppeteerBrowser, public url: string, @@ -20,6 +29,11 @@ export class Browser { private readonly channelId: string, ) {} + /** + * Opens a new page in the browser instance and initializes mouse tracking. + * + * @returns The ID associated with the started recording. + */ public async open(): Promise<string> { LOGGER.debug(`Opening page with url: ${this.url}`); this.page = await this.browser.newPage(); @@ -63,46 +77,90 @@ export class Browser { return id; } + /** + * Opens a specific website in the current page. + * + * @param url - The URL of the website to open. + */ public async openWebsite(url: string): Promise<void> { await this.page.goto(url); } + /** + * Moves the mouse cursor to a specific position. + * + * @param x - The X-coordinate of the mouse cursor. + * @param y - The Y-coordinate of the mouse cursor. + */ public async moveMouse(x: number, y: number): Promise<void> { await this.page.mouse.move(x, y); } + /** + * Initiates a mouse down event. + */ public async mouseDown(): Promise<void> { await this.page.mouse.down(); } + /** + * Initiates a mouse up event. + */ public async mouseUp(): Promise<void> { await this.page.mouse.up(); } + /** + * Initiates a key down event. + * + * @param key - The key to be pressed. + */ public async keyDown(key: string): Promise<void> { await this.page.keyboard.down(key as KeyInput); } + /** + * Initiates a key up event. + * + * @param key - The key to be released. + */ public async keyUp(key: string): Promise<void> { await this.page.keyboard.up(key as KeyInput); } + /** + * Initiates a scroll event. + * + * @param deltaY - The amount to scroll along the Y-axis. + */ public async scroll(deltaY: number): Promise<void> { await this.page.mouse.wheel({ deltaY }); } + /** + * Reloads the current page. + */ public async reload(): Promise<void> { await this.page.reload(); } + /** + * Navigates back to the previous page. + */ public async navigateBack(): Promise<void> { await this.page.goBack(); } + /** + * Navigates forward to the next page. + */ public async navigateForward(): Promise<void> { await this.page.goForward(); } + /** + * Closes the browser instance. + */ public async close(): Promise<void> { if (this.browser) { LOGGER.debug('Closing browser'); @@ -111,6 +169,11 @@ export class Browser { } } +/** + * Helper function to install mouse tracking for puppeteer pages. + * + * @param page - The puppeteer page to install the mouse tracking on. + */ async function installMouseHelper(page) { await page.evaluateOnNewDocument(() => { // Install mouse helper only for top-level frame. diff --git a/src/category/category.controller.spec.ts b/src/category/category.controller.spec.ts index bc8ce79..9eff6dc 100644 --- a/src/category/category.controller.spec.ts +++ b/src/category/category.controller.spec.ts @@ -12,6 +12,10 @@ describe('CategoryController', () => { controller = module.get<CategoryController>(CategoryController); }); + /** + * Test case to ensure that the CategoryController is defined. + * It checks whether the controller instance is created successfully. + */ it('should be defined', () => { expect(controller).toBeDefined(); }); diff --git a/src/category/category.controller.ts b/src/category/category.controller.ts index 4022f55..c116a54 100644 --- a/src/category/category.controller.ts +++ b/src/category/category.controller.ts @@ -13,6 +13,10 @@ import { CreateCategory, UpdateCategory } from './category.dto'; import { AuthGuard } from '../auth/auth.guard'; import { AuthService } from '../auth/auth.service'; +/** + * Controller responsible for handling category-related operations. + * This controller is protected by an authentication guard. + */ @Controller('category') @UseGuards(AuthGuard) export class CategoryController { @@ -21,24 +25,48 @@ export class CategoryController { private readonly auth: AuthService, ) {} + /** + * Retrieves all categories associated with the authenticated user. + * + * @returns An array of categories. + */ @Get('/') public async index() { const user = await this.auth.user(); return this.categories.allFromUser(user); } + /** + * Creates a new category for the authenticated user. + * + * @param data - The category data to be created. + * @returns The created category. + */ @Post('/') public async create(@Body() data: CreateCategory) { const user = await this.auth.user(); return this.categories.create(data.name, user); } + /** + * Updates an existing category for the authenticated user. + * + * @param id - The ID of the category to be updated. + * @param data - The updated category data. + * @returns The updated category. + */ @Put('/:id') public async update(@Param('id') id: number, @Body() data: UpdateCategory) { const user = await this.auth.user(); return this.categories.update(id, user, data.name); } + /** + * Deletes a category for the authenticated user. + * + * @param id - The ID of the category to be deleted. + * @returns A message indicating the deletion status. + */ @Delete('/:id') public async delete(@Param('id') id: number) { const user = await this.auth.user(); diff --git a/src/category/category.dto.ts b/src/category/category.dto.ts index 1d1bc23..e2ff905 100644 --- a/src/category/category.dto.ts +++ b/src/category/category.dto.ts @@ -1,10 +1,20 @@ import { IsNotEmpty } from 'class-validator'; +/** + * Data transfer object (DTO) for creating a new category. + */ export class CreateCategory { + /** + * The name of the category to be created. + */ @IsNotEmpty({ message: 'Name darf nicht leer sein', }) name: string; } +/** + * Data transfer object (DTO) for updating an existing category. + * Inherits properties from the CreateCategory DTO. + */ export class UpdateCategory extends CreateCategory {} diff --git a/src/category/category.entity.ts b/src/category/category.entity.ts index 98f60b7..273dacc 100644 --- a/src/category/category.entity.ts +++ b/src/category/category.entity.ts @@ -9,26 +9,56 @@ import { import { User } from '../user/user.entity'; import { Room } from '../room/room.entity'; +/** + * Entity representing a category for rooms. + */ @Entity({ tableName: 'categories' }) export class Category { + /** + * The primary key ID of the category. + */ @PrimaryKey() id: number; + /** + * The name of the category. + */ @Property() name: string; + /** + * The owner of the category. + * Represents a many-to-one relationship with the User entity. + */ @ManyToOne({ hidden: true, onDelete: 'cascade' }) owner: User; + /** + * Collection of rooms associated with this category. + * Represents a one-to-many relationship with the Room entity. + */ @OneToMany(() => Room, (room) => room.category) rooms = new Collection<Room>(this); + /** + * The creation timestamp of the category. + */ @Property() createdAt: Date = new Date(); + /** + * The update timestamp of the category. + * Automatically updated when the category is modified. + */ @Property({ onUpdate: () => new Date() }) updatedAt: Date = new Date(); + /** + * Initializes a new Category instance. + * + * @param name - The name of the category. + * @param owner - The owner of the category. + */ constructor(name: string, owner: User) { this.name = name; this.owner = owner; diff --git a/src/category/category.module.ts b/src/category/category.module.ts index b1b4abf..171139c 100644 --- a/src/category/category.module.ts +++ b/src/category/category.module.ts @@ -5,6 +5,10 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; import { Category } from './category.entity'; import { ChannelModule } from '../channel/channel.module'; +/** + * Global module for handling category-related functionality. + * Provides services, controllers, and exports related to categories. + */ @Global() @Module({ imports: [MikroOrmModule.forFeature([Category]), ChannelModule], diff --git a/src/category/category.service.spec.ts b/src/category/category.service.spec.ts index 7afa05f..f18af0c 100644 --- a/src/category/category.service.spec.ts +++ b/src/category/category.service.spec.ts @@ -1,9 +1,16 @@ import { Test, TestingModule } from '@nestjs/testing'; import { CategoryService } from './category.service'; +/** + * Test suite for the CategoryService class. + */ describe('CategoryService', () => { let service: CategoryService; + /** + * Setup before each test case by creating a testing module and + * obtaining an instance of the CategoryService. + */ beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [CategoryService], @@ -12,6 +19,9 @@ describe('CategoryService', () => { service = module.get<CategoryService>(CategoryService); }); + /** + * Test case to check if the CategoryService instance is defined. + */ it('should be defined', () => { expect(service).toBeDefined(); }); diff --git a/src/category/category.service.ts b/src/category/category.service.ts index 96e8a8b..169feb0 100644 --- a/src/category/category.service.ts +++ b/src/category/category.service.ts @@ -6,8 +6,18 @@ import { EntityRepository } from '@mikro-orm/mysql'; import { User } from '../user/user.entity'; import { ChannelService } from '../channel/channel.service'; +/** + * Service class responsible for handling category-related operations. + */ @Injectable() export class CategoryService { + /** + * Initializes an instance of the CategoryService. + * + * @param em - The EntityManager instance. + * @param repository - The EntityRepository for the Category entity. + * @param channels - The ChannelService instance. + */ constructor( private readonly em: EntityManager, @InjectRepository(Category) @@ -15,6 +25,12 @@ export class CategoryService { private readonly channels: ChannelService, ) {} + /** + * Retrieves all categories associated with a user, including their rooms. + * + * @param user - The user whose categories to retrieve. + * @returns A promise that resolves to an array of Category instances. + */ public async allFromUser(user: User): Promise<Category[]> { const categories = await user.categories.loadItems({ populate: ['rooms'], @@ -32,10 +48,24 @@ export class CategoryService { return categories; } + /** + * Retrieves a category by its ID and owner. + * + * @param id - The ID of the category to retrieve. + * @param owner - The owner of the category. + * @returns A promise that resolves to the retrieved Category instance. + */ public async get(id: number, owner: User): Promise<Category> { return this.repository.findOneOrFail({ id, owner }); } + /** + * Creates a new category. + * + * @param name - The name of the new category. + * @param owner - The owner of the new category. + * @returns A promise that resolves to the created Category instance. + */ public async create(name: string, owner: User): Promise<Category> { const category = new Category(name, owner); @@ -43,6 +73,14 @@ export class CategoryService { return category; } + /** + * Updates the name of a category. + * + * @param id - The ID of the category to update. + * @param owner - The owner of the category. + * @param name - The new name for the category. + * @returns A promise that resolves to the updated Category instance. + */ public async update( id: number, owner: User, @@ -57,6 +95,12 @@ export class CategoryService { return category; } + /** + * Deletes a category. + * + * @param id - The ID of the category to delete. + * @param owner - The owner of the category. + */ public async delete(id: number, owner: User): Promise<void> { const category = await this.get(id, owner); diff --git a/src/channel/browser.gateway.ts b/src/channel/browser.gateway.ts index 33d34b8..61a0ee1 100644 --- a/src/channel/browser.gateway.ts +++ b/src/channel/browser.gateway.ts @@ -19,17 +19,34 @@ const WEB_SOCKET_OPTIONS = ? {} : { cors: { origin: process.env.FRONTEND_URL } }; +/** + * WebSocket gateway for handling browser-related operations. + */ @WebSocketGateway(WEB_SOCKET_OPTIONS) export class BrowserGateway { @WebSocketServer() public server: Server; + /** + * Initializes an instance of the BrowserGateway. + * + * @param orm - The MikroORM instance. + * @param channels - The ChannelService instance. + * @param browserService - The BrowserService instance. + */ constructor( private orm: MikroORM, private channels: ChannelService, private browserService: BrowserService, ) {} + /** + * Handles the "open-website" event, allowing a client to open a website in a browser. + * + * @param client - The connected socket client. + * @param payload - The message payload containing the URL to open. + * @returns A boolean indicating the success of opening the website. + */ @SubscribeMessage('open-website') @UseRequestContext() public async openWebsite( @@ -48,6 +65,13 @@ export class BrowserGateway { return true; } + /** + * Handles the "move-mouse" event, allowing a client to move the mouse in the browser. + * + * @param client - The connected socket client. + * @param payload - The message payload containing the coordinates to move the mouse to. + * @returns A boolean indicating the success of moving the mouse. + */ @SubscribeMessage('move-mouse') @UseRequestContext() public async moveMouse( @@ -61,6 +85,12 @@ export class BrowserGateway { return true; } + /** + * Handles the "mouse-down" event, allowing a client to simulate a mouse button press. + * + * @param client - The connected socket client. + * @returns A boolean indicating the success of simulating a mouse button press. + */ @SubscribeMessage('mouse-down') @UseRequestContext() public async mouseDown(@ConnectedSocket() client: Socket) { @@ -71,6 +101,12 @@ export class BrowserGateway { return true; } + /** + * Handles the "mouse-up" event, allowing a client to simulate a mouse button release. + * + * @param client - The connected socket client. + * @returns A boolean indicating the success of simulating a mouse button release. + */ @SubscribeMessage('mouse-up') @UseRequestContext() public async mouseUp(@ConnectedSocket() client: Socket) { @@ -81,6 +117,13 @@ export class BrowserGateway { return true; } + /** + * Handles the "key-down" event, allowing a client to simulate a keyboard key press. + * + * @param client - The connected socket client. + * @param payload - The message payload containing the key to press. + * @returns A boolean indicating the success of simulating a keyboard key press. + */ @SubscribeMessage('key-down') @UseRequestContext() public async keyDown( @@ -94,6 +137,13 @@ export class BrowserGateway { return true; } + /** + * Handles the "key-up" event, allowing a client to simulate releasing a keyboard key. + * + * @param client - The connected socket client. + * @param payload - The message payload containing the key to release. + * @returns A boolean indicating the success of simulating a keyboard key release. + */ @SubscribeMessage('key-up') @UseRequestContext() public async keyUp( @@ -107,6 +157,13 @@ export class BrowserGateway { return true; } + /** + * Handles the "scroll" event, allowing a client to simulate scrolling in the browser. + * + * @param client - The connected socket client. + * @param payload - The message payload containing the deltaY value for scrolling. + * @returns A boolean indicating the success of simulating scrolling in the browser. + */ @SubscribeMessage('scroll') @UseRequestContext() public async scroll( @@ -120,6 +177,12 @@ export class BrowserGateway { return true; } + /** + * Handles the "reload" event, allowing a client to simulate reloading the browser page. + * + * @param client - The connected socket client. + * @returns A boolean indicating the success of simulating a browser page reload. + */ @SubscribeMessage('reload') public async reload(@ConnectedSocket() client: Socket) { const channel = await this.channels.fromClientOrFail(client); @@ -129,6 +192,12 @@ export class BrowserGateway { return true; } + /** + * Handles the "navigate-back" event, allowing a client to simulate navigating back in the browser. + * + * @param client - The connected socket client. + * @returns A boolean indicating the success of simulating navigating back in the browser. + */ @SubscribeMessage('navigate-back') public async navigateBack(@ConnectedSocket() client: Socket) { const channel = await this.channels.fromClientOrFail(client); @@ -138,6 +207,12 @@ export class BrowserGateway { return true; } + /** + * Handles the "navigate-forward" event, allowing a client to simulate navigating forward in the browser. + * + * @param client - The connected socket client. + * @returns A boolean indicating the success of simulating navigating forward in the browser. + */ @SubscribeMessage('navigate-forward') public async navigateForward(@ConnectedSocket() client: Socket) { const channel = await this.channels.fromClientOrFail(client); diff --git a/src/channel/channel.gateway.spec.ts b/src/channel/channel.gateway.spec.ts index 121aa07..8b5b7b8 100644 --- a/src/channel/channel.gateway.spec.ts +++ b/src/channel/channel.gateway.spec.ts @@ -12,6 +12,9 @@ describe('SocketGateway', () => { gateway = module.get<ChannelGateway>(ChannelGateway); }); + /** + * Test to check if the ChannelGateway instance is defined. + */ it('should be defined', () => { expect(gateway).toBeDefined(); }); diff --git a/src/channel/channel.gateway.ts b/src/channel/channel.gateway.ts index e7c09e0..3f24eda 100644 --- a/src/channel/channel.gateway.ts +++ b/src/channel/channel.gateway.ts @@ -21,6 +21,9 @@ const WEB_SOCKET_OPTIONS = ? {} : { cors: { origin: process.env.FRONTEND_URL } }; +/** + * WebSocket gateway for handling real-time communication related to channels. + */ @WebSocketGateway({ ...WEB_SOCKET_OPTIONS, maxHttpBufferSize: 1e8, @@ -36,6 +39,13 @@ export class ChannelGateway implements OnGatewayConnection { ) {} // todo: add authentication - only a logged in teacher should be able to open a room + /** + * Handle an 'open-room' event to open a room. + * + * @param client The connected socket client. + * @param payload The payload containing userId and roomId. + * @returns The channel state. + */ @SubscribeMessage('open-room') @UseRequestContext() public async openRoom( @@ -52,6 +62,13 @@ export class ChannelGateway implements OnGatewayConnection { return this.channelState(channel); } + /** + * Handle a 'join-room-as-student' event to join a room as a student. + * + * @param client The connected socket client. + * @param payload The payload containing name, channelId, and password (optional). + * @returns The channel state or an error object. + */ @SubscribeMessage('join-room-as-student') @UseRequestContext() public async joinChannelAsStudent( @@ -82,6 +99,13 @@ export class ChannelGateway implements OnGatewayConnection { return this.channelState(channel); } + /** + * Handle a 'join-room-as-teacher' event to join a room as a teacher. + * + * @param client The connected socket client. + * @param payload The payload containing channelId and userId. + * @returns The channel state. + */ @SubscribeMessage('join-room-as-teacher') @UseRequestContext() public async joinChannelAsTeacher( @@ -139,6 +163,11 @@ export class ChannelGateway implements OnGatewayConnection { }; } + /** + * Handle a 'leave-room' event to allow a user to leave a room. + * + * @param client The connected socket client. + */ @SubscribeMessage('leave-room') @UseRequestContext() public async leaveRoom(@ConnectedSocket() client: Socket) { @@ -150,6 +179,13 @@ export class ChannelGateway implements OnGatewayConnection { } } + /** + * Handle a 'change-name' event to change a user's name. + * + * @param client The connected socket client. + * @param payload The payload containing the new name. + * @returns A boolean indicating success. + */ @SubscribeMessage('change-name') @UseRequestContext() public async changeName( @@ -167,6 +203,13 @@ export class ChannelGateway implements OnGatewayConnection { return true; } + /** + * Handle a 'connect-webcam' event to add a webcam connection. + * + * @param client The connected socket client. + * @param payload The payload containing userId and peerId. + * @returns A boolean indicating success. + */ @SubscribeMessage('connect-webcam') @UseRequestContext() public async addWebcam( @@ -186,6 +229,13 @@ export class ChannelGateway implements OnGatewayConnection { return true; } + /** + * Handle an 'update-webcam' event to update webcam settings. + * + * @param client The connected socket client. + * @param payload The payload containing video and audio settings. + * @returns A boolean indicating success. + */ @SubscribeMessage('update-webcam') @UseRequestContext() public async updateWebcam( @@ -204,6 +254,13 @@ export class ChannelGateway implements OnGatewayConnection { return true; } + /** + * Handle an 'update-handSignal' event to update hand signal. + * + * @param client The connected socket client. + * @param payload The payload containing hand signal setting. + * @returns A boolean indicating success. + */ @SubscribeMessage('update-handSignal') @UseRequestContext() public async updateHandSignal( @@ -221,6 +278,13 @@ export class ChannelGateway implements OnGatewayConnection { return true; } + /** + * Handle an 'update-permission' event to update student permission. + * + * @param client The connected socket client. + * @param payload The payload containing studentId and permission setting. + * @returns A boolean indicating success. + */ @SubscribeMessage('update-permission') @UseRequestContext() public async updatePermission( @@ -238,6 +302,11 @@ export class ChannelGateway implements OnGatewayConnection { return true; } + /** + * Handle the connection event. + * + * @param client The connected socket client. + */ public async handleConnection(client: Socket) { /* * This is a workaround for getting the client's rooms in the disconnecting event. diff --git a/src/channel/channel.module.ts b/src/channel/channel.module.ts index 1c171e5..0bb409e 100644 --- a/src/channel/channel.module.ts +++ b/src/channel/channel.module.ts @@ -6,6 +6,9 @@ import { NotesGateway } from './notes.gateway'; import { NoteModule } from '../note/note.module'; import { WhiteboardGateway } from './whiteboard.gateway'; +/** + * Module for handling real-time communication and operations related to channels. + */ @Module({ imports: [NoteModule], providers: [ diff --git a/src/channel/channel.service.ts b/src/channel/channel.service.ts index 824b640..ed212a4 100644 --- a/src/channel/channel.service.ts +++ b/src/channel/channel.service.ts @@ -7,6 +7,9 @@ import { RoomService } from '../room/room.service'; import { Room } from '../room/room.entity'; import { BrowserService } from '../browser/browser.service'; +/** + * Service for managing channels and real-time communication. + */ @Injectable() export class ChannelService { private readonly logger = new Logger(ChannelService.name); @@ -18,6 +21,16 @@ export class ChannelService { private readonly browsers: BrowserService, ) {} + /** + * Opens a new channel for a teacher in a specific room. + * + * @param client - The socket client of the teacher. + * @param server - The socket server. + * @param userId - The ID of the teacher user. + * @param roomId - The ID of the room to open. + * @returns The created channel. + * @throws WsException if the user or room is not found. + */ public async open( client: Socket, server: Server, @@ -50,10 +63,26 @@ export class ChannelService { return channel; } + /** + * Checks if a channel with the given channelId exists. + * + * @param channelId - The ID of the channel to check. + * @returns true if the channel exists, false otherwise. + */ public exists(channelId: string): boolean { return !!this.channels[channelId]; } + /** + * Allows a student to join a channel. + * + * @param client - The socket client of the student. + * @param channelId - The ID of the channel to join. + * @param name - The name of the student. + * @param password - The password for the channel (if applicable). + * @returns The joined channel. + * @throws WsException if the channel is not found or password is incorrect. + */ public async joinAsStudent( client: Socket, channelId: string, @@ -78,6 +107,15 @@ export class ChannelService { return channel; } + /** + * Allows a teacher to join a channel. + * + * @param client - The socket client of the teacher. + * @param channelId - The ID of the channel to join. + * @param userId - The ID of the teacher user. + * @returns The joined channel. + * @throws WsException if the channel is not found or user is not found. + */ public async joinAsTeacher( client: Socket, channelId: string, @@ -103,6 +141,12 @@ export class ChannelService { return channel; } + /** + * Handles the process of a client leaving a channel. + * + * @param client - The socket client leaving the channel. + * @param channelId - The ID of the channel to leave. + */ public async leave(client: Socket, channelId: string) { const channel = this.channels[channelId]; @@ -132,6 +176,13 @@ export class ChannelService { } } + /** + * Gets a channel associated with the given socket client. + * + * @param client - The socket client. + * @returns The associated channel. + * @throws WsException if the channel is not found. + */ public fromClientOrFail(client: Socket): Channel { for (const room of client.rooms) { if (this.channels[room]) { @@ -142,6 +193,14 @@ export class ChannelService { throw new WsException('Channel not found'); } + /** + * Retrieves the other client's socket associated with the given client and ID. + * + * @param client - The socket client. + * @param otherId - The ID of the other client. + * @returns The other client's socket. + * @throws WsException if the channel is not found. + */ public async getOtherClient( client: Socket, otherId: string, @@ -151,6 +210,12 @@ export class ChannelService { return channel.getUser(otherId).client; } + /** + * Retrieves the channel associated with the given room. + * + * @param room - The room entity. + * @returns The associated channel. + */ public getChannelFromRoom(room: Room): Channel { for (const channel of Object.values(this.channels)) { if (channel.room.id === room.id) { diff --git a/src/channel/channel.ts b/src/channel/channel.ts index 783c8d4..f770dae 100644 --- a/src/channel/channel.ts +++ b/src/channel/channel.ts @@ -19,6 +19,9 @@ export interface Student extends ChannelUser { permission: boolean; } +/** + * Represents a channel for a room in the application. + */ export class Channel { public teacher?: Teacher; private closeTimeout: NodeJS.Timeout; @@ -27,6 +30,13 @@ export class Channel { public canvasJSON: string; + /** + * Creates a new Channel instance. + * + * @param room - The room associated with the channel. + * @param server - The socket server instance. + * @param id - The ID of the channel. + */ constructor( public readonly room: Room, public readonly server: Server, @@ -35,6 +45,12 @@ export class Channel { this.canvasJSON = room.whiteboardCanvas?.toString(); } + /** + * Joins a client as a student to the channel. + * + * @param client - The client socket. + * @param name - The name of the student. + */ public async joinAsStudent(client: Socket, name: string) { await client.join(this.id); @@ -57,6 +73,12 @@ export class Channel { }); } + /** + * Joins a client as a teacher to the channel. + * + * @param client - The client socket. + * @param user - The teacher's user object. + */ public async joinAsTeacher(client: Socket, user: User) { if (this.teacher) { await this.leaveAsTeacher(this.teacher.client); @@ -73,6 +95,11 @@ export class Channel { }); } + /** + * Removes the teacher from the channel. + * + * @param client - The client socket of the teacher. + */ public async leaveAsTeacher(client: Socket) { await client.leave(this.id); this.teacher = undefined; @@ -80,6 +107,11 @@ export class Channel { client.broadcast.to(this.id).emit('teacher-left', {}); } + /** + * Removes a student from the channel. + * + * @param client - The client socket of the student. + */ public async leaveAsStudent(client: Socket) { const student = this.students.get(client.id); @@ -92,14 +124,29 @@ export class Channel { } } + /** + * Checks if the channel is empty (no teacher or students). + * + * @returns `true` if the channel is empty, `false` otherwise. + */ public isEmpty(): boolean { return !this.teacher && this.students.size === 0; } + /** + * Notifies the server that the room is closing. + */ public close() { this.server.emit('room-closed', this.room.id); } + /** + * Gets the user (teacher or student) associated with the given client ID. + * + * @param clientId - The ID of the client. + * @returns The user object (teacher or student). + * @throws `WsException` if the user is not found. + */ public getUser(clientId: string): Teacher | Student { if (this.teacher && this.teacher.client.id === clientId) { return this.teacher; @@ -114,6 +161,13 @@ export class Channel { throw new WsException(`User not found in ${this}`); } + /** + * Gets the student associated with the given client ID. + * + * @param clientId - The ID of the client. + * @returns The student object. + * @throws `WsException` if the student is not found. + */ public getStudent(clientId: string): Student { const student = this.students.get(clientId); @@ -124,6 +178,12 @@ export class Channel { throw new WsException(`User not found in ${this}`); } + /** + * Changes the name of a student. + * + * @param client - The client socket of the student. + * @param name - The new name. + */ public changeName(client: Socket, name: string) { const student = this.students.get(client.id); @@ -132,6 +192,13 @@ export class Channel { } } + /** + * Updates the webcam settings of a user (teacher or student). + * + * @param client - The client socket of the user. + * @param video - The new video setting. + * @param audio - The new audio setting. + */ public updateWebcam(client: Socket, video: boolean, audio: boolean) { const user = this.getUser(client.id); @@ -141,6 +208,12 @@ export class Channel { } } + /** + * Updates the hand signal setting of a student. + * + * @param client - The client socket of the student. + * @param handSignal - The new hand signal setting. + */ public updateHandSignal(client: Socket, handSignal: boolean) { const student = this.getStudent(client.id); @@ -149,6 +222,12 @@ export class Channel { } } + /** + * Updates the permission setting of a student. + * + * @param studentId - The ID of the student. + * @param permission - The new permission setting. + */ public updatePermission(studentId: string, permission: boolean) { const student = this.getStudent(studentId); @@ -157,10 +236,18 @@ export class Channel { } } + /** + * Returns a string representation of the channel. + * + * @returns The string representation. + */ public toString(): string { return `Channel{${this.id}}`; } + /** + * Clears the close timeout if set. + */ public clearCloseTimeout() { if (this.closeTimeout) { clearTimeout(this.closeTimeout); @@ -168,6 +255,11 @@ export class Channel { } } + /** + * Sets a timeout to close the channel if it becomes empty after a period of time. + * + * @param onTimeout - The callback function to be executed when the timeout triggers. + */ public setCloseTimeout(onTimeout: () => void) { this.clearCloseTimeout(); this.closeTimeout = setTimeout(() => { diff --git a/src/channel/notes.gateway.ts b/src/channel/notes.gateway.ts index 0833a3a..e604429 100644 --- a/src/channel/notes.gateway.ts +++ b/src/channel/notes.gateway.ts @@ -18,17 +18,32 @@ const WEB_SOCKET_OPTIONS = ? {} : { cors: { origin: process.env.FRONTEND_URL } }; +/** + * WebSocket gateway for managing notes-related communication. + */ @WebSocketGateway(WEB_SOCKET_OPTIONS) export class NotesGateway { @WebSocketServer() public server: Server; + /** + * Constructor of NotesGateway. + * @param orm - MikroORM instance for database interactions. + * @param channels - Instance of ChannelService for managing channels. + * @param notes - Instance of NoteService for managing notes. + */ constructor( private orm: MikroORM, private channels: ChannelService, private notes: NoteService, ) {} + /** + * Subscribe to the 'add-note' event to add a new note. + * @param client - The connected socket client. + * @param payload - The message body containing the note's name. + * @returns The newly added note. + */ @SubscribeMessage('add-note') @UseRequestContext() public async addNote( @@ -43,6 +58,12 @@ export class NotesGateway { return note; } + /** + * Subscribe to the 'update-note' event to update an existing note. + * @param client - The connected socket client. + * @param payload - The message body containing the note's ID and updated content. + * @returns `true` if the note was successfully updated. + */ @SubscribeMessage('update-note') @UseRequestContext() public async updateNote( @@ -60,6 +81,12 @@ export class NotesGateway { return true; } + /** + * Subscribe to the 'delete-note' event to delete a note. + * @param client - The connected socket client. + * @param payload - The message body containing the note's ID. + * @returns `true` if the note was successfully deleted. + */ @SubscribeMessage('delete-note') @UseRequestContext() public async deleteNote( diff --git a/src/channel/whiteboard.gateway.ts b/src/channel/whiteboard.gateway.ts index cf17b98..ed324d4 100644 --- a/src/channel/whiteboard.gateway.ts +++ b/src/channel/whiteboard.gateway.ts @@ -16,13 +16,25 @@ const WEB_SOCKET_OPTIONS = ? {} : { cors: { origin: process.env.FRONTEND_URL } }; +/** + * WebSocket gateway for handling whiteboard-related communication. + */ @WebSocketGateway(WEB_SOCKET_OPTIONS) export class WhiteboardGateway { @WebSocketServer() public server: Server; + /** + * Constructor of WhiteboardGateway. + * @param channels - Instance of ChannelService for managing channels. + */ constructor(private channels: ChannelService) {} + /** + * Subscribe to the 'whiteboard-change' event to handle whiteboard canvas changes. + * @param client - The connected socket client. + * @param payload - The message body containing the updated canvas JSON. + */ @SubscribeMessage('whiteboard-change') public async whiteboardChange( @ConnectedSocket() client: Socket, @@ -30,8 +42,10 @@ export class WhiteboardGateway { ) { const channel = await this.channels.fromClientOrFail(client); + // Update the canvas JSON in the channel's data channel.canvasJSON = payload.canvas; + // Broadcast the whiteboard change to all clients in the channel client.broadcast.to(channel.id).emit('whiteboard-change', payload); } } diff --git a/src/common/constraints/exists.ts b/src/common/constraints/exists.ts index 7ff4f64..8b1e7ce 100644 --- a/src/common/constraints/exists.ts +++ b/src/common/constraints/exists.ts @@ -8,6 +8,11 @@ import { import { Injectable } from '@nestjs/common'; import { EntityManager } from '@mikro-orm/core'; +/** + * Custom validation decorator that checks if an entity with the given ID exists in the database. + * @param entityType - The entity type to check existence for. + * @param options - Validation options. + */ export function Exists(entityType: object, options?: ValidationOptions) { return (object: any, propertyName: string) => { registerDecorator({ @@ -20,7 +25,7 @@ export function Exists(entityType: object, options?: ValidationOptions) { }; } -/* +/** * This constraint checks if an entity with the given id exists. * * The given id must be a string, if it is a number, 0 will be considered as an empty value. @@ -30,6 +35,12 @@ export function Exists(entityType: object, options?: ValidationOptions) { export class ExistsConstraint implements ValidatorConstraintInterface { constructor(private readonly em: EntityManager) {} + /** + * Validate if an entity with the given ID exists in the database. + * @param value - The ID of the entity to check. + * @param args - Validation arguments. + * @returns A boolean indicating whether the entity exists. + */ async validate(value: string, args: ValidationArguments): Promise<boolean> { if (!value) { // if the value is empty, we don't want to diff --git a/src/common/constraints/match.ts b/src/common/constraints/match.ts index d12afd6..7fef4c2 100644 --- a/src/common/constraints/match.ts +++ b/src/common/constraints/match.ts @@ -6,6 +6,11 @@ import { ValidatorConstraintInterface, } from 'class-validator'; +/** + * Custom validation decorator that checks if a property matches another property in the object. + * @param property - The name of the related property to compare with. + * @param options - Validation options. + */ export function Match(property: string, options?: ValidationOptions) { return (object: any, propertyName: string) => { registerDecorator({ @@ -18,8 +23,17 @@ export function Match(property: string, options?: ValidationOptions) { }; } +/** + * ValidatorConstraint that checks if a property matches another property in the object. + */ @ValidatorConstraint({ name: 'Match' }) export class MatchConstraint implements ValidatorConstraintInterface { + /** + * Validate if a property matches another property in the object. + * @param value - The value of the property to validate. + * @param args - Validation arguments. + * @returns A boolean indicating whether the properties match. + */ validate(value: any, args: ValidationArguments) { const [relatedPropertyName] = args.constraints; const relatedValue = (args.object as any)[relatedPropertyName]; diff --git a/src/common/constraints/unique.ts b/src/common/constraints/unique.ts index 967c76c..e15dc7a 100644 --- a/src/common/constraints/unique.ts +++ b/src/common/constraints/unique.ts @@ -8,6 +8,11 @@ import { import { Injectable } from '@nestjs/common'; import { EntityManager } from '@mikro-orm/core'; +/** + * Custom validation decorator that checks if a property value is unique within the specified entity. + * @param entityType - The entity type to check for uniqueness. + * @param options - Validation options. + */ export function IsUnique(entityType: object, options?: ValidationOptions) { return (object: any, propertyName: string) => { registerDecorator({ @@ -20,11 +25,24 @@ export function IsUnique(entityType: object, options?: ValidationOptions) { }; } +/** + * ValidatorConstraint that checks if a property value is unique within the specified entity. + */ @ValidatorConstraint({ name: 'IsUnique', async: true }) @Injectable() export class IsUniqueConstraint implements ValidatorConstraintInterface { + /** + * Constructor that injects the EntityManager. + * @param em - The EntityManager instance. + */ constructor(private readonly em: EntityManager) {} + /** + * Validate if a property value is unique within the specified entity. + * @param value - The value of the property to validate. + * @param args - Validation arguments. + * @returns A boolean indicating whether the property value is unique. + */ async validate(value: any, args: ValidationArguments): Promise<boolean> { const [entityType] = args.constraints; diff --git a/src/common/guards/admin.guard.ts b/src/common/guards/admin.guard.ts index 7e97609..f8161b2 100644 --- a/src/common/guards/admin.guard.ts +++ b/src/common/guards/admin.guard.ts @@ -6,10 +6,23 @@ import { } from '@nestjs/common'; import { AuthService } from '../../auth/auth.service'; +/** + * Guard that checks if the user has the 'admin' role. + */ @Injectable() export class AdminGuard implements CanActivate { + /** + * Constructor that injects the AuthService. + * @param auth - The AuthService instance. + */ constructor(private auth: AuthService) {} + /** + * Determine whether the user has the 'admin' role and is authorized. + * @param context - The execution context. + * @returns A boolean indicating whether the user is authorized as an admin. + * @throws UnauthorizedException if the user is not authorized. + */ async canActivate(context: ExecutionContext): Promise<boolean> { const user = await this.auth.user(); diff --git a/src/common/not-found.interceptor.ts b/src/common/not-found.interceptor.ts index f1a9542..e3ca8b0 100644 --- a/src/common/not-found.interceptor.ts +++ b/src/common/not-found.interceptor.ts @@ -8,12 +8,20 @@ import { import { catchError, Observable } from 'rxjs'; import { NotFoundError } from '@mikro-orm/core'; -/* +/** * This interceptor catches any NotFoundError thrown by MikroORM and * rethrows it as a NotFoundException. */ @Injectable() export class NotFoundInterceptor implements NestInterceptor { + /** + * Intercepts the incoming observable stream of data. + * @param context - The execution context. + * @param next - The next handler in the chain. + * @returns An observable stream of data. + * @throws NotFoundException if a NotFoundError is caught. + * @throws Any other error that is not a NotFoundError. + */ intercept( context: ExecutionContext, next: CallHandler<any>, diff --git a/src/main.ts b/src/main.ts index a32ee08..80d47bc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,9 +9,13 @@ import { import { useContainer } from 'class-validator'; import { NotFoundInterceptor } from './common/not-found.interceptor'; +/** + * Bootstrap function to initialize the NestJS application. + */ async function bootstrap() { const app = await NestFactory.create(AppModule); + // Enable CORS in development environment if (process.env.NODE_ENV === 'development') { app.enableCors({ origin: [process.env.FRONTEND_URL], @@ -19,10 +23,19 @@ async function bootstrap() { }); } + // Set global prefix for API routes app.setGlobalPrefix('api/v1'); + + // Configure cookie parser middleware app.use(cookieParser(process.env.COOKIE_PARSER_SECRET)); + + // Configure global validation pipe app.useGlobalPipes(new ValidationPipe({ exceptionFactory })); + + // Attach global interceptor for handling not found errors app.useGlobalInterceptors(new NotFoundInterceptor()); + + // Enable shutdown hooks for graceful shutdown app.enableShutdownHooks(); // allows us to use NestJS DI in class-validator custom decorators @@ -33,7 +46,7 @@ async function bootstrap() { bootstrap(); -/* +/** * The existing factory function returns an array of errors without * associating these errors with their respective properties. * diff --git a/src/mikro-orm.config.ts b/src/mikro-orm.config.ts index e9460fc..9608d15 100644 --- a/src/mikro-orm.config.ts +++ b/src/mikro-orm.config.ts @@ -2,19 +2,31 @@ import { defineConfig } from '@mikro-orm/mysql'; import * as process from 'process'; import { TsMorphMetadataProvider } from '@mikro-orm/reflection'; +/** + * MikroORM configuration object for MySQL. + */ export default defineConfig({ + // Database connection parameters host: process.env.DB_HOST, port: +process.env.DB_PORT, user: process.env.DB_USER, password: process.env.DB_PASSWORD, dbName: process.env.DB_NAME, + + // Entity paths for JavaScript and TypeScript files entities: ['dist/**/*.entity.js'], entitiesTs: ['src/**/*.entity.ts'], + + // Metadata provider using TsMorph metadataProvider: TsMorphMetadataProvider, + + // Migration paths for JavaScript and TypeScript files migrations: { path: 'dist/database/migrations', pathTs: 'database/migrations', }, + + // Seeder paths for JavaScript and TypeScript files seeder: { path: 'dist/database/seeders', pathTs: 'database/seeders', diff --git a/src/note/note.entity.ts b/src/note/note.entity.ts index efbe677..638da5c 100644 --- a/src/note/note.entity.ts +++ b/src/note/note.entity.ts @@ -7,6 +7,9 @@ import { } from '@mikro-orm/core'; import { Room } from '../room/room.entity'; +/** + * Represents a Note entity that is associated with a Room. + */ @Entity({ tableName: 'notes' }) export class Note { @PrimaryKey() @@ -27,6 +30,11 @@ export class Note { @ManyToOne({ onDelete: 'cascade', hidden: true }) room: Room; + /** + * Creates a new instance of the Note entity. + * @param name - The name of the note. + * @param room - The associated Room entity. + */ constructor(name: string, room: Room) { this.name = name; this.room = room; diff --git a/src/note/note.module.ts b/src/note/note.module.ts index cc4e2a5..162ecd3 100644 --- a/src/note/note.module.ts +++ b/src/note/note.module.ts @@ -3,6 +3,9 @@ import { NoteService } from './note.service'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { Note } from './note.entity'; +/** + * A global module that encapsulates features related to notes. + */ @Global() @Module({ imports: [MikroOrmModule.forFeature([Note])], diff --git a/src/note/note.service.ts b/src/note/note.service.ts index 164dfea..717702f 100644 --- a/src/note/note.service.ts +++ b/src/note/note.service.ts @@ -5,20 +5,41 @@ import { Note } from './note.entity'; import { EntityRepository } from '@mikro-orm/mysql'; import { Room } from '../room/room.entity'; +/** + * A service responsible for managing notes. + */ @Injectable() export class NoteService { + /** + * Initializes the NoteService. + * @param em - The EntityManager instance provided by MikroORM. + * @param repository - The repository for the Note entity. + */ constructor( private readonly em: EntityManager, @InjectRepository(Note) private readonly repository: EntityRepository<Note>, ) {} + /** + * Adds a new note to the specified room. + * @param room - The room in which the note will be added. + * @param name - The name of the note. + * @returns The newly created Note entity. + */ public async addNote(room: Room, name: string) { const note = new Note(name, room); await this.em.persistAndFlush(note); return note; } + /** + * Updates the content of a note with the specified ID. + * @param id - The ID of the note to update. + * @param content - The new content for the note. + * @returns The updated Note entity. + * @throws EntityNotFoundException if the note with the given ID does not exist. + */ public async updateNote(id: number, content: string) { const note = await this.repository.findOneOrFail({ id }); note.content = content; @@ -26,6 +47,12 @@ export class NoteService { return note; } + /** + * Deletes a note with the specified ID. + * @param id - The ID of the note to delete. + * @returns `true` if the note was successfully deleted. + * @throws EntityNotFoundException if the note with the given ID does not exist. + */ public async deleteNoteById(id: number) { const note = await this.repository.findOneOrFail({ id }); await this.em.removeAndFlush(note); diff --git a/src/room/room.controller.spec.ts b/src/room/room.controller.spec.ts index b12398d..cf7299b 100644 --- a/src/room/room.controller.spec.ts +++ b/src/room/room.controller.spec.ts @@ -1,9 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; import { RoomController } from './room.controller'; +/** + * Test suite for the RoomController class. + */ describe('RoomController', () => { let controller: RoomController; + /** + * Before each test, create a testing module with RoomController as the controller. + */ beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [RoomController], @@ -12,6 +18,9 @@ describe('RoomController', () => { controller = module.get<RoomController>(RoomController); }); + /** + * Test if the controller is defined. + */ it('should be defined', () => { expect(controller).toBeDefined(); }); diff --git a/src/room/room.controller.ts b/src/room/room.controller.ts index 757506e..c98aa65 100644 --- a/src/room/room.controller.ts +++ b/src/room/room.controller.ts @@ -14,6 +14,9 @@ import { RoomService } from './room.service'; import { AuthService } from '../auth/auth.service'; import { CategoryService } from '../category/category.service'; +/** + * Controller responsible for handling room-related endpoints. + */ @Controller('category/:category/room') export class RoomController { constructor( @@ -22,6 +25,13 @@ export class RoomController { private readonly auth: AuthService, ) {} + /** + * Create a new room within a category. + * + * @param categoryId - The ID of the category. + * @param data - The data for creating a room. + * @returns The created room. + */ @Post('/') @UseGuards(AuthGuard) public async create( @@ -33,6 +43,14 @@ export class RoomController { return this.rooms.create(data.name, category, data.password); } + /** + * Update an existing room within a category. + * + * @param categoryId - The ID of the category. + * @param roomId - The ID of the room to be updated. + * @param data - The updated room data. + * @returns The updated room. + */ @Put('/:room') @UseGuards(AuthGuard) public async update( @@ -45,6 +63,13 @@ export class RoomController { return this.rooms.update(roomId, category, data.name); } + /** + * Delete a room within a category. + * + * @param categoryId - The ID of the category. + * @param roomId - The ID of the room to be deleted. + * @returns A boolean indicating if the deletion was successful. + */ @Delete('/:room') @UseGuards(AuthGuard) public async delete( @@ -56,6 +81,13 @@ export class RoomController { return this.rooms.delete(roomId, category); } + /** + * Get notes associated with a room. + * + * @param categoryId - The ID of the category. + * @param roomId - The ID of the room. + * @returns Notes associated with the specified room. + */ @Get('/:room/notes') public async getNotes( @Param('category') categoryId: number, diff --git a/src/room/room.dto.ts b/src/room/room.dto.ts index 686da3d..c0a23d2 100644 --- a/src/room/room.dto.ts +++ b/src/room/room.dto.ts @@ -1,5 +1,8 @@ import { IsNotEmpty } from 'class-validator'; +/** + * Data transfer object for creating a room. + */ export class CreateRoom { @IsNotEmpty({ message: 'Name darf nicht leer sein', @@ -9,4 +12,7 @@ export class CreateRoom { password?: string; } +/** + * Data transfer object for updating a room (inherits from CreateRoom). + */ export class UpdateRoom extends CreateRoom {} diff --git a/src/room/room.entity.ts b/src/room/room.entity.ts index b79b6ef..928cf9a 100644 --- a/src/room/room.entity.ts +++ b/src/room/room.entity.ts @@ -10,6 +10,9 @@ import { import { Category } from '../category/category.entity'; import { Note } from '../note/note.entity'; +/** + * Represents a room entity. + */ @Entity({ tableName: 'rooms' }) export class Room { @PrimaryKey() @@ -39,12 +42,21 @@ export class Room { @Property({ type: types.blob, nullable: true }) whiteboardCanvas?; + /** + * Creates a new instance of the Room class. + * @param name - The name of the room. + * @param category - The category that the room belongs to. + * @param password - The password for the room (optional). + */ constructor(name: string, category: Category, password?: string) { this.name = name; this.category = category; this.password = password; } + /** + * Returns a string representation of the room. + */ public toString(): string { return `Room{${this.id}: "${this.name}"}`; } diff --git a/src/room/room.module.ts b/src/room/room.module.ts index 9c448f7..bdd1c86 100644 --- a/src/room/room.module.ts +++ b/src/room/room.module.ts @@ -4,6 +4,9 @@ import { RoomController } from './room.controller'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { Room } from './room.entity'; +/** + * Represents a global module for managing rooms. + */ @Global() @Module({ imports: [MikroOrmModule.forFeature([Room])], diff --git a/src/room/room.service.spec.ts b/src/room/room.service.spec.ts index 504502b..cd60d73 100644 --- a/src/room/room.service.spec.ts +++ b/src/room/room.service.spec.ts @@ -1,9 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; import { RoomService } from './room.service'; +/** + * Test suite for the `RoomService` class. + */ describe('RoomService', () => { let service: RoomService; + /** + * Initialize the testing module and get an instance of `RoomService`. + */ beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [RoomService], @@ -12,6 +18,9 @@ describe('RoomService', () => { service = module.get<RoomService>(RoomService); }); + /** + * Test case to ensure that the `RoomService` instance is defined. + */ it('should be defined', () => { expect(service).toBeDefined(); }); diff --git a/src/room/room.service.ts b/src/room/room.service.ts index ae8957f..d4169fb 100644 --- a/src/room/room.service.ts +++ b/src/room/room.service.ts @@ -6,6 +6,9 @@ import { EntityRepository } from '@mikro-orm/mysql'; import { Category } from '../category/category.entity'; import { Note } from '../note/note.entity'; +/** + * Service responsible for managing room entities. + */ @Injectable() export class RoomService { constructor( @@ -14,14 +17,32 @@ export class RoomService { private readonly repository: EntityRepository<Room>, ) {} + /** + * Get a room by its ID and category. + * @param id - The ID of the room. + * @param category - The category of the room. + * @returns The retrieved room. + */ public async get(id: number, category: Category): Promise<Room> { return this.repository.findOneOrFail({ id, category }); } + /** + * Find a room with its associated category. + * @param id - The ID of the room. + * @returns The retrieved room with its category. + */ public async findOneWithCategory(id: number): Promise<Room | null> { return this.repository.findOne({ id }, { populate: ['category'] }); } + /** + * Create a new room. + * @param name - The name of the room. + * @param category - The category of the room. + * @param password - The optional password for the room. + * @returns The created room. + */ public async create( name: string, category: Category, @@ -33,6 +54,13 @@ export class RoomService { return room; } + /** + * Update the name of a room. + * @param id - The ID of the room. + * @param category - The category of the room. + * @param name - The new name for the room. + * @returns The updated room. + */ public async update( id: number, category: Category, @@ -47,17 +75,33 @@ export class RoomService { return room; } + /** + * Delete a room. + * @param id - The ID of the room. + * @param category - The category of the room. + */ public async delete(id: number, category: Category): Promise<void> { const room = await this.get(id, category); await this.em.removeAndFlush(room); } + /** + * Get the notes associated with a room. + * @param id - The ID of the room. + * @returns An array of notes belonging to the room. + */ public async getNotes(id: number): Promise<Note[]> { const room = await this.repository.findOneOrFail({ id }); return room.notes.loadItems(); } + /** + * Update the whiteboard canvas of a room. + * @param id - The ID of the room. + * @param canvas - The new whiteboard canvas content. + * @returns The updated room. + */ public async updateWhiteboard(id: number, canvas: string): Promise<Room> { const room = await this.repository.findOneOrFail({ id }); diff --git a/src/user/user.controller.spec.ts b/src/user/user.controller.spec.ts index 7057a1a..07e8ac9 100644 --- a/src/user/user.controller.spec.ts +++ b/src/user/user.controller.spec.ts @@ -1,18 +1,32 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UserController } from './user.controller'; +/** + * Test suite to verify the behavior of the UserController. + */ describe('UserController', () => { let controller: UserController; + /** + * Before each test case, create a testing module with UserController. + * Compile the module and obtain an instance of UserController. + * This ensures a fresh instance is available for each test case. + */ beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [UserController], }).compile(); + // Obtain an instance of UserController from the module controller = module.get<UserController>(UserController); }); + /** + * Test case: Ensure that UserController is defined. + * This test validates that the controller instance was created successfully. + */ it('should be defined', () => { + // Assertion to check if controller is defined expect(controller).toBeDefined(); }); }); diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 682b1af..cdef063 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -16,6 +16,9 @@ import { AuthService } from '../auth/auth.service'; import { ChangePassword } from '../auth/auth.dto'; import * as bcrypt from 'bcrypt'; +/** + * Interface representing user data for editing. + */ export type EditUser = { id: number; organization: string; @@ -23,6 +26,9 @@ export type EditUser = { email: string; }; +/** + * Controller managing user-related routes and operations. + */ @Controller('user') export class UserController { constructor( @@ -30,24 +36,43 @@ export class UserController { private readonly authService: AuthService, ) {} + /** + * Retrieve a list of all users. + * Requires authentication and admin privilege. + */ @UseGuards(AuthGuard, AdminGuard) @Get('findAll') public async findAll() { return await this.userService.findAll(); } + /** + * Change the role of a user. + * Requires authentication and admin privilege. + * @param data - Object containing the ID of the user. + */ @UseGuards(AuthGuard, AdminGuard) @Post('changeRole') public async changeRole(@Body() data: { id: number }) { return await this.userService.changeRole(data.id); } + /** + * Update user data. + * Requires authentication. + * @param data - Object containing updated user data. + */ @UseGuards(AuthGuard) @Put('changeUserData') public async changeUserData(@Body() data: EditUser): Promise<boolean> { return this.userService.changeUserData(data); } + /** + * Delete a user by ID. + * Requires authentication and admin privilege. + * @param userId - ID of the user to delete. + */ @UseGuards(AuthGuard, AdminGuard) @Delete('/:userId') public async delete(@Param('userId') userId: number) { @@ -56,6 +81,11 @@ export class UserController { return { message: 'success' }; } + /** + * Change user password. + * Requires authentication. + * @param data - Object containing current and new passwords. + */ @UseGuards(AuthGuard) @Post('changePassword') public async changePassword(@Body() data: ChangePassword) { diff --git a/src/user/user.entity.ts b/src/user/user.entity.ts index a9e0983..8fc6ad0 100644 --- a/src/user/user.entity.ts +++ b/src/user/user.entity.ts @@ -7,6 +7,9 @@ import { } from '@mikro-orm/core'; import { Category } from '../category/category.entity'; +/** + * Entity representing a user. + */ @Entity({ tableName: 'users' }) export class User { @PrimaryKey() @@ -36,6 +39,13 @@ export class User { @Property() role: 'user' | 'admin' = 'user'; + /** + * Constructor to create a new User instance. + * @param name - The user's name. + * @param email - The user's email. + * @param organization - The user's organization. + * @param password - The user's password. + */ constructor( name: string, email: string, @@ -48,6 +58,10 @@ export class User { this.password = password; } + /** + * Get a string representation of the user. + * @returns A string containing user ID and name. + */ public toString(): string { return `User{${this.id}: "${this.name}"}`; } diff --git a/src/user/user.module.ts b/src/user/user.module.ts index 6ec7ba9..fd3e7d2 100644 --- a/src/user/user.module.ts +++ b/src/user/user.module.ts @@ -4,6 +4,9 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; import { User } from './user.entity'; import { UserController } from './user.controller'; +/** + * Global module providing user-related functionality. + */ @Global() @Module({ imports: [MikroOrmModule.forFeature([User])], diff --git a/src/user/user.service.spec.ts b/src/user/user.service.spec.ts index 873de8a..ea9119a 100644 --- a/src/user/user.service.spec.ts +++ b/src/user/user.service.spec.ts @@ -1,18 +1,31 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UserService } from './user.service'; +/** + * Test suite for UserService. + */ describe('UserService', () => { let service: UserService; + /** + * Before each test case, create a testing module and compile it. + * Obtain an instance of UserService for testing. + */ beforeEach(async () => { + // Create a testing module const module: TestingModule = await Test.createTestingModule({ providers: [UserService], }).compile(); + // Obtain an instance of UserService from the module service = module.get<UserService>(UserService); }); + /** + * Test case to verify that UserService is defined. + */ it('should be defined', () => { + // Assertion to check if service is defined expect(service).toBeDefined(); }); }); diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 028452e..1d7068c 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -7,6 +7,9 @@ import { CreateUser } from '../auth/auth.dto'; import * as bcrypt from 'bcrypt'; import { EditUser } from './user.controller'; +/** + * Service for user-related operations. + */ @Injectable() export class UserService { public static readonly SALT_OR_ROUNDS = 9; @@ -17,22 +20,46 @@ export class UserService { private readonly repository: EntityRepository<User>, ) {} + /** + * Find a user by ID. + * @param id - The user's ID. + * @returns A Promise that resolves to the found user or null if not found. + */ public async findOne(id: number): Promise<User | null> { return this.repository.findOne(id); } + /** + * Find a user by email. + * @param email - The user's email. + * @returns A Promise that resolves to the found user or null if not found. + */ public async findByEmail(email: string): Promise<User | null> { return this.repository.findOne({ email }); } + /** + * Find all users. + * @returns A Promise that resolves to an array of all users. + */ public async findAll(): Promise<User[]> { return this.repository.findAll(); } + /** + * Find users by IDs. + * @param ids - An array of user IDs. + * @returns A Promise that resolves to an array of found users. + */ public async find(ids: number[]): Promise<User[]> { return this.repository.find(ids); } + /** + * Create a new user. + * @param data - Data for creating the user. + * @returns A Promise that resolves to the created user. + */ public async create(data: CreateUser): Promise<User> { const password = await bcrypt.hash( data.password, @@ -46,11 +73,20 @@ export class UserService { return user; } + /** + * Change a user's password. + * @param user - The user entity. + * @param password - The new password. + */ public async changePassword(user: User, password: string): Promise<void> { user.password = await bcrypt.hash(password, UserService.SALT_OR_ROUNDS); await this.em.persistAndFlush(user); } + /** + * Delete a user by ID. + * @param id - The user's ID. + */ public async delete(id: number): Promise<void> { const user = await this.repository.findOne({ id }); @@ -60,6 +96,11 @@ export class UserService { await this.repository.nativeDelete({ id }); } + /** + * Change user data. + * @param data - Updated user data. + * @returns A Promise that resolves to a boolean indicating success. + */ public async changeUserData(data: EditUser): Promise<boolean> { const user = await this.findOne(data.id); user.name = data.name; @@ -69,6 +110,11 @@ export class UserService { return true; } + /** + * Change user role. + * @param id - The user's ID. + * @returns A Promise that resolves to the updated user entity. + */ public async changeRole(id: number): Promise<User> { const user = await this.repository.findOne({ id }); -- GitLab