Skip to content

Merge feature/profile into dev #30

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

Merged
merged 2 commits into from
Oct 5, 2023
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
6 changes: 3 additions & 3 deletions api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ export * from './lib/index';
const expressServer = express();

const createFunction = async (
expressInstance: express.Express
expressInstance: express.Express,
): Promise<void> => {
const app = await NestFactory.create(
ApiModule,
new ExpressAdapter(expressInstance),
{
cors: {
origin: true,
methods: ['GET', 'POST', 'OPTIONS', 'PUT', 'DELETE'],
methods: ['GET', 'POST', 'OPTIONS', 'PUT', 'DELETE', 'PATCH'],
credentials: true,
},
}
},
);
app.use(cookieParser());
await app.init();
Expand Down
7 changes: 7 additions & 0 deletions api/src/lib/api.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import * as express from 'express';
import { User } from './user';

export interface ApiResponse {
success: boolean;
message: string | undefined;
}

export interface Request extends express.Request {
user?: User;
}
11 changes: 7 additions & 4 deletions api/src/lib/auth/auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { NextFunction, Request, Response } from 'express';
import { NextFunction, Response } from 'express';
import { UserService } from '../user/user.service';
import { User } from '../user';
import { Request } from '../api.interface';

@Injectable()
export class AuthenticationMiddleware implements NestMiddleware {
Expand All @@ -17,8 +19,9 @@ export class AuthenticationMiddleware implements NestMiddleware {
} else {
this.userService
.checkCookie(authCookie)
.then((isValid) => {
if (isValid) {
.then((user: User | undefined) => {
if (user?.verified) {
req.user = user;
next();
} else {
res.status(401).end();
Expand All @@ -27,7 +30,7 @@ export class AuthenticationMiddleware implements NestMiddleware {
.catch((e) => {
console.error(
' ~ auth.middleware.ts:25 → Failed to check cookie',
e
e,
);
res.status(500).end();
});
Expand Down
93 changes: 84 additions & 9 deletions api/src/lib/user/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
import { Controller, Post, Body, Res } from '@nestjs/common';
import { AuthRequest, NewUser } from './user.interface';
import {
Controller,
Post,
Body,
Res,
Patch,
Get,
Param,
Req,
} from '@nestjs/common';
import {
AuthRequest,
ChangePasswordRequest,
NewUser,
UserInfoResponse,
validatePassword,
} from './user.interface';
import { UserService } from './user.service';
import { Response } from 'express';
import { Request } from '../api.interface';
import { ApiResponse } from '../api.interface';
import * as argon2 from 'argon2';

export const EMAIL_REGEX =
/^(([^<>()[\]\\.,;:\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,}))$/;
Expand All @@ -11,27 +28,85 @@ export const EMAIL_REGEX =
export class UserController {
constructor(private userService: UserService) { }

@Get('info')
async getUserInfo(@Req() request: Request): Promise<UserInfoResponse> {
console.log(' 🚀 ~ user.controller.ts:33 → User info', request.user);
if (!request.user) {
return { success: false, message: 'Not logged in' };
}
return {
success: true,
message: 'User info returned',
user: {
username: request.user.username,
email: request.user.email,
verified: request.user.verified,
},
};
}

@Patch('/password')
async changePassword(
@Body() input: ChangePasswordRequest,
): Promise<ApiResponse> {
const passwordValidation = validatePassword(input.newPassword);
if (passwordValidation !== undefined) {
return { success: false, message: passwordValidation };
}

if (input.newPassword !== input.confirm) {
return { success: false, message: "Confirmation password doesn't match" };
}

try {
const userRef = await this.userService.findUserByUsernameRef(
input.username,
);

const user = (await userRef?.get())?.data();

if (!user || !userRef) {
return {
success: false,
message: 'The specified user could not be found',
};
}

if (!(await argon2.verify(user.passwordHash, input.currentPassword))) {
return {
success: false,
message: 'The supplied current password was incorrect',
};
}

await this.userService.changePassword(userRef, input.newPassword);
return { success: true, message: 'Password updated' };
} catch (e) {
return { success: false, message: 'Failed to update password' };
}
}

@Post()
async createUser(
@Body() newUser: NewUser,
@Res({ passthrough: true }) res: Response
@Res({ passthrough: true }) res: Response,
): Promise<ApiResponse> {
try {
if (!EMAIL_REGEX.test(newUser.email)) {
return { success: false, message: 'Invalid email' };
}

const existingEmail = await this.userService.findUserByEmail(
newUser.email
newUser.email,
);
const existingUsername = await this.userService.findUserByUsername(
newUser.username
newUser.username,
);

console.log(
'🚀 ~ user.controller.ts:31 → Existing users (undefined is good): ',
existingEmail,
existingUsername
existingUsername,
);

if (existingEmail !== undefined || existingUsername !== undefined) {
Expand Down Expand Up @@ -65,10 +140,10 @@ export class UserController {
@Post('/login')
async login(
@Body() loginRequest: AuthRequest,
@Res({ passthrough: true }) res: Response
@Res({ passthrough: true }) res: Response,
): Promise<ApiResponse> {
const user = await this.userService.findUserByUsername(
loginRequest.username
loginRequest.username,
);

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

const result = await this.userService.authenticate(
loginRequest.username,
loginRequest.password
loginRequest.password,
);

if (result === undefined) {
Expand Down
24 changes: 24 additions & 0 deletions api/src/lib/user/user.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DocumentReference } from 'firebase-admin/firestore';
import { ApiResponse } from '../api.interface';

export interface NewUser {
username: string;
Expand All @@ -23,3 +24,26 @@ export interface Cookie {
secret: string;
expires: number;
}

export interface ChangePasswordRequest {
username: string;
currentPassword: string;
newPassword: string;
confirm: string;
}

export interface UserInfoResponse extends ApiResponse {
user?: {
username: string;
email: string;
verified: boolean;
};
}

export function validatePassword(password: string): string | undefined {
if (password.length < 8) {
return 'Password must be longer that 8 characters';
}

return undefined;
}
23 changes: 21 additions & 2 deletions api/src/lib/user/user.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
import { Module } from '@nestjs/common';
import {
MiddlewareConsumer,
Module,
NestModule,
RequestMethod,
} from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { AuthenticationMiddleware } from '../auth/auth.middleware';

@Module({
providers: [UserService],
controllers: [UserController],
})
export class UserModule { }
export class UserModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(AuthenticationMiddleware).forRoutes(
{
path: 'user/password',
method: RequestMethod.PATCH,
},
{
path: 'user/info',
method: RequestMethod.GET,
},
);
}
}
47 changes: 39 additions & 8 deletions api/src/lib/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,45 @@ export class UserService implements OnModuleInit {
});
}

async changePassword(
userRef: DocumentReference,
newPassword: string,
): Promise<void> {
if (!this.firebaseService) {
throw 'No firebase provider';
}

await userRef.update({ passwordHash: await argon2.hash(newPassword) });
}

async findUserByUsername(username: string): Promise<User | undefined> {
try {
const ref = await this.findUserByUsernameRef(username);
const data = await ref?.get();
return data?.data();
} catch (e) {
return undefined;
}
}

async findUserByUsernameRef(
username: string,
): Promise<DocumentReference<User> | undefined> {
if (!this.firebaseService) {
throw 'No firebase connection';
}

const firestore = this.firebaseService?.getFirestore();
const collection = firestore?.collection('/users');
const result = await collection?.where('username', '==', username).get();

console.log('🚀 ~ user.service.ts:27 → result', result?.docs.length);
if (!result || result?.docs.length === 0) {
return undefined;
}

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

async authenticate(
username: string,
password: string
password: string,
): Promise<Cookie | undefined> {
const firestore = this.firebaseService?.getFirestore();
const collection = firestore?.collection('/users');
Expand Down Expand Up @@ -123,14 +148,16 @@ export class UserService implements OnModuleInit {
return undefined;
}

async checkCookie(secret: string): Promise<boolean> {
async checkCookie(secret: string): Promise<User | undefined> {
if (!this.firebaseService) {
return false;
console.error('  ~ user.service.ts:153 → No firebase service present');
return undefined;
}

const firestore = this.firebaseService.getFirestore();
if (!firestore) {
return false;
console.error('  ~ user.service.ts:153 → No firestore present');
return undefined;
}

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

if (query.docs.length !== 1) {
return false;
return undefined;
}

const cookie = query.docs[0].data() as Cookie;
const user: User | undefined = (await cookie.userRef.get()).data() as User;
console.log(
' 🚀 ~ user.service.ts:175 → User associated with cookie: ',
user,
);
if (!user.verified) {
return false;
return undefined;
}

return true;
return user;
}
}
2 changes: 0 additions & 2 deletions app/src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TestBed, waitForAsync } from '@angular/core/testing';

import { AppComponent } from './app.component';
import { NotificationService } from './notification.service';

describe('AppComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
providers: [NotificationService],
declarations: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
Expand Down
8 changes: 1 addition & 7 deletions app/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import { Component } from '@angular/core';
import { NotificationService } from './notification.service';

@Component({
selector: 'root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent {
constructor(private notificationService: NotificationService) {
console.log(
' 🚀 ~ app.component.ts:11 → Notification service',
this.notificationService,
);
}
constructor() { }
}
Loading