diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36084cd..bc46b57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: pull_request: branches: [main, master] +jobs: frontend: name: Frontend CI runs-on: ubuntu-latest diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 62167ef..142fbc6 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -21,6 +21,54 @@ Examples of unacceptable behavior by participants include: - Public or private harassment - Publishing others' private information without explicit permission +## Helping vs Hand-holding + +As a student-focused learning community, we strive to empower learners while avoiding dependency. Here's how we balance helping with fostering independence: + +### Examples of Helpful Support ✅ + +- Providing hints and guidance rather than complete solutions +- Explaining concepts and pointing to documentation +- Asking questions that lead to discovery ("Have you considered...?") +- Sharing resources for learning the fundamentals +- Pair programming where both participants contribute + +### Examples of Hand-holding to Avoid ❌ + +- Giving complete code solutions to assignments +- Doing the work for someone else +- Providing answers without explanation of the process +- Creating dependencies on specific individuals +- Undermining the learning process by shortcutting challenges + +Remember: The goal is learning, not just completion. We celebrate the journey of discovery and growth. + +## Constructive Feedback + +In our educational environment, feedback is a gift that helps us all grow. We prioritize feedback that is specific, actionable, and kind. + +### Guidelines for Giving Feedback + +- **Be Specific**: Reference exact code, behavior, or content rather than generalizations +- **Be Actionable**: Suggest concrete improvements or next steps +- **Be Kind**: Assume good intent and focus on the work, not the person +- **Be Timely**: Provide feedback when it can be most helpful +- **Be Respectful**: Consider the skill level and context of the recipient + +### Examples of Constructive Feedback + +- "Consider refactoring this function to be more testable by separating concerns" +- "The variable name could be more descriptive to improve code readability" +- "Have you thought about edge cases like empty inputs or null values?" +- "This approach works, but you might explore using the built-in method for better performance" + +### Feedback to Avoid + +- Vague criticisms like "this is bad" or "this is wrong" +- Personal attacks or questioning someone's competence +- Feedback without suggestions for improvement +- Public criticism that could embarrass or discourage + ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. diff --git a/backend/package.json b/backend/package.json index c60f4fc..a65ff4f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -27,7 +27,8 @@ "jsonwebtoken": "^9.0.2", "openai": "^6.32.0", "pg": "^8.20.0", - "prisma": "^7.5.0" + "prisma": "^7.5.0", + "zod": "^4.3.6" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", diff --git a/backend/src/auth/validation.schemas.ts b/backend/src/auth/validation.schemas.ts new file mode 100644 index 0000000..d5b93ee --- /dev/null +++ b/backend/src/auth/validation.schemas.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; + +/** + * User Registration Schema + * Validates the request body for user registration + */ +export const registerSchema = z.object({ + email: z + .string() + .email('Invalid email format') + .min(1, 'Email is required'), + password: z + .string() + .min(6, 'Password must be at least 6 characters') + .max(100, 'Password must be less than 100 characters'), + firstName: z + .string() + .min(1, 'First name is required') + .max(50, 'First name must be less than 50 characters') + .regex(/^[a-zA-Z\s'-]+$/, 'First name can only contain letters, spaces, hyphens, and apostrophes'), + lastName: z + .string() + .min(1, 'Last name is required') + .max(50, 'Last name must be less than 50 characters') + .regex(/^[a-zA-Z\s'-]+$/, 'Last name can only contain letters, spaces, hyphens, and apostrophes'), +}); + +/** + * User Login Schema + * Validates the request body for user login + */ +export const loginSchema = z.object({ + email: z + .string() + .email('Invalid email format') + .min(1, 'Email is required'), + password: z + .string() + .min(1, 'Password is required'), +}); + +/** + * Type inference for validated data + */ +export type RegisterRequest = z.infer; +export type LoginRequest = z.infer; diff --git a/backend/src/routes/auth/auth.routes.ts b/backend/src/routes/auth/auth.routes.ts index 626c972..b157d8c 100644 --- a/backend/src/routes/auth/auth.routes.ts +++ b/backend/src/routes/auth/auth.routes.ts @@ -1,7 +1,8 @@ -import { Router, Request, Response } from 'express'; -import { register, login } from '../../auth/auth.service.js'; +import { Request, Response, Router } from 'express'; import { authenticate } from '../../auth/auth.middleware.js'; -import { LoginRequest, RegisterRequest } from '../../auth/types.js'; +import { login, register } from '../../auth/auth.service.js'; +import { loginSchema, registerSchema } from '../../auth/validation.schemas.js'; +import { validateRequest } from '../../utils/validation.js'; const router = Router(); @@ -10,20 +11,10 @@ const router = Router(); * @desc Register a new student * @access Public */ -router.post('/register', async (req: Request, res: Response) => { +router.post('/register', validateRequest(registerSchema), async (req: Request, res: Response) => { try { - const { email, password, firstName, lastName }: RegisterRequest = req.body; - - // Validation - if (!email || !password || !firstName || !lastName) { - res.status(400).json({ error: 'All fields are required' }); - return; - } - - if (password.length < 6) { - res.status(400).json({ error: 'Password must be at least 6 characters' }); - return; - } + // Request body is already validated by middleware + const { email, password, firstName, lastName } = req.body; // Register the student const authResponse = await register({ email, password, firstName, lastName }); @@ -43,15 +34,10 @@ router.post('/register', async (req: Request, res: Response) => { * @desc Login student * @access Public */ -router.post('/login', async (req: Request, res: Response) => { +router.post('/login', validateRequest(loginSchema), async (req: Request, res: Response) => { try { - const { email, password }: LoginRequest = req.body; - - // Validation - if (!email || !password) { - res.status(400).json({ error: 'Email and password are required' }); - return; - } + // Request body is already validated by middleware + const { email, password } = req.body; // Login the student const authResponse = await login({ email, password }); diff --git a/backend/src/utils/validation.ts b/backend/src/utils/validation.ts new file mode 100644 index 0000000..c3685cb --- /dev/null +++ b/backend/src/utils/validation.ts @@ -0,0 +1,111 @@ +import { NextFunction, Request, Response } from 'express'; +import { ZodError, ZodSchema } from 'zod'; + +/** + * Validation Error Response Interface + */ +export interface ValidationErrorResponse { + error: string; + details: Array<{ + field: string; + message: string; + }>; +} + +/** + * Generic Validation Middleware Factory + * Creates a middleware function that validates request body against a Zod schema + * + * @param schema - Zod schema to validate against + * @returns Express middleware function + */ +export const validateRequest = (schema: ZodSchema) => { + return (req: Request, res: Response, next: NextFunction): void => { + try { + // Validate the request body against the schema + schema.parse(req.body); + + // If validation passes, continue to next middleware + next(); + } catch (error) { + if (error instanceof ZodError) { + // Handle Zod validation errors + const errorResponse: ValidationErrorResponse = { + error: 'Validation failed', + details: error.issues.map((err) => ({ + field: err.path.join('.'), + message: err.message, + })), + }; + + res.status(400).json(errorResponse); + return; + } + + // Handle other unexpected errors + res.status(500).json({ error: 'Internal server error during validation' }); + } + }; +}; + +/** + * Validation Middleware for Request Parameters + * Validates URL parameters against a Zod schema + * + * @param schema - Zod schema to validate against + * @returns Express middleware function + */ +export const validateParams = (schema: ZodSchema) => { + return (req: Request, res: Response, next: NextFunction): void => { + try { + schema.parse(req.params); + next(); + } catch (error) { + if (error instanceof ZodError) { + const errorResponse: ValidationErrorResponse = { + error: 'Parameter validation failed', + details: error.issues.map((err) => ({ + field: err.path.join('.'), + message: err.message, + })), + }; + + res.status(400).json(errorResponse); + return; + } + + res.status(500).json({ error: 'Internal server error during parameter validation' }); + } + }; +}; + +/** + * Validation Middleware for Query Parameters + * Validates query parameters against a Zod schema + * + * @param schema - Zod schema to validate against + * @returns Express middleware function + */ +export const validateQuery = (schema: ZodSchema) => { + return (req: Request, res: Response, next: NextFunction): void => { + try { + schema.parse(req.query); + next(); + } catch (error) { + if (error instanceof ZodError) { + const errorResponse: ValidationErrorResponse = { + error: 'Query validation failed', + details: error.issues.map((err) => ({ + field: err.path.join('.'), + message: err.message, + })), + }; + + res.status(400).json(errorResponse); + return; + } + + res.status(500).json({ error: 'Internal server error during query validation' }); + } + }; +}; diff --git a/backend/tests/validation.test.ts b/backend/tests/validation.test.ts new file mode 100644 index 0000000..aac2277 --- /dev/null +++ b/backend/tests/validation.test.ts @@ -0,0 +1,86 @@ +import { Request } from 'express'; +import { z } from 'zod'; +import { validateRequest } from '../src/utils/validation.js'; + +// Mock Express objects +const mockRequest = (body: any) => + ({ + body, + }) as Request; + +const mockResponse = () => { + const res: any = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + return res; +}; + +const mockNext = jest.fn(); + +describe('Validation Middleware', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('validateRequest', () => { + const testSchema = z.object({ + email: z.string().email(), + password: z.string().min(6), + }); + + it('should call next() for valid request body', () => { + const req = mockRequest({ + email: 'test@example.com', + password: 'password123', + }); + const res = mockResponse(); + const middleware = validateRequest(testSchema); + + middleware(req, res, mockNext); + + expect(mockNext).toHaveBeenCalledWith(); + expect(res.status).not.toHaveBeenCalled(); + }); + + it('should return 400 error for invalid request body', () => { + const req = mockRequest({ + email: 'invalid-email', + password: '123', + }); + const res = mockResponse(); + const middleware = validateRequest(testSchema); + + middleware(req, res, mockNext); + + expect(mockNext).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ + error: 'Validation failed', + details: expect.arrayContaining([ + expect.objectContaining({ + field: 'email', + message: 'Invalid email address', + }), + expect.objectContaining({ + field: 'password', + message: 'Too small: expected string to have >=6 characters', + }), + ]), + }); + }); + + it('should return 500 error for unexpected errors', () => { + const req = mockRequest({}); + const res = mockResponse(); + const middleware = validateRequest(null as any); + + middleware(req, res, mockNext); + + expect(mockNext).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: 'Internal server error during validation', + }); + }); + }); +});