Skip to content

Commit 22023ac

Browse files
committed
Added password change functionality
1 parent 4c2ca2a commit 22023ac

19 files changed

+505
-60
lines changed

api/src/lib/api.interface.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1+
import * as express from 'express';
2+
import { User } from './user';
3+
14
export interface ApiResponse {
25
success: boolean;
36
message: string | undefined;
47
}
8+
9+
export interface Request extends express.Request {
10+
user?: User;
11+
}

api/src/lib/auth/auth.middleware.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Injectable, NestMiddleware } from '@nestjs/common';
22
import { ModuleRef } from '@nestjs/core';
3-
import { NextFunction, Request, Response } from 'express';
3+
import { NextFunction, Response } from 'express';
44
import { UserService } from '../user/user.service';
5+
import { User } from '../user';
6+
import { Request } from '../api.interface';
57

68
@Injectable()
79
export class AuthenticationMiddleware implements NestMiddleware {
@@ -17,8 +19,9 @@ export class AuthenticationMiddleware implements NestMiddleware {
1719
} else {
1820
this.userService
1921
.checkCookie(authCookie)
20-
.then((isValid) => {
21-
if (isValid) {
22+
.then((user: User | undefined) => {
23+
if (user?.verified) {
24+
req.user = user;
2225
next();
2326
} else {
2427
res.status(401).end();
@@ -27,7 +30,7 @@ export class AuthenticationMiddleware implements NestMiddleware {
2730
.catch((e) => {
2831
console.error(
2932
' ~ auth.middleware.ts:25 → Failed to check cookie',
30-
e
33+
e,
3134
);
3235
res.status(500).end();
3336
});

api/src/lib/user/user.controller.ts

Lines changed: 95 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,25 @@
1-
import { Controller, Post, Body, Res } from '@nestjs/common';
2-
import { AuthRequest, NewUser } from './user.interface';
1+
import {
2+
Controller,
3+
Post,
4+
Body,
5+
Res,
6+
Patch,
7+
Get,
8+
Param,
9+
Req,
10+
} from '@nestjs/common';
11+
import {
12+
AuthRequest,
13+
ChangePasswordRequest,
14+
NewUser,
15+
UserInfoResponse,
16+
validatePassword,
17+
} from './user.interface';
318
import { UserService } from './user.service';
419
import { Response } from 'express';
20+
import { Request } from '../api.interface';
521
import { ApiResponse } from '../api.interface';
22+
import * as argon2 from 'argon2';
623

724
export const EMAIL_REGEX =
825
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
@@ -11,27 +28,96 @@ export const EMAIL_REGEX =
1128
export class UserController {
1229
constructor(private userService: UserService) { }
1330

31+
@Get()
32+
async getUserInfo(
33+
@Param('username') username: string,
34+
@Req() request: Request,
35+
@Res() response: Response,
36+
): Promise<UserInfoResponse> {
37+
const user = await this.userService.findUserByUsername(username);
38+
39+
if (!user) {
40+
return { success: false, message: "Couldn't find the requested user" };
41+
}
42+
43+
if (user.username !== request.user?.username) {
44+
response.status(401);
45+
return { success: false, message: "You cannot see this user's details" };
46+
}
47+
48+
return {
49+
success: true,
50+
message: 'User details retrieved',
51+
user: {
52+
username: user.username,
53+
email: user.email,
54+
verified: user.verified,
55+
},
56+
};
57+
}
58+
59+
@Patch('/password')
60+
async changePassword(
61+
@Body() input: ChangePasswordRequest,
62+
): Promise<ApiResponse> {
63+
const passwordValidation = validatePassword(input.newPassword);
64+
if (passwordValidation !== undefined) {
65+
return { success: false, message: passwordValidation };
66+
}
67+
68+
if (input.newPassword !== input.confirm) {
69+
return { success: false, message: "Confirmation password doesn't match" };
70+
}
71+
72+
try {
73+
const userRef = await this.userService.findUserByUsernameRef(
74+
input.username,
75+
);
76+
77+
const user = (await userRef?.get())?.data();
78+
79+
if (!user || !userRef) {
80+
return {
81+
success: false,
82+
message: 'The specified user could not be found',
83+
};
84+
}
85+
86+
if (!(await argon2.verify(user.passwordHash, input.currentPassword))) {
87+
return {
88+
success: false,
89+
message: 'The supplied current password was incorrect',
90+
};
91+
}
92+
93+
await this.userService.changePassword(userRef, input.newPassword);
94+
return { success: true, message: 'Password updated' };
95+
} catch (e) {
96+
return { success: false, message: 'Failed to update password' };
97+
}
98+
}
99+
14100
@Post()
15101
async createUser(
16102
@Body() newUser: NewUser,
17-
@Res({ passthrough: true }) res: Response
103+
@Res({ passthrough: true }) res: Response,
18104
): Promise<ApiResponse> {
19105
try {
20106
if (!EMAIL_REGEX.test(newUser.email)) {
21107
return { success: false, message: 'Invalid email' };
22108
}
23109

24110
const existingEmail = await this.userService.findUserByEmail(
25-
newUser.email
111+
newUser.email,
26112
);
27113
const existingUsername = await this.userService.findUserByUsername(
28-
newUser.username
114+
newUser.username,
29115
);
30116

31117
console.log(
32118
'🚀 ~ user.controller.ts:31 → Existing users (undefined is good): ',
33119
existingEmail,
34-
existingUsername
120+
existingUsername,
35121
);
36122

37123
if (existingEmail !== undefined || existingUsername !== undefined) {
@@ -65,10 +151,10 @@ export class UserController {
65151
@Post('/login')
66152
async login(
67153
@Body() loginRequest: AuthRequest,
68-
@Res({ passthrough: true }) res: Response
154+
@Res({ passthrough: true }) res: Response,
69155
): Promise<ApiResponse> {
70156
const user = await this.userService.findUserByUsername(
71-
loginRequest.username
157+
loginRequest.username,
72158
);
73159

74160
if (!user) {
@@ -81,7 +167,7 @@ export class UserController {
81167

82168
const result = await this.userService.authenticate(
83169
loginRequest.username,
84-
loginRequest.password
170+
loginRequest.password,
85171
);
86172

87173
if (result === undefined) {

api/src/lib/user/user.interface.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DocumentReference } from 'firebase-admin/firestore';
2+
import { ApiResponse } from '../api.interface';
23

34
export interface NewUser {
45
username: string;
@@ -23,3 +24,26 @@ export interface Cookie {
2324
secret: string;
2425
expires: number;
2526
}
27+
28+
export interface ChangePasswordRequest {
29+
username: string;
30+
currentPassword: string;
31+
newPassword: string;
32+
confirm: string;
33+
}
34+
35+
export interface UserInfoResponse extends ApiResponse {
36+
user?: {
37+
username: string;
38+
email: string;
39+
verified: boolean;
40+
};
41+
}
42+
43+
export function validatePassword(password: string): string | undefined {
44+
if (password.length < 8) {
45+
return 'Password must be longer that 8 characters';
46+
}
47+
48+
return undefined;
49+
}

api/src/lib/user/user.module.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
1-
import { Module } from '@nestjs/common';
1+
import {
2+
MiddlewareConsumer,
3+
Module,
4+
NestModule,
5+
RequestMethod,
6+
} from '@nestjs/common';
27
import { UserService } from './user.service';
38
import { UserController } from './user.controller';
9+
import { AuthenticationMiddleware } from '../auth/auth.middleware';
410

511
@Module({
612
providers: [UserService],
713
controllers: [UserController],
814
})
9-
export class UserModule { }
15+
export class UserModule implements NestModule {
16+
configure(consumer: MiddlewareConsumer) {
17+
consumer.apply(AuthenticationMiddleware).forRoutes(
18+
{
19+
path: 'password',
20+
method: RequestMethod.PATCH,
21+
},
22+
{
23+
path: '',
24+
method: RequestMethod.GET,
25+
},
26+
);
27+
}
28+
}

api/src/lib/user/user.service.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,45 @@ export class UserService implements OnModuleInit {
1616
});
1717
}
1818

19+
async changePassword(
20+
userRef: DocumentReference,
21+
newPassword: string,
22+
): Promise<void> {
23+
if (!this.firebaseService) {
24+
throw 'No firebase provider';
25+
}
26+
27+
await userRef.update({ passwordHash: await argon2.hash(newPassword) });
28+
}
29+
1930
async findUserByUsername(username: string): Promise<User | undefined> {
31+
try {
32+
const ref = await this.findUserByUsernameRef(username);
33+
const data = await ref?.get();
34+
return data?.data();
35+
} catch (e) {
36+
return undefined;
37+
}
38+
}
39+
40+
async findUserByUsernameRef(
41+
username: string,
42+
): Promise<DocumentReference<User> | undefined> {
2043
if (!this.firebaseService) {
2144
throw 'No firebase connection';
2245
}
2346

2447
const firestore = this.firebaseService?.getFirestore();
2548
const collection = firestore?.collection('/users');
2649
const result = await collection?.where('username', '==', username).get();
50+
2751
console.log('🚀 ~ user.service.ts:27 → result', result?.docs.length);
2852
if (!result || result?.docs.length === 0) {
2953
return undefined;
3054
}
3155

32-
const user: User | undefined = result?.docs.at(0)?.data() as User;
56+
const user: DocumentReference<User> | undefined = result?.docs.at(0)
57+
?.ref as DocumentReference<User> | undefined;
3358
console.log(`🚀 ~ user.service.ts:33 → user: ${JSON.stringify(user)}`);
3459
return user;
3560
}
@@ -85,7 +110,7 @@ export class UserService implements OnModuleInit {
85110

86111
async authenticate(
87112
username: string,
88-
password: string
113+
password: string,
89114
): Promise<Cookie | undefined> {
90115
const firestore = this.firebaseService?.getFirestore();
91116
const collection = firestore?.collection('/users');
@@ -123,14 +148,14 @@ export class UserService implements OnModuleInit {
123148
return undefined;
124149
}
125150

126-
async checkCookie(secret: string): Promise<boolean> {
151+
async checkCookie(secret: string): Promise<User | undefined> {
127152
if (!this.firebaseService) {
128-
return false;
153+
return undefined;
129154
}
130155

131156
const firestore = this.firebaseService.getFirestore();
132157
if (!firestore) {
133-
return false;
158+
return undefined;
134159
}
135160

136161
const query = await firestore
@@ -140,15 +165,15 @@ export class UserService implements OnModuleInit {
140165
.get();
141166

142167
if (query.docs.length !== 1) {
143-
return false;
168+
return undefined;
144169
}
145170

146171
const cookie = query.docs[0].data() as Cookie;
147172
const user: User | undefined = (await cookie.userRef.get()).data() as User;
148173
if (!user.verified) {
149-
return false;
174+
return undefined;
150175
}
151176

152-
return true;
177+
return user;
153178
}
154179
}

app/src/app/app.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { FormsModule } from '@angular/forms';
99
import { AuthModule } from './auth/auth.module';
1010
import { CommonModule } from '@angular/common';
1111
import { NotificationService } from './notification.service';
12+
import { StatusPageModule } from './status/status.module';
1213

1314
@NgModule({
1415
declarations: [AppComponent],
@@ -18,6 +19,7 @@ import { NotificationService } from './notification.service';
1819
FormsModule,
1920
AppRoutingModule,
2021
HttpClientModule,
22+
StatusPageModule,
2123
AuthModule,
2224
IonicModule.forRoot(),
2325
],
@@ -27,4 +29,4 @@ import { NotificationService } from './notification.service';
2729
],
2830
bootstrap: [AppComponent],
2931
})
30-
export class AppModule {}
32+
export class AppModule { }

0 commit comments

Comments
 (0)