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 }); } } }