Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
pull_request:
branches: [main, master]

jobs:
frontend:
name: Frontend CI
runs-on: ubuntu-latest
Expand Down
48 changes: 48 additions & 0 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
3 changes: 2 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
46 changes: 46 additions & 0 deletions backend/src/auth/validation.schemas.ts
Original file line number Diff line number Diff line change
@@ -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<typeof registerSchema>;
export type LoginRequest = z.infer<typeof loginSchema>;
34 changes: 10 additions & 24 deletions backend/src/routes/auth/auth.routes.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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 });
Expand All @@ -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 });
Expand Down
111 changes: 111 additions & 0 deletions backend/src/utils/validation.ts
Original file line number Diff line number Diff line change
@@ -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' });
}
};
};
86 changes: 86 additions & 0 deletions backend/tests/validation.test.ts
Original file line number Diff line number Diff line change
@@ -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',
});
});
});
});
Loading