Skip to content
Merged
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
963 changes: 873 additions & 90 deletions functions/package-lock.json

Large diffs are not rendered by default.

19 changes: 10 additions & 9 deletions functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
"description": "Cloud Functions for Firebase",
"scripts": {
"lint": "eslint --ext .js,.ts .",
"build": "tsc",
"build": "tsc && cp src/templates/*.html lib/src/templates/",
"build:watch": "tsc --watch --preserveWatchOutput",
"serve": "concurrently \"npm:build:watch\" \"firebase emulators:start --only functions\"",
"serve": "concurrently \"npm:build:watch\" \"firebase emulators:start\"",
"shell": "npm run build && firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
Expand All @@ -20,15 +20,14 @@
"busboy": "^1.6.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"dotenv": "^16.6.1",
"express": "^4.22.2",
"firebase-admin": "^12.7.0",
"firebase-functions": "^7.2.5",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"luxon": "^3.7.1",
"nodemailer": "^7.0.3",
"resend": "^6.14.0",
"nodemailer": "^7.0.13",
"validator": "^13.15.0"
},
"devDependencies": {
Expand All @@ -37,11 +36,12 @@
"@types/cookie-parser": "^1.4.9",
"@types/cors": "^2.8.18",
"@types/csurf": "^1.11.5",
"@types/express": "^5.0.1",
"@types/express": "^5.0.6",
"@types/jsonwebtoken": "^9.0.9",
"@types/lodash": "^4.17.16",
"@types/luxon": "^3.6.2",
"@types/nodemailer": "^6.4.17",
"@types/node": "^25.9.3",
"@types/nodemailer": "^6.4.24",
"@types/validator": "^13.12.2",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
Expand All @@ -51,7 +51,8 @@
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-import": "^2.25.4",
"firebase-functions-test": "^3.1.0",
"typescript": "^5.8.2"
"ts-node-dev": "^2.0.0",
"typescript": "^5.9.3"
},
"private": true,
"packageManager": "npm@11.12.1+sha512.cdca14b85d647b3192028d02aadbe82d75f79a446aceea9874be98e6d768f20ebd3555770a48d0e9906106007877bbc690f715e9372f2e2dc644a3c3157fb14c"
Expand Down
14 changes: 11 additions & 3 deletions functions/src/config/firebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as admin from "firebase-admin";
import * as dotenv from "dotenv";
import * as fs from "fs";
import * as path from "path";
import { Resend } from "resend";
import nodemailer from "nodemailer";

dotenv.config();

Expand Down Expand Up @@ -31,7 +31,15 @@ db.settings({ ignoreUndefinedProperties: true });
const auth = admin.auth();

// Email service
const resend = new Resend(process.env.RESEND_API_KEY);
const transporter = nodemailer.createTransport({
host: process.env.SES_SMTP_HOST,
port: Number(process.env.SES_SMTP_PORT),
secure: false,
auth: {
user: process.env.SES_SMTP_USERNAME,
pass: process.env.SES_SMTP_PASSWORD,
},
})

/**
* Populate Firestore with fake data if running in emulator
Expand All @@ -44,4 +52,4 @@ const resend = new Resend(process.env.RESEND_API_KEY);
// populator.generateFakeData();
// }

export { admin, db, auth, resend };
export { admin, db, auth, transporter };
12 changes: 11 additions & 1 deletion functions/src/controllers/application_controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Request, Response } from "express";
import { admin, db } from "../config/firebase";
import { admin, auth, db } from "../config/firebase";
import validator from "validator";
import Busboy from "busboy";
import { sendApplicationSubmittedEmail } from "../utils/email_sender";
import {
APPLICATION_STATES,
APPLICATION_STATUS,
Expand Down Expand Up @@ -1072,6 +1073,15 @@ export const setApplicationStatusToSubmitted = async (

await userRef.set(data, { merge: true });

if (process.env.NODE_ENV !== "development") {
auth.getUser(UID).then(user => {
if (user.email) {
sendApplicationSubmittedEmail(user.email)
.catch(err => functions.logger.error('Application submitted email failed:', err));
}
}).catch(err => functions.logger.error('Failed to fetch user for email:', err));
}

res.status(201).json({
status: 201,
success: true,
Expand Down
57 changes: 8 additions & 49 deletions functions/src/controllers/auth_controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Request, Response } from "express";
import { auth, db, resend } from "../config/firebase";
import { auth, db } from "../config/firebase";
import axios from "axios";
import validator from "validator";
import { FieldValue } from "firebase-admin/firestore";
Expand All @@ -8,6 +8,7 @@ import { FirebaseError } from "firebase-admin";
import { generateCsrfToken } from "../middlewares/csrf_middleware";
import { APPLICATION_STATUS } from "../types/application_types";
import { User, AuthResponse } from "../models/user";
import { sendEmailVerificationEmail, sendResetPasswordEmail } from "../utils/email_sender";

const SESSION_EXPIRY_SECONDS = 14 * 24 * 60 * 60 * 1000; // lasts 2 weeks

Expand Down Expand Up @@ -207,23 +208,9 @@ export const register = async (req: Request, res: Response): Promise<void> => {
try {
const isEmulator = process.env.FIREBASE_AUTH_EMULATOR_HOST !== undefined;

// Generate email verification link
if (process.env.NODE_ENV !== "development") {
const verificationLink = await auth.generateEmailVerificationLink(email);
const { error } = await resend.emails.send({
from: 'Garuda Hacks <noreply@mail.garudahacks.com>',
to: [email],
template: {
id: 'email-verification',
variables: {
link: verificationLink
}
}
})
if (error) {
res.status(500).json({ status: 500, error: error});
return
}
await sendEmailVerificationEmail(email, verificationLink);
}

const customToken = await auth.createCustomToken(user.uid);
Expand Down Expand Up @@ -545,24 +532,10 @@ export const requestPasswordReset = async (
try {
// Check if user exists
await auth.getUserByEmail(email);

// Send email

if (process.env.NODE_ENV !== "development") {
const link = await auth.generatePasswordResetLink(email);
const { error } = await resend.emails.send({
from: 'Garuda Hacks <noreply@mail.garudahacks.com>',
to: [email],
template: {
id: 'reset-password',
variables: {
link: link
}
}
})
if (error) {
res.status(500).json({ status: 500, error: error});
return
}
const passwordResetLink = await auth.generatePasswordResetLink(email);
await sendResetPasswordEmail(email, passwordResetLink);
}

// Send success response
Expand Down Expand Up @@ -613,23 +586,9 @@ export const verifyAccount = async (
return;
}

// Send email
if (process.env.NODE_ENV !== "development") {
const link = await auth.generateEmailVerificationLink(email);
const { error } = await resend.emails.send({
from: 'Garuda Hacks <noreply@mail.garudahacks.com>',
to: [email],
template: {
id: 'email-verification',
variables: {
link: link
}
}
})
if (error) {
res.status(500).json({ status: 500, error: error});
return
}
const verificationLink = await auth.generateEmailVerificationLink(email);
await sendEmailVerificationEmail(email, verificationLink);
}

res.status(200).json({
Expand Down
56 changes: 56 additions & 0 deletions functions/src/templates/applicationSubmitted.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>

<head>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap" rel="stylesheet">
</head>

<body style="font-family: 'Poppins', Helvetica, sans-serif; background:#f4f4f4; padding:20px;">
<table width="100%" cellpadding="0" cellspacing="0"
style="max-width:600px;margin:auto;background:#fff;border-radius:8px;overflow:hidden;">
<tr>
<td style="padding:24px;color:#333; text-align: center;">
<h1>We Received Your Application ✅</h1>

<div style="position: relative; display: inline-block; margin: 0 auto 20px;">
<div style="
position: absolute;
bottom: calc(89% + 8px);
left: 50%;
transform: translateX(-50%);
background: #874ffe;
color: #fff;
font-size: 10px;
font-weight: 500;
border-radius: 16px;
padding: 8px 14px;
white-space: nowrap;
line-height: 1.4;
">
Delivered! I'll keep an eye on them for you. 👀
<div style="
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #7c3aed;
width: 0; height: 0;
"></div>
</div>
<img src="https://portal.garudahacks.com/assets/garudie-review.png" alt="Garudie with Laptop" width="220" style="display:block;" />
</div>
<p>Sit back and relax ☕️ — the committee will carefully review each application and notify you once results are out!</p>
<p style="margin-top: 40px;">Regards,</p>
<p>Garuda Hacks 7.0 Committee</p>
<hr style="border-color: #f9f5ff;">
<div style="color: #909090; font-size: 10px;">
<p>© 2026 Garuda Hacks. All rights reserved.</p>
<p>Visit our <a href="https://garudahacks.com">website</a> or Contact <a href="mailto:heryan@garudahacks.com">support</a></p>
</div>
</td>
</tr>
</table>
</body>

</html>
8 changes: 8 additions & 0 deletions functions/src/templates/applicationSubmitted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { readFileSync } from "fs";
import { join } from "path";

const template = readFileSync(join(__dirname, "applicationSubmitted.html"), "utf-8");

export function applicationSubmitted(): string {
return template
}
33 changes: 33 additions & 0 deletions functions/src/templates/resetPassword.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>

<head>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap" rel="stylesheet">
</head>

<body style="font-family: 'Poppins', Helvetica, sans-serif; background:#f4f4f4; padding:20px;">
<table width="100%" cellpadding="0" cellspacing="0"
style="max-width:600px;margin:auto;background:#fff;border-radius:8px;overflow:hidden;">
<tr>
<td style="padding:24px;color:#333; text-align: center;">
<h1>Lost Your Password?</h1>
<img src="https://portal.garudahacks.com/assets/garudie-laptop-burn.png" alt="Garudie with Laptop Burn" width="250" style="display:block;margin:auto;" />
<p>Change your password using the link below. For security reason, the link will expire in one hour.</p>
<p style="text-align:center;margin:32px 0;">
<a href="${actionUrl}"
style="background:#5079ff;color:#fff;padding:12px 24px;border-radius:6px;text-decoration:none;">
Reset Password
</a>
</p>
<p style="font-size: 12px; margin-top: 50px;">If you did not do this, you can safely ignore this email.</p>
<hr style="border-color: #f9f5ff;">
<div style="color: #909090; font-size: 10px;">
<p>© 2026 Garuda Hacks. All rights reserved.</p>
<p>Visit our <a href="https://garudahacks.com">website</a> or Contact <a href="mailto:heryan@garudahacks.com">support</a></p>
</div>
</td>
</tr>
</table>
</body>

</html>
13 changes: 13 additions & 0 deletions functions/src/templates/resetPassword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { readFileSync } from "fs";
import { join } from "path";

interface ResetPasswordProps {
actionUrl: string;
}

const template = readFileSync(join(__dirname, "resetPassword.html"), "utf-8");

export function resetPassword({ actionUrl }: ResetPasswordProps): string {
return template
.replace(/\$\{actionUrl\}/g, actionUrl);
}
34 changes: 34 additions & 0 deletions functions/src/templates/welcomeEmail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>

<head>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap" rel="stylesheet">
</head>

<body style="font-family: 'Poppins', Arial, sans-serif; background:#f4f4f4; padding:20px;">
<table width="100%" cellpadding="0" cellspacing="0"
style="max-width:600px;margin:auto;background:#fff;border-radius:8px;overflow:hidden;">
<tr>
<td style="padding:24px;color:#333; text-align: center;">
<h1>Welcome to Garuda Hacks!</h1>
<img src="https://portal.garudahacks.com/assets/garudie-laptop.png" alt="Garudie with Laptop" width="250" style="display:block;margin:auto;" />
<p>Thank you for registering. Please verify your email address by clicking the button below.</p>
<p style="text-align:center;margin:32px 0;">
<a href="${actionUrl}"
style="background:#5079ff;color:#fff;padding:12px 24px;border-radius:6px;text-decoration:none;">
Verify Now
</a>
</p>
<p>Link will expire in 24 hours.</p>
<p style="font-size: 12px; margin-top: 50px;">If you did not create an account, you can safely ignore this email.</p>
<hr style="border-color: #f9f5ff;">
<div style="color: #909090; font-size: 10px;">
<p>© 2026 Garuda Hacks. All rights reserved.</p>
<p>Visit our <a href="https://garudahacks.com">website</a> or Contact <a href="mailto:heryan@garudahacks.com">support</a></p>
</div>
</td>
</tr>
</table>
</body>

</html>
13 changes: 13 additions & 0 deletions functions/src/templates/welcomeEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { readFileSync } from "fs";
import { join } from "path";

interface WelcomeEmailProps {
actionUrl: string;
}

const template = readFileSync(join(__dirname, "welcomeEmail.html"), "utf-8");

export function welcomeEmail({ actionUrl }: WelcomeEmailProps): string {
return template
.replace(/\$\{actionUrl\}/g, actionUrl);
}
Loading