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
770 changes: 324 additions & 446 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"mongoose": "^8.11.0",
"mongoose-lean-id": "^1.0.0",
"mongoose-lean-virtuals": "^1.1.0",
"resend": "^4.0.0",
"typescript": "^5.9.3"
}
}
76 changes: 76 additions & 0 deletions packages/api/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { UserService } from "@/user/user.service";
import { User } from "@/user/user.entity";
import { Request } from "express";
import sendToWebhook from "@/logging/webhook";
import { PasswordReset } from "./passwordReset.entity";
import crypto from "crypto";
import { Resend } from "resend";

@Controller()
export class AuthController {
Expand Down Expand Up @@ -80,4 +83,77 @@ export class AuthController {
return { message: "success" };
}

@Post("/forgot-password")
async requestPasswordReset(req: Request): Promise<{ success: true }> {
const email = typeof req.body?.email === "string" ? req.body.email.trim().toLowerCase() : "";
if (!email) throw new BadRequest("Email is required.");

const user = await this.userService.getUserByEmail(email);

// Always respond success to avoid account enumeration.
if (!user?.id || !user.email) return { success: true };

const token = crypto.randomBytes(32).toString("hex");
const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour validity

await PasswordReset.deleteMany({ user: user.id });
await PasswordReset.create({ user: user.id, tokenHash, expiresAt, used: false });

const resendApiKey = process.env.RESEND_API_KEY;
if (!resendApiKey) throw new BadRequest("Password reset is unavailable right now. Please try again later.");

const resend = new Resend(resendApiKey);
const frontendUrl = process.env.FRONTEND_URL || "https://sequenced.ottegi.com";
const resetUrl = `${frontendUrl.replace(/\/$/, "")}/auth/forgotPassword?token=${token}`;
const fromEmail = process.env.RESET_FROM_EMAIL || "Sequenced <sequenced@ottegi.com>";

await resend.emails.send({
from: fromEmail,
to: user.email,
subject: "Reset your Sequenced password",
html: `
<div style="font-family: Arial, sans-serif; line-height: 1.5; color: #0f172a;">
<h2 style="color:#2563eb;margin-bottom:12px;">Reset your password</h2>
<p>Hello ${user.first ?? "there"},</p>
<p>We received a request to reset your Sequenced password. Click the button below to set a new password. This link will expire in 1 hour.</p>
<p style="margin:16px 0;">
<a href="${resetUrl}" style="display:inline-block;padding:12px 18px;background:#2563eb;color:white;text-decoration:none;border-radius:10px;font-weight:600;">Reset password</a>
</p>
<p>If the button doesn't work, copy and paste this link into your browser:</p>
<p style="word-break:break-all;"><a href="${resetUrl}">${resetUrl}</a></p>
<p>If you didn't request this, you can safely ignore this email.</p>
</div>
`
});

return { success: true };
}

@Post("/reset-password")
async resetPassword(req: Request): Promise<{ success: true }> {
const token = typeof req.body?.token === "string" ? req.body.token.trim() : "";
const password = typeof req.body?.password === "string" ? req.body.password : "";

if (!token || !password) {
throw new BadRequest("Reset token and password are required.");
}

if (password.length < 8) {
throw new BadRequest("Password must be at least 8 characters long.");
}

const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
const resetRequest = await PasswordReset.findOne({ tokenHash, used: false }).lean<PasswordReset>().exec();

if (!resetRequest || !resetRequest.user || resetRequest.expiresAt < new Date()) {
throw new BadRequest("This reset link is invalid or has expired.");
}

await this.userService.updateUser(resetRequest.user as any, { password });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Hash reset passwords before persisting

The reset flow updates the user with updateUser(...), which uses findByIdAndUpdate; Mongoose setters (like the bcrypt hash setter on User.password) are not applied to update queries by default. That means reset passwords get stored in plaintext, and bcrypt.compare during login will fail or throw because it expects a hash. Users who reset their password won’t be able to sign in, and you end up storing raw passwords. Consider hashing explicitly or switching to a findById + save path (or enable runSettersOnQuery) for this update.

Useful? React with 👍 / 👎.

await PasswordReset.updateOne({ _id: resetRequest._id }, { used: true }).exec();

return { success: true };
}

}
21 changes: 21 additions & 0 deletions packages/api/src/auth/passwordReset.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Entity, Model, Prop, Index } from "@/_lib/mongoose";
import { User } from "@/user/user.entity";

@Entity({ timestamps: true })
@Index({ tokenHash: 1 }, { unique: true })
export class PasswordReset extends Model {

id: string;

@Prop({ type: String, required: true })
tokenHash: string;

@Prop({ type: Date, required: true, expires: 0 })
expiresAt: Date;

@Prop({ type: Boolean, default: false })
used: boolean;

@Prop({ type: User, required: true })
user: User;
}
3 changes: 0 additions & 3 deletions packages/app/.env.example

This file was deleted.

3 changes: 0 additions & 3 deletions packages/app/android/app/capacitor.build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ android {

apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-community-admob')
implementation project(':capacitor-community-sqlite')
implementation project(':capacitor-filesystem')
implementation project(':capacitor-local-notifications')
implementation project(':capacitor-preferences')

Expand Down
9 changes: 0 additions & 9 deletions packages/app/android/capacitor.settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,6 @@
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../../../node_modules/@capacitor/android/capacitor')

include ':capacitor-community-admob'
project(':capacitor-community-admob').projectDir = new File('../../../node_modules/@capacitor-community/admob/android')

include ':capacitor-community-sqlite'
project(':capacitor-community-sqlite').projectDir = new File('../../../node_modules/@capacitor-community/sqlite/android')

include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../../../node_modules/@capacitor/filesystem/android')

include ':capacitor-local-notifications'
project(':capacitor-local-notifications').projectDir = new File('../../../node_modules/@capacitor/local-notifications/android')

Expand Down
13 changes: 0 additions & 13 deletions packages/app/electron/.gitignore

This file was deleted.

Binary file removed packages/app/electron/assets/appIcon.ico
Binary file not shown.
Binary file removed packages/app/electron/assets/appIcon.png
Binary file not shown.
Binary file removed packages/app/electron/assets/icon.png
Binary file not shown.
Binary file removed packages/app/electron/assets/splash.gif
Binary file not shown.
Binary file removed packages/app/electron/assets/splash.png
Binary file not shown.
19 changes: 0 additions & 19 deletions packages/app/electron/build/capacitor.config.js

This file was deleted.

Binary file not shown.
13 changes: 0 additions & 13 deletions packages/app/electron/build/entitlements.mas.inherit.plist

This file was deleted.

10 changes: 0 additions & 10 deletions packages/app/electron/build/entitlements.mas.loginhelper.plist

This file was deleted.

17 changes: 0 additions & 17 deletions packages/app/electron/build/entitlements.mas.plist

This file was deleted.

13 changes: 0 additions & 13 deletions packages/app/electron/build/notarize.js

This file was deleted.

27 changes: 0 additions & 27 deletions packages/app/electron/build/resignAndPackage.sh

This file was deleted.

59 changes: 0 additions & 59 deletions packages/app/electron/build/src/index.js

This file was deleted.

4 changes: 0 additions & 4 deletions packages/app/electron/build/src/preload.js

This file was deleted.

2 changes: 0 additions & 2 deletions packages/app/electron/build/src/rt/electron-plugins.js

This file was deleted.

Loading