From 5d21ecfcdd65f54fcfb24e87bedfe9f9a82ebf04 Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Fri, 4 Nov 2022 12:44:07 -0400 Subject: [PATCH 1/9] fix: fix account, settings, and search functionalities --- .gitignore | 1 + src/controllers/account.controller.ts | 1 + src/controllers/hacker.controller.ts | 105 +++++++++++++++--------- src/controllers/search.controller.ts | 4 +- src/controllers/setting.controller.ts | 46 +++++++---- src/controllers/sponsor.controller.ts | 72 +++++++++++----- src/middlewares/validator.middleware.ts | 6 +- src/models/account.model.ts | 4 +- src/models/hacker.model.ts | 11 ++- src/services/hacker.service.ts | 4 +- src/services/search.service.ts | 34 ++++---- src/services/setting.service.ts | 38 ++++++++- 12 files changed, 219 insertions(+), 107 deletions(-) diff --git a/.gitignore b/.gitignore index d6778586..a772488d 100755 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules # All Environment Files +*.env .env.* # Google Cloud Platform Credentials diff --git a/src/controllers/account.controller.ts b/src/controllers/account.controller.ts index be8086f3..c858586f 100644 --- a/src/controllers/account.controller.ts +++ b/src/controllers/account.controller.ts @@ -202,6 +202,7 @@ export class AccountController { ) { //TODO - Implement resend e-mail confirmation and verification. //TODO - A thrifty user can update their password from here and it would not be hashed, we should attempt to block. + delete update.password; const result = await this.accountService.update(identifier, update); return result diff --git a/src/controllers/hacker.controller.ts b/src/controllers/hacker.controller.ts index 2b1da149..8f1d1d4b 100644 --- a/src/controllers/hacker.controller.ts +++ b/src/controllers/hacker.controller.ts @@ -23,6 +23,7 @@ import { import { StorageService } from "@services/storage.service"; import { upload } from "@middlewares/multer.middleware"; import { Validator } from "@app/middlewares/validator.middleware"; +import { HackerStatus } from "@app/constants/general.constant"; @autoInjectable() @Controller("/hacker") @@ -30,7 +31,7 @@ export class HackerController { constructor( private readonly hackerService: HackerService, private readonly storageService: StorageService - ) {} + ) { } @Get("/self", [ EnsureAuthenticated, @@ -46,18 +47,18 @@ export class HackerController { const hacker: | Hacker | undefined = await this.hackerService.findByIdentifier( - //@ts-ignore - request.user?.identifier - ); + //@ts-ignore + request.user?.identifier + ); return hacker ? response.status(200).json({ - message: SuccessConstants.HACKER_READ, - data: hacker - }) + message: SuccessConstants.HACKER_READ, + data: hacker + }) : response.status(404).json({ - message: ErrorConstants.HACKER_404_MESSAGE - }); + message: ErrorConstants.HACKER_404_MESSAGE + }); } @Get("/:identifier", [ @@ -77,12 +78,12 @@ export class HackerController { return hacker ? response.status(200).json({ - message: SuccessConstants.HACKER_READ, - data: hacker - }) + message: SuccessConstants.HACKER_READ, + data: hacker + }) : response.status(404).json({ - message: ErrorConstants.HACKER_404_MESSAGE - }); + message: ErrorConstants.HACKER_404_MESSAGE + }); } @Post("/", [EnsureAuthenticated, Validator(Hacker)]) @@ -92,16 +93,17 @@ export class HackerController { ) { //TODO - Check if applications are open when hacker is created. //TODO - Fix bug where Hacker status is None as it is passed into the API. (Maybe override the status variable somehow?) + hacker.status = HackerStatus.Applied; const result: Hacker = await this.hackerService.save(hacker); return result ? response.status(200).send({ - message: SuccessConstants.HACKER_CREATE, - data: result - }) + message: SuccessConstants.HACKER_CREATE, + data: result + }) : response.status(422).send({ - message: ErrorConstants.ACCOUNT_DUPLICATE_422_MESSAGE - }); + message: ErrorConstants.ACCOUNT_DUPLICATE_422_MESSAGE + }); } @Patch("/:identifier", [ @@ -117,19 +119,20 @@ export class HackerController { @Params("identifier") identifier: number, @Body() update: Partial ) { + delete update.status; const result = await this.hackerService.update(identifier, update); return result ? response.status(200).json({ - message: SuccessConstants.HACKER_UPDATE, - data: result - }) + message: SuccessConstants.HACKER_UPDATE, + data: result + }) : response.status(404).json({ - message: ErrorConstants.HACKER_404_MESSAGE, - data: { - identifier: identifier - } - }); + message: ErrorConstants.HACKER_404_MESSAGE, + data: { + identifier: identifier + } + }); } @Get("/resume/:identifier", [ @@ -153,15 +156,15 @@ export class HackerController { resume ? response.status(200).send({ - message: SuccessConstants.RESUME_DOWNLOAD, - data: { - identifier: identifier, - resume: resume - } - }) + message: SuccessConstants.RESUME_DOWNLOAD, + data: { + identifier: identifier, + resume: resume + } + }) : response.status(404).send({ - message: ErrorConstants.RESUME_404_MESSAGE - }); + message: ErrorConstants.RESUME_404_MESSAGE + }); } @Post("/resume/:identifier", [ @@ -170,7 +173,7 @@ export class HackerController { AuthorizationLevel.Staff, AuthorizationLevel.Hacker ]), - upload.single("file") + upload.single("resume") ]) async uploadResume( @Request() request: ExpressRequest, @@ -189,8 +192,8 @@ export class HackerController { //TODO - Implement affectedRows > 0 success check. const result = await this.hackerService.updateApplicationField( identifier, - "general.URL.resume", - request.file + "{general,URL,resume}", + fileName ); response.status(200).send({ @@ -199,6 +202,32 @@ export class HackerController { }); } } + + @Patch("/status/:identifier", [ + EnsureAuthenticated, + EnsureAuthorization([ + AuthorizationLevel.Staff + ]), + ]) + async updateStatus( + @Params("identifier") identifier: number, + @Body("status") status: string, + @Response() response: ExpressResponse, + ) { + const result = await this.hackerService.update(identifier, { status }); + + return result + ? response.status(200).json({ + message: SuccessConstants.HACKER_UPDATE, + data: result + }) + : response.status(404).json({ + message: ErrorConstants.HACKER_404_MESSAGE, + data: { + identifier: identifier + } + }); + } } //TODO - Implement statistics features, batch accept/application change features, and status change emails. diff --git a/src/controllers/search.controller.ts b/src/controllers/search.controller.ts index 1ae95300..ae0d84f6 100644 --- a/src/controllers/search.controller.ts +++ b/src/controllers/search.controller.ts @@ -23,9 +23,9 @@ export class SearchController { async execute( @Response() response: ExpressResponse, @Query("model") model: string, - @Body("filters") filters: Array + @Query("q") filters: string ) { - const result = await this.searchService.executeQuery(model, filters); + const result = await this.searchService.executeQuery(model, JSON.parse(filters)); response.status(200).send({ message: diff --git a/src/controllers/setting.controller.ts b/src/controllers/setting.controller.ts index 4fb9c137..c92687f4 100644 --- a/src/controllers/setting.controller.ts +++ b/src/controllers/setting.controller.ts @@ -21,7 +21,7 @@ import { Validator } from "@app/middlewares/validator.middleware"; @autoInjectable() @Controller("/settings") export class SettingController { - constructor(private readonly settingService: SettingService) {} + constructor(private readonly settingService: SettingService) { } @Get("/") async getAll(@Response() response: ExpressResponse) { @@ -55,12 +55,26 @@ export class SettingController { return result ? response.status(200).send({ - message: SuccessConstants.SETTINGS_POST, - data: result - }) + message: SuccessConstants.SETTINGS_POST, + data: result + }) : response.status(422).send({ - message: ErrorConstants.SETTINGS_422_MESSAGE - }); + message: ErrorConstants.SETTINGS_422_MESSAGE + }); + } + + @Patch("", [ + EnsureAuthenticated, + EnsureAuthorization([AuthorizationLevel.Staff]), + ]) + async update( + @Response() response: ExpressResponse, + @Body() settings: { [setting: string]: string | number | boolean } + ) { + await this.settingService.update(settings); + return response.status(200).json({ + message: SuccessConstants.SETTINGS_PATCH + }) } @Patch("/:identifier", [ @@ -68,23 +82,23 @@ export class SettingController { EnsureAuthorization([AuthorizationLevel.Staff]), Validator(Setting) ]) - async update( + async updateOne( @Response() response: ExpressResponse, @Params("identifier") identifier: number, @Body() setting: Partial ) { - const result = await this.settingService.update(identifier, setting); + const result = await this.settingService.updateOne(identifier, setting); return result ? response.status(200).json({ - message: SuccessConstants.SETTINGS_PATCH, - data: result - }) + message: SuccessConstants.SETTINGS_PATCH, + data: result + }) : response.status(404).json({ - message: ErrorConstants.SETTINGS_404_MESSAGE, - data: { - identifier: identifier - } - }); + message: ErrorConstants.SETTINGS_404_MESSAGE, + data: { + identifier: identifier + } + }); } } diff --git a/src/controllers/sponsor.controller.ts b/src/controllers/sponsor.controller.ts index 4f9a5581..605ad572 100644 --- a/src/controllers/sponsor.controller.ts +++ b/src/controllers/sponsor.controller.ts @@ -5,6 +5,7 @@ import { Params, Patch, Post, + Request, Response } from "@decorators/express"; import { autoInjectable } from "tsyringe"; @@ -13,7 +14,7 @@ import { EnsureAuthenticated } from "@middlewares/authenticated.middleware"; import { EnsureAuthorization } from "@middlewares/authorization.middleware"; import Sponsor from "@models/sponsor.model"; import { SponsorService } from "@services/sponsor.service"; -import { Response as ExpressResponse } from "express"; +import { Request as ExpressRequest, Response as ExpressResponse } from "express"; import * as SuccessConstants from "@constants/success.constant"; import * as ErrorConstants from "@constants/error.constant"; import { Validator } from "@app/middlewares/validator.middleware"; @@ -21,7 +22,34 @@ import { Validator } from "@app/middlewares/validator.middleware"; @autoInjectable() @Controller("/sponsor") export class SponsorController { - constructor(private readonly sponsorService: SponsorService) {} + constructor(private readonly sponsorService: SponsorService) { } + + @Get("/self", [ + EnsureAuthenticated, + EnsureAuthorization([ + AuthorizationLevel.Staff, + AuthorizationLevel.Sponsor + ]) + ]) + async getSelf( + @Request() request: ExpressRequest, + @Response() response: ExpressResponse, + ) { + const sponsor: + | Sponsor + | undefined = await this.sponsorService.findByIdentifier( + //@ts-ignore + request.user?.identifier + ); + return sponsor + ? response.status(200).json({ + message: SuccessConstants.SPONSOR_READ, + data: sponsor + }) + : response.status(404).json({ + message: ErrorConstants.SPONSOR_404_MESSAGE + }); + } @Get("/:identifier", [ EnsureAuthenticated, @@ -37,17 +65,17 @@ export class SponsorController { const sponsor: | Sponsor | undefined = await this.sponsorService.findByIdentifier( - identifier - ); + identifier + ); return sponsor ? response.status(200).json({ - message: SuccessConstants.SPONSOR_READ, - data: sponsor - }) + message: SuccessConstants.SPONSOR_READ, + data: sponsor + }) : response.status(404).json({ - message: ErrorConstants.SPONSOR_404_MESSAGE - }); + message: ErrorConstants.SPONSOR_404_MESSAGE + }); } @Post("/", [ @@ -63,12 +91,12 @@ export class SponsorController { return result ? response.status(200).send({ - message: SuccessConstants.SPONSOR_CREATE, - data: result - }) + message: SuccessConstants.SPONSOR_CREATE, + data: result + }) : response.status(422).send({ - message: ErrorConstants.ACCOUNT_DUPLICATE_422_MESSAGE - }); + message: ErrorConstants.ACCOUNT_DUPLICATE_422_MESSAGE + }); } @Patch("/:identifier", [ @@ -88,14 +116,14 @@ export class SponsorController { return result ? response.status(200).json({ - message: SuccessConstants.SPONSOR_UPDATE, - data: result - }) + message: SuccessConstants.SPONSOR_UPDATE, + data: result + }) : response.status(404).json({ - message: ErrorConstants.SPONSOR_404_MESSAGE, - data: { - identifier: identifier - } - }); + message: ErrorConstants.SPONSOR_404_MESSAGE, + data: { + identifier: identifier + } + }); } } diff --git a/src/middlewares/validator.middleware.ts b/src/middlewares/validator.middleware.ts index 2fc9a50b..6647f5f0 100644 --- a/src/middlewares/validator.middleware.ts +++ b/src/middlewares/validator.middleware.ts @@ -1,6 +1,6 @@ import { Middleware } from "@decorators/express"; -import { plainToClass } from "class-transformer"; -import { validateOrReject } from "class-validator"; +import { plainToInstance } from "class-transformer"; +import { validate, validateOrReject } from "class-validator"; import { Request, Response, NextFunction } from "express"; import { ParamsDictionary } from "express-serve-static-core"; import { ParsedQs } from "qs"; @@ -20,7 +20,7 @@ export function Validator(model: any): any { next: NextFunction ): Promise { await validateOrReject( - plainToClass(model, request.body), + plainToInstance(model, request.body), request.method === "PATCH" ? { skipMissingProperties: true } : {} diff --git a/src/models/account.model.ts b/src/models/account.model.ts index 016e2583..1fe6e253 100644 --- a/src/models/account.model.ts +++ b/src/models/account.model.ts @@ -55,11 +55,11 @@ class Account { @IsEnum(UserType) accountType: string; - @Column("date", { nullable: false }) + @Column("timestamp", { nullable: false }) birthDate: Date; @Column() - @IsPhoneNumber() + // @IsPhoneNumber() phoneNumber: string; toJSON() { diff --git a/src/models/hacker.model.ts b/src/models/hacker.model.ts index 0de1df30..9f4d64cb 100644 --- a/src/models/hacker.model.ts +++ b/src/models/hacker.model.ts @@ -1,14 +1,17 @@ import { HackerStatus } from "@constants/general.constant"; -import { Column, Entity, JoinColumn, ManyToOne, OneToOne } from "typeorm"; +import { Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn } from "typeorm"; import Account from "@models/account.model"; import { ApplicationSchema } from "@models/application.model"; import Team from "@models/team.model"; @Entity() class Hacker { - @OneToOne(() => Account, { primary: true, cascade: true }) - @JoinColumn({ name: "identifier" }) - account: Account; + @PrimaryGeneratedColumn() + readonly identifier: number; + + @OneToOne(() => Account) + @JoinColumn() + readonly account: Account; @Column({ enum: HackerStatus, diff --git a/src/services/hacker.service.ts b/src/services/hacker.service.ts index 3a9f4a86..0b2c24a4 100644 --- a/src/services/hacker.service.ts +++ b/src/services/hacker.service.ts @@ -43,14 +43,14 @@ export class HackerService { public async updateApplicationField( identifier: number, - key: string, + path: string, value: any ): Promise { return await this.hackerRepository .createQueryBuilder() .update() .set({ - application: () => `jsonb_set(application,${key},${value})` + application: () => `jsonb_set(application, '${path}', '"${value}"')` }) .where("identifier = :identifier", { identifier: identifier diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 9fc5d432..9b267256 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -2,9 +2,9 @@ import { singleton } from "tsyringe"; import { Connection, getConnection } from "typeorm"; export interface Filter { - parameter: string; + param: string; operation: Operation; - value: string; + value: string | string[]; } enum Operation { @@ -23,6 +23,11 @@ export class SearchService { this.connection = getConnection(); } + public parseParam(param: string) { + const path = param.split('.'); + return path[0] + '->' + path.slice(1, path.length - 1).map((s) => `'${s}'`).join('->') + "->>" + `'${path[path.length - 1]}'`; + } + public async executeQuery( model: string, query: Array @@ -31,30 +36,29 @@ export class SearchService { const builder = this.connection .getRepository(metadata) .createQueryBuilder(model) - .loadAllRelationIds(); + .leftJoinAndSelect("hacker.account", "account"); - query.forEach(({ parameter, operation, value }: Filter) => { - switch (operation) { + query.forEach(({ param, operation, value }: Filter) => { + param = this.parseParam(param); + switch (operation.toUpperCase()) { case Operation.Equal: case Operation.In: - builder.andWhere(`:parameter :operation :value`, { - parameter: parameter, - operation: operation, - value: value + builder.andWhere(`${param} ${operation} (:...value)`, { + value }); break; case Operation.Like: - builder.andWhere(`:parameter :operation %:value%`, { - parameter: parameter, - operation: operation, - value: value + builder.andWhere(`${param} ${operation} %:value%`, { + param, + operation, + value }); break; case Operation.Limit: - builder.skip(Number.parseInt(value)); + builder.skip(Number.parseInt(value as string)); break; case Operation.Skip: - builder.skip(Number.parseInt(value)); + builder.skip(Number.parseInt(value as string)); break; } }); diff --git a/src/services/setting.service.ts b/src/services/setting.service.ts index e011aaba..10bfc5c9 100644 --- a/src/services/setting.service.ts +++ b/src/services/setting.service.ts @@ -8,8 +8,27 @@ export class SettingService { this.settingRepository = getRepository(Setting); } - public async find(): Promise> { - return await this.settingRepository.find(); + public flattenOne(setting: Setting) { + const key = setting.key; + let value: string | number | boolean = setting.value; + if (!isNaN(+value)) { + value = +value; + } else if (value == "true") { + value = true; + } else if (value == "false") { + value = false; + } + return { [key]: value } + } + + public flatten(settings: Array): { [setting: string]: string | number | boolean } { + const flattened = {}; + settings.forEach((setting) => Object.assign(flattened, this.flattenOne(setting))) + return flattened; + } + + public async find(): Promise { + return this.flatten(await this.settingRepository.find()); } public async findByIdentifier( @@ -28,7 +47,20 @@ export class SettingService { return await this.settingRepository.save(setting); } - public async update( + public async update(settings: { [setting: string]: string | number | boolean }) { + for (let [key, value] of Object.entries(settings)) { + value = value.toString(); + const setting = await this.findByKey(key); + if (setting) { + setting.value = value.toString(); + await this.settingRepository.save(setting); + } else { + await this.settingRepository.save({key, value}); + } + } + } + + public async updateOne( identifier: number, setting: Partial ): Promise { From 2ea2305d8f3c164316b2d51f6838467f0ca8c319 Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Fri, 4 Nov 2022 13:09:22 -0400 Subject: [PATCH 2/9] fix: fix search with multiple values --- src/services/search.service.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 9b267256..0adac2ed 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -23,6 +23,10 @@ export class SearchService { this.connection = getConnection(); } + public parseValueList(values: string[]) { + return "(" + values.map((value) => `'${value}'`) + ")"; + } + public parseParam(param: string) { const path = param.split('.'); return path[0] + '->' + path.slice(1, path.length - 1).map((s) => `'${s}'`).join('->') + "->>" + `'${path[path.length - 1]}'`; @@ -43,9 +47,7 @@ export class SearchService { switch (operation.toUpperCase()) { case Operation.Equal: case Operation.In: - builder.andWhere(`${param} ${operation} (:...value)`, { - value - }); + builder.andWhere(`${param} ${operation} ${this.parseValueList(value as string[])}`); break; case Operation.Like: builder.andWhere(`${param} ${operation} %:value%`, { @@ -62,7 +64,6 @@ export class SearchService { break; } }); - return builder.getMany(); } } From 6d0a7d539ec487bf292bfff9f892513ab12f4a35 Mon Sep 17 00:00:00 2001 From: Anmol Brar Date: Fri, 18 Nov 2022 17:22:18 -0500 Subject: [PATCH 3/9] fix: hacker application submission --- src/controllers/hacker.controller.ts | 101 ++++++++++++++------------- src/models/hacker.model.ts | 15 ++-- src/services/storage.service.ts | 2 - 3 files changed, 62 insertions(+), 56 deletions(-) diff --git a/src/controllers/hacker.controller.ts b/src/controllers/hacker.controller.ts index 8f1d1d4b..4cf0b84e 100644 --- a/src/controllers/hacker.controller.ts +++ b/src/controllers/hacker.controller.ts @@ -24,14 +24,18 @@ import { StorageService } from "@services/storage.service"; import { upload } from "@middlewares/multer.middleware"; import { Validator } from "@app/middlewares/validator.middleware"; import { HackerStatus } from "@app/constants/general.constant"; +import { AccountService } from "@app/services/account.service"; +import Account from "@app/models/account.model"; +import { QueryFailedError } from "typeorm"; @autoInjectable() @Controller("/hacker") export class HackerController { constructor( private readonly hackerService: HackerService, - private readonly storageService: StorageService - ) { } + private readonly storageService: StorageService, + private readonly accountService: AccountService + ) {} @Get("/self", [ EnsureAuthenticated, @@ -47,18 +51,18 @@ export class HackerController { const hacker: | Hacker | undefined = await this.hackerService.findByIdentifier( - //@ts-ignore - request.user?.identifier - ); + //@ts-ignore + request.user?.identifier + ); return hacker ? response.status(200).json({ - message: SuccessConstants.HACKER_READ, - data: hacker - }) + message: SuccessConstants.HACKER_READ, + data: hacker + }) : response.status(404).json({ - message: ErrorConstants.HACKER_404_MESSAGE - }); + message: ErrorConstants.HACKER_404_MESSAGE + }); } @Get("/:identifier", [ @@ -78,12 +82,12 @@ export class HackerController { return hacker ? response.status(200).json({ - message: SuccessConstants.HACKER_READ, - data: hacker - }) + message: SuccessConstants.HACKER_READ, + data: hacker + }) : response.status(404).json({ - message: ErrorConstants.HACKER_404_MESSAGE - }); + message: ErrorConstants.HACKER_404_MESSAGE + }); } @Post("/", [EnsureAuthenticated, Validator(Hacker)]) @@ -98,12 +102,12 @@ export class HackerController { return result ? response.status(200).send({ - message: SuccessConstants.HACKER_CREATE, - data: result - }) + message: SuccessConstants.HACKER_CREATE, + data: result + }) : response.status(422).send({ - message: ErrorConstants.ACCOUNT_DUPLICATE_422_MESSAGE - }); + message: ErrorConstants.ACCOUNT_DUPLICATE_422_MESSAGE + }); } @Patch("/:identifier", [ @@ -117,22 +121,21 @@ export class HackerController { async update( @Response() response: ExpressResponse, @Params("identifier") identifier: number, - @Body() update: Partial + @Body() update: Hacker ) { - delete update.status; const result = await this.hackerService.update(identifier, update); return result ? response.status(200).json({ - message: SuccessConstants.HACKER_UPDATE, - data: result - }) + message: SuccessConstants.HACKER_UPDATE, + data: result + }) : response.status(404).json({ - message: ErrorConstants.HACKER_404_MESSAGE, - data: { - identifier: identifier - } - }); + message: ErrorConstants.HACKER_404_MESSAGE, + data: { + identifier: identifier + } + }); } @Get("/resume/:identifier", [ @@ -156,15 +159,15 @@ export class HackerController { resume ? response.status(200).send({ - message: SuccessConstants.RESUME_DOWNLOAD, - data: { - identifier: identifier, - resume: resume - } - }) + message: SuccessConstants.RESUME_DOWNLOAD, + data: { + identifier: identifier, + resume: resume + } + }) : response.status(404).send({ - message: ErrorConstants.RESUME_404_MESSAGE - }); + message: ErrorConstants.RESUME_404_MESSAGE + }); } @Post("/resume/:identifier", [ @@ -205,28 +208,26 @@ export class HackerController { @Patch("/status/:identifier", [ EnsureAuthenticated, - EnsureAuthorization([ - AuthorizationLevel.Staff - ]), + EnsureAuthorization([AuthorizationLevel.Staff]) ]) async updateStatus( @Params("identifier") identifier: number, @Body("status") status: string, - @Response() response: ExpressResponse, + @Response() response: ExpressResponse ) { const result = await this.hackerService.update(identifier, { status }); return result ? response.status(200).json({ - message: SuccessConstants.HACKER_UPDATE, - data: result - }) + message: SuccessConstants.HACKER_UPDATE, + data: result + }) : response.status(404).json({ - message: ErrorConstants.HACKER_404_MESSAGE, - data: { - identifier: identifier - } - }); + message: ErrorConstants.HACKER_404_MESSAGE, + data: { + identifier: identifier + } + }); } } diff --git a/src/models/hacker.model.ts b/src/models/hacker.model.ts index 9f4d64cb..e071de1d 100644 --- a/src/models/hacker.model.ts +++ b/src/models/hacker.model.ts @@ -1,16 +1,23 @@ import { HackerStatus } from "@constants/general.constant"; -import { Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn } from "typeorm"; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToOne, + PrimaryColumn +} from "typeorm"; import Account from "@models/account.model"; import { ApplicationSchema } from "@models/application.model"; import Team from "@models/team.model"; @Entity() class Hacker { - @PrimaryGeneratedColumn() - readonly identifier: number; + @PrimaryColumn() + identifier: number; @OneToOne(() => Account) - @JoinColumn() + @JoinColumn({ name: "identifier" }) readonly account: Account; @Column({ diff --git a/src/services/storage.service.ts b/src/services/storage.service.ts index 338df912..a8597519 100644 --- a/src/services/storage.service.ts +++ b/src/services/storage.service.ts @@ -16,8 +16,6 @@ export class StorageService { } catch (error) { loggerService.getLogger().error(error); } - if (process.env.NODE_ENV !== "production") - this.bucket = this.storage.bucket(this.bucketName); } /** From 6d3e854d6204cf6634d029bc02b480ab2632300e Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Sat, 19 Nov 2022 13:00:43 -0500 Subject: [PATCH 4/9] fix: fix account confirmation --- src/models/account-confirmation-token.model.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/models/account-confirmation-token.model.ts b/src/models/account-confirmation-token.model.ts index d6ca5543..a5fd29e5 100644 --- a/src/models/account-confirmation-token.model.ts +++ b/src/models/account-confirmation-token.model.ts @@ -4,14 +4,15 @@ import { Column, JoinColumn, OneToOne, - PrimaryGeneratedColumn + PrimaryGeneratedColumn, + PrimaryColumn } from "typeorm"; import Account from "@models/account.model"; import * as GeneralConstants from "@constants/general.constant"; @Entity() class AccountConfirmation { - @PrimaryGeneratedColumn() + @PrimaryColumn() readonly identifier: number; @OneToOne(() => Account) From cc012e26a9cfa3831f1c70d7f1549524bb605af8 Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Sat, 19 Nov 2022 15:11:19 -0500 Subject: [PATCH 5/9] fix: fix email confirmation and invites --- src/controllers/account.controller.ts | 126 ++++++++++--------- src/models/invitation.model.ts | 22 ++++ src/services/account-confirmation.service.ts | 2 +- src/services/invitation.service.ts | 30 +++++ 4 files changed, 119 insertions(+), 61 deletions(-) create mode 100644 src/models/invitation.model.ts create mode 100644 src/services/invitation.service.ts diff --git a/src/controllers/account.controller.ts b/src/controllers/account.controller.ts index c858586f..f94ffdeb 100644 --- a/src/controllers/account.controller.ts +++ b/src/controllers/account.controller.ts @@ -28,6 +28,7 @@ import { join } from "path"; import { Validator } from "@middlewares/validator.middleware"; import AccountConfirmation from "@models/account-confirmation-token.model"; import * as jwt from "jsonwebtoken"; +import { InvitationService } from "@app/services/invitation.service"; @autoInjectable() @Controller("/account") @@ -35,9 +36,74 @@ export class AccountController { constructor( private readonly accountService: AccountService, private readonly accountConfirmationService: AccountConfirmationService, + private readonly invitationService: InvitationService, private readonly mailer: EmailService ) {} + @Post("/invite", [ + EnsureAuthenticated, + EnsureAuthorization([AuthorizationLevel.Staff]) + ]) + async createWithInvite( + @Request() request: ExpressRequest, + @Response() response: ExpressResponse, + @Body("email") email: string, + @Body("accountType") accountType: string + ) { + const inviter = await this.accountService.findByIdentifier( + //@ts-ignore + request.user?.identifier + ); + + if (inviter) { + const invitation = await this.invitationService.save({email, accountType, inviter}); + await this.mailer.send( + { + to: invitation.email, + subject: "Account Confirmation Instructions", + html: join( + __dirname, + "../assets/email/AccountConfirmation.mjml" + ) + }, + { + link: this.accountConfirmationService.generateLink( + "confirm", + this.invitationService.generateToken(invitation) + ) + }, + (error?: any) => { + if (error) + response.status(500).send({ + message: ErrorConstants.EMAIL_500_MESSAGE, + data: error + }); + } + ); + return response.status(200).send({ + message: SuccessConstants.ACCOUNT_INVITE, + data: {} + }); + } else { + return response.status(404).json({ + message: ErrorConstants.ACCOUNT_404_MESSAGE, + }); + } + } + + @Get("/invite", [ + EnsureAuthenticated, + EnsureAuthorization([AuthorizationLevel.Staff]) + ]) + async getInvited(@Response() response: ExpressResponse) { + const result = await this.invitationService.find(); + + response.status(200).json({ + message: SuccessConstants.ACCOUNT_READ, + data: result + }); + } + @Get("/", [ EnsureAuthenticated, EnsureAuthorization([ @@ -217,64 +283,4 @@ export class AccountController { } }); } - - @Post("/invite", [ - EnsureAuthenticated, - EnsureAuthorization([AuthorizationLevel.Staff]) - ]) - async createWithInvite( - @Response() response: ExpressResponse, - @Body("email") email: string, - @Body("accountType") accountType: string - ) { - const model = await this.accountConfirmationService.save({ - email: email, - accountType: accountType - }); - - await this.mailer.send( - { - to: model.email, - subject: "Account Confirmation Instructions", - html: join( - __dirname, - "../assets/email/AccountConfirmation.mjml" - ) - }, - { - link: this.accountConfirmationService.generateLink( - "confirm", - this.accountConfirmationService.generateToken( - model.identifier, - model.account!.identifier - ) - ) - }, - (error?: any) => { - if (error) - response.status(500).send({ - message: ErrorConstants.EMAIL_500_MESSAGE, - data: error - }); - } - ); - - return response.status(200).send({ - message: SuccessConstants.ACCOUNT_INVITE, - data: {} - }); - } - - @Get("/invites", [ - EnsureAuthenticated, - EnsureAuthorization([AuthorizationLevel.Staff]) - ]) - async getInvited(@Response() response: ExpressResponse) { - const result: Array = await this.accountConfirmationService.find(); - - response.status(200).json({ - message: SuccessConstants.ACCOUNT_READ, - data: result - }); - } } diff --git a/src/models/invitation.model.ts b/src/models/invitation.model.ts new file mode 100644 index 00000000..3e94b48e --- /dev/null +++ b/src/models/invitation.model.ts @@ -0,0 +1,22 @@ +import { IsEmail } from "class-validator"; +import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm"; +import Account from "./account.model"; +import { UserType } from "@constants/general.constant"; + +@Entity() +class Invitation { + @PrimaryColumn() + @IsEmail() + email: string; + + @Column({ + enum: UserType, + default: UserType.Hacker + }) + accountType: string; + + @ManyToOne(() => Account) + inviter: Account; +} + +export default Invitation; \ No newline at end of file diff --git a/src/services/account-confirmation.service.ts b/src/services/account-confirmation.service.ts index ef4a1193..941ad850 100644 --- a/src/services/account-confirmation.service.ts +++ b/src/services/account-confirmation.service.ts @@ -49,7 +49,7 @@ export class AccountConfirmationService { } public generateLink(route: string, token: string): string { - return `${process.env.FRONTEND_ADDRESS_DEV}/${route}?token=${token}`; + return `${process.env.FRONTEND_ADDRESS}/account/${route}?token=${token}`; } public generateToken(identifier: number, account: number): string { diff --git a/src/services/invitation.service.ts b/src/services/invitation.service.ts new file mode 100644 index 00000000..63370e67 --- /dev/null +++ b/src/services/invitation.service.ts @@ -0,0 +1,30 @@ +import Invitation from "@app/models/invitation.model"; +import { autoInjectable, singleton } from "tsyringe"; +import { getRepository, Repository } from "typeorm"; +import jwt from "jsonwebtoken"; + +@autoInjectable() +@singleton() +export class InvitationService { + private readonly invitationRepository: Repository; + + constructor() { + this.invitationRepository = getRepository(Invitation); + } + + public async find() { + return await this.invitationRepository.find(); + } + + public async save(invitation: Invitation) { + return await this.invitationRepository.save(invitation); + } + + public generateLink(route: string, token: string): string { + return `${process.env.FRONTEND_ADDRESS}/${route}?token=${token}`; +} + + public generateToken(invitation: Invitation) { + return jwt.sign(invitation, process.env.JWT_ ?? "default", { expiresIn: "1 week" }); + } +} \ No newline at end of file From 82423cec81d639c79925ff1a7fe7ed111486c7a3 Mon Sep 17 00:00:00 2001 From: Rohan Agarwal <40421647+ronyboi@users.noreply.github.com> Date: Wed, 1 Feb 2023 11:47:38 -0500 Subject: [PATCH 6/9] fix: team api fixes for creating and deleting a team --- src/models/team.model.ts | 10 ++++------ src/services/team.service.ts | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/models/team.model.ts b/src/models/team.model.ts index fa21247d..67284aa7 100644 --- a/src/models/team.model.ts +++ b/src/models/team.model.ts @@ -26,13 +26,11 @@ class Team { @JoinColumn() members: Array; - @Column({ nullable: true }) - @IsString() - submission?: string; + @Column({ type: String, nullable: true }) + submission?: string | null; - @Column({ nullable: true }) - @IsString() - project?: string; + @Column({ type: String, nullable: true }) + project?: string | null; } export default Team; diff --git a/src/services/team.service.ts b/src/services/team.service.ts index ed261074..1715cc78 100644 --- a/src/services/team.service.ts +++ b/src/services/team.service.ts @@ -56,11 +56,26 @@ export class TeamService { } public async removeMember(hacker: Hacker): Promise { + // hacker.team does return a number (it's the identifier). + // However, the underlying model dictates that team attribute of Hacker should be Team. + // I'm not sure why? I didn't want to change that incase it breaked something else. + // This throws a parsing error. But it will work. + const team = await this.findByIdentifier(hacker.team); + await this.teamRepository .createQueryBuilder("team") .relation(Hacker, "team") .of(hacker) .set({ team: null }); + + // If the person we're removing is the last one left on the team. Clean up teams as well. + if (team && team.members.length == 1) { + await this.teamRepository + .createQueryBuilder("team") + .delete() + .where("name = :name", { name: team.name }) + .execute(); + } } public async update( From 605f78279b3611cb8a6f01ca6e95aecef17a8dec Mon Sep 17 00:00:00 2001 From: Rohan Agarwal <40421647+ronyboi@users.noreply.github.com> Date: Thu, 27 Apr 2023 11:47:23 -0400 Subject: [PATCH 7/9] fix: account invites with correct acc types (#920) --- src/assets/email/AccountInvitation.mjml | 60 +++++++++++++ src/constants/authorization-level.constant.ts | 12 +-- src/constants/error.constant.ts | 88 +++++++++---------- src/controllers/account.controller.ts | 39 ++++---- src/controllers/search.controller.ts | 5 +- src/controllers/setting.controller.ts | 32 +++---- src/controllers/sponsor.controller.ts | 65 +++++++------- src/models/invitation.model.ts | 22 ++--- src/services/hacker.service.ts | 3 +- src/services/invitation.service.ts | 56 ++++++++---- src/services/search.service.ts | 19 +++- src/services/setting.service.ts | 16 ++-- 12 files changed, 264 insertions(+), 153 deletions(-) create mode 100644 src/assets/email/AccountInvitation.mjml diff --git a/src/assets/email/AccountInvitation.mjml b/src/assets/email/AccountInvitation.mjml new file mode 100644 index 00000000..b4bcd59c --- /dev/null +++ b/src/assets/email/AccountInvitation.mjml @@ -0,0 +1,60 @@ + + + We've got your application! + + + + + + + + + + Hey there! 👋 + + + + You've been invited to create an account on our sponsor + dashboard. On the sponsor dashboard you can find important + information about check-in, mentoring, judging, workshops, Discord, and + the schedule for the weekend. On the sponsor dashboard you will be able + to look up hacker applications and information. Additionally, if you + have resume access you will be able to download hacker resumes through + the dashboard. + + + + Sponsor check-in starts at 6:00 pm on Saturday, January 28th on the + McHacks Discord server and opening ceremonies will begin at 7:00 pm. Be + sure to get set up on the McHacks Discord server at https://discord.gg/XVSdW9pc + and contact your coordinator to set up your server roles. + + + + Get access to the sponsor dashboard by clicking on the button below. + + + + + Create your account + + + + + If you have any questions, feel free to reach out to your coordinator. + + + + McHacks Team +
+ mchacks.ca +
+
+
+ + +
+
diff --git a/src/constants/authorization-level.constant.ts b/src/constants/authorization-level.constant.ts index 791ed425..7ac13322 100644 --- a/src/constants/authorization-level.constant.ts +++ b/src/constants/authorization-level.constant.ts @@ -1,8 +1,8 @@ export enum AuthorizationLevel { - Staff = "Staff", - Sponsor = "Sponsor", - Volunteer = "Volunteer", - Hacker = "Hacker", - Account = "Account", - None = "None", + Staff = "Staff", + Sponsor = "Sponsor", + Volunteer = "Volunteer", + Hacker = "Hacker", + Account = "Account", + None = "None" } diff --git a/src/constants/error.constant.ts b/src/constants/error.constant.ts index 89a38941..f3228b30 100644 --- a/src/constants/error.constant.ts +++ b/src/constants/error.constant.ts @@ -13,7 +13,7 @@ const SPONSOR_ID_409_MESSAGE = "Conflict with sponsor accountId link"; const VOLUNTEER_ID_409_MESSAGE = "Conflict with volunteer accountId link"; const HACKER_ID_409_MESSAGE = "Conflict with hacker accountId link"; const TEAM_MEMBER_409_MESSAGE = - "Conflict with team member being in another team"; + "Conflict with team member being in another team"; const TEAM_NAME_409_MESSAGE = "Conflict with team name already in use"; const HACKER_STATUS_409_MESSAGE = "Conflict with hacker status"; const TEAM_SIZE_409_MESSAGE = "Team full"; @@ -24,7 +24,7 @@ const VALIDATION_422_MESSAGE = "Validation failed"; const ACCOUNT_DUPLICATE_422_MESSAGE = "Account already exists"; const ROLE_DUPLICATE_422_MESSAGE = "Role already exists"; const SETTINGS_422_MESSAGE = - "openTime must be before closeTime, and closeTime must be before confirmTime"; + "openTime must be before closeTime, and closeTime must be before confirmTime"; const ACCOUNT_TOKEN_401_MESSAGE = "Invalid token for account"; const AUTH_401_MESSAGE = "Invalid Authentication"; @@ -49,46 +49,46 @@ const ROLE_CREATE_500_MESSAGE = "Error while creating role"; const TRAVEL_CREATE_500_MESSAGE = "Error while creating travel"; export { - ACCOUNT_404_MESSAGE, - HACKER_404_MESSAGE, - TEAM_404_MESSAGE, - RESUME_404_MESSAGE, - ACCOUNT_TYPE_409_MESSAGE, - ACCOUNT_EMAIL_409_MESSAGE, - SPONSOR_ID_409_MESSAGE, - VOLUNTEER_ID_409_MESSAGE, - TEAM_MEMBER_409_MESSAGE, - TEAM_MEMBER_422_MESSAGE, - VALIDATION_422_MESSAGE, - ACCOUNT_TOKEN_401_MESSAGE, - AUTH_401_MESSAGE, - AUTH_403_MESSAGE, - ACCOUNT_403_MESSAGE, - TEAM_UPDATE_500_MESSAGE, - HACKER_UPDATE_500_MESSAGE, - HACKER_ID_409_MESSAGE, - ACCOUNT_UPDATE_500_MESSAGE, - HACKER_CREATE_500_MESSAGE, - SPONSOR_404_MESSAGE, - SPONSOR_CREATE_500_MESSAGE, - TEAM_CREATE_500_MESSAGE, - VOLUNTEER_CREATE_500_MESSAGE, - EMAIL_500_MESSAGE, - GENERIC_500_MESSAGE, - ACCOUNT_DUPLICATE_422_MESSAGE, - LOGIN_500_MESSAGE, - HACKER_STATUS_409_MESSAGE, - TEAM_SIZE_409_MESSAGE, - ROLE_DUPLICATE_422_MESSAGE, - SETTINGS_422_MESSAGE, - ROLE_CREATE_500_MESSAGE, - TEAM_NAME_409_MESSAGE, - TEAM_JOIN_SAME_409_MESSAGE, - TEAM_READ_500_MESSAGE, - VOLUNTEER_404_MESSAGE, - SPONSOR_UPDATE_500_MESSAGE, - SETTINGS_404_MESSAGE, - SETTINGS_403_MESSAGE, - TRAVEL_404_MESSAGE, - TRAVEL_CREATE_500_MESSAGE, + ACCOUNT_404_MESSAGE, + HACKER_404_MESSAGE, + TEAM_404_MESSAGE, + RESUME_404_MESSAGE, + ACCOUNT_TYPE_409_MESSAGE, + ACCOUNT_EMAIL_409_MESSAGE, + SPONSOR_ID_409_MESSAGE, + VOLUNTEER_ID_409_MESSAGE, + TEAM_MEMBER_409_MESSAGE, + TEAM_MEMBER_422_MESSAGE, + VALIDATION_422_MESSAGE, + ACCOUNT_TOKEN_401_MESSAGE, + AUTH_401_MESSAGE, + AUTH_403_MESSAGE, + ACCOUNT_403_MESSAGE, + TEAM_UPDATE_500_MESSAGE, + HACKER_UPDATE_500_MESSAGE, + HACKER_ID_409_MESSAGE, + ACCOUNT_UPDATE_500_MESSAGE, + HACKER_CREATE_500_MESSAGE, + SPONSOR_404_MESSAGE, + SPONSOR_CREATE_500_MESSAGE, + TEAM_CREATE_500_MESSAGE, + VOLUNTEER_CREATE_500_MESSAGE, + EMAIL_500_MESSAGE, + GENERIC_500_MESSAGE, + ACCOUNT_DUPLICATE_422_MESSAGE, + LOGIN_500_MESSAGE, + HACKER_STATUS_409_MESSAGE, + TEAM_SIZE_409_MESSAGE, + ROLE_DUPLICATE_422_MESSAGE, + SETTINGS_422_MESSAGE, + ROLE_CREATE_500_MESSAGE, + TEAM_NAME_409_MESSAGE, + TEAM_JOIN_SAME_409_MESSAGE, + TEAM_READ_500_MESSAGE, + VOLUNTEER_404_MESSAGE, + SPONSOR_UPDATE_500_MESSAGE, + SETTINGS_404_MESSAGE, + SETTINGS_403_MESSAGE, + TRAVEL_404_MESSAGE, + TRAVEL_CREATE_500_MESSAGE }; diff --git a/src/controllers/account.controller.ts b/src/controllers/account.controller.ts index f94ffdeb..48d823f2 100644 --- a/src/controllers/account.controller.ts +++ b/src/controllers/account.controller.ts @@ -29,6 +29,7 @@ import { Validator } from "@middlewares/validator.middleware"; import AccountConfirmation from "@models/account-confirmation-token.model"; import * as jwt from "jsonwebtoken"; import { InvitationService } from "@app/services/invitation.service"; +import Invitation from "@app/models/invitation.model"; @autoInjectable() @Controller("/account") @@ -56,20 +57,25 @@ export class AccountController { ); if (inviter) { - const invitation = await this.invitationService.save({email, accountType, inviter}); + const invitation = await this.invitationService.save({ + email, + accountType, + inviter + }); await this.mailer.send( { to: invitation.email, - subject: "Account Confirmation Instructions", + subject: "Account Creation Instructions", html: join( __dirname, - "../assets/email/AccountConfirmation.mjml" + "../assets/email/AccountInvitation.mjml" ) }, { - link: this.accountConfirmationService.generateLink( - "confirm", - this.invitationService.generateToken(invitation) + link: this.invitationService.generateLink( + "account/create", + this.invitationService.generateToken(invitation), + accountType ) }, (error?: any) => { @@ -86,7 +92,7 @@ export class AccountController { }); } else { return response.status(404).json({ - message: ErrorConstants.ACCOUNT_404_MESSAGE, + message: ErrorConstants.ACCOUNT_404_MESSAGE }); } } @@ -186,25 +192,24 @@ export class AccountController { async create( @Response() response: ExpressResponse, @Body() account: Account, - @Headers("X-Invite-Token") token?: string + // @Params("token") token?: string, + // @Params("accountType") accType?: string, + @Headers("token") token?: string ) { if (token) { - const data = jwt.verify( - token, - process.env.JWT_CONFIRM_ACC_SECRET! - ) as { - identifier: number; + const data = jwt.verify(token, process.env.JWT_INVITE_SECRET!) as { + invitation: Invitation; }; - const result = await this.accountConfirmationService.findByIdentifier( - data.identifier + const result = await this.invitationService.findByIdentifier( + data.email ); if (result) { account.confirmed = true; account.accountType = result.accountType; - this.accountConfirmationService.delete(result.identifier); + this.invitationService.delete(data.email); } } @@ -212,7 +217,7 @@ export class AccountController { if (result) { const model = await this.accountConfirmationService.save({ - accountType: GeneralConstants.HACKER, + accountType: result.accountType, email: result.email, confirmationType: GeneralConstants.CONFIRMATION_TYPE_ORGANIC, account: result diff --git a/src/controllers/search.controller.ts b/src/controllers/search.controller.ts index ae0d84f6..69c847dc 100644 --- a/src/controllers/search.controller.ts +++ b/src/controllers/search.controller.ts @@ -25,7 +25,10 @@ export class SearchController { @Query("model") model: string, @Query("q") filters: string ) { - const result = await this.searchService.executeQuery(model, JSON.parse(filters)); + const result = await this.searchService.executeQuery( + model, + JSON.parse(filters) + ); response.status(200).send({ message: diff --git a/src/controllers/setting.controller.ts b/src/controllers/setting.controller.ts index c92687f4..4ab2664d 100644 --- a/src/controllers/setting.controller.ts +++ b/src/controllers/setting.controller.ts @@ -21,7 +21,7 @@ import { Validator } from "@app/middlewares/validator.middleware"; @autoInjectable() @Controller("/settings") export class SettingController { - constructor(private readonly settingService: SettingService) { } + constructor(private readonly settingService: SettingService) {} @Get("/") async getAll(@Response() response: ExpressResponse) { @@ -55,17 +55,17 @@ export class SettingController { return result ? response.status(200).send({ - message: SuccessConstants.SETTINGS_POST, - data: result - }) + message: SuccessConstants.SETTINGS_POST, + data: result + }) : response.status(422).send({ - message: ErrorConstants.SETTINGS_422_MESSAGE - }); + message: ErrorConstants.SETTINGS_422_MESSAGE + }); } @Patch("", [ EnsureAuthenticated, - EnsureAuthorization([AuthorizationLevel.Staff]), + EnsureAuthorization([AuthorizationLevel.Staff]) ]) async update( @Response() response: ExpressResponse, @@ -74,7 +74,7 @@ export class SettingController { await this.settingService.update(settings); return response.status(200).json({ message: SuccessConstants.SETTINGS_PATCH - }) + }); } @Patch("/:identifier", [ @@ -91,14 +91,14 @@ export class SettingController { return result ? response.status(200).json({ - message: SuccessConstants.SETTINGS_PATCH, - data: result - }) + message: SuccessConstants.SETTINGS_PATCH, + data: result + }) : response.status(404).json({ - message: ErrorConstants.SETTINGS_404_MESSAGE, - data: { - identifier: identifier - } - }); + message: ErrorConstants.SETTINGS_404_MESSAGE, + data: { + identifier: identifier + } + }); } } diff --git a/src/controllers/sponsor.controller.ts b/src/controllers/sponsor.controller.ts index 605ad572..8789f82e 100644 --- a/src/controllers/sponsor.controller.ts +++ b/src/controllers/sponsor.controller.ts @@ -14,7 +14,10 @@ import { EnsureAuthenticated } from "@middlewares/authenticated.middleware"; import { EnsureAuthorization } from "@middlewares/authorization.middleware"; import Sponsor from "@models/sponsor.model"; import { SponsorService } from "@services/sponsor.service"; -import { Request as ExpressRequest, Response as ExpressResponse } from "express"; +import { + Request as ExpressRequest, + Response as ExpressResponse +} from "express"; import * as SuccessConstants from "@constants/success.constant"; import * as ErrorConstants from "@constants/error.constant"; import { Validator } from "@app/middlewares/validator.middleware"; @@ -22,7 +25,7 @@ import { Validator } from "@app/middlewares/validator.middleware"; @autoInjectable() @Controller("/sponsor") export class SponsorController { - constructor(private readonly sponsorService: SponsorService) { } + constructor(private readonly sponsorService: SponsorService) {} @Get("/self", [ EnsureAuthenticated, @@ -33,22 +36,22 @@ export class SponsorController { ]) async getSelf( @Request() request: ExpressRequest, - @Response() response: ExpressResponse, + @Response() response: ExpressResponse ) { const sponsor: | Sponsor | undefined = await this.sponsorService.findByIdentifier( - //@ts-ignore - request.user?.identifier - ); + //@ts-ignore + request.user?.identifier + ); return sponsor ? response.status(200).json({ - message: SuccessConstants.SPONSOR_READ, - data: sponsor - }) + message: SuccessConstants.SPONSOR_READ, + data: sponsor + }) : response.status(404).json({ - message: ErrorConstants.SPONSOR_404_MESSAGE - }); + message: ErrorConstants.SPONSOR_404_MESSAGE + }); } @Get("/:identifier", [ @@ -65,17 +68,17 @@ export class SponsorController { const sponsor: | Sponsor | undefined = await this.sponsorService.findByIdentifier( - identifier - ); + identifier + ); return sponsor ? response.status(200).json({ - message: SuccessConstants.SPONSOR_READ, - data: sponsor - }) + message: SuccessConstants.SPONSOR_READ, + data: sponsor + }) : response.status(404).json({ - message: ErrorConstants.SPONSOR_404_MESSAGE - }); + message: ErrorConstants.SPONSOR_404_MESSAGE + }); } @Post("/", [ @@ -91,12 +94,12 @@ export class SponsorController { return result ? response.status(200).send({ - message: SuccessConstants.SPONSOR_CREATE, - data: result - }) + message: SuccessConstants.SPONSOR_CREATE, + data: result + }) : response.status(422).send({ - message: ErrorConstants.ACCOUNT_DUPLICATE_422_MESSAGE - }); + message: ErrorConstants.ACCOUNT_DUPLICATE_422_MESSAGE + }); } @Patch("/:identifier", [ @@ -116,14 +119,14 @@ export class SponsorController { return result ? response.status(200).json({ - message: SuccessConstants.SPONSOR_UPDATE, - data: result - }) + message: SuccessConstants.SPONSOR_UPDATE, + data: result + }) : response.status(404).json({ - message: ErrorConstants.SPONSOR_404_MESSAGE, - data: { - identifier: identifier - } - }); + message: ErrorConstants.SPONSOR_404_MESSAGE, + data: { + identifier: identifier + } + }); } } diff --git a/src/models/invitation.model.ts b/src/models/invitation.model.ts index 3e94b48e..6f13d04b 100644 --- a/src/models/invitation.model.ts +++ b/src/models/invitation.model.ts @@ -5,18 +5,18 @@ import { UserType } from "@constants/general.constant"; @Entity() class Invitation { - @PrimaryColumn() - @IsEmail() - email: string; + @PrimaryColumn() + @IsEmail() + email: string; - @Column({ - enum: UserType, - default: UserType.Hacker - }) - accountType: string; + @Column({ + enum: UserType, + default: UserType.Hacker + }) + accountType: string; - @ManyToOne(() => Account) - inviter: Account; + @ManyToOne(() => Account) + inviter: Account; } -export default Invitation; \ No newline at end of file +export default Invitation; diff --git a/src/services/hacker.service.ts b/src/services/hacker.service.ts index 0b2c24a4..6b952f43 100644 --- a/src/services/hacker.service.ts +++ b/src/services/hacker.service.ts @@ -50,7 +50,8 @@ export class HackerService { .createQueryBuilder() .update() .set({ - application: () => `jsonb_set(application, '${path}', '"${value}"')` + application: () => + `jsonb_set(application, '${path}', '"${value}"')` }) .where("identifier = :identifier", { identifier: identifier diff --git a/src/services/invitation.service.ts b/src/services/invitation.service.ts index 63370e67..f506daad 100644 --- a/src/services/invitation.service.ts +++ b/src/services/invitation.service.ts @@ -1,30 +1,50 @@ import Invitation from "@app/models/invitation.model"; import { autoInjectable, singleton } from "tsyringe"; -import { getRepository, Repository } from "typeorm"; +import { DeleteResult, getRepository, Repository } from "typeorm"; import jwt from "jsonwebtoken"; @autoInjectable() @singleton() export class InvitationService { - private readonly invitationRepository: Repository; + private readonly invitationRepository: Repository; - constructor() { - this.invitationRepository = getRepository(Invitation); - } + constructor() { + this.invitationRepository = getRepository(Invitation); + } - public async find() { - return await this.invitationRepository.find(); - } + public async find() { + return await this.invitationRepository.find(); + } - public async save(invitation: Invitation) { - return await this.invitationRepository.save(invitation); - } + public async save(invitation: Invitation) { + return await this.invitationRepository.save(invitation); + } - public generateLink(route: string, token: string): string { - return `${process.env.FRONTEND_ADDRESS}/${route}?token=${token}`; -} + public async delete(email: string): Promise { + return await this.invitationRepository.delete(email); + } + + public async findByIdentifier( + email: string + ): Promise { + return await this.invitationRepository.findOne(email); + } - public generateToken(invitation: Invitation) { - return jwt.sign(invitation, process.env.JWT_ ?? "default", { expiresIn: "1 week" }); - } -} \ No newline at end of file + public generateLink( + route: string, + token: string, + accountType: string + ): string { + return `${process.env.FRONTEND_ADDRESS}/${route}?token=${token}&accountType=${accountType}`; + } + + public generateToken(invitation: Invitation) { + return jwt.sign( + invitation, + process.env.JWT_INVITE_SECRET ?? "default", + { + expiresIn: "1 week" + } + ); + } +} diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 0adac2ed..96d043a2 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -28,8 +28,17 @@ export class SearchService { } public parseParam(param: string) { - const path = param.split('.'); - return path[0] + '->' + path.slice(1, path.length - 1).map((s) => `'${s}'`).join('->') + "->>" + `'${path[path.length - 1]}'`; + const path = param.split("."); + return ( + path[0] + + "->" + + path + .slice(1, path.length - 1) + .map((s) => `'${s}'`) + .join("->") + + "->>" + + `'${path[path.length - 1]}'` + ); } public async executeQuery( @@ -47,7 +56,11 @@ export class SearchService { switch (operation.toUpperCase()) { case Operation.Equal: case Operation.In: - builder.andWhere(`${param} ${operation} ${this.parseValueList(value as string[])}`); + builder.andWhere( + `${param} ${operation} ${this.parseValueList( + value as string[] + )}` + ); break; case Operation.Like: builder.andWhere(`${param} ${operation} %:value%`, { diff --git a/src/services/setting.service.ts b/src/services/setting.service.ts index 10bfc5c9..718d875c 100644 --- a/src/services/setting.service.ts +++ b/src/services/setting.service.ts @@ -18,12 +18,16 @@ export class SettingService { } else if (value == "false") { value = false; } - return { [key]: value } + return { [key]: value }; } - public flatten(settings: Array): { [setting: string]: string | number | boolean } { + public flatten( + settings: Array + ): { [setting: string]: string | number | boolean } { const flattened = {}; - settings.forEach((setting) => Object.assign(flattened, this.flattenOne(setting))) + settings.forEach((setting) => + Object.assign(flattened, this.flattenOne(setting)) + ); return flattened; } @@ -47,7 +51,9 @@ export class SettingService { return await this.settingRepository.save(setting); } - public async update(settings: { [setting: string]: string | number | boolean }) { + public async update(settings: { + [setting: string]: string | number | boolean; + }) { for (let [key, value] of Object.entries(settings)) { value = value.toString(); const setting = await this.findByKey(key); @@ -55,7 +61,7 @@ export class SettingService { setting.value = value.toString(); await this.settingRepository.save(setting); } else { - await this.settingRepository.save({key, value}); + await this.settingRepository.save({ key, value }); } } } From b1713ea82ec8af1dc09e95a4853c34b77789f16a Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Thu, 27 Apr 2023 14:03:03 -0400 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20=F0=9F=90=9B=20fix=20resume=20upload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/hacker.controller.ts | 9 +++++---- src/models/hacker.model.ts | 4 ++-- src/services/storage.service.ts | 21 ++++++++++++--------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/controllers/hacker.controller.ts b/src/controllers/hacker.controller.ts index 4cf0b84e..454c35d2 100644 --- a/src/controllers/hacker.controller.ts +++ b/src/controllers/hacker.controller.ts @@ -96,7 +96,6 @@ export class HackerController { @Body() hacker: Hacker ) { //TODO - Check if applications are open when hacker is created. - //TODO - Fix bug where Hacker status is None as it is passed into the API. (Maybe override the status variable somehow?) hacker.status = HackerStatus.Applied; const result: Hacker = await this.hackerService.save(hacker); @@ -121,9 +120,11 @@ export class HackerController { async update( @Response() response: ExpressResponse, @Params("identifier") identifier: number, - @Body() update: Hacker + @Body() update: Partial ) { - const result = await this.hackerService.update(identifier, update); + // Project only application to prevent overposting + const toUpdate: Partial = { application: update.application }; + const result = await this.hackerService.update(identifier, toUpdate); return result ? response.status(200).json({ @@ -176,7 +177,7 @@ export class HackerController { AuthorizationLevel.Staff, AuthorizationLevel.Hacker ]), - upload.single("resume") + upload.single("file") ]) async uploadResume( @Request() request: ExpressRequest, diff --git a/src/models/hacker.model.ts b/src/models/hacker.model.ts index e071de1d..da8a7d70 100644 --- a/src/models/hacker.model.ts +++ b/src/models/hacker.model.ts @@ -16,9 +16,9 @@ class Hacker { @PrimaryColumn() identifier: number; - @OneToOne(() => Account) + @OneToOne(() => Account, { cascade: false }) @JoinColumn({ name: "identifier" }) - readonly account: Account; + account: Account; @Column({ enum: HackerStatus, diff --git a/src/services/storage.service.ts b/src/services/storage.service.ts index a8597519..325354c7 100644 --- a/src/services/storage.service.ts +++ b/src/services/storage.service.ts @@ -6,8 +6,8 @@ import { LoggerService } from "@services/logger.service"; @autoInjectable() export class StorageService { bucketName: string | undefined; - storage: any; - bucket: any; + storage: GStorage.Storage; + bucket: GStorage.Bucket; constructor(loggerService: LoggerService) { this.bucketName = process.env.BUCKET_NAME || ""; @@ -16,6 +16,7 @@ export class StorageService { } catch (error) { loggerService.getLogger().error(error); } + this.bucket = this.storage.bucket(this.bucketName); } /** @@ -48,14 +49,16 @@ export class StorageService { * @param {string} filename path to file in bucket * @returns {Promise<[Buffer]>} the file data that was returned */ - download(filename: string): Promise { + download(filename: string): Promise<[Buffer]> { const file = this.bucket.file(filename); - return new Promise((resolve, reject) => { - file.exists().then((doesExist: boolean) => { + return new Promise<[Buffer]>((resolve, reject) => { + file.exists().then((doesExist: [boolean]) => { if (doesExist) { file.download() - .then(resolve) - .catch(reject); + .then((res) => resolve(res)) + .catch(() => + reject("error occured when downloading from bucket") + ); } else { reject("file does not exist"); } @@ -75,9 +78,9 @@ export class StorageService { /** * * @param {*} filename the file that you want to check exists - * @returns {Promise<[Boolean]>} + * @returns {Promise<[boolean]>} */ - exists(filename: string): boolean { + exists(filename: string): Promise<[boolean]> { const file = this.bucket.file(filename); return file.exists(); } From 7423855faba88297a1054a2fc03a74f122717729 Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Thu, 27 Apr 2023 14:03:51 -0400 Subject: [PATCH 9/9] =?UTF-8?q?fix:=20=F0=9F=90=9B=20fix=20husky=20commit-?= =?UTF-8?q?msg?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .husky/commit-msg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.husky/commit-msg b/.husky/commit-msg index c9ff16ad..72e16c61 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1 +1 @@ -npx --no -- commitlint --edit "\${1}" +npx --no -- commitlint --edit