diff --git a/create table dataservice microservice.sql b/create table dataservice microservice.sql index e8d9018..1149498 100644 --- a/create table dataservice microservice.sql +++ b/create table dataservice microservice.sql @@ -93,7 +93,6 @@ CREATE TABLE "rules_details_ref" ( "deletedAt" DATE, "version" NUMERIC ); - CREATE TABLE "users" ( "id" BIGSERIAL PRIMARY KEY NOT NULL, "email" TEXT UNIQUE NOT NULL, @@ -102,6 +101,8 @@ CREATE TABLE "users" ( "name" TEXT, "phoneNumber" TEXT, "primaryRole" TEXT, + "resetCode" TEXT, + "resetCodeExpires" TIMESTAMP, "status" TEXT, "validFrom" DATE, "validTill" DATE, @@ -476,3 +477,20 @@ CREATE TABLE "event_analytics" ( "version" NUMERIC ); +CREATE TABLE "locations" ( + "id" BIGSERIAL PRIMARY KEY NOT NULL, + "name" TEXT, + "code" TEXT, + "images" TEXT[], + "status" TEXT, + "validFrom" DATE, + "validTill" DATE, + "createdAt" DATE, + "updatedAt" DATE, + "createdBy" TEXT, + "modifiedBy" TEXT, + "deletedAt" DATE, + "version" NUMERIC +); + + diff --git a/package-lock.json b/package-lock.json index 0888424..bdc5b2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@nestjs/websockets": "^10.4.15", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", "dotenv": "^16.3.1", "handlebars": "^4.7.8", "ioredis": "^5.6.0", @@ -4172,6 +4173,28 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -13800,6 +13823,22 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" }, + "cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "requires": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + } + } + }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", diff --git a/package.json b/package.json index bfb98ad..6657d78 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@nestjs/websockets": "^10.4.15", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", "dotenv": "^16.3.1", "handlebars": "^4.7.8", "ioredis": "^5.6.0", diff --git a/src/app.module.ts b/src/app.module.ts index 1ae74aa..06b19d1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -23,6 +23,7 @@ import { TimeSlotModule } from './timeSlot/timeSlot.module'; import { PushSubscriptionModule } from './push-subscription/push-subscription.module'; import { RedisModule } from './redis/redis.module'; import { BookingGatewayModule } from './booking-gateway/booking-gateway.module'; +import { LocationsModule } from './locations/locations.module'; @Module({ imports: [ @@ -44,7 +45,8 @@ import { BookingGatewayModule } from './booking-gateway/booking-gateway.module'; UserModule, PushSubscriptionModule, RedisModule, - BookingGatewayModule + BookingGatewayModule, + LocationsModule ], controllers: [AppController, AppConfigController], diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 3b4146a..a944c9e 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -6,10 +6,13 @@ import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { GoogleOauthGuard } from 'src/google-oauth/google-oauth.guard'; import { User } from 'src/user/user.entity'; import { UserService } from 'src/user/user.service'; +import { ForgotPasswordDto, LoginDto, ResetPasswordDto, SignupDto, VerifyCodeDto } from './auth.dto'; +import { MailService } from 'src/mail/mail.service'; + @ApiTags('Auth') @Controller('auth') export class AuthController { - constructor(private authService: AuthService, private userService: UserService) { } + constructor(private authService: AuthService, private userService: UserService, private mailService: MailService) { } @Post('login') @ApiOperation({ summary: 'Login' }) @@ -30,7 +33,7 @@ export class AuthController { "data": null } }) - async login(@Body() userDto: { email: string; password: string }, @Res() res: Response) { + async login(@Body() userDto: LoginDto, @Res() res: Response) { const user = await this.authService.validateUser(userDto); if (!user) { const errorResponse = new GenericResponse({ @@ -42,12 +45,26 @@ export class AuthController { res.status(404).send(errorResponse); } const tokens = await this.authService.login(user); - const httpResponse = new GenericResponse(null, tokens); + + res.header('Authorization', 'Bearer ' + tokens.access_token); + + res.cookie('refresh_token', tokens.refresh_token, { + httpOnly: true, + secure: false, + sameSite: 'lax', + path: '/' + }); + const httpResponse = new GenericResponse(null, { + // id: user.id, + email: user.email, + name: user.name, + userTypeCode: user.userTypeCode, + }); res.status(200).send(httpResponse); } @Post('signup') - async signup(@Body() userDto: User, @Res() res: Response) { + async signup(@Body() userDto: SignupDto, @Res() res: Response) { const user = await this.userService.findByEmail(userDto.email); if (user) { const errorResponse = new GenericResponse({ @@ -56,37 +73,112 @@ export class AuthController { exceptionMessage: 'ERR.NOT_FOUND', stackTrace: 'User already exists' }, null); - res.status(404).send(errorResponse); + res.status(401).send(errorResponse); } const userCreated = await this.userService.upsert(userDto, true); - const tokens = await this.authService.login(userCreated);//MARK: NEED MAILER TO SEND CONFIRMATION EMAIL - const httpResponse = new GenericResponse(null, tokens); + const tokens = await this.authService.login(userCreated); + res.header('Authorization', 'Bearer ' + tokens.access_token); + res.cookie('refresh_token', tokens.refresh_token, { + httpOnly: true, + secure: false, + sameSite: 'lax', + path: '/' + }) + const httpResponse = new GenericResponse(null, + { + email: tokens.email, + name: tokens.name, + userTypeCode: tokens.userTypeCode, + } + ); res.status(200).send(httpResponse); } + + @Post('refresh') - async refresh(@Body() body: { refresh_token: string }, @Res() res: Response) { - const newToken = await this.authService.refreshAccessToken(body.refresh_token); - // console.log("new token is",newToken); - if (!newToken) { - const errorResponse = new GenericResponse({ + async refresh(@Req() req: Request, @Res() res: Response) { + console.log("req.cookies"); + console.log(req.cookies); + console.log('headers:', req.headers.cookie); + + const refreshToken = req.cookies['refresh_token']; + if (!refreshToken) { + return res.status(401).json({ message: 'Refresh token missing' }); + } + try { + const newToken = await this.authService.refreshAccessToken(refreshToken); + + res.header('Authorization', 'Bearer ' + newToken.access_token); + return res.status(200).json(new GenericResponse(null, { + email: newToken.email, + name: newToken.name, + userTypeCode: newToken.userTypeCode + })); + } catch (err) { + return res.status(403).json({ + message: 'Invalid or expired refresh token', + error: err.message, + }); + } + } + + + + @Post('forgot-password') + @ApiOperation({ summary: 'Send reset code to email' }) + async forgotPassword(@Body() dto: ForgotPasswordDto, @Res() res: Response) { + await this.authService.sendResetCode(dto.email); + return res.status(200).send(new GenericResponse(null, { + message: 'If your email is registered, a code has been sent.', + })); + } + + @Post('verify-reset-code') + @ApiOperation({ summary: 'Verify 4-digit reset code' }) + async verifyCode(@Body() dto: VerifyCodeDto, @Res() res: Response) { + const isValid = await this.authService.verifyResetCode(dto.email, dto.code); + if (!isValid) { + return res.status(400).send(new GenericResponse({ exception: true, exceptionSeverity: 'HIGH', - exceptionMessage: 'ERR.NOT_FOUND', - stackTrace: 'Token not found' - }, null) - res.status(404).send(errorResponse); + exceptionMessage: 'ERR.INVALID_OR_EXPIRED_CODE', + stackTrace: 'Code is invalid or expired', + }, null)); } - const httpResponse = new GenericResponse(null, newToken); - res.status(201).send(httpResponse); + + return res.status(200).send(new GenericResponse(null, { + message: 'Code verified successfully.', + })); + } + + + @Post('reset-password') + @ApiOperation({ summary: 'Reset password using code' }) + async resetPassword(@Body() dto: ResetPasswordDto, @Res() res: Response) { + const success = await this.authService.resetPasswordWithCode(dto.email, dto.code, dto.password); + + if (!success) { + return res.status(400).send(new GenericResponse({ + exception: true, + exceptionSeverity: 'HIGH', + exceptionMessage: 'ERR.INVALID_OR_EXPIRED_CODE', + stackTrace: 'Code is invalid or expired', + }, null)); + } + + return res.status(200).send(new GenericResponse(null, { + message: 'Password has been reset successfully.', + })); } @Delete('logout') - delete(@Body() body: { refresh_token: string }, @Res() res: Response) { - const deleteToken = this.authService.logout(body.refresh_token); + delete(@Res() res: Response, @Req() req: Request) { + const refreshToken = req.cookies['refresh_token']; + const deleteToken = this.authService.logout(refreshToken); res.status(200).send(deleteToken); } - @Post('verifyAccess') + // @Post('verifyAccess') async verifyAccessToken(@Body() body: { access_token: string }, @Res() res: Response) { const token = await this.authService.verifyAccessToken(body.access_token); if (token) { @@ -103,7 +195,7 @@ export class AuthController { } } - @Post('verifyRefresh') + // @Post('verifyRefresh') async verifyRefreshToken(@Body() body: { refresh_token: string }, @Res() res: Response) { const token = await this.authService.verifyRefreshToken(body.refresh_token); if (token) { @@ -126,15 +218,20 @@ export class AuthController { @UseGuards(GoogleOauthGuard) async googleLogin(@Res() res: Response) { console.log("inside google login"); - } //basically this is used to call googleLogin for login credentials + } //basically this is used to call googleLogin for login credentials @Get('google-redirect') @UseGuards(GoogleOauthGuard) async googleOauthRedirect(@Req() req: Request, @Res() res: Response) { console.log("inside google redirect"); const httpResponse = await this.authService.googleOauthRedirect(req.user); - return res.status(httpResponse.statusCode).send(httpResponse); + res.header('Authorization', 'Bearer ' + httpResponse.access_token); + res.cookie('refresh_token', httpResponse.refresh_token, { + httpOnly: true, + secure: false, + sameSite: 'lax', + path: '/' + }) + return res.status(httpResponse.statusCode).send(); } - - } diff --git a/src/auth/auth.dto.ts b/src/auth/auth.dto.ts new file mode 100644 index 0000000..028e1ed --- /dev/null +++ b/src/auth/auth.dto.ts @@ -0,0 +1,101 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEmail, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, MinLength } from 'class-validator'; + +export class SignupDto { + @ApiProperty({ + description: 'User email address', + example: 'user@example.com', + }) + @IsEmail() + @IsNotEmpty() + email: string; + + @ApiProperty({ + description: 'User phone number in international format', + example: '+1234567890', + }) + @IsPhoneNumber(null) + @IsOptional() + @IsNotEmpty() + phoneNumber?: string; + + @ApiPropertyOptional({ + description: 'User full name (optional)', + example: 'John Doe', + }) + @IsOptional() + @IsString() + name?: string; + + @ApiProperty({ + // description: 'Password with a minimum of 6 characters', + example: 'securePassword123', + }) + @IsString() + // @MinLength(6) + @IsNotEmpty() + password: string; + + @ApiProperty({ + description: 'User type code', + example: 'ADMIN', + }) + @IsString() + @IsNotEmpty() + userTypeCode: string +} + + +export class LoginDto { + @ApiProperty({ + description: 'User email address', + example: 'user@example.com', + }) + @IsEmail() + @IsNotEmpty() + email: string; + + @ApiProperty({ + // description: 'Password with a minimum of 6 characters', + example: 'securePassword123', + }) + @IsString() + // @MinLength(6) + @IsNotEmpty() + password: string; +} + +export class ForgotPasswordDto { + @ApiProperty({ example: 'user@example.com' }) + @IsEmail() + @IsNotEmpty() + email: string; +} + +export class VerifyCodeDto { + @ApiProperty({ example: 'user@example.com' }) + @IsEmail() + @IsNotEmpty() + email: string; + + @ApiProperty({ example: '1234' }) + @IsString() + code: string; +} + + +export class ResetPasswordDto { + @ApiProperty({ example: 'user@example.com' }) + @IsEmail() + @IsNotEmpty() + email: string; + + @ApiProperty({ example: '1234' }) + @IsString() + code: string; + + @ApiProperty({ example: 'newStrongPassword123' }) + @IsString() + @MinLength(6) + password: string; +} \ No newline at end of file diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index f017b11..d56b47c 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -8,6 +8,7 @@ import { JwtStrategy } from 'src/jwt/jwt.strategy'; import { Utility } from 'src/common/Utility'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { GoogleStrategy } from 'src/google-oauth/google.strategy'; +import { MailModule } from 'src/mail/mail.module'; @Module({ imports: [ @@ -25,6 +26,7 @@ import { GoogleStrategy } from 'src/google-oauth/google.strategy'; inject: [ConfigService], }), UserModule, + MailModule ], controllers: [AuthController], providers: [AuthService, JwtStrategy,GoogleStrategy], diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 5b2d623..c39149f 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -7,11 +7,16 @@ import RefreshToken from 'src/jwt/refresh-token.entity'; import { User } from 'src/user/user.entity'; import { UserService } from 'src/user/user.service'; import { Request } from 'express'; +import * as crypto from 'crypto'; +import { MailService } from 'src/mail/mail.service'; +import { instanceToPlain } from 'class-transformer'; + + @Injectable() export class AuthService { - constructor(private userService: UserService, private jwtService: JwtService) { } + constructor(private userService: UserService, private jwtService: JwtService, private mailService: MailService) { } private signToken(payload: any, type: 'accessToken' | 'refreshToken') { const config = Utility.jwtConfig[type]; @@ -33,16 +38,23 @@ export class AuthService { } async validateUser(payload: any) { - return this.userService.findByEmail(payload.email); + return this.userService.findByEmailWithPassword(payload.email, payload.password); } async login(user: any) { - const { password, ...rest } = user; - const payload = { rest }; + // const plainUser = instanceToPlain(user); + const payload = { + id: user.id, + email: user.email, + userTypeCode: user.userTypeCode, + }; const accessToken = this.signToken(payload, 'accessToken'); const refreshToken = this.signToken(payload, 'refreshToken'); await RefreshToken.create({ email: user.email, token: refreshToken, type: 'jwt' }); return { + name: user.name, + email: user.email, + userTypeCode: user.userTypeCode, access_token: accessToken, refresh_token: refreshToken, }; @@ -69,10 +81,18 @@ export class AuthService { if (!user) { throw new Error('User not found'); } - const { password, ...rest } = user; - const newPayload = { rest }; - const accessToken = this.signToken({ newPayload }, 'accessToken'); - return { access_token: accessToken }; + const newPayload = { + id: user.id, + email: user.email, + userTypeCode: user.userTypeCode, + }; + const accessToken = this.signToken(newPayload, 'accessToken'); + return { + name: user.name, + email: user.email, + userTypeCode: user.userTypeCode, + access_token: accessToken + }; } async verifyRefreshToken(refreshToken: string) { @@ -90,6 +110,44 @@ export class AuthService { } + async sendResetCode(email: string): Promise { + const user = await this.userService.findByEmail(email); + if (!user) return; + const plainUser = user.get({ plain: true }); + console.log("user", plainUser); + const code = Math.floor(1000 + Math.random() * 9000).toString(); + const expires = new Date(Date.now() + 10 * 60 * 1000); + // console.log("code", code); + // console.log("expires", expires); + user.resetCode = code; + user.resetCodeExpires = expires; + const updatedUser = await this.userService.update(plainUser); + console.log("updatedUser", updatedUser); + const subject = 'Your Password Reset Code'; + const html = `

Your reset code is ${code}. It will expire in 10 minutes.

`; + await this.mailService.sendMail(email, subject, `Code: ${code}`, html); + } + + async resetPasswordWithCode(email: string, code: string, password: string): Promise { + const userObj = await this.userService.findByEmail(email); + const user = userObj.get({ plain: true }); + if (!user || user.resetCode != code) return false; + + user.password = this.encryptPassword(password); + user.resetCode = null; + user.resetCodeExpires = null; + await this.userService.update(user); + return true; + } + + async verifyResetCode(email: string, code: string): Promise { + const user = await this.userService.findByEmail(email); + let plainUser = user.get({ plain: true }); + console.log("plainUser", plainUser); + if (!plainUser || !plainUser.resetCode || !plainUser.resetCodeExpires) return false; + return plainUser.resetCode == code.toString(); + } + //google services async googleOauthRedirect(user) { @@ -101,25 +159,35 @@ export class AuthService { } } console.log("user.email in service is", user.email); - let existingUser = await User.findOne({ where: { email: user.email } }); + // let existingUser = await User.findOne({ where: { email: user.email } }); + let existingUser = await this.userService.findByEmail(user.email); if (!existingUser) { existingUser = await User.create({ email: user.email, name: user.name, userTypeCode: 'user' }); - } - const payload = existingUser.get(); - const { password, ...rest } = payload - const accessToken = this.signToken(rest, 'accessToken'); - const refreshToken = this.signToken(rest, 'refreshToken'); + const newuser = await this.userService.findByEmail(user.email); + const rest = { + email: newuser.email, + name: newuser.name, + userTypeCode: newuser.userTypeCode, + }; + + const payload = existingUser.get({ plain: true }); + // const { } = payload + const accessToken = this.signToken(rest, 'accessToken'); + const refreshToken = this.signToken(rest, 'refreshToken'); await RefreshToken.create({ email: payload.email, token: refreshToken, type: 'jwt' }); return { - statusCode: 200, access_token: accessToken, refresh_token: refreshToken } } + + encryptPassword(password: string) { + return Buffer.from(password).toString('base64'); + } } diff --git a/src/cast/cast.entity.ts b/src/cast/cast.entity.ts index 5dd8954..5cf8969 100644 --- a/src/cast/cast.entity.ts +++ b/src/cast/cast.entity.ts @@ -1,7 +1,7 @@ import { Table, Column, Model, DataType } from 'sequelize-typescript'; import { ApiProperty } from '@nestjs/swagger'; -@Table({ tableName: 'cast', paranoid: true }) +@Table({ tableName: 'cast_members', paranoid: true }) export default class Cast extends Model { @ApiProperty({ type: Number }) diff --git a/src/locations/dto/create-location.dto.ts b/src/locations/dto/create-location.dto.ts new file mode 100644 index 0000000..cb44755 --- /dev/null +++ b/src/locations/dto/create-location.dto.ts @@ -0,0 +1,30 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateLocationDto { + @ApiProperty() + name: string; + + @ApiProperty() + code: string; + + @ApiProperty({ type: [String], required: false }) + images?: string[]; + + @ApiProperty({ required: false }) + status?: string; + + @ApiProperty({ required: false, type: String, format: 'date' }) + validFrom?: Date; + + @ApiProperty({ required: false, type: String, format: 'date' }) + validTill?: Date; + + @ApiProperty({ required: false }) + createdBy?: string; + + @ApiProperty({ required: false }) + modifiedBy?: string; + + @ApiProperty({ required: false }) + version?: number; +} diff --git a/src/locations/dto/update-location.dto.ts b/src/locations/dto/update-location.dto.ts new file mode 100644 index 0000000..dad72a9 --- /dev/null +++ b/src/locations/dto/update-location.dto.ts @@ -0,0 +1,7 @@ +import { PartialType, ApiProperty } from '@nestjs/swagger'; +import { CreateLocationDto } from './create-location.dto'; + +export class UpdateLocationDto extends PartialType(CreateLocationDto) { + @ApiProperty() + id: number; +} diff --git a/src/locations/entities/location.entity.ts b/src/locations/entities/location.entity.ts new file mode 100644 index 0000000..94b0a66 --- /dev/null +++ b/src/locations/entities/location.entity.ts @@ -0,0 +1,67 @@ +import { + Table, + Column, + Model, + DataType, + PrimaryKey, + AutoIncrement, + Default, +} from 'sequelize-typescript'; +import { ApiProperty } from '@nestjs/swagger'; + +@Table({ tableName: 'locations' }) +export class Location extends Model { + @ApiProperty({ type: Number }) + @PrimaryKey + @AutoIncrement + @Column({ type: DataType.BIGINT }) + id: number; + + @ApiProperty({ type: String }) + @Column({ type: DataType.TEXT }) + name: string; + + @ApiProperty({ type: String }) + @Column({ type: DataType.TEXT }) + code: string; + + @ApiProperty({ type: [String] }) + @Column({ type: DataType.ARRAY(DataType.TEXT) }) + images: string[]; + + @ApiProperty({ type: String }) + @Column({ type: DataType.TEXT }) + status: string; + + @ApiProperty({ type: Date }) + @Column({ type: DataType.DATEONLY }) + validFrom: Date; + + @ApiProperty({ type: Date }) + @Column({ type: DataType.DATEONLY }) + validTill: Date; + + @ApiProperty({ type: Date }) + @Column({ type: DataType.DATE }) + createdAt: Date; + + @ApiProperty({ type: Date }) + @Column({ type: DataType.DATE }) + updatedAt: Date; + + @ApiProperty({ type: String }) + @Column({ type: DataType.TEXT }) + createdBy: string; + + @ApiProperty({ type: String }) + @Column({ type: DataType.TEXT }) + modifiedBy: string; + + @ApiProperty({ type: Date }) + @Column({ type: DataType.DATE }) + deletedAt: Date; + + @ApiProperty({ type: Number }) + @Column({ type: DataType.NUMBER }) + version: number; +} diff --git a/src/locations/locations.controller.spec.ts b/src/locations/locations.controller.spec.ts new file mode 100644 index 0000000..ef03a32 --- /dev/null +++ b/src/locations/locations.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { LocationsController } from './locations.controller'; +import { LocationsService } from './locations.service'; + +describe('LocationsController', () => { + let controller: LocationsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [LocationsController], + providers: [LocationsService], + }).compile(); + + controller = module.get(LocationsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/locations/locations.controller.ts b/src/locations/locations.controller.ts new file mode 100644 index 0000000..043c9c6 --- /dev/null +++ b/src/locations/locations.controller.ts @@ -0,0 +1,41 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common'; +import { LocationsService } from './locations.service'; +import { CreateLocationDto } from './dto/create-location.dto'; +import { UpdateLocationDto } from './dto/update-location.dto'; + +@Controller('locations') +export class LocationsController { + constructor(private readonly locationsService: LocationsService) { } + + @Post() + create(@Body() createLocationDto: CreateLocationDto) { + return this.locationsService.create(createLocationDto); + } + + @Get() + findAll() { + return this.locationsService.findAll(); + } + + @Get('search') + async search(@Query('q') query: string) { + return this.locationsService.search(query); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.locationsService.findOne(+id); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() updateLocationDto: UpdateLocationDto) { + return this.locationsService.update(+id, updateLocationDto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.locationsService.remove(+id); + } + + +} diff --git a/src/locations/locations.module.ts b/src/locations/locations.module.ts new file mode 100644 index 0000000..1c788be --- /dev/null +++ b/src/locations/locations.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { LocationsService } from './locations.service'; +import { LocationsController } from './locations.controller'; + +@Module({ + controllers: [LocationsController], + providers: [LocationsService], +}) +export class LocationsModule {} diff --git a/src/locations/locations.service.spec.ts b/src/locations/locations.service.spec.ts new file mode 100644 index 0000000..a8b0eae --- /dev/null +++ b/src/locations/locations.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { LocationsService } from './locations.service'; + +describe('LocationsService', () => { + let service: LocationsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [LocationsService], + }).compile(); + + service = module.get(LocationsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/locations/locations.service.ts b/src/locations/locations.service.ts new file mode 100644 index 0000000..4881063 --- /dev/null +++ b/src/locations/locations.service.ts @@ -0,0 +1,56 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { CreateLocationDto } from './dto/create-location.dto'; +import { UpdateLocationDto } from './dto/update-location.dto'; +import { Location } from './entities/location.entity'; +import { Op } from 'sequelize'; + +@Injectable() +export class LocationsService { + async create(createLocationDto: CreateLocationDto) { + return await Location.create(createLocationDto as any); + } + + async findAll() { + return await Location.findAll(); + } + + async findOne(id: number) { + const location = await Location.findByPk(id); + if (!location) { + throw new NotFoundException(`Location with ID ${id} not found`); + } + return location; + } + + async update(id: number, updateLocationDto: UpdateLocationDto) { + const location = await Location.findByPk(id); + if (!location) { + throw new NotFoundException(`Location with ID ${id} not found`); + } + await location.update(updateLocationDto); + return location; + } + + async remove(id: number) { + const location = await Location.findByPk(id); + if (!location) { + throw new NotFoundException(`Location with ID ${id} not found`); + } + await location.destroy(); + return { message: `Location with ID ${id} has been deleted.` }; + } + + async search(q: string) { + if (!q) return []; // no query, return empty list or all + + const locations = await Location.findAll({ + where: { + [Op.or]: [ + { name: { [Op.iLike]: `%${q}%` } }, + { code: { [Op.iLike]: `%${q}%` } } + ], + }, + }); + return locations; + } +} diff --git a/src/mail/mail.controller.ts b/src/mail/mail.controller.ts new file mode 100644 index 0000000..80d53f3 --- /dev/null +++ b/src/mail/mail.controller.ts @@ -0,0 +1,17 @@ +import { Body, Controller, Post } from '@nestjs/common'; +import { MailService } from './mail.service'; + +@Controller('mail') +export class MailController { + constructor(private readonly mailService: MailService) {} + + @Post() + async sendTestEmail(@Body() body: { email: string }) { + const { email } = body; + return this.mailService.sendMail( + email, + 'Test Email', + 'This is a test email.', + ); + } +} \ No newline at end of file diff --git a/src/mail/mail.module.ts b/src/mail/mail.module.ts index 34616cc..d0e8e3f 100644 --- a/src/mail/mail.module.ts +++ b/src/mail/mail.module.ts @@ -4,6 +4,7 @@ import { Module } from '@nestjs/common'; import { MailService } from './mail.service'; import { join } from 'path'; import { Utility } from 'src/common/Utility'; +import { MailController } from './mail.controller'; @Module({ imports: [ @@ -23,5 +24,6 @@ import { Utility } from 'src/common/Utility'; ], providers: [MailService], exports: [MailService], // 👈 export for DI + controllers: [MailController], }) -export class MailModule {} +export class MailModule { } diff --git a/src/mail/mail.service.ts b/src/mail/mail.service.ts index a4ef152..d59da72 100644 --- a/src/mail/mail.service.ts +++ b/src/mail/mail.service.ts @@ -1,19 +1,25 @@ -// import { MailerService } from '@nestjs-modules/mailer'/; import { Injectable } from '@nestjs/common'; +import * as nodemailer from 'nodemailer'; +import { Utility } from 'src/common/Utility'; @Injectable() export class MailService { - // constructor(private mailerService: MailerService) { } + private transporter: nodemailer.Transporter; + constructor() { + this.transporter = nodemailer.createTransport(Utility.mailConfig.transport); + } - // async sendEmail(templateName: string, subject: string, context: any, toEmail: string, ccEmails?: string[], bccEmails?: string[]) { - // await this.mailerService.sendMail({ - // to: toEmail, - // cc: ccEmails, - // bcc: bccEmails, - // subject: subject, - // template: templateName, // `.hbs` extension is appended automatically - // context, - // }) - // } + async sendMail(to: string, subject: string, text: string, html?: string) { + const info = await this.transporter.sendMail({ + from: Utility.mailConfig.defaults.from, + to, + subject, + text, + html, + }); + + console.log('Email sent:', info.messageId); + return info; + } } diff --git a/src/main.ts b/src/main.ts index b883223..99c719b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,8 @@ import * as bodyParser from 'body-parser'; import * as configMaster from './app-config/config.json'; import { Utility } from './common/Utility'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import * as cookieParser from 'cookie-parser'; + async function bootstrap() { Utility.appPort = configMaster.local.appConfig.port; @@ -24,7 +26,15 @@ async function bootstrap() { .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); - + + app.enableCors( + { + origin: true, + credentials: true, + } + ); + app.use(cookieParser()); + app.use(bodyParser.json({ limit: '50mb' })); app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); await app.listen(Utility.appPort); diff --git a/src/user/user.entity.ts b/src/user/user.entity.ts index eaab2b6..743802a 100644 --- a/src/user/user.entity.ts +++ b/src/user/user.entity.ts @@ -33,6 +33,14 @@ export class User extends Model { @Column({ type: DataType.TEXT }) primaryRole: string; + @ApiProperty({ type: String }) + @Column({ type: DataType.TEXT }) + resetCode: string; + + @ApiProperty({ type: Date }) + @Column({ type: DataType.DATE }) + resetCodeExpires: Date; + @ApiProperty({ type: String }) @Column({ type: DataType.TEXT }) status: string; @@ -73,3 +81,8 @@ export class User extends Model { @HasMany(() => UserAdditionalDetail) additionalDetails: UserAdditionalDetail[]; } +// @Column({ nullable: true }) +// resetCode: string; + +// @Column({ type: 'timestamp', nullable: true }) +// resetCodeExpires: Date; \ No newline at end of file diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 8bd0a28..ad70595 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -6,39 +6,63 @@ import UserAdditionalDetail from './user-additional-details/user-additional-deta export class UserService { constructor() { } - async findAll(): Promise<{rows: User[], count: number}> { + async findAll(): Promise<{ rows: User[], count: number }> { + // return User.findAndCountAll({ attributes: { exclude: ['password'] } }); return User.findAndCountAll(); } findByPk(id: number): Promise { - return User.findByPk(id, {include: UserAdditionalDetail}) + return User.findByPk(id, { attributes: { exclude: ['password'] } }) } findOne(user: any): Promise { - return User.findOne({where: user as any, include: UserAdditionalDetail}) + return User.findOne({ where: user as any, attributes: { exclude: ['password'] } }) } - findByEmail(email: string): Promise { - return User.findOne({where: {email: email}}) + async findByEmail(email: string): Promise { + return User.findOne({ where: { email: email }, attributes: { exclude: ['password'] } }); } - filter(user: User) : Promise { - return User.findAll({where: user as any}) + async findByResetToken(token: string): Promise { + return User.findOne({ where: { resetToken: token, attributes: { exclude: ['password'] } } }); + } + + + findByEmailWithPassword(email: string, password: string): Promise { + return User.findOne({ where: { email: email, password: this.encryptPassword(password) }, attributes: ['email', 'name', 'id', 'userTypeCode'] }) + } + + filter(user: any): Promise { + return User.findAll({ where: user as any, attributes: { exclude: ['password'] } }) } async remove(id: number): Promise { - return User.destroy({where: {id: id}}); + return User.destroy({ where: { id: id, attributes: { exclude: ['password'] } } }); } async upsert(userDetail: any, insertIfNotFound: boolean): Promise { - if(userDetail.id) { + if (userDetail.password) { + userDetail.password = this.encryptPassword(userDetail.password); + } + if (userDetail.id) { const existingUser = await this.findByPk(userDetail.id); - if(existingUser) { - return User.update(userDetail, {where: {id: userDetail.id}}); + if (existingUser) { + return User.update(userDetail, { where: { id: userDetail.id } }); } } - if(insertIfNotFound) { + if (insertIfNotFound) { return User.create(userDetail as any) } } + + async update(userDetail: any): Promise { + if (userDetail.password) { + userDetail.password = this.encryptPassword(userDetail.password); + } + return User.update(userDetail, { where: { id: userDetail.id } }); + } + + encryptPassword(password: string) { + return Buffer.from(password).toString('base64'); + } }