Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Module } from "@nestjs/common";
import { ObjectionModule } from "common/objection";
import { OrganizerApplication } from "entities/organizer-application.entity";
import { Organizer } from "entities/organizer.entity";
import { OrganizerApplicationController } from "./organizer-application.controller";
import { OrganizerApplicationService } from "./organizer-application.service";
import { SendGridModule } from "common/sendgrid/sendgrid.module";

@Module({
imports: [ObjectionModule.forFeature([OrganizerApplication])],
imports: [
ObjectionModule.forFeature([OrganizerApplication, Organizer]),
SendGridModule,
],
controllers: [OrganizerApplicationController],
providers: [OrganizerApplicationService],
})
Expand Down
107 changes: 105 additions & 2 deletions src/modules/organizer-application/organizer-application.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import {
OrganizerTeam,
} from "entities/organizer-application.entity";
import { InjectRepository, Repository } from "common/objection";
import { Organizer } from "entities/organizer.entity";
import { Role, FirebaseAuthService } from "common/gcp";
import { nanoid } from "nanoid";
import {
DefaultFromEmail,
DefaultTemplate,
SendGridService,
} from "common/sendgrid";

@Injectable()
export class OrganizerApplicationService {
Expand All @@ -15,6 +23,10 @@ export class OrganizerApplicationService {
constructor(
@InjectRepository(OrganizerApplication)
private readonly applicationRepo: Repository<OrganizerApplication>,
@InjectRepository(Organizer)
private readonly organizerRepo: Repository<Organizer>,
private readonly sendGridService: SendGridService,
private readonly firebaseAuth: FirebaseAuthService,
) {}

private get resumeBucket() {
Expand Down Expand Up @@ -55,6 +67,7 @@ export class OrganizerApplicationService {
* Logic:
* - If the team is firstChoiceTeam and firstChoiceStatus is pending, accept it
* - If the team is secondChoiceTeam, secondChoiceStatus is pending, and firstChoiceStatus is rejected, accept it
* - When accepted, automatically create an organizer account
* - Otherwise, throw an error
*/
async acceptApplication(
Expand All @@ -69,6 +82,8 @@ export class OrganizerApplicationService {
throw new BadRequestException("Application not found");
}

let updatedApplication: OrganizerApplication;

// Case 1: Accepting for first choice team
if (application.firstChoiceTeam === team) {
const status = application.firstChoiceStatus || ApplicationStatus.PENDING;
Expand All @@ -79,12 +94,15 @@ export class OrganizerApplicationService {
);
}

return this.applicationRepo
updatedApplication = await this.applicationRepo
.patchOne(applicationId, {
firstChoiceStatus: ApplicationStatus.ACCEPTED,
assignedTeam: team,
})
.exec();

await this.createOrganizerFromApplication(application, team);
return updatedApplication;
}

// Case 2: Accepting for second choice team
Expand All @@ -109,12 +127,15 @@ export class OrganizerApplicationService {
);
}

return this.applicationRepo
updatedApplication = await this.applicationRepo
.patchOne(applicationId, {
secondChoiceStatus: ApplicationStatus.ACCEPTED,
assignedTeam: team,
})
.exec();

await this.createOrganizerFromApplication(application, team);
return updatedApplication;
}

// Team doesn't match either first or second choice
Expand All @@ -124,6 +145,88 @@ export class OrganizerApplicationService {
);
}

/**
* Create an organizer account from an accepted application
*/
private async createOrganizerFromApplication(
application: OrganizerApplication,
team: OrganizerTeam,
): Promise<void> {
// Parse name into first and last name
const nameParts = application.name.trim().split(/\s+/);
const firstName = nameParts[0];
const lastName = nameParts.slice(1).join(" ") || "";

let uid: string;

try {
// Check if user already exists with this email
const existingUser = await this.firebaseAuth.getUserByEmail(
application.email,
);
uid = existingUser.uid;

// Update user's privilege to TEAM role
await this.firebaseAuth.updateUserPrivilege(uid, Role.TEAM);
} catch (error) {
// If user doesn't exist, create a new one
const tempPassword = nanoid(16);
uid = await this.firebaseAuth.createUserWithPrivilege(
application.email,
tempPassword,
Role.TEAM,
);
}

// Check if organizer already exists
const existingOrganizer = await this.organizerRepo.findOne(uid).exec();

if (!existingOrganizer) {
// Create organizer in database
await this.organizerRepo
.createOne({
id: uid,
firstName,
lastName,
email: application.email,
privilege: Role.TEAM,
team,
isActive: true,
})
.exec();
} else {
// Update existing organizer
await this.organizerRepo
.patchOne(uid, {
privilege: Role.TEAM,
team,
isActive: true,
})
.exec();
}

// Send welcome email with password reset link
const passwordResetLink = await this.firebaseAuth.generatePasswordResetLink(
application.email,
);

const message = await this.sendGridService.populateTemplate(
DefaultTemplate.organizerFirstLogin,
{
previewText: "HackPSU Organizer Account",
passwordResetLink,
firstName,
},
);

await this.sendGridService.send({
to: application.email,
from: DefaultFromEmail,
subject: "HackPSU Organizer Account",
message,
});
}

/**
* Reject an application from a specific team.
* Logic:
Expand Down