From f7e99fc326aecf2bdc89a097190787cb83424a81 Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Sun, 22 Sep 2024 14:40:59 +0800 Subject: [PATCH 1/9] feat: add update password route --- backend/user-service/package.json | 6 ++-- .../src/controllers/user.controller.ts | 17 +++++++++++ backend/user-service/src/index.ts | 10 ++++--- .../src/models/user.repository.ts | 3 +- .../user-service/src/routes/auth.routes.ts | 7 +++-- .../user-service/src/routes/user.routes.ts | 8 ++++- .../user-service/src/types/UserPasswordDto.ts | 20 +++++++++++++ package-lock.json | 30 ++++++++++++++----- 8 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 backend/user-service/src/types/UserPasswordDto.ts diff --git a/backend/user-service/package.json b/backend/user-service/package.json index 14d5b5cf66..38457783d8 100644 --- a/backend/user-service/package.json +++ b/backend/user-service/package.json @@ -13,6 +13,7 @@ "license": "MIT", "description": "", "dependencies": { + "@types/nodemailer": "^6.4.16", "bcrypt": "^5.1.1", "class-validator": "^0.14.1", "cors": "^2.8.5", @@ -22,6 +23,7 @@ "helmet": "^7.1.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.6.3", + "nodemailer": "^6.9.15", "passport": "^0.7.0", "passport-local": "^1.0.0", "winston": "^3.14.2" @@ -35,7 +37,6 @@ "@types/helmet": "^0.0.48", "@types/jest": "^29.5.13", "@types/jsonwebtoken": "^9.0.7", - "@types/node": "^22.5.5", "@types/passport": "^1.0.16", "@types/passport-local": "^1.0.38", "@types/supertest": "^6.0.2", @@ -44,7 +45,6 @@ "nodemon": "^3.1.4", "supertest": "^7.0.0", "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "typescript": "^5.6.2" + "ts-node": "^10.9.2" } } diff --git a/backend/user-service/src/controllers/user.controller.ts b/backend/user-service/src/controllers/user.controller.ts index 2bab185527..7e454969ec 100644 --- a/backend/user-service/src/controllers/user.controller.ts +++ b/backend/user-service/src/controllers/user.controller.ts @@ -10,6 +10,7 @@ import { CreateUserDto } from '../types/CreateUserDto' import { Response } from 'express' import { TypedRequest } from '../types/TypedRequest' import { UserDto } from '../types/UserDto' +import { UserPasswordDto } from '../types/UserPasswordDto' import { UserProfileDto } from '../types/UserProfileDto' import { ValidationError } from 'class-validator' import { hashPassword } from './auth.controller' @@ -67,3 +68,19 @@ export async function handleDeleteUser(request: TypedRequest, response: Re await deleteUser(id) response.status(200).send() } + +export async function handleUpdatePassword(request: TypedRequest, response: Response): Promise { + const createDto = UserPasswordDto.fromRequest(request) + const errors = await createDto.validate() + if (errors.length) { + const errorMessages = errors.map((error: ValidationError) => `INVALID_${error.property.toUpperCase()}`) + response.status(400).json(errorMessages).send() + return + } + + const id = request.params.id + createDto.password = await hashPassword(createDto.password) + + const user = await updateUser(id, createDto) + response.status(200).json(user).send() +} diff --git a/backend/user-service/src/index.ts b/backend/user-service/src/index.ts index 2202fecb87..c26d47886b 100644 --- a/backend/user-service/src/index.ts +++ b/backend/user-service/src/index.ts @@ -1,10 +1,12 @@ -import cors from 'cors' -import express, { Express, NextFunction, Request, Response } from 'express' import 'express-async-errors' + +import express, { Express, NextFunction, Request, Response } from 'express' + +import authRouter from './routes/auth.routes' +import cors from 'cors' +import defaultErrorHandler from './middlewares/errorHandler.middleware' import helmet from 'helmet' import passport from 'passport' -import defaultErrorHandler from './middlewares/errorHandler.middleware' -import authRouter from './routes/auth.routes' import userRouter from './routes/user.routes' const app: Express = express() diff --git a/backend/user-service/src/models/user.repository.ts b/backend/user-service/src/models/user.repository.ts index 6a4e031d82..131cd13ebb 100644 --- a/backend/user-service/src/models/user.repository.ts +++ b/backend/user-service/src/models/user.repository.ts @@ -3,6 +3,7 @@ import { Model, model } from 'mongoose' import { CreateUserDto } from '../types/CreateUserDto' import { IUser } from '../types/IUser' import { UserDto } from '../types/UserDto' +import { UserPasswordDto } from '../types/UserPasswordDto' import { UserProfileDto } from '../types/UserProfileDto' import userSchema from './user.model' @@ -28,7 +29,7 @@ export async function createUser(dto: CreateUserDto): Promise { return userModel.create(dto) } -export async function updateUser(id: string, dto: UserDto | UserProfileDto): Promise { +export async function updateUser(id: string, dto: UserDto | UserProfileDto | UserPasswordDto): Promise { return userModel.findByIdAndUpdate(id, dto, { new: true }) } diff --git a/backend/user-service/src/routes/auth.routes.ts b/backend/user-service/src/routes/auth.routes.ts index 1cdff46a62..33c5eadb0b 100644 --- a/backend/user-service/src/routes/auth.routes.ts +++ b/backend/user-service/src/routes/auth.routes.ts @@ -1,7 +1,8 @@ +import { handleAuthentication, handleLogin } from '../controllers/auth.controller' + +import { Strategy as LocalStrategy } from 'passport-local' import { Router } from 'express' import passport from 'passport' -import { Strategy as LocalStrategy } from 'passport-local' -import { handleAuthentication, handleLogin } from '../controllers/auth.controller' passport.use( 'local', @@ -19,5 +20,7 @@ passport.use( const router = Router() router.post('/login', passport.authenticate('local', { session: false }), handleLogin) +router.post('/password') +router.put('/password') export default router diff --git a/backend/user-service/src/routes/user.routes.ts b/backend/user-service/src/routes/user.routes.ts index 29664096e6..59d63fa533 100644 --- a/backend/user-service/src/routes/user.routes.ts +++ b/backend/user-service/src/routes/user.routes.ts @@ -1,4 +1,9 @@ -import { handleCreateUser, handleDeleteUser, handleUpdateProfile } from '../controllers/user.controller' +import { + handleCreateUser, + handleDeleteUser, + handleUpdatePassword, + handleUpdateProfile, +} from '../controllers/user.controller' import { Router } from 'express' @@ -7,5 +12,6 @@ const router = Router() router.post('/', handleCreateUser) router.put('/:id', handleUpdateProfile) router.delete('/:id', handleDeleteUser) +router.put('/password/:id', handleUpdatePassword) export default router diff --git a/backend/user-service/src/types/UserPasswordDto.ts b/backend/user-service/src/types/UserPasswordDto.ts new file mode 100644 index 0000000000..78da6405d9 --- /dev/null +++ b/backend/user-service/src/types/UserPasswordDto.ts @@ -0,0 +1,20 @@ +import { IsStrongPassword, ValidationError, validate } from 'class-validator' + +import { TypedRequest } from './TypedRequest' + +export class UserPasswordDto { + @IsStrongPassword({ minLength: 8, minLowercase: 1, minUppercase: 1, minNumbers: 1, minSymbols: 1 }) + password: string + + constructor(password: string) { + this.password = password + } + + static fromRequest({ body: { password } }: TypedRequest): UserPasswordDto { + return new UserPasswordDto(password) + } + + async validate(): Promise { + return validate(this) + } +} diff --git a/package-lock.json b/package-lock.json index 567c25e846..a818fd2f29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,6 +76,7 @@ "version": "0.1.0", "license": "MIT", "dependencies": { + "@types/nodemailer": "^6.4.16", "bcrypt": "^5.1.1", "class-validator": "^0.14.1", "cors": "^2.8.5", @@ -85,6 +86,7 @@ "helmet": "^7.1.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.6.3", + "nodemailer": "^6.9.15", "passport": "^0.7.0", "passport-local": "^1.0.0", "winston": "^3.14.2" @@ -98,7 +100,6 @@ "@types/helmet": "^0.0.48", "@types/jest": "^29.5.13", "@types/jsonwebtoken": "^9.0.7", - "@types/node": "^22.5.5", "@types/passport": "^1.0.16", "@types/passport-local": "^1.0.38", "@types/supertest": "^6.0.2", @@ -107,8 +108,7 @@ "nodemon": "^3.1.4", "supertest": "^7.0.0", "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "typescript": "^5.6.2" + "ts-node": "^10.9.2" } }, "backend/user-service/node_modules/globals": { @@ -3231,12 +3231,20 @@ }, "node_modules/@types/node": { "version": "22.5.5", - "devOptional": true, - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", + "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==", "dependencies": { "undici-types": "~6.19.2" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.16", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.16.tgz", + "integrity": "sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/passport": { "version": "1.0.16", "dev": true, @@ -9647,6 +9655,14 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.15.tgz", + "integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.7", "dev": true, @@ -12301,8 +12317,9 @@ }, "node_modules/typescript": { "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "devOptional": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12365,7 +12382,6 @@ }, "node_modules/undici-types": { "version": "6.19.8", - "devOptional": true, "license": "MIT" }, "node_modules/universalify": { From 1219d228df4763aa5b7295e36e5324bc17c0d738 Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Sun, 22 Sep 2024 14:52:23 +0800 Subject: [PATCH 2/9] test: add unit tests for update password route --- .../__tests__/routes/user.routes.test.ts | 23 +++++++++++++++++++ .../user-service/src/routes/auth.routes.ts | 4 ++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/backend/user-service/__tests__/routes/user.routes.test.ts b/backend/user-service/__tests__/routes/user.routes.test.ts index 3618d2bca9..e714fbffb3 100644 --- a/backend/user-service/__tests__/routes/user.routes.test.ts +++ b/backend/user-service/__tests__/routes/user.routes.test.ts @@ -148,4 +148,27 @@ describe('User Routes', () => { expect(response.status).toBe(400) }) }) + + describe('PUT /users/password/:id', () => { + it('should return 200 for successful update', async () => { + const user1 = await request(app).post('/users').send(CREATE_USER_DTO1) + const response = await request(app).put(`/users/password/${user1.body.id}`).send({ + password: 'Test12345!', + }) + expect(response.status).toBe(200) + expect(response.body.password).not.toEqual(user1.body.password) + }) + it('should return 400 for requests with invalid ids', async () => { + const response = await request(app).put('/users/password/111').send() + expect(response.status).toBe(400) + }) + it('should return 400 for requests with invalid passwords', async () => { + const user1 = await request(app).post('/users').send(CREATE_USER_DTO1) + const response = await request(app).put(`/users/password/${user1.body.id}`).send({ + password: 'Test1234', + }) + expect(response.status).toBe(400) + expect(response.body).toHaveLength(1) + }) + }) }) diff --git a/backend/user-service/src/routes/auth.routes.ts b/backend/user-service/src/routes/auth.routes.ts index 33c5eadb0b..541aed15c4 100644 --- a/backend/user-service/src/routes/auth.routes.ts +++ b/backend/user-service/src/routes/auth.routes.ts @@ -20,7 +20,7 @@ passport.use( const router = Router() router.post('/login', passport.authenticate('local', { session: false }), handleLogin) -router.post('/password') -router.put('/password') +// router.post('/password') +// router.put('/password') export default router From 96b2a16a60f70730cef8d23abe1c471cc6ef0fd3 Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Sun, 22 Sep 2024 21:04:54 +0800 Subject: [PATCH 3/9] feat: add ui for reset password functionality --- .../src/controllers/auth.controller.ts | 5 +- .../src/types/EmailVerificationDto.ts | 3 +- frontend/components/{login => auth}/Login.tsx | 7 +- frontend/components/auth/PasswordReset.tsx | 72 ++++++++++++++ .../components/{login => auth}/Signup.tsx | 5 +- frontend/components/dashboard/new-session.tsx | 2 +- frontend/components/login/ui/toast/Logo.tsx | 7 -- frontend/components/ui/dialog.tsx | 97 +++++++++++++++++++ frontend/components/ui/input-otp.tsx | 63 ++++++++++++ .../{login/ui/toast => ui}/toast.tsx | 0 .../{login/ui/toast => ui}/toaster.tsx | 0 .../{login/ui/toast => ui}/use-toast.tsx | 0 frontend/package.json | 2 + frontend/pages/_app.tsx | 4 +- frontend/pages/auth/index.tsx | 6 +- package-lock.json | 46 +++++++++ 16 files changed, 297 insertions(+), 22 deletions(-) rename frontend/components/{login => auth}/Login.tsx (89%) create mode 100644 frontend/components/auth/PasswordReset.tsx rename frontend/components/{login => auth}/Signup.tsx (95%) delete mode 100644 frontend/components/login/ui/toast/Logo.tsx create mode 100644 frontend/components/ui/dialog.tsx create mode 100644 frontend/components/ui/input-otp.tsx rename frontend/components/{login/ui/toast => ui}/toast.tsx (100%) rename frontend/components/{login/ui/toast => ui}/toaster.tsx (100%) rename frontend/components/{login/ui/toast => ui}/use-toast.tsx (100%) diff --git a/backend/user-service/src/controllers/auth.controller.ts b/backend/user-service/src/controllers/auth.controller.ts index 77f8d1e604..294595ee10 100644 --- a/backend/user-service/src/controllers/auth.controller.ts +++ b/backend/user-service/src/controllers/auth.controller.ts @@ -121,7 +121,7 @@ export async function handleReset(request: TypedRequest, r response.status(404).json('USER_NOT_FOUND').send() return } - if (user.verificationToken === '') { + if (user.verificationToken !== '') { response.status(400).json('TOKEN_ALREADY_SENT').send() return } @@ -153,12 +153,13 @@ export async function handleVerify(request: TypedRequest, return } - if (user.verificationToken !== '' && user.verificationToken !== createDto.verificationToken) { + if (user.verificationToken == '' || user.verificationToken !== createDto.verificationToken) { response.status(400).json('INVALID_OTP').send() return } createDto.verificationToken = '' await updateUser(user.id, createDto) + response.status(200).send() } diff --git a/backend/user-service/src/types/EmailVerificationDto.ts b/backend/user-service/src/types/EmailVerificationDto.ts index 405cc49176..1e7c0b444e 100644 --- a/backend/user-service/src/types/EmailVerificationDto.ts +++ b/backend/user-service/src/types/EmailVerificationDto.ts @@ -1,9 +1,10 @@ -import { IsDate, IsEmail, IsString, ValidationError, validate } from 'class-validator' +import { IsDate, IsEmail, IsNotEmpty, IsString, ValidationError, validate } from 'class-validator' import { TypedRequest } from './TypedRequest' export class EmailVerificationDto { @IsEmail() + @IsNotEmpty() email: string @IsString() diff --git a/frontend/components/login/Login.tsx b/frontend/components/auth/Login.tsx similarity index 89% rename from frontend/components/login/Login.tsx rename to frontend/components/auth/Login.tsx index 9e6ae6af78..be6d0a60f2 100644 --- a/frontend/components/login/Login.tsx +++ b/frontend/components/auth/Login.tsx @@ -1,8 +1,8 @@ 'use client' +import { PasswordReset } from './PasswordReset' import { useState } from 'react' -import { Logo } from './ui/toast/Logo' -import { useToast } from './ui/toast/use-toast' +import { useToast } from '../ui/use-toast' export default function Login() { const [email, setEmail] = useState('') @@ -27,7 +27,7 @@ export default function Login() { return (
- + Logo

Start your journey with us!

@@ -51,6 +51,7 @@ export default function Login() { > Login +
) } diff --git a/frontend/components/auth/PasswordReset.tsx b/frontend/components/auth/PasswordReset.tsx new file mode 100644 index 0000000000..91e77460dc --- /dev/null +++ b/frontend/components/auth/PasswordReset.tsx @@ -0,0 +1,72 @@ +'use client' + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog' +import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from '../ui/input-otp' + +import { Button } from '@/components/ui/button' +import { useState } from 'react' + +export function PasswordReset() { + const [showOtp, setShowOtp] = useState(false) + + const enterResetEmail = ( + + + Forgot your password + + Enter your email address below and we'll send you an otp to reset your password. + + + + + + + + ) + + const enterOTP = ( + + + Enter your OTP + Enter the OTP sent to your email address to reset your password. + + + + + + + + + + + + + + + + + + + ) + + return ( + + +

Forgot your password?

+
+ {showOtp ? enterOTP : enterResetEmail} +
+ ) +} diff --git a/frontend/components/login/Signup.tsx b/frontend/components/auth/Signup.tsx similarity index 95% rename from frontend/components/login/Signup.tsx rename to frontend/components/auth/Signup.tsx index cd2251c2a5..9cc0606f18 100644 --- a/frontend/components/login/Signup.tsx +++ b/frontend/components/auth/Signup.tsx @@ -1,8 +1,7 @@ 'use client' import { useState } from 'react' -import { Logo } from './ui/toast/Logo' -import { useToast } from './ui/toast/use-toast' +import { useToast } from '../ui/use-toast' export default function Signup() { const [email, setEmail] = useState('') @@ -39,7 +38,7 @@ export default function Signup() { return (
- + Logo

Start your journey with us!

diff --git a/frontend/components/dashboard/new-session.tsx b/frontend/components/dashboard/new-session.tsx index e6fdccf7e7..8547c2f9ea 100644 --- a/frontend/components/dashboard/new-session.tsx +++ b/frontend/components/dashboard/new-session.tsx @@ -38,7 +38,7 @@ export const NewSession = () => { return (
-
Recent Sessions
+
Start a New Session

Choose a Topic and Difficulty level to start your collaborative diff --git a/frontend/components/login/ui/toast/Logo.tsx b/frontend/components/login/ui/toast/Logo.tsx deleted file mode 100644 index 5d800ec3f6..0000000000 --- a/frontend/components/login/ui/toast/Logo.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export const Logo = () => { - return ( -

- P -
- ) -} diff --git a/frontend/components/ui/dialog.tsx b/frontend/components/ui/dialog.tsx new file mode 100644 index 0000000000..65c1bd3027 --- /dev/null +++ b/frontend/components/ui/dialog.tsx @@ -0,0 +1,97 @@ +'use client' + +import * as DialogPrimitive from '@radix-ui/react-dialog' +import * as React from 'react' + +import { Cross2Icon } from '@radix-ui/react-icons' +import { cn } from '@/lib/utils' + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = 'DialogHeader' + +const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = 'DialogFooter' + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/frontend/components/ui/input-otp.tsx b/frontend/components/ui/input-otp.tsx new file mode 100644 index 0000000000..5185993de7 --- /dev/null +++ b/frontend/components/ui/input-otp.tsx @@ -0,0 +1,63 @@ +'use client' + +import * as React from 'react' +import { DashIcon } from '@radix-ui/react-icons' +import { OTPInput, OTPInputContext } from 'input-otp' + +import { cn } from '@/lib/utils' + +const InputOTP = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, containerClassName, ...props }, ref) => ( + + ) +) +InputOTP.displayName = 'InputOTP' + +const InputOTPGroup = React.forwardRef, React.ComponentPropsWithoutRef<'div'>>( + ({ className, ...props }, ref) =>
+) +InputOTPGroup.displayName = 'InputOTPGroup' + +const InputOTPSlot = React.forwardRef< + React.ElementRef<'div'>, + React.ComponentPropsWithoutRef<'div'> & { index: number } +>(({ index, className, ...props }, ref) => { + const inputOTPContext = React.useContext(OTPInputContext) + const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index] + + return ( +
+ {char} + {hasFakeCaret && ( +
+
+
+ )} +
+ ) +}) +InputOTPSlot.displayName = 'InputOTPSlot' + +const InputOTPSeparator = React.forwardRef, React.ComponentPropsWithoutRef<'div'>>( + ({ ...props }, ref) => ( +
+ +
+ ) +) +InputOTPSeparator.displayName = 'InputOTPSeparator' + +export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } diff --git a/frontend/components/login/ui/toast/toast.tsx b/frontend/components/ui/toast.tsx similarity index 100% rename from frontend/components/login/ui/toast/toast.tsx rename to frontend/components/ui/toast.tsx diff --git a/frontend/components/login/ui/toast/toaster.tsx b/frontend/components/ui/toaster.tsx similarity index 100% rename from frontend/components/login/ui/toast/toaster.tsx rename to frontend/components/ui/toaster.tsx diff --git a/frontend/components/login/ui/toast/use-toast.tsx b/frontend/components/ui/use-toast.tsx similarity index 100% rename from frontend/components/login/ui/toast/use-toast.tsx rename to frontend/components/ui/use-toast.tsx diff --git a/frontend/package.json b/frontend/package.json index 919635ca13..b2110f49ef 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "format": "prettier --write \"./**/*.{js,jsx,mjs,cjs,ts,tsx,json}\"" }, "dependencies": { + "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-navigation-menu": "^1.2.0", @@ -21,6 +22,7 @@ "@tanstack/react-table": "^8.20.5", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "input-otp": "^1.2.4", "lucide-react": "^0.441.0", "next": "14.2.11", "react": "^18", diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index 21241213d6..3a033a2c09 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -1,8 +1,8 @@ -import Layout from '@/components/layout/layout' -import Toaster from '@/components/login/ui/toast/toaster' import '@/styles/globals.css' import { AppProps } from 'next/app' +import Layout from '@/components/layout/layout' +import Toaster from '@/components/ui/toaster' export default function App({ Component, pageProps }: AppProps) { return ( diff --git a/frontend/pages/auth/index.tsx b/frontend/pages/auth/index.tsx index 49e5186216..78a98d9389 100644 --- a/frontend/pages/auth/index.tsx +++ b/frontend/pages/auth/index.tsx @@ -1,9 +1,9 @@ 'use client' -import { useState } from 'react' -import Login from '../../components/login/Login' -import Signup from '../../components/login/Signup' import Image from 'next/image' +import Login from '../../components/auth/Login' +import Signup from '../../components/auth/Signup' +import { useState } from 'react' export default function Home() { const [isLoginPage, setIsLoginPage] = useState(false) diff --git a/package-lock.json b/package-lock.json index a818fd2f29..b74c799ebb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -125,6 +125,7 @@ "frontend": { "version": "0.1.0", "dependencies": { + "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-navigation-menu": "^1.2.0", @@ -135,6 +136,7 @@ "@tanstack/react-table": "^8.20.5", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "input-otp": "^1.2.4", "lucide-react": "^0.441.0", "next": "14.2.11", "react": "^18", @@ -2235,6 +2237,41 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz", + "integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.0", "license": "MIT", @@ -7181,6 +7218,15 @@ "version": "2.0.4", "license": "ISC" }, + "node_modules/input-otp": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.2.4.tgz", + "integrity": "sha512-md6rhmD+zmMnUh5crQNSQxq3keBRYvE3odbr4Qb9g2NWzQv9azi+t1a3X4TBTbh98fsGHgEEJlzbe1q860uGCA==", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/internal-slot": { "version": "1.0.7", "dev": true, From 067e6e6a030b2a72f242c3fc32fb89117f7e141c Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Sun, 22 Sep 2024 21:17:47 +0800 Subject: [PATCH 4/9] fix: resolve warnings and lint --- frontend/components/auth/Login.tsx | 3 ++- frontend/components/auth/PasswordReset.tsx | 2 +- frontend/components/auth/Signup.tsx | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/components/auth/Login.tsx b/frontend/components/auth/Login.tsx index be6d0a60f2..3c0807d671 100644 --- a/frontend/components/auth/Login.tsx +++ b/frontend/components/auth/Login.tsx @@ -1,5 +1,6 @@ 'use client' +import Img from 'next/image' import { PasswordReset } from './PasswordReset' import { useState } from 'react' import { useToast } from '../ui/use-toast' @@ -27,7 +28,7 @@ export default function Login() { return (
- Logo + Logo

Start your journey with us!

diff --git a/frontend/components/auth/PasswordReset.tsx b/frontend/components/auth/PasswordReset.tsx index 91e77460dc..a1998933f4 100644 --- a/frontend/components/auth/PasswordReset.tsx +++ b/frontend/components/auth/PasswordReset.tsx @@ -22,7 +22,7 @@ export function PasswordReset() { Forgot your password - Enter your email address below and we'll send you an otp to reset your password. + Enter your email address below and we will send you an otp to reset your password. diff --git a/frontend/components/auth/Signup.tsx b/frontend/components/auth/Signup.tsx index 9cc0606f18..d2c134abe2 100644 --- a/frontend/components/auth/Signup.tsx +++ b/frontend/components/auth/Signup.tsx @@ -1,5 +1,6 @@ 'use client' +import Img from 'next/image' import { useState } from 'react' import { useToast } from '../ui/use-toast' @@ -38,7 +39,7 @@ export default function Signup() { return (
- Logo + Logo

Start your journey with us!

From 7b22a743c5e13e47ad964ee71c34dc7120019535 Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Mon, 23 Sep 2024 12:20:01 +0800 Subject: [PATCH 5/9] fix: resolve pr comments --- backend/user-service/package.json | 3 ++- backend/user-service/src/controllers/auth.controller.ts | 6 +++--- backend/user-service/src/routes/auth.routes.ts | 4 ++-- backend/user-service/src/routes/user.routes.ts | 2 +- package-lock.json | 6 +++++- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/backend/user-service/package.json b/backend/user-service/package.json index 38457783d8..7ac876947c 100644 --- a/backend/user-service/package.json +++ b/backend/user-service/package.json @@ -13,7 +13,6 @@ "license": "MIT", "description": "", "dependencies": { - "@types/nodemailer": "^6.4.16", "bcrypt": "^5.1.1", "class-validator": "^0.14.1", "cors": "^2.8.5", @@ -37,6 +36,8 @@ "@types/helmet": "^0.0.48", "@types/jest": "^29.5.13", "@types/jsonwebtoken": "^9.0.7", + "@types/node": "^22.5.5", + "@types/nodemailer": "^6.4.16", "@types/passport": "^1.0.16", "@types/passport-local": "^1.0.38", "@types/supertest": "^6.0.2", diff --git a/backend/user-service/src/controllers/auth.controller.ts b/backend/user-service/src/controllers/auth.controller.ts index 294595ee10..e9abc72c27 100644 --- a/backend/user-service/src/controllers/auth.controller.ts +++ b/backend/user-service/src/controllers/auth.controller.ts @@ -88,12 +88,12 @@ export async function sendMail(to: string, subject: string, text: string, html: port: 587, secure: false, auth: { - user: process.env.EMAIL, - pass: process.env.PASSWORD, + user: process.env.NODEMAILER_EMAIL, + pass: process.env.NODEMAILER_PASSWORD, }, }) await transporter.sendMail({ - from: process.env.EMAIL, + from: process.env.NODEMAILER_EMAIL, to, subject, text, diff --git a/backend/user-service/src/routes/auth.routes.ts b/backend/user-service/src/routes/auth.routes.ts index fdf179ab53..bb117dcbe7 100644 --- a/backend/user-service/src/routes/auth.routes.ts +++ b/backend/user-service/src/routes/auth.routes.ts @@ -20,7 +20,7 @@ passport.use( const router = Router() router.post('/login', passport.authenticate('local', { session: false }), handleLogin) -router.put('/reset', handleReset) -router.put('/verify', handleVerify) +router.post('/reset', handleReset) +router.post('/verify', handleVerify) export default router diff --git a/backend/user-service/src/routes/user.routes.ts b/backend/user-service/src/routes/user.routes.ts index ff83799729..61e4ff50db 100644 --- a/backend/user-service/src/routes/user.routes.ts +++ b/backend/user-service/src/routes/user.routes.ts @@ -14,6 +14,6 @@ router.post('/', handleCreateUser) router.put('/:id', handleUpdateProfile) router.get('/:id', handleGetCurrentProfile) router.delete('/:id', handleDeleteUser) -router.put('/password/:id', handleUpdatePassword) +router.put('/:id/password', handleUpdatePassword) export default router diff --git a/package-lock.json b/package-lock.json index b74c799ebb..1f8a244497 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,7 +76,6 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@types/nodemailer": "^6.4.16", "bcrypt": "^5.1.1", "class-validator": "^0.14.1", "cors": "^2.8.5", @@ -100,6 +99,8 @@ "@types/helmet": "^0.0.48", "@types/jest": "^29.5.13", "@types/jsonwebtoken": "^9.0.7", + "@types/node": "^22.5.5", + "@types/nodemailer": "^6.4.16", "@types/passport": "^1.0.16", "@types/passport-local": "^1.0.38", "@types/supertest": "^6.0.2", @@ -3270,6 +3271,7 @@ "version": "22.5.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==", + "devOptional": true, "dependencies": { "undici-types": "~6.19.2" } @@ -3278,6 +3280,7 @@ "version": "6.4.16", "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.16.tgz", "integrity": "sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -12428,6 +12431,7 @@ }, "node_modules/undici-types": { "version": "6.19.8", + "devOptional": true, "license": "MIT" }, "node_modules/universalify": { From 0ad612192838136466ae9920857f41c9821190af Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Mon, 23 Sep 2024 12:24:12 +0800 Subject: [PATCH 6/9] test: resolve failing test cases --- backend/user-service/__tests__/routes/user.routes.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/user-service/__tests__/routes/user.routes.test.ts b/backend/user-service/__tests__/routes/user.routes.test.ts index a44faa4fdf..e236482163 100644 --- a/backend/user-service/__tests__/routes/user.routes.test.ts +++ b/backend/user-service/__tests__/routes/user.routes.test.ts @@ -177,22 +177,22 @@ describe('User Routes', () => { }) }) - describe('PUT /users/password/:id', () => { + describe('PUT /users/:id/password', () => { it('should return 200 for successful update', async () => { const user1 = await request(app).post('/users').send(CREATE_USER_DTO1) - const response = await request(app).put(`/users/password/${user1.body.id}`).send({ + const response = await request(app).put(`/users/${user1.body.id}/password`).send({ password: 'Test12345!', }) expect(response.status).toBe(200) expect(response.body.password).not.toEqual(user1.body.password) }) it('should return 400 for requests with invalid ids', async () => { - const response = await request(app).put('/users/password/111').send() + const response = await request(app).put('/users/111/password').send() expect(response.status).toBe(400) }) it('should return 400 for requests with invalid passwords', async () => { const user1 = await request(app).post('/users').send(CREATE_USER_DTO1) - const response = await request(app).put(`/users/password/${user1.body.id}`).send({ + const response = await request(app).put(`/users/${user1.body.id}/password`).send({ password: 'Test1234', }) expect(response.status).toBe(400) From e132926765ef9395d99f0d82b7914f55f3efa9d9 Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Mon, 23 Sep 2024 12:30:45 +0800 Subject: [PATCH 7/9] fix: update string to numeric string --- backend/user-service/src/controllers/auth.controller.ts | 8 ++++---- backend/user-service/src/models/user.model.ts | 2 +- backend/user-service/src/types/EmailVerificationDto.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/user-service/src/controllers/auth.controller.ts b/backend/user-service/src/controllers/auth.controller.ts index e9abc72c27..5db598ac13 100644 --- a/backend/user-service/src/controllers/auth.controller.ts +++ b/backend/user-service/src/controllers/auth.controller.ts @@ -107,7 +107,7 @@ export function generateOTP(): string { export async function handleReset(request: TypedRequest, response: Response): Promise { const createDto = EmailVerificationDto.fromRequest(request) - createDto.verificationToken = '' + createDto.verificationToken = '0' const errors = await createDto.validate() if (errors.length) { const errorMessages = errors.map((error: ValidationError) => `INVALID_${error.property.toUpperCase()}`) @@ -121,7 +121,7 @@ export async function handleReset(request: TypedRequest, r response.status(404).json('USER_NOT_FOUND').send() return } - if (user.verificationToken !== '') { + if (user.verificationToken !== '0') { response.status(400).json('TOKEN_ALREADY_SENT').send() return } @@ -153,12 +153,12 @@ export async function handleVerify(request: TypedRequest, return } - if (user.verificationToken == '' || user.verificationToken !== createDto.verificationToken) { + if (user.verificationToken == '0' || user.verificationToken !== createDto.verificationToken) { response.status(400).json('INVALID_OTP').send() return } - createDto.verificationToken = '' + createDto.verificationToken = '0' await updateUser(user.id, createDto) response.status(200).send() diff --git a/backend/user-service/src/models/user.model.ts b/backend/user-service/src/models/user.model.ts index 8588c60d96..fc22f5139b 100644 --- a/backend/user-service/src/models/user.model.ts +++ b/backend/user-service/src/models/user.model.ts @@ -40,6 +40,6 @@ export default new Schema({ verificationToken: { type: String, required: false, - default: '', + default: '0', }, }) diff --git a/backend/user-service/src/types/EmailVerificationDto.ts b/backend/user-service/src/types/EmailVerificationDto.ts index 1e7c0b444e..a0c6fb11d3 100644 --- a/backend/user-service/src/types/EmailVerificationDto.ts +++ b/backend/user-service/src/types/EmailVerificationDto.ts @@ -1,4 +1,4 @@ -import { IsDate, IsEmail, IsNotEmpty, IsString, ValidationError, validate } from 'class-validator' +import { IsDate, IsEmail, IsNotEmpty, IsNumberString, ValidationError, validate } from 'class-validator' import { TypedRequest } from './TypedRequest' @@ -7,7 +7,7 @@ export class EmailVerificationDto { @IsNotEmpty() email: string - @IsString() + @IsNumberString() verificationToken?: string @IsDate() From 4b0cda956bdabc9f27d2c09a78d82746c133cea3 Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Mon, 23 Sep 2024 12:46:44 +0800 Subject: [PATCH 8/9] fix: update logic to handle ts transpilation --- .../user-service/{src/util => public}/passwordResetEmail.html | 0 backend/user-service/src/controllers/auth.controller.ts | 4 ++-- backend/user-service/tsconfig.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename backend/user-service/{src/util => public}/passwordResetEmail.html (100%) diff --git a/backend/user-service/src/util/passwordResetEmail.html b/backend/user-service/public/passwordResetEmail.html similarity index 100% rename from backend/user-service/src/util/passwordResetEmail.html rename to backend/user-service/public/passwordResetEmail.html diff --git a/backend/user-service/src/controllers/auth.controller.ts b/backend/user-service/src/controllers/auth.controller.ts index 5db598ac13..2e02a5dc8d 100644 --- a/backend/user-service/src/controllers/auth.controller.ts +++ b/backend/user-service/src/controllers/auth.controller.ts @@ -131,8 +131,8 @@ export async function handleReset(request: TypedRequest, r await updateUser(user.id, createDto) - const htmlFile = await getHTMLTemplate('../util/passwordResetEmail.html') - await sendMail('shishdoescs@gmail.com', 'Password Reset Request', 'PeerPrep', htmlFile.replace('{otp}', otp)) + const htmlFile = await getHTMLTemplate('../../public/passwordResetEmail.html') + await sendMail(user.email, 'Password Reset Request', 'PeerPrep', htmlFile.replace('{otp}', otp)) response.status(200).send() } diff --git a/backend/user-service/tsconfig.json b/backend/user-service/tsconfig.json index 17f8f67dd5..aa6bdd29dc 100644 --- a/backend/user-service/tsconfig.json +++ b/backend/user-service/tsconfig.json @@ -108,5 +108,5 @@ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "include": ["src/**/*"], - "exclude": ["**/*.test.ts"] + "exclude": ["**/*.test.ts", "**/*.html"] } From 10aa4c0ecec737bb6421de2bfe3f01a3688e53df Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Mon, 23 Sep 2024 19:18:27 +0800 Subject: [PATCH 9/9] fix: update config file to validate env --- backend/user-service/src/types/Config.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/backend/user-service/src/types/Config.ts b/backend/user-service/src/types/Config.ts index d70f825259..c7b07c2c16 100644 --- a/backend/user-service/src/types/Config.ts +++ b/backend/user-service/src/types/Config.ts @@ -1,4 +1,4 @@ -import { IsBase64, IsEnum, IsNotEmpty, IsNumberString, IsString, validateOrReject } from 'class-validator' +import { IsBase64, IsEmail, IsEnum, IsNotEmpty, IsNumberString, IsString, validateOrReject } from 'class-validator' export class Config { @IsEnum(['development', 'production', 'test']) @@ -18,18 +18,29 @@ export class Config { @IsBase64() ACCESS_TOKEN_PUBLIC_KEY: string + @IsEmail() + NODEMAILER_EMAIL: string + + @IsString() + @IsNotEmpty() + NODEMAILER_PASSWORD: string + constructor( NODE_ENV: string, PORT: string, DB_URL: string, ACCESS_TOKEN_PRIVATE_KEY: string, - ACCESS_TOKEN_PUBLIC_KEY: string + ACCESS_TOKEN_PUBLIC_KEY: string, + NODEMAILER_EMAIL: string, + NODEMAILER_PASSWORD: string ) { this.NODE_ENV = NODE_ENV ?? 'development' this.PORT = PORT ?? '3002' this.DB_URL = DB_URL this.ACCESS_TOKEN_PRIVATE_KEY = ACCESS_TOKEN_PRIVATE_KEY this.ACCESS_TOKEN_PUBLIC_KEY = ACCESS_TOKEN_PUBLIC_KEY + this.NODEMAILER_EMAIL = NODEMAILER_EMAIL + this.NODEMAILER_PASSWORD = NODEMAILER_PASSWORD } static fromEnv(env: { [key: string]: string | undefined }): Config { @@ -38,7 +49,9 @@ export class Config { env.PORT!, env.DB_URL!, env.ACCESS_TOKEN_PRIVATE_KEY!, - env.ACCESS_TOKEN_PUBLIC_KEY! + env.ACCESS_TOKEN_PUBLIC_KEY!, + env.NODEMAILER_EMAIL!, + env.NODEMAILER_PASSWORD! ) }