Skip to content

Commit

Permalink
implement reset-password email link process
Browse files Browse the repository at this point in the history
  • Loading branch information
RaceFPV committed Dec 2, 2024
1 parent 1ebe6f9 commit 42a2bcb
Show file tree
Hide file tree
Showing 25 changed files with 562 additions and 5 deletions.
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 [email protected]
ENV EMAIL_PASS=your-16-char-app-password
ENV SITE_URL=http://localhost:3000
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
31 changes: 31 additions & 0 deletions app/api/utils/emailService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import nodemailer from 'nodemailer';

const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST || 'localhost',
port: parseInt(process.env.SMTP_PORT || '25'),
secure: false, // true for 465, false for other ports
auth: process.env.SMTP_USER ? {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
} : undefined
});

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

const mailOptions = {
from: process.env.SMTP_FROM || '[email protected]',
to: email,
subject: 'Password Reset Request',
text: `To reset your password, click this link: ${process.env.SITE_URL}/reset-password?token=${token}`
};

try {
const info = await transporter.sendMail(mailOptions);
console.log('Reset email sent:', info.response);
return info;
} catch (error) {
console.error('Error sending reset email:', error);
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
- SMTP_HOST=mailhog
- SMTP_PORT=1025
- [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"
20 changes: 20 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

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)

### Email Configuration (SMTP)
- `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])

### 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
2 changes: 2 additions & 0 deletions frontend/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import { ComponentBlueprintParentComponent } from "./module-blueprint/components/component-blueprint-parent/component-blueprint-parent.component";
import { ResetPasswordComponent } from "./module-blueprint/components/user-auth/reset-password/reset-password.component";

const routes: Routes = [
{ path: "", component: ComponentBlueprintParentComponent },
Expand All @@ -12,6 +13,7 @@ const routes: Routes = [
{ path: "openfromurl/:url", component: ComponentBlueprintParentComponent },
{ path: "browse", component: ComponentBlueprintParentComponent },
{ path: "about", component: ComponentBlueprintParentComponent },
{ path: "reset-password", component: ResetPasswordComponent },
{ path: "", redirectTo: "/", pathMatch: "prefix" },
];

Expand Down
5 changes: 4 additions & 1 deletion frontend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,25 @@ import {
NgxGoogleAnalyticsRouterModule,
} from "ngx-google-analytics";
import * as Sentry from "@sentry/angular-ivy";
import { FormsModule } from "@angular/forms";

import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";

import { ModuleBlueprintModule } from "./module-blueprint/module-blueprint.module";
import { CustomEventManager } from "./module-blueprint/directives/custom-event-manager";
import { RequestResetComponent } from "./password-reset/request-reset.component";

@NgModule({
declarations: [AppComponent],
declarations: [AppComponent, RequestResetComponent],
imports: [
BrowserModule,
NgxGoogleAnalyticsModule.forRoot(process.env.NG_APP_GA_TRACKING_CODE),
NgxGoogleAnalyticsRouterModule.forRoot(),
ModuleBlueprintModule,
HttpClientModule,
AppRoutingModule,
FormsModule,
],
providers: [
{ provide: EventManager, useClass: CustomEventManager },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,23 @@
font-size: x-small;
width: 25em;
}

.links-container {
display: flex;
justify-content: space-between;
margin: 0.5em 0;
}

.links-container a {
color: var(--primary-color, #007ad9);
text-decoration: none;
cursor: pointer;
}

.links-container a:hover {
text-decoration: underline;
}

.forgot-password {
margin-left: 1em;
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@
<div *ngIf="authError" class="error-red" i18n>Authentication error</div>

<br />
<a [routerLink]="" (click)="registration()" i18n>First registration ?</a>
<div class="links-container">
<a [routerLink]="" (click)="registration()" i18n>First registration ?</a>
<a [routerLink]="" (click)="forgotPassword()" class="forgot-password" i18n
>Forgot Password?</a
>
</div>

<br />
<br />
Expand All @@ -48,3 +53,23 @@
<!--This is just here so that enter submits the form-->
<button type="submit" style="visibility: hidden"></button>
</form>

<p-dialog
[(visible)]="showResetDialog"
header="Reset Password"
[modal]="true"
[style]="{ width: '350px' }"
>
<div class="p-field">
<label for="email">Enter your email address</label>
<input
type="email"
pInputText
[(ngModel)]="resetEmail"
style="width: 100%"
/>
</div>
<p-footer>
<p-button label="Send Reset Email" (click)="requestReset()"></p-button>
</p-footer>
</p-dialog>
Loading

0 comments on commit 42a2bcb

Please sign in to comment.