From bdba7d16fe88ebc2ce05f7984f42fdb934ac5520 Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Sun, 8 Mar 2026 03:26:10 -0700 Subject: [PATCH 1/4] Implement Authentication Controller & Routes --- prisma/schema.prisma | 21 +++- src/config/database.ts | 5 + src/controllers/auth.controller.ts | 158 +++++++++++++++++++++++++++++ src/routes/index.ts | 4 +- src/routes/v1/auth.routes.ts | 28 +++++ src/schemas/auth.schema.ts | 17 ++++ tests/auth.controller.test.ts | 157 ++++++++++++++++++++++++++++ 7 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 src/schemas/auth.schema.ts create mode 100644 tests/auth.controller.test.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b8085d0..797b1a4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -3,10 +3,29 @@ generator client { } datasource db { - provider = "sqlite" + provider = "postgresql" url = env("DATABASE_URL") } +enum Role { + ADMIN + LEARNER + INSTRUCTOR +} + +model User { + id String @id @default(uuid()) + email String @unique + username String @unique + password String + role Role @default(LEARNER) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + lastLoginAt DateTime? + + @@map("users") +} + model WebhookEndpoint { id String @id @default(uuid()) url String diff --git a/src/config/database.ts b/src/config/database.ts index e69de29..4e54f7a 100644 --- a/src/config/database.ts +++ b/src/config/database.ts @@ -0,0 +1,5 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +export default prisma; diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index e69de29..d6fd0dc 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -0,0 +1,158 @@ +import { Request, Response } from 'express'; +import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; +import prisma from '../config/database'; +import { RegisterInput, loginSchema, registerSchema } from '../schemas/auth.schema'; +import { UserRole } from '../types/user.types'; + +const JWT_SECRET = process.env.JWT_SECRET || 'your-default-secret'; +const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '1d'; + +export class AuthController { + /** + * @route POST /api/v1/auth/register + * @desc Register a new user + * @access Public + */ + async register(req: Request, res: Response): Promise { + try { + // Validate input + const validation = registerSchema.safeParse(req.body); + if (!validation.success) { + res.status(400).json({ + error: 'Validation failed', + details: validation.error.format() + }); + return; + } + + const { email, password, username, role } = validation.data; + + // Check if user already exists + const existingUser = await prisma.user.findFirst({ + where: { + OR: [ + { email }, + { username } + ] + } + }); + + if (existingUser) { + res.status(409).json({ error: 'User with this email or username already exists' }); + return; + } + + // Hash password + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); + + // Create user + const user = await prisma.user.create({ + data: { + email, + username, + password: hashedPassword, + role: (role as any) || UserRole.LEARNER, + } + }); + + // Generate token + const token = this.generateToken(user.id, user.role); + + res.status(201).json({ + message: 'User registered successfully', + token, + user: { + id: user.id, + email: user.email, + username: user.username, + role: user.role + } + }); + } catch (error) { + console.error('Registration error:', error); + res.status(500).json({ error: 'Internal server error during registration' }); + } + } + + /** + * @route POST /api/v1/auth/login + * @desc Login a user + * @access Public + */ + async login(req: Request, res: Response): Promise { + try { + // Validate input + const validation = loginSchema.safeParse(req.body); + if (!validation.success) { + res.status(400).json({ + error: 'Validation failed', + details: validation.error.format() + }); + return; + } + + const { email, password } = validation.data; + + // Find user + const user = await prisma.user.findUnique({ + where: { email } + }); + + if (!user) { + res.status(401).json({ error: 'Invalid credentials' }); + return; + } + + // Verify password + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) { + res.status(401).json({ error: 'Invalid credentials' }); + return; + } + + // Update last login + await prisma.user.update({ + where: { id: user.id }, + data: { lastLoginAt: new Date() } + }); + + // Generate token + const token = this.generateToken(user.id, user.role); + + res.status(200).json({ + message: 'Login successful', + token, + user: { + id: user.id, + email: user.email, + username: user.username, + role: user.role + } + }); + } catch (error) { + console.error('Login error:', error); + res.status(500).json({ error: 'Internal server error during login' }); + } + } + + /** + * @route POST /api/v1/auth/logout + * @desc Logout user (client-side usually handles this by deleting token, but can track server-side) + * @access Private (optional, here public) + */ + async logout(req: Request, res: Response): Promise { + // For stateless JWT, we can't truly "logout" unless we blacklist tokens. + // For now, let's just return success message as the client will clear the token. + res.status(200).json({ message: 'Logged out successfully. Please clear your token client-side.' }); + } + + private generateToken(userId: string, role: string): string { + return jwt.sign( + { id: userId, role }, + JWT_SECRET as string, + { expiresIn: JWT_EXPIRES_IN as any } + ); + } +} diff --git a/src/routes/index.ts b/src/routes/index.ts index 5b98443..f660490 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,12 +1,14 @@ import { Router } from 'express' import userRoutes from './v1/users.routes' +import authRoutes from './v1/auth.routes' -const router = Router() +const router: Router = Router() router.get('/', (req, res) => { res.json({ message: 'API is running' }) }) router.use('/v1/users', userRoutes) +router.use('/v1/auth', authRoutes) export default router diff --git a/src/routes/v1/auth.routes.ts b/src/routes/v1/auth.routes.ts index e69de29..560c2df 100644 --- a/src/routes/v1/auth.routes.ts +++ b/src/routes/v1/auth.routes.ts @@ -0,0 +1,28 @@ +import { Router } from 'express'; +import { AuthController } from '../../controllers/auth.controller'; + +const router: Router = Router(); +const authController = new AuthController(); + +/** + * @route POST /api/v1/auth/register + * @desc Register a new user + * @access Public + */ +router.post('/register', authController.register.bind(authController)); + +/** + * @route POST /api/v1/auth/login + * @desc Login user + * @access Public + */ +router.post('/login', authController.login.bind(authController)); + +/** + * @route POST /api/v1/auth/logout + * @desc Logout user + * @access Public + */ +router.post('/logout', authController.logout.bind(authController)); + +export default router; diff --git a/src/schemas/auth.schema.ts b/src/schemas/auth.schema.ts new file mode 100644 index 0000000..4973303 --- /dev/null +++ b/src/schemas/auth.schema.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; +import { UserRole } from '../types/user.types'; + +export const registerSchema = z.object({ + email: z.string().email('Invalid email address'), + password: z.string().min(8, 'Password must be at least 8 characters long'), + username: z.string().min(3, 'Username must be at least 3 characters long'), + role: z.nativeEnum(UserRole).optional().default(UserRole.LEARNER), +}); + +export const loginSchema = z.object({ + email: z.string().email('Invalid email address'), + password: z.string(), +}); + +export type RegisterInput = z.infer; +export type LoginInput = z.infer; diff --git a/tests/auth.controller.test.ts b/tests/auth.controller.test.ts new file mode 100644 index 0000000..ecc1f76 --- /dev/null +++ b/tests/auth.controller.test.ts @@ -0,0 +1,157 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { Request, Response } from 'express'; +import { AuthController } from '../src/controllers/auth.controller'; +import prisma from '../src/config/database'; +import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; + +// Mock dependencies +vi.mock('../src/config/database', () => ({ + default: { + user: { + findFirst: vi.fn(), + findUnique: vi.fn(), + create: vi.fn(), + update: vi.fn(), + }, + }, +})); + +vi.mock('bcryptjs', () => ({ + default: { + genSalt: vi.fn().mockResolvedValue('salt'), + hash: vi.fn().mockResolvedValue('hashed_password'), + compare: vi.fn(), + }, +})); + +vi.mock('jsonwebtoken', () => ({ + default: { + sign: vi.fn().mockReturnValue('mock_token'), + }, +})); + +describe('AuthController', () => { + let authController: AuthController; + let mockRequest: Partial; + let mockResponse: Partial; + + beforeEach(() => { + authController = new AuthController(); + mockRequest = {}; + mockResponse = { + json: vi.fn(), + status: vi.fn().mockReturnThis(), + }; + vi.clearAllMocks(); + }); + + describe('register', () => { + it('should register a new user successfully', async () => { + mockRequest.body = { + email: 'test@example.com', + password: 'Password123!', + username: 'testuser', + }; + + (prisma.user.findFirst as any).mockResolvedValue(null); + (prisma.user.create as any).mockResolvedValue({ + id: '1', + email: 'test@example.com', + username: 'testuser', + role: 'LEARNER', + }); + + await authController.register(mockRequest as Request, mockResponse as Response); + + expect(prisma.user.create).toHaveBeenCalled(); + expect(mockResponse.status).toHaveBeenCalledWith(201); + expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({ + message: 'User registered successfully', + token: 'mock_token', + })); + }); + + it('should return 400 for invalid input', async () => { + mockRequest.body = { + email: 'invalid-email', + password: 'short', + }; + + await authController.register(mockRequest as Request, mockResponse as Response); + + expect(mockResponse.status).toHaveBeenCalledWith(400); + expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({ + error: 'Validation failed', + })); + }); + + it('should return 409 if user already exists', async () => { + mockRequest.body = { + email: 'exists@example.com', + password: 'Password123!', + username: 'exists', + }; + + (prisma.user.findFirst as any).mockResolvedValue({ id: '1' }); + + await authController.register(mockRequest as Request, mockResponse as Response); + + expect(mockResponse.status).toHaveBeenCalledWith(409); + expect(mockResponse.json).toHaveBeenCalledWith({ error: 'User with this email or username already exists' }); + }); + }); + + describe('login', () => { + it('should login successfully with valid credentials', async () => { + mockRequest.body = { + email: 'test@example.com', + password: 'Password123!', + }; + + const mockUser = { + id: '1', + email: 'test@example.com', + password: 'hashed_password', + username: 'testuser', + role: 'LEARNER', + }; + + (prisma.user.findUnique as any).mockResolvedValue(mockUser); + (bcrypt.compare as any).mockResolvedValue(true); + (prisma.user.update as any).mockResolvedValue(mockUser); + + await authController.login(mockRequest as Request, mockResponse as Response); + + expect(mockResponse.status).toHaveBeenCalledWith(200); + expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({ + message: 'Login successful', + token: 'mock_token', + })); + }); + + it('should return 401 for invalid credentials', async () => { + mockRequest.body = { + email: 'test@example.com', + password: 'wrong_password', + }; + + (prisma.user.findUnique as any).mockResolvedValue({ id: '1', password: 'hashed' }); + (bcrypt.compare as any).mockResolvedValue(false); + + await authController.login(mockRequest as Request, mockResponse as Response); + + expect(mockResponse.status).toHaveBeenCalledWith(401); + expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Invalid credentials' }); + }); + }); + + describe('logout', () => { + it('should return success message', async () => { + await authController.logout(mockRequest as Request, mockResponse as Response); + + expect(mockResponse.status).toHaveBeenCalledWith(200); + expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Logged out successfully. Please clear your token client-side.' }); + }); + }); +}); From 3d97a43c21db653e87550df2d72e934dfea92404 Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Mon, 9 Mar 2026 04:33:11 -0700 Subject: [PATCH 2/4] fix --- lint_output.json | Bin 0 -> 352000 bytes src/config/database.ts | 6 +- src/controllers/auth.controller.ts | 83 ++++++++++++----------- src/routes/v1/auth.routes.ts | 16 ++--- src/schemas/auth.schema.ts | 8 +-- tests/auth.controller.test.ts | 104 ++++++++++++++--------------- 6 files changed, 111 insertions(+), 106 deletions(-) create mode 100644 lint_output.json diff --git a/lint_output.json b/lint_output.json new file mode 100644 index 0000000000000000000000000000000000000000..c716141d1698bf5d80c21b5d5d3a5d83627c4947 GIT binary patch literal 352000 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�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 literal 0 HcmV?d00001 diff --git a/src/config/database.ts b/src/config/database.ts index 4e54f7a..4590932 100644 --- a/src/config/database.ts +++ b/src/config/database.ts @@ -1,5 +1,5 @@ -import { PrismaClient } from '@prisma/client'; +import { PrismaClient } from '@prisma/client' -const prisma = new PrismaClient(); +const prisma = new PrismaClient() -export default prisma; +export default prisma diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index d6fd0dc..aa089af 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -1,12 +1,12 @@ -import { Request, Response } from 'express'; -import bcrypt from 'bcryptjs'; -import jwt from 'jsonwebtoken'; -import prisma from '../config/database'; -import { RegisterInput, loginSchema, registerSchema } from '../schemas/auth.schema'; -import { UserRole } from '../types/user.types'; +import { Request, Response } from 'express' +import bcrypt from 'bcryptjs' +import jwt from 'jsonwebtoken' +import prisma from '../config/database' +import { RegisterInput, loginSchema, registerSchema } from '../schemas/auth.schema' +import { UserRole } from '../types/user.types' -const JWT_SECRET = process.env.JWT_SECRET || 'your-default-secret'; -const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '1d'; +const JWT_SECRET = process.env.JWT_SECRET || 'your-default-secret' +const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '1d' export class AuthController { /** @@ -17,16 +17,17 @@ export class AuthController { async register(req: Request, res: Response): Promise { try { // Validate input - const validation = registerSchema.safeParse(req.body); + const validation = registerSchema.safeParse(req.body) if (!validation.success) { res.status(400).json({ error: 'Validation failed', details: validation.error.format() - }); - return; + }) + +return } - const { email, password, username, role } = validation.data; + const { email, password, username, role } = validation.data // Check if user already exists const existingUser = await prisma.user.findFirst({ @@ -36,16 +37,17 @@ export class AuthController { { username } ] } - }); + }) if (existingUser) { - res.status(409).json({ error: 'User with this email or username already exists' }); - return; + res.status(409).json({ error: 'User with this email or username already exists' }) + +return } // Hash password - const salt = await bcrypt.genSalt(10); - const hashedPassword = await bcrypt.hash(password, salt); + const salt = await bcrypt.genSalt(10) + const hashedPassword = await bcrypt.hash(password, salt) // Create user const user = await prisma.user.create({ @@ -55,10 +57,10 @@ export class AuthController { password: hashedPassword, role: (role as any) || UserRole.LEARNER, } - }); + }) // Generate token - const token = this.generateToken(user.id, user.role); + const token = this.generateToken(user.id, user.role) res.status(201).json({ message: 'User registered successfully', @@ -69,10 +71,10 @@ export class AuthController { username: user.username, role: user.role } - }); + }) } catch (error) { - console.error('Registration error:', error); - res.status(500).json({ error: 'Internal server error during registration' }); + console.error('Registration error:', error) + res.status(500).json({ error: 'Internal server error during registration' }) } } @@ -84,42 +86,45 @@ export class AuthController { async login(req: Request, res: Response): Promise { try { // Validate input - const validation = loginSchema.safeParse(req.body); + const validation = loginSchema.safeParse(req.body) if (!validation.success) { res.status(400).json({ error: 'Validation failed', details: validation.error.format() - }); - return; + }) + +return } - const { email, password } = validation.data; + const { email, password } = validation.data // Find user const user = await prisma.user.findUnique({ where: { email } - }); + }) if (!user) { - res.status(401).json({ error: 'Invalid credentials' }); - return; + res.status(401).json({ error: 'Invalid credentials' }) + +return } // Verify password - const isMatch = await bcrypt.compare(password, user.password); + const isMatch = await bcrypt.compare(password, user.password) if (!isMatch) { - res.status(401).json({ error: 'Invalid credentials' }); - return; + res.status(401).json({ error: 'Invalid credentials' }) + +return } // Update last login await prisma.user.update({ where: { id: user.id }, data: { lastLoginAt: new Date() } - }); + }) // Generate token - const token = this.generateToken(user.id, user.role); + const token = this.generateToken(user.id, user.role) res.status(200).json({ message: 'Login successful', @@ -130,10 +135,10 @@ export class AuthController { username: user.username, role: user.role } - }); + }) } catch (error) { - console.error('Login error:', error); - res.status(500).json({ error: 'Internal server error during login' }); + console.error('Login error:', error) + res.status(500).json({ error: 'Internal server error during login' }) } } @@ -145,7 +150,7 @@ export class AuthController { async logout(req: Request, res: Response): Promise { // For stateless JWT, we can't truly "logout" unless we blacklist tokens. // For now, let's just return success message as the client will clear the token. - res.status(200).json({ message: 'Logged out successfully. Please clear your token client-side.' }); + res.status(200).json({ message: 'Logged out successfully. Please clear your token client-side.' }) } private generateToken(userId: string, role: string): string { @@ -153,6 +158,6 @@ export class AuthController { { id: userId, role }, JWT_SECRET as string, { expiresIn: JWT_EXPIRES_IN as any } - ); + ) } } diff --git a/src/routes/v1/auth.routes.ts b/src/routes/v1/auth.routes.ts index 560c2df..6c06d60 100644 --- a/src/routes/v1/auth.routes.ts +++ b/src/routes/v1/auth.routes.ts @@ -1,28 +1,28 @@ -import { Router } from 'express'; -import { AuthController } from '../../controllers/auth.controller'; +import { Router } from 'express' +import { AuthController } from '../../controllers/auth.controller' -const router: Router = Router(); -const authController = new AuthController(); +const router: Router = Router() +const authController = new AuthController() /** * @route POST /api/v1/auth/register * @desc Register a new user * @access Public */ -router.post('/register', authController.register.bind(authController)); +router.post('/register', authController.register.bind(authController)) /** * @route POST /api/v1/auth/login * @desc Login user * @access Public */ -router.post('/login', authController.login.bind(authController)); +router.post('/login', authController.login.bind(authController)) /** * @route POST /api/v1/auth/logout * @desc Logout user * @access Public */ -router.post('/logout', authController.logout.bind(authController)); +router.post('/logout', authController.logout.bind(authController)) -export default router; +export default router diff --git a/src/schemas/auth.schema.ts b/src/schemas/auth.schema.ts index 4973303..7b552a4 100644 --- a/src/schemas/auth.schema.ts +++ b/src/schemas/auth.schema.ts @@ -1,17 +1,17 @@ -import { z } from 'zod'; -import { UserRole } from '../types/user.types'; +import { z } from 'zod' +import { UserRole } from '../types/user.types' export const registerSchema = z.object({ email: z.string().email('Invalid email address'), password: z.string().min(8, 'Password must be at least 8 characters long'), username: z.string().min(3, 'Username must be at least 3 characters long'), role: z.nativeEnum(UserRole).optional().default(UserRole.LEARNER), -}); +}) export const loginSchema = z.object({ email: z.string().email('Invalid email address'), password: z.string(), -}); +}) export type RegisterInput = z.infer; export type LoginInput = z.infer; diff --git a/tests/auth.controller.test.ts b/tests/auth.controller.test.ts index ecc1f76..f483038 100644 --- a/tests/auth.controller.test.ts +++ b/tests/auth.controller.test.ts @@ -1,9 +1,9 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { Request, Response } from 'express'; -import { AuthController } from '../src/controllers/auth.controller'; -import prisma from '../src/config/database'; -import bcrypt from 'bcryptjs'; -import jwt from 'jsonwebtoken'; +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Request, Response } from 'express' +import { AuthController } from '../src/controllers/auth.controller' +import prisma from '../src/config/database' +import bcrypt from 'bcryptjs' +import jwt from 'jsonwebtoken' // Mock dependencies vi.mock('../src/config/database', () => ({ @@ -15,7 +15,7 @@ vi.mock('../src/config/database', () => ({ update: vi.fn(), }, }, -})); +})) vi.mock('bcryptjs', () => ({ default: { @@ -23,28 +23,28 @@ vi.mock('bcryptjs', () => ({ hash: vi.fn().mockResolvedValue('hashed_password'), compare: vi.fn(), }, -})); +})) vi.mock('jsonwebtoken', () => ({ default: { sign: vi.fn().mockReturnValue('mock_token'), }, -})); +})) describe('AuthController', () => { - let authController: AuthController; - let mockRequest: Partial; - let mockResponse: Partial; + let authController: AuthController + let mockRequest: Partial + let mockResponse: Partial beforeEach(() => { - authController = new AuthController(); - mockRequest = {}; + authController = new AuthController() + mockRequest = {} mockResponse = { json: vi.fn(), status: vi.fn().mockReturnThis(), - }; - vi.clearAllMocks(); - }); + } + vi.clearAllMocks() + }) describe('register', () => { it('should register a new user successfully', async () => { @@ -60,31 +60,31 @@ describe('AuthController', () => { email: 'test@example.com', username: 'testuser', role: 'LEARNER', - }); + }) - await authController.register(mockRequest as Request, mockResponse as Response); + await authController.register(mockRequest as Request, mockResponse as Response) - expect(prisma.user.create).toHaveBeenCalled(); - expect(mockResponse.status).toHaveBeenCalledWith(201); + expect(prisma.user.create).toHaveBeenCalled() + expect(mockResponse.status).toHaveBeenCalledWith(201) expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({ message: 'User registered successfully', token: 'mock_token', - })); - }); + })) + }) it('should return 400 for invalid input', async () => { mockRequest.body = { email: 'invalid-email', password: 'short', - }; + } - await authController.register(mockRequest as Request, mockResponse as Response); + await authController.register(mockRequest as Request, mockResponse as Response) - expect(mockResponse.status).toHaveBeenCalledWith(400); + expect(mockResponse.status).toHaveBeenCalledWith(400) expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({ error: 'Validation failed', - })); - }); + })) + }) it('should return 409 if user already exists', async () => { mockRequest.body = { @@ -93,21 +93,21 @@ describe('AuthController', () => { username: 'exists', }; - (prisma.user.findFirst as any).mockResolvedValue({ id: '1' }); + (prisma.user.findFirst as any).mockResolvedValue({ id: '1' }) - await authController.register(mockRequest as Request, mockResponse as Response); + await authController.register(mockRequest as Request, mockResponse as Response) - expect(mockResponse.status).toHaveBeenCalledWith(409); - expect(mockResponse.json).toHaveBeenCalledWith({ error: 'User with this email or username already exists' }); - }); - }); + expect(mockResponse.status).toHaveBeenCalledWith(409) + expect(mockResponse.json).toHaveBeenCalledWith({ error: 'User with this email or username already exists' }) + }) + }) describe('login', () => { it('should login successfully with valid credentials', async () => { mockRequest.body = { email: 'test@example.com', password: 'Password123!', - }; + } const mockUser = { id: '1', @@ -119,16 +119,16 @@ describe('AuthController', () => { (prisma.user.findUnique as any).mockResolvedValue(mockUser); (bcrypt.compare as any).mockResolvedValue(true); - (prisma.user.update as any).mockResolvedValue(mockUser); + (prisma.user.update as any).mockResolvedValue(mockUser) - await authController.login(mockRequest as Request, mockResponse as Response); + await authController.login(mockRequest as Request, mockResponse as Response) - expect(mockResponse.status).toHaveBeenCalledWith(200); + expect(mockResponse.status).toHaveBeenCalledWith(200) expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({ message: 'Login successful', token: 'mock_token', - })); - }); + })) + }) it('should return 401 for invalid credentials', async () => { mockRequest.body = { @@ -137,21 +137,21 @@ describe('AuthController', () => { }; (prisma.user.findUnique as any).mockResolvedValue({ id: '1', password: 'hashed' }); - (bcrypt.compare as any).mockResolvedValue(false); + (bcrypt.compare as any).mockResolvedValue(false) - await authController.login(mockRequest as Request, mockResponse as Response); + await authController.login(mockRequest as Request, mockResponse as Response) - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Invalid credentials' }); - }); - }); + expect(mockResponse.status).toHaveBeenCalledWith(401) + expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Invalid credentials' }) + }) + }) describe('logout', () => { it('should return success message', async () => { - await authController.logout(mockRequest as Request, mockResponse as Response); + await authController.logout(mockRequest as Request, mockResponse as Response) - expect(mockResponse.status).toHaveBeenCalledWith(200); - expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Logged out successfully. Please clear your token client-side.' }); - }); - }); -}); + expect(mockResponse.status).toHaveBeenCalledWith(200) + expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Logged out successfully. Please clear your token client-side.' }) + }) + }) +}) From 62d5068a99fb66109289cec94fe993528c42c569 Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Mon, 9 Mar 2026 04:47:58 -0700 Subject: [PATCH 3/4] fix --- prisma/schema.prisma | 20 ++++++-------------- src/config/database.ts | 16 +++++++++------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6e43a1d..b2e0274 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -4,20 +4,23 @@ generator client { datasource db { provider = "postgresql" - url = env("DATABASE_URL") } model User { id String @id @default(uuid()) email String @unique - name String + username String @unique + password String + role Role @default(LEARNER) walletAddress String? @unique - passwordHash String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + lastLoginAt DateTime? completions Completion[] credentials Credential[] transactions Transaction[] + + @@map("users") } model Module { @@ -78,18 +81,7 @@ enum Role { INSTRUCTOR } -model User { - id String @id @default(uuid()) - email String @unique - username String @unique - password String - role Role @default(LEARNER) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - lastLoginAt DateTime? - @@map("users") -} model WebhookEndpoint { id String @id @default(uuid()) diff --git a/src/config/database.ts b/src/config/database.ts index cf4f808..888ef86 100644 --- a/src/config/database.ts +++ b/src/config/database.ts @@ -1,12 +1,14 @@ -import { PrismaClient } from '@prisma/client' +import { PrismaClient } from "@prisma/client" -const prisma = new PrismaClient() - -export default prisma const globalForPrisma = globalThis as unknown as { - prisma: any | undefined + prisma: PrismaClient | undefined } -export const prisma = globalForPrisma.prisma ?? new PrismaClient() +const prisma = globalForPrisma.prisma ?? new PrismaClient() -if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma +if (process.env.NODE_ENV !== 'production') { + globalForPrisma.prisma = prisma +} + +export default prisma +export { prisma } From de682409d00870876cac1588c46d0a3bf1df3f9d Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Mon, 9 Mar 2026 04:58:40 -0700 Subject: [PATCH 4/4] fix --- src/config/database.ts | 2 +- src/controllers/auth.controller.ts | 22 +++++++++++----------- tests/auth.controller.test.ts | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/config/database.ts b/src/config/database.ts index 888ef86..719caf2 100644 --- a/src/config/database.ts +++ b/src/config/database.ts @@ -1,4 +1,4 @@ -import { PrismaClient } from "@prisma/client" +import { PrismaClient } from '@prisma/client' const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index aa089af..bea9947 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express' import bcrypt from 'bcryptjs' import jwt from 'jsonwebtoken' import prisma from '../config/database' -import { RegisterInput, loginSchema, registerSchema } from '../schemas/auth.schema' +import { loginSchema, registerSchema } from '../schemas/auth.schema' import { UserRole } from '../types/user.types' const JWT_SECRET = process.env.JWT_SECRET || 'your-default-secret' @@ -23,8 +23,8 @@ export class AuthController { error: 'Validation failed', details: validation.error.format() }) - -return + + return } const { email, password, username, role } = validation.data @@ -41,8 +41,8 @@ return if (existingUser) { res.status(409).json({ error: 'User with this email or username already exists' }) - -return + + return } // Hash password @@ -92,8 +92,8 @@ return error: 'Validation failed', details: validation.error.format() }) - -return + + return } const { email, password } = validation.data @@ -105,16 +105,16 @@ return if (!user) { res.status(401).json({ error: 'Invalid credentials' }) - -return + + return } // Verify password const isMatch = await bcrypt.compare(password, user.password) if (!isMatch) { res.status(401).json({ error: 'Invalid credentials' }) - -return + + return } // Update last login diff --git a/tests/auth.controller.test.ts b/tests/auth.controller.test.ts index f483038..f916d4c 100644 --- a/tests/auth.controller.test.ts +++ b/tests/auth.controller.test.ts @@ -3,7 +3,7 @@ import { Request, Response } from 'express' import { AuthController } from '../src/controllers/auth.controller' import prisma from '../src/config/database' import bcrypt from 'bcryptjs' -import jwt from 'jsonwebtoken' +// import jwt from 'jsonwebtoken' // Mock dependencies vi.mock('../src/config/database', () => ({