From 225d0b0be76052ea8cc9a8921199934f766f295d Mon Sep 17 00:00:00 2001 From: adityapat24 Date: Sat, 24 Jan 2026 22:00:34 -0500 Subject: [PATCH 1/3] logging and swagger implementation --- backend/src/grant/grant.controller.ts | 92 +- backend/src/grant/grant.service.ts | 187 +-- backend/src/grant/types/grant.types.ts | 102 ++ .../main-page/header/styles/AccountInfo.css | 2 +- package-lock.json | 1312 +++++++++++++++++ package.json | 6 + 6 files changed, 1593 insertions(+), 108 deletions(-) create mode 100644 backend/src/grant/types/grant.types.ts create mode 100644 package-lock.json create mode 100644 package.json diff --git a/backend/src/grant/grant.controller.ts b/backend/src/grant/grant.controller.ts index 287cd27..b788fc4 100644 --- a/backend/src/grant/grant.controller.ts +++ b/backend/src/grant/grant.controller.ts @@ -2,58 +2,116 @@ import { Controller, Get, Param, Put, Body, Patch, Post, Delete, ValidationPipe, import { GrantService } from './grant.service'; import { Grant } from '../../../middle-layer/types/Grant'; import { VerifyUserGuard } from '../guards/auth.guard'; -import { ApiBearerAuth } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOperation, ApiResponse, ApiParam, ApiBody, ApiTags } from '@nestjs/swagger'; +import { InactivateGrantBody, AddGrantBody, UpdateGrantBody } from './types/grant.types'; +@ApiTags('grant') @Controller('grant') export class GrantController { + private readonly logger = new Logger(GrantController.name); + constructor(private readonly grantService: GrantService) { } @Get() @UseGuards(VerifyUserGuard) @ApiBearerAuth() - async getAllGrants() { - return await this.grantService.getAllGrants(); + @ApiOperation({ summary: 'Retrieve all grants', description: 'Returns a list of all grants in the database. Automatically inactivates expired grants.' }) + @ApiResponse({ status: 200, description: 'Successfully retrieved all grants', type: [Grant] }) + @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) + @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) + async getAllGrants(): Promise { + this.logger.log('GET /grant - Retrieving all grants'); + const grants = await this.grantService.getAllGrants(); + this.logger.log(`GET /grant - Successfully retrieved ${grants.length} grants`); + return grants; } - - @Put('inactivate') @UseGuards(VerifyUserGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Inactivate grants', description: 'Marks one or more grants as inactive by their grant IDs' }) + @ApiBody({ type: InactivateGrantBody, description: 'Array of grant IDs to inactivate' }) + @ApiResponse({ status: 200, description: 'Successfully inactivated grants', type: [Grant] }) + @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) + @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) async inactivate( - @Body('grantIds') grantIds: number[] + @Body() body: InactivateGrantBody ): Promise { + this.logger.log(`PUT /grant/inactivate - Inactivating ${body.grantIds.length} grant(s)`); let grants: Grant[] = []; - for(const id of grantIds){ - Logger.log(`Inactivating grant with ID: ${id}`); + for(const id of body.grantIds){ + this.logger.debug(`Inactivating grant with ID: ${id}`); let newGrant = await this.grantService.makeGrantsInactive(id) grants.push(newGrant); } + this.logger.log(`PUT /grant/inactivate - Successfully inactivated ${grants.length} grant(s)`); return grants; } @Post('new-grant') @UseGuards(VerifyUserGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Create a new grant', description: 'Creates a new grant in the database with a generated grant ID' }) + @ApiBody({ type: AddGrantBody, description: 'Grant data to create' }) + @ApiResponse({ status: 201, description: 'Successfully created grant', type: Number, example: 1234567890 }) + @ApiResponse({ status: 400, description: 'Bad Request - Invalid grant data', example: '{Error encountered}' }) + @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) + @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) async addGrant( @Body(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })) - grant: Grant - ) { - return await this.grantService.addGrant(grant); + grant: AddGrantBody + ): Promise { + this.logger.log(`POST /grant/new-grant - Creating new grant for organization: ${grant.organization}`); + const grantId = await this.grantService.addGrant(grant as Grant); + this.logger.log(`POST /grant/new-grant - Successfully created grant with ID: ${grantId}`); + return grantId; } @Put('save') @UseGuards(VerifyUserGuard) - async saveGrant(@Body() grantData: Grant) { - return await this.grantService.updateGrant(grantData) + @ApiBearerAuth() + @ApiOperation({ summary: 'Update an existing grant', description: 'Updates an existing grant in the database with new grant data' }) + @ApiBody({ type: UpdateGrantBody, description: 'Updated grant data including grantId' }) + @ApiResponse({ status: 200, description: 'Successfully updated grant', type: String, example: '{"Attributes": {...}}' }) + @ApiResponse({ status: 400, description: 'Bad Request - Invalid grant data', example: '{Error encountered}' }) + @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) + @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) + async saveGrant(@Body() grantData: UpdateGrantBody): Promise { + this.logger.log(`PUT /grant/save - Updating grant with ID: ${grantData.grantId}`); + const result = await this.grantService.updateGrant(grantData as Grant); + this.logger.log(`PUT /grant/save - Successfully updated grant ${grantData.grantId}`); + return result; } @Delete(':grantId') @UseGuards(VerifyUserGuard) - async deleteGrant(@Param('grantId') grantId: number) { - return await this.grantService.deleteGrantById(grantId); + @ApiBearerAuth() + @ApiOperation({ summary: 'Delete a grant', description: 'Deletes a grant from the database by its grant ID' }) + @ApiParam({ name: 'grantId', type: Number, description: 'The ID of the grant to delete' }) + @ApiResponse({ status: 200, description: 'Successfully deleted grant', type: String, example: 'Grant 1234567890 deleted successfully' }) + @ApiResponse({ status: 400, description: 'Bad Request - Grant does not exist', example: '{Error encountered}' }) + @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) + @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) + async deleteGrant(@Param('grantId') grantId: number): Promise { + this.logger.log(`DELETE /grant/${grantId} - Deleting grant`); + const result = await this.grantService.deleteGrantById(grantId); + this.logger.log(`DELETE /grant/${grantId} - Successfully deleted grant`); + return result; } + @Get(':id') @UseGuards(VerifyUserGuard) - async getGrantById(@Param('id') GrantId: string) { - return await this.grantService.getGrantById(parseInt(GrantId, 10)); + @ApiBearerAuth() + @ApiOperation({ summary: 'Get a grant by ID', description: 'Retrieves a single grant from the database by its grant ID' }) + @ApiParam({ name: 'id', type: String, description: 'The ID of the grant to retrieve' }) + @ApiResponse({ status: 200, description: 'Successfully retrieved grant', type: Grant }) + @ApiResponse({ status: 404, description: 'Grant not found', example: '{Error encountered}' }) + @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) + @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) + async getGrantById(@Param('id') GrantId: string): Promise { + this.logger.log(`GET /grant/${GrantId} - Retrieving grant by ID`); + const grant = await this.grantService.getGrantById(parseInt(GrantId, 10)); + this.logger.log(`GET /grant/${GrantId} - Successfully retrieved grant`); + return grant; } } \ No newline at end of file diff --git a/backend/src/grant/grant.service.ts b/backend/src/grant/grant.service.ts index 8671c03..419cdb8 100644 --- a/backend/src/grant/grant.service.ts +++ b/backend/src/grant/grant.service.ts @@ -12,20 +12,23 @@ export class GrantService { constructor(private readonly notificationService: NotificationService) {} - // function to retrieve all grants in our database + // Retrieves all grants from the database and automatically inactivates expired grants async getAllGrants(): Promise { - console.log("GETTING ALL GRANTS"); - // loads in the environment variable for the table now + this.logger.log('Starting to retrieve all grants from database'); const params = { TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || 'TABLE_FAILURE', }; try { + this.logger.debug(`Scanning DynamoDB table: ${params.TableName}`); const data = await this.dynamoDb.scan(params).promise(); const grants = (data.Items as Grant[]) || []; + this.logger.log(`Retrieved ${grants.length} grants from database`); + const inactiveGrantIds: number[] = []; const now = new Date(); + this.logger.debug('Checking for expired active grants'); for (const grant of grants) { if (grant.status === "Active") { const startDate = new Date(grant.grant_start_date); @@ -37,6 +40,7 @@ export class GrantService { ); if (now >= endDate) { + this.logger.warn(`Grant ${grant.grantId} has expired and will be marked inactive`); inactiveGrantIds.push(grant.grantId); let newGrant = this.makeGrantsInactive(grant.grantId) grants.filter(g => g.grantId !== grant.grantId); @@ -45,16 +49,22 @@ export class GrantService { } } } - return grants; + + if (inactiveGrantIds.length > 0) { + this.logger.log(`Automatically inactivated ${inactiveGrantIds.length} expired grants`); + } + + this.logger.log(`Successfully retrieved ${grants.length} grants`); + return grants; } catch (error) { - console.log(error) + this.logger.error('Failed to retrieve grants from database', error instanceof Error ? error.stack : undefined); throw new Error('Could not retrieve grants.'); } } - // function to retrieve a grant by its ID + // Retrieves a single grant from the database by its unique grant ID async getGrantById(grantId: number): Promise { - + this.logger.log(`Retrieving grant with ID: ${grantId}`); const params = { TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || 'TABLE_FAILURE', Key: { @@ -63,24 +73,31 @@ export class GrantService { }; try { + this.logger.debug(`Querying DynamoDB for grant ID: ${grantId}`); const data = await this.dynamoDb.get(params).promise(); if (!data.Item) { + this.logger.warn(`Grant with ID ${grantId} not found in database`); throw new NotFoundException('No grant with id ' + grantId + ' found.'); } + this.logger.log(`Successfully retrieved grant ${grantId} from database`); return data.Item as Grant; } catch (error) { - if (error instanceof NotFoundException) throw error; + if (error instanceof NotFoundException) { + this.logger.warn(`Grant ${grantId} not found: ${error.message}`); + throw error; + } - console.log(error) + this.logger.error(`Failed to retrieve grant ${grantId}`, error instanceof Error ? error.stack : undefined); throw new Error('Failed to retrieve grant.'); } } - // Method to make grants inactive -async makeGrantsInactive(grantId: number): Promise { - let updatedGrant: Grant = {} as Grant; + // Marks a grant as inactive by updating its status in the database + async makeGrantsInactive(grantId: number): Promise { + this.logger.log(`Marking grant ${grantId} as inactive`); + let updatedGrant: Grant = {} as Grant; const params = { TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || "TABLE_FAILURE", @@ -96,37 +113,34 @@ async makeGrantsInactive(grantId: number): Promise { }; try { + this.logger.debug(`Updating grant ${grantId} status to inactive in DynamoDB`); const res = await this.dynamoDb.update(params).promise(); if (res.Attributes?.status === Status.Inactive) { - console.log(`Grant ${grantId} successfully marked as inactive.`); - + this.logger.log(`Grant ${grantId} successfully marked as inactive`); const currentGrant = res.Attributes as Grant; - console.log(currentGrant); - updatedGrant = currentGrant + updatedGrant = currentGrant; } else { - console.log(`Grant ${grantId} update failed or no change in status.`); + this.logger.warn(`Grant ${grantId} update failed or no change in status`); } } catch (err) { - console.log(err); + this.logger.error(`Failed to update grant ${grantId} status to inactive`, err instanceof Error ? err.stack : undefined); throw new Error(`Failed to update Grant ${grantId} status.`); } - return updatedGrant; -} + return updatedGrant; + } - /** - * Will push or overwrite new grant data to database - * @param grantData - */ + // Updates an existing grant in the database with new grant data async updateGrant(grantData: Grant): Promise { + this.logger.log(`Updating grant with ID: ${grantData.grantId}`); const updateKeys = Object.keys(grantData).filter( key => key != 'grantId' ); - this.logger.warn('Update keys: ' + JSON.stringify(updateKeys)); + this.logger.debug(`Updating ${updateKeys.length} fields for grant ${grantData.grantId}: ${updateKeys.join(', ')}`); const UpdateExpression = "SET " + updateKeys.map((key) => `#${key} = :${key}`).join(", "); const ExpressionAttributeNames = updateKeys.reduce((acc, key) => @@ -144,67 +158,70 @@ async makeGrantsInactive(grantId: number): Promise { }; try { + this.logger.debug(`Executing DynamoDB update for grant ${grantData.grantId}`); const result = await this.dynamoDb.update(params).promise(); - this.logger.warn('✅ Update successful!'); + this.logger.log(`Successfully updated grant ${grantData.grantId} in database`); //await this.updateGrantNotifications(grantData); return JSON.stringify(result); } catch(err: unknown) { - this.logger.error('=== DYNAMODB ERROR ==='); - this.logger.error('Unknown error type: ' + JSON.stringify(err)); + this.logger.error(`Failed to update grant ${grantData.grantId} in DynamoDB`, err instanceof Error ? err.stack : undefined); + this.logger.error(`Error details: ${JSON.stringify(err)}`); throw new Error(`Failed to update Grant ${grantData.grantId}`); } } - // Add a new grant using the Grant interface from middleware. - async addGrant(grant: Grant): Promise { - // Generate a unique grant ID (using Date.now() for simplicity, needs proper UUID) - const newGrantId = Date.now(); + // Creates a new grant in the database and generates a unique grant ID + async addGrant(grant: Grant): Promise { + this.logger.log(`Creating new grant for organization: ${grant.organization}`); + // Generate a unique grant ID (using Date.now() for simplicity, needs proper UUID) + const newGrantId = Date.now(); + this.logger.debug(`Generated grant ID: ${newGrantId}`); - const params = { - TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || 'TABLE_FAILURE', - Item: { - grantId: newGrantId, - organization: grant.organization, - does_bcan_qualify: grant.does_bcan_qualify, - status: grant.status, - amount: grant.amount, - grant_start_date: grant.grant_start_date, - application_deadline: grant.application_deadline, - report_deadlines: grant.report_deadlines, - description: grant.description, - timeline: grant.timeline, - estimated_completion_time: grant.estimated_completion_time, - grantmaker_poc: grant.grantmaker_poc, - bcan_poc: grant.bcan_poc, - attachments: grant.attachments, - isRestricted: grant.isRestricted, + const params = { + TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || 'TABLE_FAILURE', + Item: { + grantId: newGrantId, + organization: grant.organization, + does_bcan_qualify: grant.does_bcan_qualify, + status: grant.status, + amount: grant.amount, + grant_start_date: grant.grant_start_date, + application_deadline: grant.application_deadline, + report_deadlines: grant.report_deadlines, + description: grant.description, + timeline: grant.timeline, + estimated_completion_time: grant.estimated_completion_time, + grantmaker_poc: grant.grantmaker_poc, + bcan_poc: grant.bcan_poc, + attachments: grant.attachments, + isRestricted: grant.isRestricted, + } + }; + + try { + this.logger.debug(`Inserting grant ${newGrantId} into DynamoDB`); + await this.dynamoDb.put(params).promise(); + this.logger.log(`Successfully created grant ${newGrantId} for organization: ${grant.organization}`); + + const userId = grant.bcan_poc.POC_email; + this.logger.debug(`Preparing to create notifications for user: ${userId}`); + + //await this.createGrantNotifications({ ...grant, grantId: newGrantId }, userId); + + this.logger.log(`Successfully created grant ${newGrantId} with all associated data`); + } catch (error: any) { + this.logger.error(`Failed to create new grant for organization: ${grant.organization}`); + this.logger.error(`Error details: ${error.message}`); + this.logger.error(`Stack trace: ${error.stack}`); + throw new Error(`Failed to upload new grant from ${grant.organization}`); } - }; - try { - await this.dynamoDb.put(params).promise(); - this.logger.log(`Uploaded grant from ${grant.organization}`); - - const userId = grant.bcan_poc.POC_email; - this.logger.log(`Creating notifications for user: ${userId}`); - - //await this.createGrantNotifications({ ...grant, grantId: newGrantId }, userId); - - this.logger.log(`Successfully created notifications for grant ${newGrantId}`); - } catch (error: any) { - this.logger.error(`Failed to upload new grant from ${grant.organization}`); - this.logger.error(`Error details: ${error.message}`); - this.logger.error(`Stack trace: ${error.stack}`); - throw new Error(`Failed to upload new grant from ${grant.organization}`); + return newGrantId; } - return newGrantId; - } - - /* Deletes a grant from database based on its grant ID number - * @param grantId - */ + // Deletes a grant from the database by its grant ID async deleteGrantById(grantId: number): Promise { + this.logger.log(`Deleting grant with ID: ${grantId}`); const params = { TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || "TABLE_FAILURE", Key: { grantId: Number(grantId) }, @@ -212,22 +229,21 @@ async makeGrantsInactive(grantId: number): Promise { }; try { + this.logger.debug(`Executing DynamoDB delete for grant ${grantId}`); await this.dynamoDb.delete(params).promise(); - this.logger.log(`Grant ${grantId} deleted successfully`); + this.logger.log(`Successfully deleted grant ${grantId} from database`); return `Grant ${grantId} deleted successfully`; } catch (error: any) { if (error.code === "ConditionalCheckFailedException") { + this.logger.warn(`Grant ${grantId} does not exist in database`); throw new Error(`Grant ${grantId} does not exist`); } - this.logger.error(`Failed to delete Grant ${grantId}`, error.stack); + this.logger.error(`Failed to delete grant ${grantId}`, error.stack); throw new Error(`Failed to delete Grant ${grantId}`); } } - /* - Helper method that takes in a deadline in ISO format and returns an array of ISO strings representing the notification times - for 14 days, 7 days, and 3 days before the deadline. - */ + // Calculates notification times for a deadline (14, 7, and 3 days before) private getNotificationTimes(deadlineISO: string): string[] { const deadline = new Date(deadlineISO); const daysBefore = [14, 7, 3]; @@ -238,11 +254,7 @@ async makeGrantsInactive(grantId: number): Promise { }); } - /** - * Helper method that creates notifications for a grant's application and report deadlines - * @param grant represents the grant of which we want to create a notification for - * @param userId represents the user to whom we want to send the notification - */ + // Creates notifications for a grant's application and report deadlines private async createGrantNotifications(grant: Grant, userId: string) { const { grantId, organization, application_deadline, report_deadlines } = grant; @@ -281,10 +293,7 @@ async makeGrantsInactive(grantId: number): Promise { } } - /** - * Helper method to update notifications for a grant's application and report deadlines - * @param grant represents the grant of which we want to update notifications for - */ + // Updates notifications for a grant's application and report deadlines private async updateGrantNotifications(grant: Grant) { const { grantId, organization, application_deadline, report_deadlines } = grant; @@ -319,9 +328,7 @@ async makeGrantsInactive(grantId: number): Promise { } } - /* - Helper method that calculates the number of days between alert time and deadline - */ + // Calculates the number of days between an alert time and deadline private daysUntil(alertTime: string, deadline: string): number { const diffMs = +new Date(deadline) - +new Date(alertTime); return Math.round(diffMs / (1000 * 60 * 60 * 24)); diff --git a/backend/src/grant/types/grant.types.ts b/backend/src/grant/types/grant.types.ts new file mode 100644 index 0000000..dfa9ba7 --- /dev/null +++ b/backend/src/grant/types/grant.types.ts @@ -0,0 +1,102 @@ +import { Grant } from '../../../../middle-layer/types/Grant'; +import { ApiProperty } from '@nestjs/swagger'; + +export class InactivateGrantBody { + @ApiProperty({ + description: 'Array of grant IDs to inactivate', + type: [Number], + example: [1234567890, 1234567891] + }) + grantIds!: number[]; +} + +export class AddGrantBody { + @ApiProperty({ description: 'Organization giving the grant', example: 'Example Foundation' }) + organization!: string; + + @ApiProperty({ description: 'Whether BCAN qualifies for this grant', example: true }) + does_bcan_qualify!: boolean; + + @ApiProperty({ description: 'Current status of the grant', example: 'Active' }) + status!: string; + + @ApiProperty({ description: 'Amount of money given by the grant', example: 50000 }) + amount!: number; + + @ApiProperty({ description: 'When the grant money will start being issued', example: '2024-01-01T00:00:00.000Z' }) + grant_start_date!: string; + + @ApiProperty({ description: 'When grant submission is due', example: '2024-06-01T00:00:00.000Z' }) + application_deadline!: string; + + @ApiProperty({ description: 'Multiple report dates', type: [String], required: false, example: ['2024-12-01T00:00:00.000Z'] }) + report_deadlines?: string[]; + + @ApiProperty({ description: 'Additional information about the grant', required: false, example: 'Grant for research purposes' }) + description?: string; + + @ApiProperty({ description: 'How long the grant will last in years', example: 1 }) + timeline!: number; + + @ApiProperty({ description: 'Estimated time to complete the grant application in hours', example: 40 }) + estimated_completion_time!: number; + + @ApiProperty({ description: 'Person of contact at organization giving the grant', required: false }) + grantmaker_poc?: any; + + @ApiProperty({ description: 'Person of contact at BCAN' }) + bcan_poc!: any; + + @ApiProperty({ description: 'Attachments related to the grant', type: [Object] }) + attachments!: any[]; + + @ApiProperty({ description: 'Whether the grant is restricted (specific purpose) or unrestricted', example: false }) + isRestricted!: boolean; +} + +export class UpdateGrantBody { + @ApiProperty({ description: 'Unique ID for the grant', example: 1234567890 }) + grantId!: number; + + @ApiProperty({ description: 'Organization giving the grant', example: 'Example Foundation', required: false }) + organization?: string; + + @ApiProperty({ description: 'Whether BCAN qualifies for this grant', required: false }) + does_bcan_qualify?: boolean; + + @ApiProperty({ description: 'Current status of the grant', required: false }) + status?: string; + + @ApiProperty({ description: 'Amount of money given by the grant', required: false }) + amount?: number; + + @ApiProperty({ description: 'When the grant money will start being issued', required: false }) + grant_start_date?: string; + + @ApiProperty({ description: 'When grant submission is due', required: false }) + application_deadline?: string; + + @ApiProperty({ description: 'Multiple report dates', type: [String], required: false }) + report_deadlines?: string[]; + + @ApiProperty({ description: 'Additional information about the grant', required: false }) + description?: string; + + @ApiProperty({ description: 'How long the grant will last in years', required: false }) + timeline?: number; + + @ApiProperty({ description: 'Estimated time to complete the grant application in hours', required: false }) + estimated_completion_time?: number; + + @ApiProperty({ description: 'Person of contact at organization giving the grant', required: false }) + grantmaker_poc?: any; + + @ApiProperty({ description: 'Person of contact at BCAN', required: false }) + bcan_poc?: any; + + @ApiProperty({ description: 'Attachments related to the grant', type: [Object], required: false }) + attachments?: any[]; + + @ApiProperty({ description: 'Whether the grant is restricted or unrestricted', required: false }) + isRestricted?: boolean; +} diff --git a/frontend/src/main-page/header/styles/AccountInfo.css b/frontend/src/main-page/header/styles/AccountInfo.css index e2c2894..11eb4f2 100644 --- a/frontend/src/main-page/header/styles/AccountInfo.css +++ b/frontend/src/main-page/header/styles/AccountInfo.css @@ -1,6 +1,6 @@ .account-modal { - position: fixed; + position: absolute; top: 70px; right: 20px; z-index: 9999; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a43fa14 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1312 @@ +{ + "name": "bcan", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@nestjs/swagger": "^11.2.5", + "swagger-ui-express": "^5.0.1" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz", + "integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", + "integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==", + "license": "MIT" + }, + "node_modules/@nestjs/common": { + "version": "11.1.12", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.12.tgz", + "integrity": "sha512-v6U3O01YohHO+IE3EIFXuRuu3VJILWzyMmSYZXpyBbnp0hk0mFyHxK2w3dF4I5WnbwiRbWlEXdeXFvPQ7qaZzw==", + "license": "MIT", + "peer": true, + "dependencies": { + "file-type": "21.3.0", + "iterare": "1.2.1", + "load-esm": "1.0.3", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/core": { + "version": "11.1.12", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.12.tgz", + "integrity": "sha512-97DzTYMf5RtGAVvX1cjwpKRiCUpkeQ9CCzSAenqkAhOmNVVFaApbhuw+xrDt13rsCa2hHVOYPrV4dBgOYMJjsA==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nuxt/opencollective": "0.4.1", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.3.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", + "integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/swagger": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.5.tgz", + "integrity": "sha512-wCykbEybMqiYcvkyzPW4SbXKcwra9AGdajm0MvFgKR3W+gd1hfeKlo67g/s9QCRc/mqUU4KOE5Qtk7asMeFuiA==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.16.0", + "@nestjs/mapped-types": "2.1.0", + "js-yaml": "4.1.1", + "lodash": "4.17.21", + "path-to-regexp": "8.3.0", + "swagger-ui-dist": "5.31.0" + }, + "peerDependencies": { + "@fastify/static": "^8.0.0 || ^9.0.0", + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nuxt/opencollective": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.4.1.tgz", + "integrity": "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "consola": "^3.2.3" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0", + "npm": ">=5.10.0" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT", + "peer": true + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT", + "peer": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT", + "peer": true + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT", + "peer": true + }, + "node_modules/file-type": { + "version": "21.3.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.0.tgz", + "integrity": "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC", + "peer": true + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT", + "peer": true + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/load-esm": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz", + "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=13.2.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "peer": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "peer": true + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC", + "peer": true + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz", + "integrity": "sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "peer": true, + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "peer": true + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "peer": true, + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "peer": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..665732c --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "@nestjs/swagger": "^11.2.5", + "swagger-ui-express": "^5.0.1" + } +} From 819b1abcd54e9b150608937f002038309225db34 Mon Sep 17 00:00:00 2001 From: adityapat24 Date: Sat, 24 Jan 2026 23:01:46 -0500 Subject: [PATCH 2/3] error fixes --- backend/src/grant/grant.controller.ts | 8 ++--- backend/src/grant/types/grant.types.ts | 47 ++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/backend/src/grant/grant.controller.ts b/backend/src/grant/grant.controller.ts index b788fc4..d22aba1 100644 --- a/backend/src/grant/grant.controller.ts +++ b/backend/src/grant/grant.controller.ts @@ -3,7 +3,7 @@ import { GrantService } from './grant.service'; import { Grant } from '../../../middle-layer/types/Grant'; import { VerifyUserGuard } from '../guards/auth.guard'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiParam, ApiBody, ApiTags } from '@nestjs/swagger'; -import { InactivateGrantBody, AddGrantBody, UpdateGrantBody } from './types/grant.types'; +import { InactivateGrantBody, AddGrantBody, UpdateGrantBody, GrantResponseDto } from './types/grant.types'; @ApiTags('grant') @Controller('grant') @@ -16,7 +16,7 @@ export class GrantController { @UseGuards(VerifyUserGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Retrieve all grants', description: 'Returns a list of all grants in the database. Automatically inactivates expired grants.' }) - @ApiResponse({ status: 200, description: 'Successfully retrieved all grants', type: [Grant] }) + @ApiResponse({ status: 200, description: 'Successfully retrieved all grants', type: [GrantResponseDto] }) @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) async getAllGrants(): Promise { @@ -31,7 +31,7 @@ export class GrantController { @ApiBearerAuth() @ApiOperation({ summary: 'Inactivate grants', description: 'Marks one or more grants as inactive by their grant IDs' }) @ApiBody({ type: InactivateGrantBody, description: 'Array of grant IDs to inactivate' }) - @ApiResponse({ status: 200, description: 'Successfully inactivated grants', type: [Grant] }) + @ApiResponse({ status: 200, description: 'Successfully inactivated grants', type: [GrantResponseDto] }) @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) async inactivate( @@ -104,7 +104,7 @@ export class GrantController { @ApiBearerAuth() @ApiOperation({ summary: 'Get a grant by ID', description: 'Retrieves a single grant from the database by its grant ID' }) @ApiParam({ name: 'id', type: String, description: 'The ID of the grant to retrieve' }) - @ApiResponse({ status: 200, description: 'Successfully retrieved grant', type: Grant }) + @ApiResponse({ status: 200, description: 'Successfully retrieved grant', type: GrantResponseDto }) @ApiResponse({ status: 404, description: 'Grant not found', example: '{Error encountered}' }) @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) diff --git a/backend/src/grant/types/grant.types.ts b/backend/src/grant/types/grant.types.ts index dfa9ba7..2d090a2 100644 --- a/backend/src/grant/types/grant.types.ts +++ b/backend/src/grant/types/grant.types.ts @@ -1,6 +1,53 @@ import { Grant } from '../../../../middle-layer/types/Grant'; import { ApiProperty } from '@nestjs/swagger'; +export class GrantResponseDto { + @ApiProperty({ description: 'Unique ID for the grant', example: 1234567890 }) + grantId!: number; + + @ApiProperty({ description: 'Organization giving the grant', example: 'Example Foundation' }) + organization!: string; + + @ApiProperty({ description: 'Whether BCAN qualifies for this grant', example: true }) + does_bcan_qualify!: boolean; + + @ApiProperty({ description: 'Current status of the grant', example: 'Active' }) + status!: string; + + @ApiProperty({ description: 'Amount of money given by the grant', example: 50000 }) + amount!: number; + + @ApiProperty({ description: 'When the grant money will start being issued', example: '2024-01-01T00:00:00.000Z' }) + grant_start_date!: string; + + @ApiProperty({ description: 'When grant submission is due', example: '2024-06-01T00:00:00.000Z' }) + application_deadline!: string; + + @ApiProperty({ description: 'Multiple report dates', type: [String], required: false }) + report_deadlines?: string[]; + + @ApiProperty({ description: 'Additional information about the grant', required: false }) + description?: string; + + @ApiProperty({ description: 'How long the grant will last in years', example: 1 }) + timeline!: number; + + @ApiProperty({ description: 'Estimated time to complete the grant application in hours', example: 40 }) + estimated_completion_time!: number; + + @ApiProperty({ description: 'Person of contact at organization giving the grant', required: false }) + grantmaker_poc?: any; + + @ApiProperty({ description: 'Person of contact at BCAN' }) + bcan_poc!: any; + + @ApiProperty({ description: 'Attachments related to the grant', type: [Object] }) + attachments!: any[]; + + @ApiProperty({ description: 'Whether the grant is restricted or unrestricted', example: false }) + isRestricted!: boolean; +} + export class InactivateGrantBody { @ApiProperty({ description: 'Array of grant IDs to inactivate', From 92e37128b73ce168db8cde48f5ba66af00f5bd57 Mon Sep 17 00:00:00 2001 From: adityapat24 Date: Tue, 27 Jan 2026 18:37:14 -0500 Subject: [PATCH 3/3] added 403 and logging to create and update grant --- backend/src/grant/grant.controller.ts | 6 +++ backend/src/grant/grant.service.ts | 71 +++++++++++++++++++++++---- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/backend/src/grant/grant.controller.ts b/backend/src/grant/grant.controller.ts index d22aba1..0a590e8 100644 --- a/backend/src/grant/grant.controller.ts +++ b/backend/src/grant/grant.controller.ts @@ -18,6 +18,7 @@ export class GrantController { @ApiOperation({ summary: 'Retrieve all grants', description: 'Returns a list of all grants in the database. Automatically inactivates expired grants.' }) @ApiResponse({ status: 200, description: 'Successfully retrieved all grants', type: [GrantResponseDto] }) @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) + @ApiResponse({ status: 403, description: 'Forbidden - User does not have access to this resource' }) @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) async getAllGrants(): Promise { this.logger.log('GET /grant - Retrieving all grants'); @@ -33,6 +34,7 @@ export class GrantController { @ApiBody({ type: InactivateGrantBody, description: 'Array of grant IDs to inactivate' }) @ApiResponse({ status: 200, description: 'Successfully inactivated grants', type: [GrantResponseDto] }) @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) + @ApiResponse({ status: 403, description: 'Forbidden - User does not have access to this resource' }) @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) async inactivate( @Body() body: InactivateGrantBody @@ -56,6 +58,7 @@ export class GrantController { @ApiResponse({ status: 201, description: 'Successfully created grant', type: Number, example: 1234567890 }) @ApiResponse({ status: 400, description: 'Bad Request - Invalid grant data', example: '{Error encountered}' }) @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) + @ApiResponse({ status: 403, description: 'Forbidden - User does not have access to this resource' }) @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) async addGrant( @Body(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })) @@ -75,6 +78,7 @@ export class GrantController { @ApiResponse({ status: 200, description: 'Successfully updated grant', type: String, example: '{"Attributes": {...}}' }) @ApiResponse({ status: 400, description: 'Bad Request - Invalid grant data', example: '{Error encountered}' }) @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) + @ApiResponse({ status: 403, description: 'Forbidden - User does not have access to this resource' }) @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) async saveGrant(@Body() grantData: UpdateGrantBody): Promise { this.logger.log(`PUT /grant/save - Updating grant with ID: ${grantData.grantId}`); @@ -91,6 +95,7 @@ export class GrantController { @ApiResponse({ status: 200, description: 'Successfully deleted grant', type: String, example: 'Grant 1234567890 deleted successfully' }) @ApiResponse({ status: 400, description: 'Bad Request - Grant does not exist', example: '{Error encountered}' }) @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) + @ApiResponse({ status: 403, description: 'Forbidden - User does not have access to this resource' }) @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) async deleteGrant(@Param('grantId') grantId: number): Promise { this.logger.log(`DELETE /grant/${grantId} - Deleting grant`); @@ -107,6 +112,7 @@ export class GrantController { @ApiResponse({ status: 200, description: 'Successfully retrieved grant', type: GrantResponseDto }) @ApiResponse({ status: 404, description: 'Grant not found', example: '{Error encountered}' }) @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) + @ApiResponse({ status: 403, description: 'Forbidden - User does not have access to this resource' }) @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) async getGrantById(@Param('id') GrantId: string): Promise { this.logger.log(`GET /grant/${GrantId} - Retrieving grant by ID`); diff --git a/backend/src/grant/grant.service.ts b/backend/src/grant/grant.service.ts index 419cdb8..9903fb8 100644 --- a/backend/src/grant/grant.service.ts +++ b/backend/src/grant/grant.service.ts @@ -257,11 +257,20 @@ export class GrantService { // Creates notifications for a grant's application and report deadlines private async createGrantNotifications(grant: Grant, userId: string) { const { grantId, organization, application_deadline, report_deadlines } = grant; - + this.logger.log( + `Creating notifications for grant ${grantId} (${organization}) for user ${userId}`, + ); + // Application deadline notifications if (application_deadline) { + this.logger.debug( + `Creating application deadline notifications for grant ${grantId} with deadline ${application_deadline}`, + ); const alertTimes = this.getNotificationTimes(application_deadline); for (const alertTime of alertTimes) { + this.logger.debug( + `Creating application notification for grant ${grantId} at alertTime ${alertTime}`, + ); const message = `Application due in ${this.daysUntil(alertTime, application_deadline)} days for ${organization}`; const notification: Notification = { notificationId: `${grantId}-app`, @@ -272,13 +281,23 @@ export class GrantService { }; await this.notificationService.createNotification(notification); } + } else { + this.logger.debug( + `No application_deadline found for grant ${grantId}; skipping application notifications`, + ); } - + // Report deadlines notifications if (report_deadlines && Array.isArray(report_deadlines)) { + this.logger.debug( + `Creating report deadline notifications for grant ${grantId} with ${report_deadlines.length} report_deadlines`, + ); for (const reportDeadline of report_deadlines) { const alertTimes = this.getNotificationTimes(reportDeadline); for (const alertTime of alertTimes) { + this.logger.debug( + `Creating report notification for grant ${grantId} at alertTime ${alertTime} (report deadline ${reportDeadline})`, + ); const message = `Report due in ${this.daysUntil(alertTime, reportDeadline)} days for ${organization}`; const notification: Notification = { notificationId: `${grantId}-report`, @@ -290,50 +309,82 @@ export class GrantService { await this.notificationService.createNotification(notification); } } + } else { + this.logger.debug( + `No report_deadlines configured for grant ${grantId}; skipping report notifications`, + ); } + + this.logger.log( + `Finished creating notifications for grant ${grantId} (${organization}) for user ${userId}`, + ); } // Updates notifications for a grant's application and report deadlines private async updateGrantNotifications(grant: Grant) { const { grantId, organization, application_deadline, report_deadlines } = grant; - + this.logger.log( + `Updating notifications for grant ${grantId} (${organization})`, + ); + // Application notifications if (application_deadline) { + this.logger.debug( + `Updating application deadline notifications for grant ${grantId} with deadline ${application_deadline}`, + ); const alertTimes = this.getNotificationTimes(application_deadline); for (const alertTime of alertTimes) { const notificationId = `${grantId}-app`; const message = `Application due in ${this.daysUntil(alertTime, application_deadline)} days for ${organization}`; - + + this.logger.debug( + `Updating application notification ${notificationId} for grant ${grantId} to alertTime ${alertTime}`, + ); await this.notificationService.updateNotification(notificationId, { message, alertTime: alertTime as TDateISO, }); } + } else { + this.logger.debug( + `No application_deadline found for grant ${grantId}; skipping application notification updates`, + ); } - + // Report notifications if (report_deadlines && Array.isArray(report_deadlines)) { + this.logger.debug( + `Updating report deadline notifications for grant ${grantId} with ${report_deadlines.length} report_deadlines`, + ); for (const reportDeadline of report_deadlines) { const alertTimes = this.getNotificationTimes(reportDeadline); for (const alertTime of alertTimes) { const notificationId = `${grantId}-report`; const message = `Report due in ${this.daysUntil(alertTime, reportDeadline)} days for ${organization}`; - + + this.logger.debug( + `Updating report notification ${notificationId} for grant ${grantId} to alertTime ${alertTime} (report deadline ${reportDeadline})`, + ); await this.notificationService.updateNotification(notificationId, { message, alertTime: alertTime as TDateISO, }); } } + } else { + this.logger.debug( + `No report_deadlines configured for grant ${grantId}; skipping report notification updates`, + ); } + + this.logger.log( + `Finished updating notifications for grant ${grantId} (${organization})`, + ); } - + // Calculates the number of days between an alert time and deadline private daysUntil(alertTime: string, deadline: string): number { const diffMs = +new Date(deadline) - +new Date(alertTime); return Math.round(diffMs / (1000 * 60 * 60 * 24)); } - - - } \ No newline at end of file