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/.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
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 be8086f3..48d823f2 100644
--- a/src/controllers/account.controller.ts
+++ b/src/controllers/account.controller.ts
@@ -28,6 +28,8 @@ 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";
+import Invitation from "@app/models/invitation.model";
@autoInjectable()
@Controller("/account")
@@ -35,9 +37,79 @@ 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 Creation Instructions",
+ html: join(
+ __dirname,
+ "../assets/email/AccountInvitation.mjml"
+ )
+ },
+ {
+ link: this.invitationService.generateLink(
+ "account/create",
+ this.invitationService.generateToken(invitation),
+ accountType
+ )
+ },
+ (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([
@@ -120,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);
}
}
@@ -146,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
@@ -202,6 +273,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
@@ -216,64 +288,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/controllers/hacker.controller.ts b/src/controllers/hacker.controller.ts
index 2b1da149..454c35d2 100644
--- a/src/controllers/hacker.controller.ts
+++ b/src/controllers/hacker.controller.ts
@@ -23,13 +23,18 @@ 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";
+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", [
@@ -91,7 +96,7 @@ 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);
return result
@@ -117,7 +122,9 @@ export class HackerController {
@Params("identifier") identifier: number,
@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({
@@ -189,8 +196,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 +206,30 @@ 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..69c847dc 100644
--- a/src/controllers/search.controller.ts
+++ b/src/controllers/search.controller.ts
@@ -23,9 +23,12 @@ 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..4ab2664d 100644
--- a/src/controllers/setting.controller.ts
+++ b/src/controllers/setting.controller.ts
@@ -63,17 +63,31 @@ export class SettingController {
});
}
+ @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", [
EnsureAuthenticated,
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({
diff --git a/src/controllers/sponsor.controller.ts b/src/controllers/sponsor.controller.ts
index 4f9a5581..8789f82e 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,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 { 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";
@@ -23,6 +27,33 @@ import { Validator } from "@app/middlewares/validator.middleware";
export class SponsorController {
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,
EnsureAuthorization([
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-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)
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..da8a7d70 100644
--- a/src/models/hacker.model.ts
+++ b/src/models/hacker.model.ts
@@ -1,12 +1,22 @@
import { HackerStatus } from "@constants/general.constant";
-import { Column, Entity, JoinColumn, ManyToOne, OneToOne } 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 {
- @OneToOne(() => Account, { primary: true, cascade: true })
+ @PrimaryColumn()
+ identifier: number;
+
+ @OneToOne(() => Account, { cascade: false })
@JoinColumn({ name: "identifier" })
account: Account;
diff --git a/src/models/invitation.model.ts b/src/models/invitation.model.ts
new file mode 100644
index 00000000..6f13d04b
--- /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;
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/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/hacker.service.ts b/src/services/hacker.service.ts
index 3a9f4a86..6b952f43 100644
--- a/src/services/hacker.service.ts
+++ b/src/services/hacker.service.ts
@@ -43,14 +43,15 @@ 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/invitation.service.ts b/src/services/invitation.service.ts
new file mode 100644
index 00000000..f506daad
--- /dev/null
+++ b/src/services/invitation.service.ts
@@ -0,0 +1,50 @@
+import Invitation from "@app/models/invitation.model";
+import { autoInjectable, singleton } from "tsyringe";
+import { DeleteResult, 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 async delete(email: string): Promise {
+ return await this.invitationRepository.delete(email);
+ }
+
+ public async findByIdentifier(
+ email: string
+ ): Promise {
+ return await this.invitationRepository.findOne(email);
+ }
+
+ 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 9fc5d432..96d043a2 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,24 @@ 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]}'`
+ );
+ }
+
public async executeQuery(
model: string,
query: Array
@@ -31,34 +49,34 @@ 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} ${this.parseValueList(
+ value as string[]
+ )}`
+ );
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;
}
});
-
return builder.getMany();
}
}
diff --git a/src/services/setting.service.ts b/src/services/setting.service.ts
index e011aaba..718d875c 100644
--- a/src/services/setting.service.ts
+++ b/src/services/setting.service.ts
@@ -8,8 +8,31 @@ 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 +51,22 @@ 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 {
diff --git a/src/services/storage.service.ts b/src/services/storage.service.ts
index 338df912..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,8 +16,7 @@ export class StorageService {
} catch (error) {
loggerService.getLogger().error(error);
}
- if (process.env.NODE_ENV !== "production")
- this.bucket = this.storage.bucket(this.bucketName);
+ this.bucket = this.storage.bucket(this.bucketName);
}
/**
@@ -50,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");
}
@@ -77,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();
}
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(