Commit b3781a4a authored by Kulpybaev Ilyas's avatar Kulpybaev Ilyas

Урок-94

parent a9467abb
This diff is collapsed.
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"dev": "nodemon", "dev": "nodemon",
"lint": "eslint", "lint": "eslint",
"lint:fix": "eslint --fix" "lint:fix": "eslint --fix",
"seed": "node -r tsconfig-paths/register -r ts-node/register src/database/init.seeds.ts"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
...@@ -29,6 +30,7 @@ ...@@ -29,6 +30,7 @@
"typescript": "^5.1.6" "typescript": "^5.1.6"
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^8.4.1",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
...@@ -43,6 +45,7 @@ ...@@ -43,6 +45,7 @@
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"tsconfig-paths": "^4.2.0" "tsconfig-paths": "^4.2.0",
"typeorm-extension": "^3.5.1"
} }
} }
import { DataSource } from 'typeorm'; import { DataSource, DataSourceOptions } from 'typeorm';
import { Product } from '@/entities/product.entity'; import { Product } from '@/entities/product.entity';
import { Category } from '@/entities/category.entity'; import { Category } from '@/entities/category.entity';
import { User } from '@/entities/user.entity'; import { User } from '@/entities/user.entity';
import { SeederOptions } from 'typeorm-extension';
import { userFactory } from '@/database/factories/user.factory';
import MainSeeder from '@/database/seeds/main.seeder';
import { categoriesFactory } from '@/database/factories/category.factory';
import { productFactory } from '@/database/factories/product.factory';
export const appDataSource = new DataSource({ const options: DataSourceOptions & SeederOptions = {
type: 'mysql', type: 'mysql',
host: 'localhost', host: 'localhost',
port: 3306, port: 3306,
...@@ -13,4 +18,7 @@ export const appDataSource = new DataSource({ ...@@ -13,4 +18,7 @@ export const appDataSource = new DataSource({
synchronize: true, synchronize: true,
logging: true, logging: true,
entities: [Product, Category, User], entities: [Product, Category, User],
}); seeds: [MainSeeder],
factories: [userFactory, categoriesFactory, productFactory],
};
export const appDataSource = new DataSource(options);
...@@ -5,6 +5,7 @@ import { RegisterUserDto } from '@/dto/register-user.dto'; ...@@ -5,6 +5,7 @@ import { RegisterUserDto } from '@/dto/register-user.dto';
import { SignInUserDto } from '@/dto/sign-in-user.dto'; import { SignInUserDto } from '@/dto/sign-in-user.dto';
import { validate } from 'class-validator'; import { validate } from 'class-validator';
import { formatErrors } from '@/helpers/formatErrors'; import { formatErrors } from '@/helpers/formatErrors';
import { IRequestWithUser } from '@/interfaces/IRequestWithUser';
export class AuthController { export class AuthController {
private service: AuthService; private service: AuthService;
...@@ -60,4 +61,15 @@ export class AuthController { ...@@ -60,4 +61,15 @@ export class AuthController {
return res.status(500).send({ error: { message: 'Internal server error' } }); return res.status(500).send({ error: { message: 'Internal server error' } });
} }
}; };
logout: RequestHandler = async (req: IRequestWithUser, res) => {
if (!req.user?.token) return res.send({ message: 'success' });
try {
const { token } = req.user;
await this.service.logout(token);
} catch (e) {
return res.status(500).send({ error: { message: 'Internal server error' } });
}
return res.send({ message: 'success' });
};
} }
import { setSeederFactory } from 'typeorm-extension';
import { Category } from '@/entities/category.entity';
import { Faker } from '@faker-js/faker';
export const categoriesFactory = setSeederFactory(Category, (faker: Faker) => {
const category = new Category();
category.title = faker.commerce.department();
category.description = faker.lorem.sentence();
return category;
});
import { setSeederFactory } from 'typeorm-extension';
import { Product } from '@/entities/product.entity';
import { Faker } from '@faker-js/faker';
export const productFactory = setSeederFactory(Product, (faker: Faker) => {
const product = new Product();
product.title = faker.commerce.productName();
product.price = faker.number.int({ min: 100, max: 2000 });
product.description = faker.lorem.sentence();
return product;
});
import { Faker } from '@faker-js/faker';
import { setSeederFactory } from 'typeorm-extension';
import { User } from '../../entities/user.entity';
export const userFactory = setSeederFactory(User, (faker: Faker) => {
const user = new User();
user.username = faker.internet.userName();
user.displayName = faker.person.firstName();
user.password = 'password';
user.generateToken();
return user;
});
import { runSeeders } from 'typeorm-extension';
import { appDataSource } from '@/config/dataSource';
appDataSource.initialize().then(async () => {
await appDataSource.synchronize(true);
await runSeeders(appDataSource);
process.exit();
});
import { Seeder, SeederFactoryManager } from 'typeorm-extension';
import { DataSource } from 'typeorm';
import { User } from '@/entities/user.entity';
import { Category } from '@/entities/category.entity';
import { Product } from '@/entities/product.entity';
import { faker } from '@faker-js/faker';
export default class MainSeeder implements Seeder {
async run(dataSource: DataSource, factoryManager: SeederFactoryManager): Promise<void> {
const userFactory = factoryManager.get(User);
const categoryFactory = factoryManager.get(Category);
const productFactory = factoryManager.get(Product);
await userFactory.saveMany(7);
await userFactory.save({ displayName: 'User', username: 'user', password: '123456' });
await userFactory.save({ displayName: 'Admin', username: 'admin', password: '123456', role: 'admin' });
const categories = await categoryFactory.saveMany(3);
await productFactory.saveMany(4, { category: faker.helpers.arrayElement(categories) });
await productFactory.saveMany(4, { category: faker.helpers.arrayElement(categories) });
await productFactory.saveMany(4, { category: faker.helpers.arrayElement(categories) });
}
}
import { Seeder, SeederFactoryManager } from 'typeorm-extension';
import { DataSource } from 'typeorm';
import { User } from '@/entities/user.entity';
export default class UserSeeder implements Seeder {
async run(dataSource: DataSource, factoryManager: SeederFactoryManager): Promise<void> {
const userFactory = factoryManager.get(User);
await userFactory.saveMany(10);
}
}
import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm'; import { BeforeInsert, Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
@Entity('users') @Entity('users')
...@@ -19,6 +19,9 @@ export class User { ...@@ -19,6 +19,9 @@ export class User {
@Column() @Column()
token!: string; token!: string;
@Column({ default: 'user' })
role!: 'user' | 'admin';
async comparePassword(password: string): Promise<boolean> { async comparePassword(password: string): Promise<boolean> {
if (this.password) return await bcrypt.compare(password, this.password); if (this.password) return await bcrypt.compare(password, this.password);
return false; return false;
...@@ -27,4 +30,14 @@ export class User { ...@@ -27,4 +30,14 @@ export class User {
generateToken() { generateToken() {
this.token = crypto.randomUUID(); this.token = crypto.randomUUID();
} }
@BeforeInsert()
async hashPassword() {
const SALT_WORK_FACTORY = 10;
if (this.password) {
const salt = await bcrypt.genSalt(SALT_WORK_FACTORY);
const hashedPassword = await bcrypt.hash(this.password, salt);
this.password = hashedPassword;
}
}
} }
import { IUser } from '@/interfaces/IUser.interface';
import { Request } from 'express';
export interface IRequestWithUser extends Request {
user?: IUser;
}
...@@ -3,4 +3,6 @@ export interface IUser { ...@@ -3,4 +3,6 @@ export interface IUser {
username: string; username: string;
password?: string; password?: string;
displayName: string; displayName: string;
token?: string;
role: 'user' | 'admin';
} }
import { NextFunction, Request, Response } from 'express';
import { AuthService } from '@/services/auth.service';
import { IRequestWithUser } from '@/interfaces/IRequestWithUser';
import { IUser } from '@/interfaces/IUser.interface';
const service = new AuthService();
export const authValidate = async (req: Request, res: Response, next: NextFunction) => {
const token = req.header('Authorization');
if (!token) {
return res.status(401).send({ error: { message: 'Token not passed' } });
}
const user = await service.getUserByToken(token);
if (!user) {
return res.status(401).send({ error: { message: 'Invalid token' } });
}
(req as IRequestWithUser).user = user as unknown as IUser;
next();
return;
};
import { IRequestWithUser } from '@/interfaces/IRequestWithUser';
import { NextFunction, Response } from 'express';
export function checkRole(...allowedRoles: string[]) {
return (req: IRequestWithUser, res: Response, next: NextFunction) => {
const user = req.user;
if (user && allowedRoles.includes(user.role)) {
next();
} else {
res.status(403).send({ error: 'Permission denied' });
}
};
}
...@@ -4,9 +4,7 @@ import { appDataSource } from '@/config/dataSource'; ...@@ -4,9 +4,7 @@ import { appDataSource } from '@/config/dataSource';
import { SignInUserDto } from '@/dto/sign-in-user.dto'; import { SignInUserDto } from '@/dto/sign-in-user.dto';
import { RegisterUserDto } from '@/dto/register-user.dto'; import { RegisterUserDto } from '@/dto/register-user.dto';
import { IUser } from '@/interfaces/IUser.interface'; import { IUser } from '@/interfaces/IUser.interface';
import bcrypt from 'bcrypt';
const SALT_WORK_FACTORY = 10;
export class UserRepository extends Repository<User> { export class UserRepository extends Repository<User> {
constructor() { constructor() {
super(User, appDataSource.createEntityManager()); super(User, appDataSource.createEntityManager());
...@@ -14,7 +12,7 @@ export class UserRepository extends Repository<User> { ...@@ -14,7 +12,7 @@ export class UserRepository extends Repository<User> {
async signIn(signInUserDto: SignInUserDto): Promise<User> { async signIn(signInUserDto: SignInUserDto): Promise<User> {
const user = await this.findOne({ const user = await this.findOne({
select: ['username', 'displayName', 'id', 'password'], select: ['username', 'displayName', 'id', 'password', 'role'],
where: { username: signInUserDto.username }, where: { username: signInUserDto.username },
}); });
...@@ -30,9 +28,7 @@ export class UserRepository extends Repository<User> { ...@@ -30,9 +28,7 @@ export class UserRepository extends Repository<User> {
} }
async register(registerUserDto: RegisterUserDto): Promise<IUser> { async register(registerUserDto: RegisterUserDto): Promise<IUser> {
const salt = await bcrypt.genSalt(SALT_WORK_FACTORY);
const userData = await this.create(registerUserDto); const userData = await this.create(registerUserDto);
userData.password = await bcrypt.hash(registerUserDto.password, salt);
userData.generateToken(); userData.generateToken();
const user = await this.save(userData); const user = await this.save(userData);
delete user.password; delete user.password;
...@@ -40,7 +36,15 @@ export class UserRepository extends Repository<User> { ...@@ -40,7 +36,15 @@ export class UserRepository extends Repository<User> {
return user; return user;
} }
async getUserByToken(token: string): Promise<IUser | null> { async getUserByToken(token: string): Promise<User | null> {
return await this.findOneBy({ token }); return await this.findOneBy({ token });
} }
async clearToken(token: string) {
const user = await this.getUserByToken(token);
if (user) {
user.generateToken();
await this.save(user);
}
}
} }
import { IRoute } from '@/interfaces/IRoute.interface'; import { IRoute } from '@/interfaces/IRoute.interface';
import { Router } from 'express'; import { Router } from 'express';
import { AuthController } from '@/controllers/auth.controller'; import { AuthController } from '@/controllers/auth.controller';
import { authValidate } from '@/middlewares/authValidate';
export class AuthRoute implements IRoute { export class AuthRoute implements IRoute {
public path = '/auth'; public path = '/auth';
...@@ -16,5 +17,6 @@ export class AuthRoute implements IRoute { ...@@ -16,5 +17,6 @@ export class AuthRoute implements IRoute {
this.router.post('/register', this.controller.register); this.router.post('/register', this.controller.register);
this.router.post('/sign-in', this.controller.signIn); this.router.post('/sign-in', this.controller.signIn);
this.router.get('/secret', this.controller.secret); this.router.get('/secret', this.controller.secret);
this.router.delete('/logout', authValidate, this.controller.logout);
} }
} }
import { IRoute } from '@/interfaces/IRoute.interface'; import { IRoute } from '@/interfaces/IRoute.interface';
import { Router } from 'express'; import { Router } from 'express';
import { CategoryController } from '@/controllers/category.controller'; import { CategoryController } from '@/controllers/category.controller';
import { authValidate } from '@/middlewares/authValidate';
import { checkRole } from '@/middlewares/checkRole';
export class CategoryRoute implements IRoute { export class CategoryRoute implements IRoute {
public path = '/categories'; public path = '/categories';
...@@ -14,6 +16,6 @@ export class CategoryRoute implements IRoute { ...@@ -14,6 +16,6 @@ export class CategoryRoute implements IRoute {
private init() { private init() {
this.router.get('/', this.controller.getCategories); this.router.get('/', this.controller.getCategories);
this.router.post('/', this.controller.createCategory); this.router.post('/', authValidate, checkRole('admin'), this.controller.createCategory);
} }
} }
...@@ -2,6 +2,8 @@ import { Router } from 'express'; ...@@ -2,6 +2,8 @@ import { Router } from 'express';
import { IRoute } from '@/interfaces/IRoute.interface'; import { IRoute } from '@/interfaces/IRoute.interface';
import { ProductController } from '@/controllers/product.controller'; import { ProductController } from '@/controllers/product.controller';
import { upload } from '@/middlewares/upload'; import { upload } from '@/middlewares/upload';
import { authValidate } from '@/middlewares/authValidate';
import { checkRole } from '@/middlewares/checkRole';
export class ProductRoute implements IRoute { export class ProductRoute implements IRoute {
public path = '/products'; public path = '/products';
...@@ -16,6 +18,6 @@ export class ProductRoute implements IRoute { ...@@ -16,6 +18,6 @@ export class ProductRoute implements IRoute {
private init() { private init() {
this.router.get('/', this.controller.getProducts); this.router.get('/', this.controller.getProducts);
this.router.get('/:id', this.controller.getProduct); this.router.get('/:id', this.controller.getProduct);
this.router.post('/', upload.single('image'), this.controller.createProduct); this.router.post('/', authValidate, checkRole('admin'), upload.single('image'), this.controller.createProduct);
} }
} }
...@@ -21,4 +21,8 @@ export class AuthService { ...@@ -21,4 +21,8 @@ export class AuthService {
getUserByToken = async (token: string): Promise<IUser | null> => { getUserByToken = async (token: string): Promise<IUser | null> => {
return await this.repository.getUserByToken(token); return await this.repository.getUserByToken(token);
}; };
logout = async (token: string) => {
await this.repository.clearToken(token);
};
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment