Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement reset-password email link process #41

Merged
merged 5 commits into from
Dec 10, 2024
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
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
14
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,12 @@ EXPOSE 3000

#RUN npm run dev
ENTRYPOINT npm run dev


ENV SITE_URL=http://localhost:3000
ENV ENV_NAME=development
ENV SMTP_HOST=localhost
ENV SMTP_PORT=25
ENV SMTP_USER=
ENV SMTP_PASS=
ENV [email protected]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ It is a combined curated version of the original blueprintnotincluded web app.
`docker-compose up`

Visit http://localhost:3000
To check incoming emails visit: http://localhost:8025

## Docker image building

Expand Down
24 changes: 24 additions & 0 deletions app/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import passport from 'passport';
import { Strategy } from 'passport-local'
import { UserModel } from './models/user';
import { Router } from 'express';
import crypto from 'crypto';
import { sendResetEmail } from './utils/emailService'; // Assume you have an email service

const router = Router();

export class Auth
{
Expand Down Expand Up @@ -35,3 +40,22 @@ export class Auth
passport.use(localStrategy);
}
}

router.post('/reset-password', async (req, res) => {
const { token, newPassword } = req.body;
const user = await UserModel.model.findOne({
resetToken: token,
resetTokenExpiration: { $gt: new Date() }
});

if (!user) {
return res.status(400).send('Invalid or expired token');
}

user.setPassword(newPassword);
user.resetToken = undefined;
user.resetTokenExpiration = undefined;
await user.save();

res.send('Password has been reset');
});
63 changes: 63 additions & 0 deletions app/api/login-controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Request, Response } from "express";
import { User, UserModel } from "./models/user";
import passport from 'passport';
import { sendResetEmail } from './utils/emailService';
import crypto from 'crypto-js';
import { randomBytes } from 'crypto';

export class LoginController {
public login(req: Request, res: Response)
Expand Down Expand Up @@ -43,4 +46,64 @@ export class LoginController {
})(req, res);
}
}

public async requestPasswordReset(req: Request, res: Response) {
console.log('Password reset request received for email:', req.body.email);

const { email } = req.body;

try {
const user = await UserModel.model.findOne({ email });
if (!user) {
console.log('User not found for email:', email);
return res.status(404).json({ message: 'User not found' });
}

// Generate reset token
const resetToken = randomBytes(32).toString('hex');
user.resetToken = resetToken;
user.resetTokenExpiration = new Date(Date.now() + 3600000); // 1 hour

await user.save();
console.log('Reset token generated for user:', user.username);

try {
await sendResetEmail(email, resetToken);
console.log('Reset email sent successfully to:', email);
res.json({ message: 'Password reset email sent' });
} catch (emailError) {
console.error('Error sending reset email:', emailError);
res.status(500).json({ message: 'Error sending reset email' });
}

} catch (error) {
console.error('Password reset request error:', error);
res.status(500).json({ message: 'Error processing request' });
}
}

public async resetPassword(req: Request, res: Response) {
const { token, newPassword } = req.body;

try {
const user = await UserModel.model.findOne({
resetToken: token,
resetTokenExpiration: { $gt: Date.now() }
});

if (!user) {
return res.status(400).json({ message: 'Invalid or expired reset token' });
}

user.setPassword(newPassword);
user.resetToken = undefined;
user.resetTokenExpiration = undefined;
await user.save();

res.json({ message: 'Password successfully reset' });
} catch (error) {
console.error('Password reset error:', error);
res.status(500).json({ message: 'Error resetting password' });
}
}
}
6 changes: 5 additions & 1 deletion app/api/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export interface User extends Document
password?: string;
hash?: string;
salt: string;
resetToken?: string;
resetTokenExpiration?: Date;

setPassword(password: string): void;
validPassword(password: string): boolean;
Expand All @@ -33,7 +35,9 @@ export class UserModel
required: true
},
hash: String,
salt: String
salt: String,
resetToken: String,
resetTokenExpiration: Date
});


Expand Down
30 changes: 30 additions & 0 deletions app/api/types/mailchimp-transactional.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
declare module '@mailchimp/mailchimp_transactional' {
interface MailchimpMessage {
message: {
from_email: string;
from_name?: string;
subject: string;
text?: string;
html?: string;
to: Array<{
email: string;
type: 'to' | 'cc' | 'bcc';
name?: string;
}>;
};
}

interface MailchimpClient {
messages: {
send(message: MailchimpMessage): Promise<Array<{
email: string;
status: string;
_id: string;
reject_reason?: string;
}>>;
};
}

function mailchimp(apiKey: string): MailchimpClient;
export = mailchimp;
}
122 changes: 122 additions & 0 deletions app/api/utils/emailService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import Mailjet from 'node-mailjet';
import nodemailer, { Transporter } from 'nodemailer';

// Define custom error type for Mailjet errors
interface MailjetError extends Error {
statusCode?: number;
errorMessage?: string;
errorType?: string;
}

let transporter: Transporter | null = null;
let mailjetClient: Mailjet | null = null;

if (process.env.ENV_NAME === 'production') {
if (!process.env.MAILJET_API_KEY || !process.env.MAILJET_SECRET_KEY) {
console.error('MAILJET_API_KEY and MAILJET_SECRET_KEY environment variables must be set');
} else {
mailjetClient = new Mailjet({
apiKey: process.env.MAILJET_API_KEY,
apiSecret: process.env.MAILJET_SECRET_KEY
});
console.log('Mailjet client initialized');
console.log('Using sender email:', process.env.MAILJET_FROM_EMAIL);
}
} else {
transporter = nodemailer.createTransport({
host: 'mailhog',
port: 1025,
secure: false,
auth: undefined
});

transporter.verify(function(error, success) {
if (error) {
console.error('SMTP connection error:', error);
} else {
console.log('SMTP server is ready to take our messages');
}
});
}

export async function sendResetEmail(email: string, token: string) {
console.log('Attempting to send reset email to:', email);

const emailContent = {
subject: 'Password Reset Request - Blueprint Not Included',
text: `To reset your password, click this link: ${process.env.SITE_URL}/reset-password?token=${token}`,
html: `
<h2>Password Reset Request</h2>
<p>You requested a password reset for your Blueprint Not Included account.</p>
<p>Click the link below to reset your password:</p>
<p><a href="${process.env.SITE_URL}/reset-password?token=${token}">Reset Password</a></p>
<p>If you didn't request this, you can safely ignore this email.</p>
<p>The link will expire in 1 hour.</p>
`
};

try {
if (process.env.ENV_NAME === 'production') {
if (!mailjetClient) {
throw new Error('Mailjet client not configured');
}
if (!process.env.MAILJET_FROM_EMAIL) {
throw new Error('Mailjet sender email not configured');
}

console.log('Sending via Mailjet with configuration:', {
to: email,
from: process.env.MAILJET_FROM_EMAIL,
subject: emailContent.subject
});

const result = await mailjetClient
.post('send', { version: 'v3.1' })
.request({
Messages: [
{
From: {
Email: process.env.MAILJET_FROM_EMAIL,
Name: 'Blueprint Not Included'
},
To: [
{
Email: email
}
],
Subject: emailContent.subject,
TextPart: emailContent.text,
HTMLPart: emailContent.html
}
]
});

console.log('Mailjet API Response:', result.body);
return result.body;
} else {
if (!transporter) {
throw new Error('Email transporter not configured');
}
const info = await transporter.sendMail({
from: process.env.MAILJET_FROM_EMAIL || '[email protected]',
to: email,
subject: emailContent.subject,
text: emailContent.text,
html: emailContent.html
});
console.log('Reset email sent:', info.response);
return info;
}
} catch (error) {
console.error('Error sending reset email:', error);
const mailjetError = error as MailjetError;
if (mailjetError.statusCode) {
console.error('Mailjet error details:', {
statusCode: mailjetError.statusCode,
message: mailjetError.errorMessage,
type: mailjetError.errorType
});
}
throw error;
}
}
7 changes: 7 additions & 0 deletions app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,18 @@ export class Routes {
console.log('Initializing routes without recaptcha verification');
app.route("/api/login").post(this.loginController.login);
app.route("/api/register").post(this.registerController.register);
app.route("/api/request-reset").post(this.loginController.requestPasswordReset);
app.route("/api/reset-password").post(this.loginController.resetPassword);
}
else {
console.log('Initializing routes with recaptcha verification');
app.route("/api/login").post(recaptcha.middleware.verify, this.loginController.login);
app.route("/api/register").post(recaptcha.middleware.verify, this.registerController.register);
app.route("/api/request-reset").post(
recaptcha.middleware.verify,
this.loginController.requestPasswordReset
);
app.route("/api/reset-password").post(recaptcha.middleware.verify, this.loginController.resetPassword);
}

// Anonymous access
Expand Down
13 changes: 12 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ services:
- JWT_SECRET=anylongstringhere
- CAPTCHA_SITE=localhost
- CAPTCHA_SECRET=YOURCAPTCHASECRET
- MAILJET_API_KEY=produseonly
- MAILJET_SECRET_KEY=produseonly
- [email protected]
links:
- "mongodb-bpni:database"
- "mailhog:mailhog"
depends_on:
- mongodb-bpni
- mailhog
ports:
- "3000:3000"
mongodb-bpni:
Expand All @@ -25,4 +30,10 @@ services:
volumes:
- ./mongo/docker-entrypoint-initdb.d/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
ports:
- "27017:27017"
- "27017:27017"
mailhog:
image: mailhog/mailhog
container_name: mailhog-bpni
ports:
- "1025:1025"
- "8025:8025"
34 changes: 34 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,40 @@

This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.3.8.

## Environment Variables

The following environment variables are required for the application to run:

### Required

- `DB_URI` - MongoDB connection string
- `JWT_SECRET` - Secret key for JWT token generation
- `SITE_URL` - Base URL of the site (e.g., http://localhost:3000)
- `ENV_NAME` - If not set, will default to mailhog for dev testing, if set to 'production' sendgrid will be used

### Email Configuration

#### Development (Mailhog)

(these do not have to be set by default, only if you want to override)

- `SMTP_HOST` - SMTP server hostname (default: localhost)
- `SMTP_PORT` - SMTP server port (default: 25)
- `SMTP_USER` - SMTP username (optional)
- `SMTP_PASS` - SMTP password (optional)
- `SMTP_FROM` - From email address (default: [email protected])

#### Production (Mailjet)

- `MAILJET_API_KEY` - Your Mailjet API key (required in production)
- `MAILJET_SECRET_KEY` - Your Mailjet Secret key (required in production)
- `MAILJET_FROM_EMAIL` - Verified sender email address for Mailjet

### reCAPTCHA (Production Only)

- `CAPTCHA_SITE` - Google reCAPTCHA site key
- `CAPTCHA_SECRET` - Google reCAPTCHA secret key

## Development server

Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
Expand Down
Loading
Loading