Commit b3781a4a authored by Kulpybaev Ilyas's avatar Kulpybaev Ilyas

Урок-94

parent a9467abb
This diff is collapsed.
......@@ -7,7 +7,8 @@
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "nodemon",
"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": [],
"author": "",
......@@ -29,6 +30,7 @@
"typescript": "^5.1.6"
},
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"@types/bcrypt": "^5.0.2",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.17",
......@@ -43,6 +45,7 @@
"eslint-plugin-prettier": "^5.0.0",
"nodemon": "^3.0.1",
"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 { Category } from '@/entities/category.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',
host: 'localhost',
port: 3306,
......@@ -13,4 +18,7 @@ export const appDataSource = new DataSource({
synchronize: true,
logging: true,
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';
import { SignInUserDto } from '@/dto/sign-in-user.dto';
import { validate } from 'class-validator';
import { formatErrors } from '@/helpers/formatErrors';
import { IRequestWithUser } from '@/interfaces/IRequestWithUser';
export class AuthController {
private service: AuthService;
......@@ -60,4 +61,15 @@ export class AuthController {
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';
@Entity('users')
......@@ -19,6 +19,9 @@ export class User {
@Column()
token!: string;
@Column({ default: 'user' })
role!: 'user' | 'admin';
async comparePassword(password: string): Promise<boolean> {
if (this.password) return await bcrypt.compare(password, this.password);
return false;
......@@ -27,4 +30,14 @@ export class User {
generateToken() {
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 {
username: string;
password?: 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';
import { SignInUserDto } from '@/dto/sign-in-user.dto';
import { RegisterUserDto } from '@/dto/register-user.dto';
import { IUser } from '@/interfaces/IUser.interface';
import bcrypt from 'bcrypt';
const SALT_WORK_FACTORY = 10;
export class UserRepository extends Repository<User> {
constructor() {
super(User, appDataSource.createEntityManager());
......@@ -14,7 +12,7 @@ export class UserRepository extends Repository<User> {
async signIn(signInUserDto: SignInUserDto): Promise<User> {
const user = await this.findOne({
select: ['username', 'displayName', 'id', 'password'],
select: ['username', 'displayName', 'id', 'password', 'role'],
where: { username: signInUserDto.username },
});
......@@ -30,9 +28,7 @@ export class UserRepository extends Repository<User> {
}
async register(registerUserDto: RegisterUserDto): Promise<IUser> {
const salt = await bcrypt.genSalt(SALT_WORK_FACTORY);
const userData = await this.create(registerUserDto);
userData.password = await bcrypt.hash(registerUserDto.password, salt);
userData.generateToken();
const user = await this.save(userData);
delete user.password;
......@@ -40,7 +36,15 @@ export class UserRepository extends Repository<User> {
return user;
}
async getUserByToken(token: string): Promise<IUser | null> {
async getUserByToken(token: string): Promise<User | null> {
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 { Router } from 'express';
import { AuthController } from '@/controllers/auth.controller';
import { authValidate } from '@/middlewares/authValidate';
export class AuthRoute implements IRoute {
public path = '/auth';
......@@ -16,5 +17,6 @@ export class AuthRoute implements IRoute {
this.router.post('/register', this.controller.register);
this.router.post('/sign-in', this.controller.signIn);
this.router.get('/secret', this.controller.secret);
this.router.delete('/logout', authValidate, this.controller.logout);
}
}
import { IRoute } from '@/interfaces/IRoute.interface';
import { Router } from 'express';
import { CategoryController } from '@/controllers/category.controller';
import { authValidate } from '@/middlewares/authValidate';
import { checkRole } from '@/middlewares/checkRole';
export class CategoryRoute implements IRoute {
public path = '/categories';
......@@ -14,6 +16,6 @@ export class CategoryRoute implements IRoute {
private init() {
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';
import { IRoute } from '@/interfaces/IRoute.interface';
import { ProductController } from '@/controllers/product.controller';
import { upload } from '@/middlewares/upload';
import { authValidate } from '@/middlewares/authValidate';
import { checkRole } from '@/middlewares/checkRole';
export class ProductRoute implements IRoute {
public path = '/products';
......@@ -16,6 +18,6 @@ export class ProductRoute implements IRoute {
private init() {
this.router.get('/', this.controller.getProducts);
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 {
getUserByToken = async (token: string): Promise<IUser | null> => {
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