From 351433fcad377610272628c540d58572dd1ef5ce Mon Sep 17 00:00:00 2001 From: Assad Isah Date: Thu, 26 Feb 2026 12:34:21 +0100 Subject: [PATCH 1/4] implement user login feat: implement user login functionality with validation and token generation --- apps/api/src/modules/auth/auth.controller.ts | 50 +++++++++++++++++++- apps/api/src/modules/auth/auth.routes.ts | 3 +- apps/api/src/modules/auth/auth.validation.ts | 6 +++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/apps/api/src/modules/auth/auth.controller.ts b/apps/api/src/modules/auth/auth.controller.ts index ad39210..73ed968 100644 --- a/apps/api/src/modules/auth/auth.controller.ts +++ b/apps/api/src/modules/auth/auth.controller.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express'; import bcrypt from 'bcryptjs'; import User from '../users/user.model'; import { generateToken } from '../../services/jwt.service'; -import { registerSchema } from './auth.validation'; +import { loginSchema, registerSchema } from './auth.validation'; const SALT_ROUNDS = 10; @@ -68,3 +68,51 @@ export const register = async (req: Request, res: Response): Promise => { res.status(500).json({ message: 'Internal server error' }); } }; + +export const login = async (req: Request, res: Response): Promise => { + const parsedPayload = loginSchema.safeParse(req.body); + + if (!parsedPayload.success) { + res.status(400).json({ + message: 'Validation failed', + errors: parsedPayload.error.flatten(), + }); + return; + } + + const { email, password } = parsedPayload.data; + + try { + const normalizedEmail = email.toLowerCase(); + const existingUser = await User.findOne({ email: normalizedEmail }); + + if (!existingUser) { + res.status(401).json({ message: 'Invalid email or password' }); + return; + } + + const passwordMatches = await bcrypt.compare(password, existingUser.passwordHash); + + if (!passwordMatches) { + res.status(401).json({ message: 'Invalid email or password' }); + return; + } + + const token = generateToken(existingUser.id, existingUser.role); + + res.status(200).json({ + token, + user: { + id: existingUser.id, + name: existingUser.name, + email: existingUser.email, + role: existingUser.role, + onboardingCompleted: existingUser.onboardingCompleted, + createdAt: existingUser.createdAt, + updatedAt: existingUser.updatedAt, + }, + }); + } catch (error) { + res.status(500).json({ message: 'Internal server error' }); + } +}; diff --git a/apps/api/src/modules/auth/auth.routes.ts b/apps/api/src/modules/auth/auth.routes.ts index 7919d08..b6643d0 100644 --- a/apps/api/src/modules/auth/auth.routes.ts +++ b/apps/api/src/modules/auth/auth.routes.ts @@ -1,8 +1,9 @@ import { Router } from 'express'; -import { register } from './auth.controller'; +import { login, register } from './auth.controller'; const authRouter = Router(); authRouter.post('/register', register); +authRouter.post('/login', login); export default authRouter; diff --git a/apps/api/src/modules/auth/auth.validation.ts b/apps/api/src/modules/auth/auth.validation.ts index 10493f1..70389ef 100644 --- a/apps/api/src/modules/auth/auth.validation.ts +++ b/apps/api/src/modules/auth/auth.validation.ts @@ -7,4 +7,10 @@ export const registerSchema = z.object({ role: z.enum([UserRole.DJ, UserRole.PLANNER, UserRole.MUSIC_LOVER]), }); +export const loginSchema = z.object({ + email: z.string().trim().email(), + password: z.string().min(1, 'Password is required'), +}); + export type RegisterInput = z.infer; +export type LoginInput = z.infer; From e001645799949203adecf57aace27ec1b886b4cf Mon Sep 17 00:00:00 2001 From: Muzainat Date: Thu, 26 Feb 2026 12:42:18 +0100 Subject: [PATCH 2/4] feat: add authentication middleware and extend Express Request type feat: add authentication middleware and extend Express Request type --- apps/api/src/middleware/auth.middleware.ts | 69 ++++++++++++++++++++++ apps/api/src/types/express.d.ts | 11 ++++ 2 files changed, 80 insertions(+) create mode 100644 apps/api/src/middleware/auth.middleware.ts create mode 100644 apps/api/src/types/express.d.ts diff --git a/apps/api/src/middleware/auth.middleware.ts b/apps/api/src/middleware/auth.middleware.ts new file mode 100644 index 0000000..704d64f --- /dev/null +++ b/apps/api/src/middleware/auth.middleware.ts @@ -0,0 +1,69 @@ +import { NextFunction, Request, Response } from 'express'; +import { UserRole } from '@mixmatch/types'; +import { + JsonWebTokenError, + TokenExpiredError, + verifyToken, +} from '../services/jwt.service'; + +export interface AuthenticatedRequestUser { + userId: string; + role: UserRole; +} + +const extractBearerToken = (authorizationHeader?: string): string | null => { + if (!authorizationHeader) { + return null; + } + + const [scheme, token] = authorizationHeader.split(' '); + + if (scheme !== 'Bearer' || !token) { + return null; + } + + return token; +}; + +export const requireAuth = (req: Request, res: Response, next: NextFunction): void => { + const token = extractBearerToken(req.header('authorization')); + + if (!token) { + res.status(401).json({ message: 'Unauthorized: missing or invalid token' }); + return; + } + + try { + const payload = verifyToken(token); + + req.user = { + userId: payload.userId, + role: payload.role, + }; + + next(); + } catch (error) { + if (error instanceof TokenExpiredError || error instanceof JsonWebTokenError) { + res.status(401).json({ message: 'Unauthorized: missing or invalid token' }); + return; + } + + res.status(500).json({ message: 'Internal server error' }); + } +}; + +export const requireRole = (roles: UserRole[]) => { + return (req: Request, res: Response, next: NextFunction): void => { + if (!req.user) { + res.status(401).json({ message: 'Unauthorized: missing or invalid token' }); + return; + } + + if (!roles.includes(req.user.role)) { + res.status(403).json({ message: 'Forbidden: insufficient role permissions' }); + return; + } + + next(); + }; +}; diff --git a/apps/api/src/types/express.d.ts b/apps/api/src/types/express.d.ts new file mode 100644 index 0000000..e7a0f69 --- /dev/null +++ b/apps/api/src/types/express.d.ts @@ -0,0 +1,11 @@ +import { AuthenticatedRequestUser } from '../middleware/auth.middleware'; + +declare global { + namespace Express { + interface Request { + user?: AuthenticatedRequestUser; + } + } +} + +export {}; From b285671ab404021612759e81036ac22d78c05f97 Mon Sep 17 00:00:00 2001 From: Muzainat Date: Thu, 26 Feb 2026 12:46:56 +0100 Subject: [PATCH 3/4] feat: add UI components (Button, Input, Select) and example form feat: add UI components (Button, Input, Select) and example form --- packages/ui/package.json | 14 +++++ packages/ui/src/components/Button.tsx | 29 +++++++++ packages/ui/src/components/Input.tsx | 39 ++++++++++++ packages/ui/src/components/Select.tsx | 53 ++++++++++++++++ .../src/examples/ValidatedAuthFormExample.tsx | 63 +++++++++++++++++++ packages/ui/src/index.ts | 4 ++ 6 files changed, 202 insertions(+) create mode 100644 packages/ui/package.json create mode 100644 packages/ui/src/components/Button.tsx create mode 100644 packages/ui/src/components/Input.tsx create mode 100644 packages/ui/src/components/Select.tsx create mode 100644 packages/ui/src/examples/ValidatedAuthFormExample.tsx create mode 100644 packages/ui/src/index.ts diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 0000000..f2782a7 --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,14 @@ +{ + "name": "@mixmatch/ui", + "version": "0.0.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts", + "peerDependencies": { + "@hookform/resolvers": "^5.2.2", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.66.1", + "zod": "^4.3.6" + } +} diff --git a/packages/ui/src/components/Button.tsx b/packages/ui/src/components/Button.tsx new file mode 100644 index 0000000..53d49cc --- /dev/null +++ b/packages/ui/src/components/Button.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +type ButtonVariant = 'primary' | 'secondary' | 'danger'; + +export interface ButtonProps extends Omit, 'className'> { + variant?: ButtonVariant; +} + +const variantClasses: Record = { + primary: 'bg-slate-900 text-white hover:bg-slate-700', + secondary: 'bg-slate-200 text-slate-900 hover:bg-slate-300', + danger: 'bg-red-600 text-white hover:bg-red-500', +}; + +export const Button = React.forwardRef( + ({ variant = 'primary', type = 'button', disabled, ...props }, ref) => ( +