Compare commits
No commits in common. '50e54e492890e8f773a540e5e2faa3d06164fb51' and '61ac8246ab3d14bb1b916822551aa765a70fe9ff' have entirely different histories.
50e54e4928
...
61ac8246ab
23 changed files with 505 additions and 257 deletions
@ -0,0 +1,68 @@ |
|||||||
|
'use strict'; |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
up: async (queryInterface, Sequelize) => { |
||||||
|
await queryInterface.dropTable('Admins', { cascade: true }); |
||||||
|
await queryInterface.createTable('Admins', { |
||||||
|
id: { |
||||||
|
type: Sequelize.INTEGER, |
||||||
|
autoIncrement: true, |
||||||
|
primaryKey: true, |
||||||
|
allowNull: false, |
||||||
|
}, |
||||||
|
email: { |
||||||
|
type: Sequelize.STRING, |
||||||
|
allowNull: false, |
||||||
|
unique: true, |
||||||
|
}, |
||||||
|
password: { |
||||||
|
type: Sequelize.STRING, |
||||||
|
allowNull: false, |
||||||
|
}, |
||||||
|
role: { |
||||||
|
type: Sequelize.STRING, |
||||||
|
defaultValue: 'admin', |
||||||
|
}, |
||||||
|
firstName: { |
||||||
|
type: Sequelize.STRING, |
||||||
|
allowNull: false, |
||||||
|
}, |
||||||
|
lastName: { |
||||||
|
type: Sequelize.STRING, |
||||||
|
allowNull: false, |
||||||
|
}, |
||||||
|
username: { |
||||||
|
type: Sequelize.STRING, |
||||||
|
allowNull: false, |
||||||
|
unique: true, |
||||||
|
}, |
||||||
|
phoneNumber: { |
||||||
|
type: Sequelize.STRING, |
||||||
|
allowNull: false, |
||||||
|
unique: true, |
||||||
|
}, |
||||||
|
refreshToken: { |
||||||
|
type: Sequelize.STRING, |
||||||
|
allowNull: true, |
||||||
|
}, |
||||||
|
gender: { |
||||||
|
type: Sequelize.ENUM('male', 'female'), |
||||||
|
allowNull: false, |
||||||
|
}, |
||||||
|
createdAt: { |
||||||
|
type: Sequelize.DATE, |
||||||
|
allowNull: false, |
||||||
|
defaultValue: Sequelize.fn('NOW'), |
||||||
|
}, |
||||||
|
updatedAt: { |
||||||
|
type: Sequelize.DATE, |
||||||
|
allowNull: false, |
||||||
|
defaultValue: Sequelize.fn('NOW'), |
||||||
|
}, |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
down: async (queryInterface, Sequelize) => { |
||||||
|
await queryInterface.dropTable('Admins'); |
||||||
|
}, |
||||||
|
}; |
@ -0,0 +1,40 @@ |
|||||||
|
import { Controller, Get, Post, Body, Request, Put, UseGuards } from "@nestjs/common"; |
||||||
|
import { AdminService } from "./admin.service"; |
||||||
|
import { CreateAdminDto } from "./dto/create-Admin.dto"; |
||||||
|
import { LoginAdminDto } from "./dto/login-Admin.dto"; |
||||||
|
import { UpdateUserDto } from "./dto/update-user.dto"; |
||||||
|
import { RoleGuard } from "src/guard/role.guard"; |
||||||
|
|
||||||
|
@Controller("admin") |
||||||
|
export class AdminController { |
||||||
|
constructor(private readonly adminService: AdminService) {} |
||||||
|
// register as a admin
|
||||||
|
@Post("register") |
||||||
|
async register(@Body() createAdminDto: CreateAdminDto): Promise<{ message }> { |
||||||
|
return this.adminService.register(createAdminDto); |
||||||
|
} |
||||||
|
//login as admin
|
||||||
|
@Post("login") |
||||||
|
async login(@Body() loginAdminDto: LoginAdminDto): Promise<{ accessToken; refreshToken }> { |
||||||
|
return this.adminService.login(loginAdminDto); |
||||||
|
} |
||||||
|
//logout user
|
||||||
|
@UseGuards(RoleGuard) |
||||||
|
@Get("logout") |
||||||
|
async logout(@Request() req) { |
||||||
|
const userId = req.user.id; |
||||||
|
return this.adminService.logout(userId); |
||||||
|
} |
||||||
|
//get a new access token
|
||||||
|
@Post("new-token") |
||||||
|
async newAccessToken(@Body("token") token: string) { |
||||||
|
return this.adminService.newAccessToken(token); |
||||||
|
} |
||||||
|
//edit admin profile
|
||||||
|
@UseGuards(RoleGuard) |
||||||
|
@Put() |
||||||
|
async editAdminProfile(@Request() req, @Body() updateAdminDto: UpdateUserDto): Promise<{ message }> { |
||||||
|
const userId = req.user.id; |
||||||
|
return this.adminService.editAdminProfile(userId, updateAdminDto); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
import { Module } from '@nestjs/common'; |
||||||
|
import { AdminService } from './admin.service'; |
||||||
|
import { AdminController } from './admin.controller'; |
||||||
|
import { SequelizeModule } from '@nestjs/sequelize'; |
||||||
|
import { Admin } from './entities/admin.entity'; |
||||||
|
import { JwtModule } from '@nestjs/jwt'; |
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config'; |
||||||
|
|
||||||
|
@Module({ |
||||||
|
imports:[SequelizeModule.forFeature([Admin]), |
||||||
|
JwtModule.registerAsync({ |
||||||
|
imports: [ConfigModule], |
||||||
|
inject: [ConfigService], |
||||||
|
useFactory: (config: ConfigService) => { |
||||||
|
return { |
||||||
|
secret: config.get<string>('JWT_SECRET'), |
||||||
|
signOptions: { |
||||||
|
expiresIn: config.get<string | number>('JWT_EXPIRES', '1h'), |
||||||
|
}, |
||||||
|
}; |
||||||
|
}, |
||||||
|
}), |
||||||
|
], |
||||||
|
controllers: [AdminController], |
||||||
|
providers: [AdminService], |
||||||
|
}) |
||||||
|
export class AdminModule {} |
@ -0,0 +1,163 @@ |
|||||||
|
import { HttpException, HttpStatus, Injectable } from "@nestjs/common"; |
||||||
|
import { InjectModel } from "@nestjs/sequelize"; |
||||||
|
import { Admin } from "./entities/admin.entity"; |
||||||
|
import * as bcrypt from "bcrypt"; |
||||||
|
import { JwtService } from "@nestjs/jwt"; |
||||||
|
import { ConfigService } from "@nestjs/config"; |
||||||
|
import { CreateAdminDto } from "./dto/create-Admin.dto"; |
||||||
|
import { LoginAdminDto } from "./dto/login-Admin.dto"; |
||||||
|
import { UpdateUserDto } from "./dto/update-user.dto"; |
||||||
|
@Injectable() |
||||||
|
export class AdminService { |
||||||
|
constructor( |
||||||
|
@InjectModel(Admin) private readonly adminModel: typeof Admin, |
||||||
|
private readonly jwtService: JwtService, |
||||||
|
private readonly configService: ConfigService, |
||||||
|
) {} |
||||||
|
//register method
|
||||||
|
async register(createAdminDto: CreateAdminDto): Promise<{ message }> { |
||||||
|
try { |
||||||
|
const existingAdminByEmail = await this.adminModel.findOne({ |
||||||
|
where: { email: createAdminDto.email }, |
||||||
|
}); |
||||||
|
if (existingAdminByEmail) { |
||||||
|
throw new HttpException("The provided email is already registered.", HttpStatus.CONFLICT); |
||||||
|
} |
||||||
|
|
||||||
|
const existingAdminByUsername = await this.adminModel.findOne({ |
||||||
|
where: { username: createAdminDto.username }, |
||||||
|
}); |
||||||
|
if (existingAdminByUsername) { |
||||||
|
throw new HttpException("The provided username is already taken.", HttpStatus.CONFLICT); |
||||||
|
} |
||||||
|
|
||||||
|
const existingAdminByPhoneNumber = await this.adminModel.findOne({ |
||||||
|
where: { phoneNumber: createAdminDto.phoneNumber }, |
||||||
|
}); |
||||||
|
if (existingAdminByPhoneNumber) { |
||||||
|
throw new HttpException("The provided phone number is already registered.", HttpStatus.CONFLICT); |
||||||
|
} |
||||||
|
|
||||||
|
createAdminDto.password = await bcrypt.hash(createAdminDto.password, 10); |
||||||
|
|
||||||
|
await this.adminModel.create(createAdminDto); |
||||||
|
return { message: "Admin registration is successful." }; |
||||||
|
} catch (error) { |
||||||
|
if (error instanceof HttpException) { |
||||||
|
throw error; |
||||||
|
} |
||||||
|
throw new HttpException("An unexpected error occurred. Please try again later.", HttpStatus.INTERNAL_SERVER_ERROR); |
||||||
|
} |
||||||
|
} |
||||||
|
//login method
|
||||||
|
async login(loginAdminDto: LoginAdminDto): Promise<{ accessToken: string; refreshToken: string }> { |
||||||
|
try { |
||||||
|
const admin = await this.adminModel.findOne({ |
||||||
|
where: { email: loginAdminDto.email,username:loginAdminDto.username }, |
||||||
|
}); |
||||||
|
|
||||||
|
if (!admin) { |
||||||
|
throw new HttpException("Invalid email, username or password.", HttpStatus.UNAUTHORIZED); |
||||||
|
} |
||||||
|
|
||||||
|
const isValidPassword = await bcrypt.compare(loginAdminDto.password, admin.password); |
||||||
|
if (!isValidPassword) { |
||||||
|
throw new HttpException("Invalid email or password.", HttpStatus.UNAUTHORIZED); |
||||||
|
} |
||||||
|
|
||||||
|
const accessToken = this.jwtService.sign( |
||||||
|
{ id: admin.id, role: admin.role }, |
||||||
|
{ |
||||||
|
secret: this.configService.get<string>("JWT_SECRET"), |
||||||
|
expiresIn: this.configService.get<string | number>("JWT_EXPIRES") || "1h", |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
const refreshToken = this.jwtService.sign( |
||||||
|
{ id: admin.id, role: admin.role }, |
||||||
|
{ |
||||||
|
secret: this.configService.get<string>("JWT_REFRESH_SECRET"), |
||||||
|
expiresIn: this.configService.get<string | number>("JWT_REFRESH_EXPIRES") || "7d", |
||||||
|
}, |
||||||
|
); |
||||||
|
await this.adminModel.update( |
||||||
|
{ refreshToken }, //
|
||||||
|
{ where: { id: admin.id } }, |
||||||
|
); |
||||||
|
|
||||||
|
return { accessToken, refreshToken }; |
||||||
|
} catch (error) { |
||||||
|
if (error instanceof HttpException) { |
||||||
|
throw error; |
||||||
|
} |
||||||
|
throw new HttpException("An unexpected error occurred. Please try again later.", HttpStatus.INTERNAL_SERVER_ERROR); |
||||||
|
} |
||||||
|
} |
||||||
|
//logout (delete refresh token from database)
|
||||||
|
async logout(userId: number): Promise<{ message: string }> { |
||||||
|
try { |
||||||
|
if (!userId) { |
||||||
|
throw new HttpException("User ID is required.", HttpStatus.BAD_REQUEST); |
||||||
|
} |
||||||
|
const user = await this.adminModel.findOne({ where: { id: userId } }); |
||||||
|
if (!user) { |
||||||
|
throw new HttpException("User not found.", HttpStatus.NOT_FOUND); |
||||||
|
} |
||||||
|
await this.adminModel.update({ refreshToken: null }, { where: { id: userId } }); |
||||||
|
return { message: "Logout is successful" }; |
||||||
|
} catch (error) { |
||||||
|
throw new HttpException("An unexpected error occurred during logout. Please try again later.", HttpStatus.INTERNAL_SERVER_ERROR); |
||||||
|
} |
||||||
|
} |
||||||
|
//getting new access token
|
||||||
|
async newAccessToken(refreshToken: string) { |
||||||
|
if (!refreshToken) { |
||||||
|
throw new HttpException("Refresh token is required.", HttpStatus.BAD_REQUEST); |
||||||
|
} |
||||||
|
|
||||||
|
let decoded; |
||||||
|
try { |
||||||
|
decoded = this.jwtService.verify(refreshToken, { secret: this.configService.get<string>("JWT_REFRESH_SECRET") }); |
||||||
|
} catch (error) { |
||||||
|
throw new HttpException("Invalid or expired token.", HttpStatus.UNAUTHORIZED); |
||||||
|
} |
||||||
|
|
||||||
|
const user = await this.adminModel.findOne({where:{id:decoded.id}}); |
||||||
|
if (!user) { |
||||||
|
throw new HttpException("User not found.", HttpStatus.NOT_FOUND); |
||||||
|
} |
||||||
|
|
||||||
|
if (user.refreshToken !== refreshToken) { |
||||||
|
throw new HttpException("Invalid refresh token.", HttpStatus.FORBIDDEN); |
||||||
|
} |
||||||
|
|
||||||
|
const accessToken = this.jwtService.sign( |
||||||
|
{ id: user.id, role: user.role }, |
||||||
|
{ |
||||||
|
secret: this.configService.get<string>("JWT_SECRET"), |
||||||
|
expiresIn: this.configService.get<string | number>("JWT_EXPIRES") || "1h", |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
return { accessToken }; |
||||||
|
} |
||||||
|
//edit admin profile method
|
||||||
|
async editAdminProfile(userId: number, updateAdminDto: UpdateUserDto) { |
||||||
|
try { |
||||||
|
let user = await this.adminModel.findOne({ where: { id: userId } }); |
||||||
|
|
||||||
|
if (!user) { |
||||||
|
throw new Error("Admin not found"); |
||||||
|
} |
||||||
|
|
||||||
|
await user.update(updateAdminDto); |
||||||
|
user = await this.adminModel.findOne({ |
||||||
|
where: { id: userId }, |
||||||
|
attributes: { exclude: ["password"] }, |
||||||
|
}); |
||||||
|
return { message: "user account updated successful", user }; |
||||||
|
} catch (error) { |
||||||
|
throw new Error(`An error occurred while updating admin: ${error.message}`); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
import { IsString, IsEmail, IsEnum, IsNotEmpty, IsOptional, Matches } from "class-validator"; |
||||||
|
|
||||||
|
export class CreateAdminDto { |
||||||
|
@IsEmail({}, { message: "Invalid email format" }) |
||||||
|
email: string; |
||||||
|
|
||||||
|
@IsString() |
||||||
|
@IsNotEmpty({ message: "Password is required" }) |
||||||
|
password: string; |
||||||
|
|
||||||
|
@IsString() |
||||||
|
@IsNotEmpty({ message: "First name is required" }) |
||||||
|
firstName: string; |
||||||
|
|
||||||
|
@IsString() |
||||||
|
@IsNotEmpty({ message: "Last name is required" }) |
||||||
|
lastName: string; |
||||||
|
|
||||||
|
@IsString() |
||||||
|
@IsNotEmpty({ message: "Username is required" }) |
||||||
|
username: string; |
||||||
|
|
||||||
|
@IsString() |
||||||
|
@Matches(/^[0-9]{11}$/, { message: "Phone number must be 10 digits" }) |
||||||
|
phoneNumber: string; |
||||||
|
|
||||||
|
@IsEnum(["male", "female"], { message: "Gender must be 'male' or 'female'" }) |
||||||
|
gender: string; |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
import { IsString, IsEmail,IsNotEmpty} from "class-validator"; |
||||||
|
|
||||||
|
export class LoginAdminDto { |
||||||
|
@IsEmail({}, { message: "Invalid email format" }) |
||||||
|
email: string; |
||||||
|
|
||||||
|
@IsString() |
||||||
|
@IsNotEmpty({ message: "Password is required" }) |
||||||
|
password: string; |
||||||
|
|
||||||
|
@IsString() |
||||||
|
@IsNotEmpty({ message: "Username is required" }) |
||||||
|
username: string; |
||||||
|
|
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
|
||||||
|
import { IsOptional, IsString, IsEmail, IsEnum } from 'class-validator'; |
||||||
|
import {Gender } from '../entities/admin.entity';
|
||||||
|
|
||||||
|
export class UpdateUserDto { |
||||||
|
@IsOptional()
|
||||||
|
@IsString() |
||||||
|
username?: string; |
||||||
|
|
||||||
|
@IsOptional() |
||||||
|
@IsEmail() |
||||||
|
email?: string; |
||||||
|
|
||||||
|
@IsOptional() |
||||||
|
@IsString() |
||||||
|
password?: string; |
||||||
|
|
||||||
|
@IsOptional() |
||||||
|
@IsString() |
||||||
|
firstName?: string; |
||||||
|
|
||||||
|
@IsOptional() |
||||||
|
@IsString() |
||||||
|
lastName?: string; |
||||||
|
|
||||||
|
@IsOptional() |
||||||
|
@IsString() |
||||||
|
phoneNumber?: string; |
||||||
|
|
||||||
|
@IsOptional() |
||||||
|
@IsEnum(Gender) |
||||||
|
gender?: Gender;
|
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
import { Column, Table, Model, DataType } from "sequelize-typescript"; |
||||||
|
|
||||||
|
@Table |
||||||
|
export class Admin extends Model<Admin> { |
||||||
|
@Column({ unique: true }) |
||||||
|
email: string; |
||||||
|
|
||||||
|
@Column |
||||||
|
password: string; |
||||||
|
|
||||||
|
@Column({ defaultValue: "admin" }) |
||||||
|
role: string; |
||||||
|
|
||||||
|
@Column |
||||||
|
firstName: string; |
||||||
|
|
||||||
|
@Column |
||||||
|
lastName: string; |
||||||
|
|
||||||
|
@Column({ unique: true }) |
||||||
|
username: string; |
||||||
|
|
||||||
|
@Column({ unique: true }) |
||||||
|
phoneNumber: string; |
||||||
|
@Column({ |
||||||
|
type: DataType.STRING, |
||||||
|
allowNull: true, |
||||||
|
}) |
||||||
|
refreshToken: string; |
||||||
|
|
||||||
|
@Column({ |
||||||
|
type: DataType.ENUM("male", "female"), |
||||||
|
allowNull: false, |
||||||
|
}) |
||||||
|
gender: string; |
||||||
|
} |
||||||
|
export enum Gender { |
||||||
|
Male = "male", |
||||||
|
Female = "female", |
||||||
|
} |
@ -1,97 +0,0 @@ |
|||||||
import { Injectable, HttpException, HttpStatus } from "@nestjs/common"; |
|
||||||
import { InjectModel } from "@nestjs/sequelize"; |
|
||||||
import { AddBalanceResponse } from "./add-balance-response.interface"; |
|
||||||
import { Transaction } from "./entities/transaction.entity"; |
|
||||||
import { Wallet } from "./entities/wallet.entity"; |
|
||||||
|
|
||||||
@Injectable() |
|
||||||
export class WalletService { |
|
||||||
constructor( |
|
||||||
@InjectModel(Wallet) private walletModel: typeof Wallet, |
|
||||||
@InjectModel(Transaction) private transactionModel: typeof Transaction, |
|
||||||
) {} |
|
||||||
//get wallet info
|
|
||||||
async getWalletInfo(userId: number) { |
|
||||||
const wallet = await this.walletModel.findOne({ where: { userId } }); |
|
||||||
|
|
||||||
if (!wallet) { |
|
||||||
const newWallet = await this.walletModel.create({ userId, balance: 0 }); |
|
||||||
return { walletId: newWallet.id, userId: newWallet.userId, balance: newWallet.balance }; |
|
||||||
} |
|
||||||
|
|
||||||
return { walletId: wallet.id, userId: wallet.userId, balance: wallet.balance }; |
|
||||||
} |
|
||||||
//get wallet balance
|
|
||||||
async getBalance(userId: number) { |
|
||||||
const wallet = await this.walletModel.findOne({ where: { userId } }); |
|
||||||
|
|
||||||
if (!wallet) { |
|
||||||
throw new HttpException("Wallet not found!", HttpStatus.NOT_FOUND); |
|
||||||
} |
|
||||||
|
|
||||||
return { balance: wallet.balance }; |
|
||||||
} |
|
||||||
//charge balance of wallet by user
|
|
||||||
async addBalance(userId: number, amount: number): Promise<AddBalanceResponse> { |
|
||||||
try { |
|
||||||
const wallet = await this.walletModel.findOne({ where: { userId } }); |
|
||||||
if (wallet) { |
|
||||||
wallet.balance += Number(amount); |
|
||||||
await wallet.save(); |
|
||||||
return { message: "Balance updated successfully.", balance: wallet.balance }; |
|
||||||
} else { |
|
||||||
const newWallet = await this.walletModel.create({ userId, balance: amount }); |
|
||||||
return { message: "Wallet created and balance added successfully.", balance: newWallet.balance }; |
|
||||||
} |
|
||||||
} catch (error) { |
|
||||||
throw new HttpException("An error occurred while adding balance to the wallet.", HttpStatus.INTERNAL_SERVER_ERROR); |
|
||||||
} |
|
||||||
} |
|
||||||
//process of payment
|
|
||||||
async processPayment(userId: number, amount: number): Promise<string> { |
|
||||||
const wallet = await this.walletModel.findOne({ where: { userId } }); |
|
||||||
|
|
||||||
if (!wallet) { |
|
||||||
throw new HttpException("Please Charge your wallet", HttpStatus.NOT_FOUND); |
|
||||||
} |
|
||||||
|
|
||||||
if (wallet.balance < amount) { |
|
||||||
throw new HttpException("Insufficient funds", HttpStatus.BAD_REQUEST); |
|
||||||
} |
|
||||||
try { |
|
||||||
wallet.balance -= amount; |
|
||||||
|
|
||||||
await this.transactionModel.create({ |
|
||||||
walletId: wallet.id, |
|
||||||
amount: `-${amount}`, |
|
||||||
}); |
|
||||||
|
|
||||||
await wallet.save(); |
|
||||||
|
|
||||||
return "Payment processed successfully"; |
|
||||||
} catch (error) { |
|
||||||
console.error("Error processing payment:", error.message); |
|
||||||
throw new HttpException("An error occurred while processing the payment.", HttpStatus.INTERNAL_SERVER_ERROR); |
|
||||||
} |
|
||||||
} |
|
||||||
//getting transaction
|
|
||||||
async getTransactionById(userId: number) { |
|
||||||
const wallet = await this.getWalletInfo(userId); |
|
||||||
if (!wallet) { |
|
||||||
throw new HttpException("Wallet not found for the user.", HttpStatus.NOT_FOUND); |
|
||||||
} |
|
||||||
return await this.transactionModel.findAll({ |
|
||||||
where: { walletId: wallet.walletId }, |
|
||||||
}); |
|
||||||
} |
|
||||||
//getting transaction a user (admin)
|
|
||||||
async getTransactionByIdForAdmin(userId: number) { |
|
||||||
const wallet = await this.getWalletInfo(userId); |
|
||||||
if (!wallet) { |
|
||||||
throw new HttpException("Wallet not found for the user.", HttpStatus.NOT_FOUND); |
|
||||||
} |
|
||||||
return await this.transactionModel.findAll({ |
|
||||||
where: { walletId: wallet.walletId }, |
|
||||||
}); |
|
||||||
} |
|
||||||
} |
|
Loading…
Reference in new issue