Skip to content

Commit 7f74b57

Browse files
db relationships
1 parent 3962ab3 commit 7f74b57

34 files changed

+400
-112
lines changed

makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
refresh-db:
2+
docker-compose -f docker/docker-compose.yml rm -v
3+
docker-compose -f docker/docker-compose.yml build
4+
5+
start-db:
6+
docker-compose -f docker/docker-compose.yml up

src/app.module.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1+
import { APP_FILTER } from '@nestjs/core';
12
import * as Joi from '@hapi/joi';
2-
import { Module, NotFoundException } from '@nestjs/common';
3+
import { Module } from '@nestjs/common';
34
import { ConfigModule } from '@nestjs/config';
45
import { PostsModule } from './posts/posts.module';
56
import { DatabaseModule } from './database/database.module';
7+
import { CategoriesModule } from './categories/categories.module';
68
import { AuthenticationModule } from './authentication/authentication.module';
9+
import { CustomHttpExceptionFilter } from './utils/exceptionsLogger.filter';
710
import { UserModule } from './users/users.module';
8-
import { APP_FILTER } from '@nestjs/core';
9-
import { ExceptionsLoggerFilter, CustomHttpExceptionFilter } from './utils/exceptionsLogger.filter';
1011

1112
@Module({
1213
// modules in the application
1314
// importing a module here instructs nestjs where to get
1415
// the controllers and providers (services) for that module
15-
imports: [PostsModule, ConfigModule.forRoot({
16+
imports: [ConfigModule.forRoot({
1617
//ConfigModule reads from the .env file and Joi converts
1718
// them to ts datatypes
1819
validationSchema: Joi.object({
@@ -25,7 +26,11 @@ import { ExceptionsLoggerFilter, CustomHttpExceptionFilter } from './utils/excep
2526
JWT_SECRET: Joi.string().required(),
2627
JWT_EXPIRATION_TIME: Joi.string().required(),
2728
})
28-
}), DatabaseModule, AuthenticationModule],
29+
}), DatabaseModule,
30+
AuthenticationModule,
31+
CategoriesModule,
32+
PostsModule,
33+
UserModule],
2934

3035
// controllers to instantiate
3136
controllers: [],
Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Body, Req, Controller, HttpCode, Post, UseGuards, Res, Get, SerializeOptions, UseInterceptors, ClassSerializerInterceptor } from '@nestjs/common';
2-
import { Request, Response } from 'express';
1+
import { Body, Req, Controller, HttpCode, Post, UseGuards, Res, Get } from '@nestjs/common';
2+
import { Response } from 'express';
33

44
import { AuthenticationService } from "./authentication.service";
55
import { LocalAuthenticationGuard } from './guards/localAuthentication.guard';
@@ -8,23 +8,14 @@ import RequestWithUser from "./requestWithUser.interface";
88
import JwtAuthenticationGuard from "./guards/jwt-authentication.guard";
99

1010
@Controller('authentication')
11-
@UseInterceptors(ClassSerializerInterceptor)
12-
// * By default, all properties of our entities are exposed.
13-
// * We can change this strategy by providing additional options to
14-
// * the class-transformer.
15-
// * This will force you to use the Expose() on the entities variables you want
16-
// * to expose to the user
17-
@SerializeOptions({
18-
strategy: 'excludeAll'
19-
})
2011
export class AuthenticationController {
2112
constructor(
2213
private readonly authenticationService: AuthenticationService
2314
) { }
2415

2516
@UseGuards(JwtAuthenticationGuard)
2617
@Get()
27-
authentmicatedMe(@Req() request: RequestWithUser) {
18+
authenticatedMe(@Req() request: RequestWithUser) {
2819
const user = request.user;
2920
user.password = undefined;
3021
return user;
@@ -38,16 +29,10 @@ export class AuthenticationController {
3829
@HttpCode(200)
3930
@UseGuards(LocalAuthenticationGuard)
4031
@Post('log-in')
41-
async login(@Req() request: RequestWithUser, @Res() response: Response) {
32+
async login(@Req() request: RequestWithUser) {
4233
const user = request.user
43-
44-
// * this will be set to the caller's cookie and they do not need to manage it them
45-
// * Might not be useful for cli or api client apps
46-
47-
const cookie = this.authenticationService.getCookieWithJwtToken(user.id);
48-
response.set('Set-Cookie', cookie)
49-
50-
// user.password = undefined;
51-
return response.send(user)
34+
return {
35+
"access_token": this.authenticationService.getBearerToken(user.id)
36+
}
5237
}
5338
}

src/authentication/authentication.module.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1+
import { JwtModule } from "@nestjs/jwt";
12
import { Module } from "@nestjs/common";
3+
import { ConfigModule, ConfigService } from '@nestjs/config';
24
import { PassportModule } from "@nestjs/passport";
35

6+
import { UserModule } from "src/users/users.module";
47
import { LocalStrategy } from "./strategies/local.strategy";
58
import { AuthenticationService } from "./authentication.service";
6-
import { UserModule } from "src/users/users.module";
79
import { AuthenticationController } from './authentication.controller';
8-
import { ConfigModule, ConfigService } from '@nestjs/config';
9-
import { JwtModule } from "@nestjs/jwt";
10-
import { JwtInCookieStrategy } from './strategies/jwt.cookie.strategy';
10+
import { JWTFromAuthHeaderStrategy } from './strategies/jwt.header.strategy';
1111

1212
@Module({
1313
imports: [
@@ -24,7 +24,7 @@ import { JwtInCookieStrategy } from './strategies/jwt.cookie.strategy';
2424
})
2525
})
2626
],
27-
providers: [AuthenticationService, LocalStrategy, JwtInCookieStrategy],
27+
providers: [AuthenticationService, LocalStrategy, JWTFromAuthHeaderStrategy],
2828
controllers: [AuthenticationController]
2929
})
3030
export class AuthenticationModule { }

src/authentication/authentication.service.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { HttpException, HttpStatus, Inject } from '@nestjs/common';
33

44

55
import RegisterDto from './dto/register.dto';
6-
import User from 'src/users/user.entity';
6+
import User from 'src/users/entities/user.entity';
77
import { UsersService } from '../users/users.service';
88
import { PostgresErrorCode } from './../database/postgresErrorCodes.enum';
99
import { JwtService } from '@nestjs/jwt';
@@ -27,12 +27,17 @@ export class AuthenticationService {
2727
return `Authentication=; HttpOnly; Path=/; Max-Age=0`;
2828
}
2929

30-
public getCookieWithJwtToken(userId: number) {
30+
public getCookieWithJwtToken(userId: string) {
3131
const payload: TokenPayload = { userId };
3232
const token = this.jwtService.sign(payload)
3333
return `Authentication=${token}; HttpOnly; Path=/; Max-Age=${this.configService.get('JWT_EXPIRATION_TIME')}`;
3434
}
3535

36+
public getBearerToken(userId: string) {
37+
const payload: TokenPayload = { userId };
38+
return this.jwtService.sign(payload)
39+
}
40+
3641
public async register(registrationData: RegisterDto): Promise<User> {
3742
const hashedPassword = await bcrypt.hash(registrationData.password, NUMBER_OF_ROUNDS)
3843

@@ -52,11 +57,11 @@ export class AuthenticationService {
5257
}
5358
}
5459

55-
public async getAuthenticatedUser(email: string, plainTextPassword: string) {
60+
public async getAuthenticatedUser(email: string, plainTextPassword: string): Promise<User> {
5661
try {
5762
const user = await this.usersService.getByEmail(email)
5863
await this.verifyPassword(plainTextPassword, user.password)
59-
user.password = undefined;
64+
delete user.password
6065
return user;
6166
} catch (error) {
6267
throw new HttpException('Wrong credentials provided', HttpStatus.BAD_REQUEST)

src/authentication/dto/register.dto.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ export class RegisterDto {
1212
@IsNotEmpty()
1313
@MinLength(7)
1414
password: string;
15+
16+
17+
// address DTO is not here, cascade was set to true in
18+
// user entity file making it possible to send address alongside
19+
// why this is so, I do not know as at now.
1520
}
1621

1722
export default RegisterDto;

src/authentication/requestWithUser.interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import User from "src/users/user.entity";
1+
import User from "src/users/entities/user.entity";
22

33
interface RequestWithUser extends Request {
44
user: User
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Request } from 'express';
2+
import { ExtractJwt, Strategy } from "passport-jwt";
3+
import { PassportStrategy } from "@nestjs/passport";
4+
import { Injectable } from "@nestjs/common";
5+
import { ConfigService } from "@nestjs/config";
6+
7+
import { UsersService } from "src/users/users.service";
8+
import TokenPayload from "../tokenPayload.interface";
9+
10+
@Injectable()
11+
export class JWTFromAuthHeaderStrategy extends PassportStrategy(Strategy) {
12+
constructor(
13+
private readonly configService: ConfigService,
14+
private readonly userService: UsersService,
15+
) {
16+
super({
17+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
18+
secretOrKey: configService.get('JWT_SECRET')
19+
})
20+
}
21+
22+
async validate(payload: TokenPayload) {
23+
return this.userService.getById(payload.userId)
24+
}
25+
}

src/authentication/strategies/local.strategy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { PassportStrategy } from "@nestjs/passport";
33
import { Injectable } from "@nestjs/common";
44

55
import { AuthenticationService } from "../authentication.service";
6-
import User from "src/users/user.entity";
6+
import User from "src/users/entities/user.entity";
77

88
@Injectable()
99
export class LocalStrategy extends PassportStrategy(Strategy) {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export default interface TokenPayload {
2-
userId: number
2+
userId: string
33
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Body, Controller, Delete, Get, Param, Patch, Post, Req, UseGuards, ClassSerializerInterceptor, UseInterceptors } from '@nestjs/common';
2+
import JwtAuthenticationGuard from '../authentication/guards/jwt-authentication.guard';
3+
import FindOneParams from 'src/utils/findOneParams';
4+
import RequestWithUser from 'src/authentication/requestWithUser.interface';
5+
import CategoryService from './categories.service';
6+
import CreateCategoryDTO from './dto/createCategory.dto';
7+
import UpdateCategory from './dto/UpdateCategory.dto';
8+
9+
@Controller('categories')
10+
export default class CategoriesController {
11+
constructor(
12+
private readonly categoriesService: CategoryService
13+
) { }
14+
15+
16+
// GET /categories
17+
@Get()
18+
async getAllPosts() {
19+
return await this.categoriesService.getAllCategories();
20+
}
21+
22+
// // GET /posts/123
23+
@Get(':id')
24+
async getPostById(@Param() { id }: FindOneParams) {
25+
return this.categoriesService.getCategoryById((id))
26+
}
27+
28+
@Post()
29+
@UseGuards(JwtAuthenticationGuard)
30+
async createCategory(@Body() category: CreateCategoryDTO, @Req() req: RequestWithUser) {
31+
return await this.categoriesService.createCategory(category);
32+
}
33+
34+
35+
@Patch(':id')
36+
async updateCategory(@Param() { id }: FindOneParams, @Body() category: UpdateCategory) {
37+
return this.categoriesService.updateCategory(id, category);
38+
}
39+
40+
@Delete(':id')
41+
async deletePost(@Param() { id }: FindOneParams) {
42+
await this.categoriesService.deleteCategory(id)
43+
}
44+
}

src/categories/categories.module.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Module } from '@nestjs/common';
2+
import { TypeOrmModule } from '@nestjs/typeorm';
3+
4+
import CategoriesController from './categories.controller';
5+
import CategoryService from './categories.service';
6+
import Category from './category.entity';
7+
8+
@Module({
9+
imports: [TypeOrmModule.forFeature([Category])],
10+
controllers: [CategoriesController],
11+
providers: [CategoryService],
12+
exports: [CategoryService]
13+
})
14+
export class CategoriesModule { }

src/categories/categories.service.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { InjectRepository } from '@nestjs/typeorm';
2+
import { Injectable, NotFoundException, HttpStatus } from '@nestjs/common';
3+
4+
import { In, Repository } from 'typeorm';
5+
import Category from './category.entity';
6+
import createCategoryDto from './dto/createCategory.dto';
7+
import CategoryNotFoundException from 'src/exceptions/notFoundExceptions';
8+
import DuplicateResourceException from 'src/exceptions/duplicateResource.exception';
9+
import UpdateCategory from './dto/UpdateCategory.dto';
10+
11+
@Injectable()
12+
export default class CategoryService {
13+
14+
constructor(
15+
@InjectRepository(Category)
16+
private categoriesRepository: Repository<Category>
17+
) { }
18+
19+
20+
async getCategoriesByIds(categories: string[]): Promise<Category[]> {
21+
const returnedCategories = await this.categoriesRepository.find({
22+
where: { id: In(categories) }
23+
// order: { createDate: "ASC" }
24+
})
25+
26+
if (returnedCategories.length == categories.length) {
27+
return returnedCategories
28+
}
29+
30+
throw new NotFoundException(`Some of the categories you supplied were not found ${categories}`)
31+
}
32+
33+
async getAllCategories() {
34+
// return each post with their posts
35+
return this.categoriesRepository.find({ relations: ['posts'] });
36+
}
37+
38+
async createCategory(category: createCategoryDto) {
39+
try {
40+
await this.getCategoryByName(category.name)
41+
throw new DuplicateResourceException(`There is a category with this name ${category.name}`)
42+
} catch (e) {
43+
if (!(e instanceof CategoryNotFoundException)) {
44+
throw e
45+
}
46+
}
47+
const newCategory = this.categoriesRepository.create({ ...category })
48+
await this.categoriesRepository.save(newCategory);
49+
return newCategory
50+
}
51+
52+
private async getCategoryByName(name: string) {
53+
const category = await this.categoriesRepository.findOne({ where: { name: name } })
54+
if (category) {
55+
return category
56+
}
57+
throw new CategoryNotFoundException(name)
58+
}
59+
60+
async updateCategory(id: string, category: UpdateCategory): Promise<Category> {
61+
const updateResult = await this.categoriesRepository.update(id, category)
62+
if (!updateResult.affected) {
63+
throw new CategoryNotFoundException(id)
64+
}
65+
return await this.getCategoryById(id);
66+
}
67+
68+
async getCategoryById(id: string) {
69+
const category = await this.categoriesRepository.findOne({ where: { id: id } })
70+
if (category) {
71+
return category
72+
}
73+
throw new CategoryNotFoundException(id)
74+
}
75+
76+
async deleteCategory(id: string) {
77+
const deleteResponse = await this.categoriesRepository.delete(id)
78+
console.log(deleteResponse.affected)
79+
if (!deleteResponse.affected) {
80+
throw new CategoryNotFoundException(id)
81+
}
82+
}
83+
}

src/categories/category.entity.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
2+
import Post from 'src/posts/post.entity';
3+
4+
@Entity()
5+
class Category {
6+
@PrimaryGeneratedColumn('uuid')
7+
public id: string
8+
9+
@Column()
10+
public name: string;
11+
12+
// Bi-Direction, we do not use JoinTable here since it has been used at the other side
13+
@ManyToMany(() => Post, (post: Post) => post.categories)
14+
public posts: Post[];
15+
}
16+
17+
export default Category;

0 commit comments

Comments
 (0)