Skip to content

Commit 485a682

Browse files
authored
Merge pull request #30 from CodeLog-Development/feature/profile
Merge feature/profile into dev
2 parents 87be9ab + 349cbfc commit 485a682

23 files changed

+577
-84
lines changed

api/src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@ export * from './lib/index';
1010
const expressServer = express();
1111

1212
const createFunction = async (
13-
expressInstance: express.Express
13+
expressInstance: express.Express,
1414
): Promise<void> => {
1515
const app = await NestFactory.create(
1616
ApiModule,
1717
new ExpressAdapter(expressInstance),
1818
{
1919
cors: {
2020
origin: true,
21-
methods: ['GET', 'POST', 'OPTIONS', 'PUT', 'DELETE'],
21+
methods: ['GET', 'POST', 'OPTIONS', 'PUT', 'DELETE', 'PATCH'],
2222
credentials: true,
2323
},
24-
}
24+
},
2525
);
2626
app.use(cookieParser());
2727
await app.init();

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: 84 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,85 @@ export const EMAIL_REGEX =
1128
export class UserController {
1229
constructor(private userService: UserService) { }
1330

31+
@Get('info')
32+
async getUserInfo(@Req() request: Request): Promise<UserInfoResponse> {
33+
console.log(' 🚀 ~ user.controller.ts:33 → User info', request.user);
34+
if (!request.user) {
35+
return { success: false, message: 'Not logged in' };
36+
}
37+
return {
38+
success: true,
39+
message: 'User info returned',
40+
user: {
41+
username: request.user.username,
42+
email: request.user.email,
43+
verified: request.user.verified,
44+
},
45+
};
46+
}
47+
48+
@Patch('/password')
49+
async changePassword(
50+
@Body() input: ChangePasswordRequest,
51+
): Promise<ApiResponse> {
52+
const passwordValidation = validatePassword(input.newPassword);
53+
if (passwordValidation !== undefined) {
54+
return { success: false, message: passwordValidation };
55+
}
56+
57+
if (input.newPassword !== input.confirm) {
58+
return { success: false, message: "Confirmation password doesn't match" };
59+
}
60+
61+
try {
62+
const userRef = await this.userService.findUserByUsernameRef(
63+
input.username,
64+
);
65+
66+
const user = (await userRef?.get())?.data();
67+
68+
if (!user || !userRef) {
69+
return {
70+
success: false,
71+
message: 'The specified user could not be found',
72+
};
73+
}
74+
75+
if (!(await argon2.verify(user.passwordHash, input.currentPassword))) {
76+
return {
77+
success: false,
78+
message: 'The supplied current password was incorrect',
79+
};
80+
}
81+
82+
await this.userService.changePassword(userRef, input.newPassword);
83+
return { success: true, message: 'Password updated' };
84+
} catch (e) {
85+
return { success: false, message: 'Failed to update password' };
86+
}
87+
}
88+
1489
@Post()
1590
async createUser(
1691
@Body() newUser: NewUser,
17-
@Res({ passthrough: true }) res: Response
92+
@Res({ passthrough: true }) res: Response,
1893
): Promise<ApiResponse> {
1994
try {
2095
if (!EMAIL_REGEX.test(newUser.email)) {
2196
return { success: false, message: 'Invalid email' };
2297
}
2398

2499
const existingEmail = await this.userService.findUserByEmail(
25-
newUser.email
100+
newUser.email,
26101
);
27102
const existingUsername = await this.userService.findUserByUsername(
28-
newUser.username
103+
newUser.username,
29104
);
30105

31106
console.log(
32107
'🚀 ~ user.controller.ts:31 → Existing users (undefined is good): ',
33108
existingEmail,
34-
existingUsername
109+
existingUsername,
35110
);
36111

37112
if (existingEmail !== undefined || existingUsername !== undefined) {
@@ -65,10 +140,10 @@ export class UserController {
65140
@Post('/login')
66141
async login(
67142
@Body() loginRequest: AuthRequest,
68-
@Res({ passthrough: true }) res: Response
143+
@Res({ passthrough: true }) res: Response,
69144
): Promise<ApiResponse> {
70145
const user = await this.userService.findUserByUsername(
71-
loginRequest.username
146+
loginRequest.username,
72147
);
73148

74149
if (!user) {
@@ -81,7 +156,7 @@ export class UserController {
81156

82157
const result = await this.userService.authenticate(
83158
loginRequest.username,
84-
loginRequest.password
159+
loginRequest.password,
85160
);
86161

87162
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: 'user/password',
20+
method: RequestMethod.PATCH,
21+
},
22+
{
23+
path: 'user/info',
24+
method: RequestMethod.GET,
25+
},
26+
);
27+
}
28+
}

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

Lines changed: 39 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,16 @@ 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+
console.error('  ~ user.service.ts:153 → No firebase service present');
154+
return undefined;
129155
}
130156

131157
const firestore = this.firebaseService.getFirestore();
132158
if (!firestore) {
133-
return false;
159+
console.error('  ~ user.service.ts:153 → No firestore present');
160+
return undefined;
134161
}
135162

136163
const query = await firestore
@@ -140,15 +167,19 @@ export class UserService implements OnModuleInit {
140167
.get();
141168

142169
if (query.docs.length !== 1) {
143-
return false;
170+
return undefined;
144171
}
145172

146173
const cookie = query.docs[0].data() as Cookie;
147174
const user: User | undefined = (await cookie.userRef.get()).data() as User;
175+
console.log(
176+
' 🚀 ~ user.service.ts:175 → User associated with cookie: ',
177+
user,
178+
);
148179
if (!user.verified) {
149-
return false;
180+
return undefined;
150181
}
151182

152-
return true;
183+
return user;
153184
}
154185
}

app/src/app/app.component.spec.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
22
import { TestBed, waitForAsync } from '@angular/core/testing';
33

44
import { AppComponent } from './app.component';
5-
import { NotificationService } from './notification.service';
65

76
describe('AppComponent', () => {
87
beforeEach(waitForAsync(() => {
98
TestBed.configureTestingModule({
10-
providers: [NotificationService],
119
declarations: [AppComponent],
1210
schemas: [CUSTOM_ELEMENTS_SCHEMA],
1311
}).compileComponents();

app/src/app/app.component.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
11
import { Component } from '@angular/core';
2-
import { NotificationService } from './notification.service';
32

43
@Component({
54
selector: 'root',
65
templateUrl: 'app.component.html',
76
styleUrls: ['app.component.scss'],
87
})
98
export class AppComponent {
10-
constructor(private notificationService: NotificationService) {
11-
console.log(
12-
' 🚀 ~ app.component.ts:11 → Notification service',
13-
this.notificationService,
14-
);
15-
}
9+
constructor() { }
1610
}

0 commit comments

Comments
 (0)