diff --git a/__tests__/user_schema.test.ts b/__tests__/user_schema.test.ts new file mode 100644 index 0000000..6873a39 --- /dev/null +++ b/__tests__/user_schema.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect } from 'vitest'; +import { UserSchema, UserRegistrationSchema, SavedJobSchema } from '../types/user'; + +describe('User Authentication Schema', () => { + it('should validate a complete user object', () => { + const validUser = { + id: '123e4567-e89b-12d3-a456-426614174000', + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + profilePictureUrl: 'https://example.com/profile.jpg', + emailVerified: true, + createdAt: new Date(), + updatedAt: new Date(), + lastLogin: new Date(), + }; + + const result = UserSchema.safeParse(validUser); + expect(result.success).toBe(true); + }); + + it('should validate user registration', () => { + const validRegistration = { + email: 'test@example.com', + password: 'securePassword123!', + firstName: 'John', + lastName: 'Doe', + }; + + const result = UserRegistrationSchema.safeParse(validRegistration); + expect(result.success).toBe(true); + }); + + it('should reject invalid email', () => { + const invalidUser = { + email: 'invalid-email', + password: 'short', + }; + + const result = UserRegistrationSchema.safeParse(invalidUser); + expect(result.success).toBe(false); + }); + + it('should validate saved job', () => { + const validSavedJob = { + id: '123e4567-e89b-12d3-a456-426614174000', + userId: '123e4567-e89b-12d3-a456-426614174001', + jobId: 'job123', + savedAt: new Date(), + }; + + const result = SavedJobSchema.safeParse(validSavedJob); + expect(result.success).toBe(true); + }); +}); \ No newline at end of file diff --git a/db/migrations/001_create_users_table.sql b/db/migrations/001_create_users_table.sql new file mode 100644 index 0000000..6a1506c --- /dev/null +++ b/db/migrations/001_create_users_table.sql @@ -0,0 +1,42 @@ +-- Create users table for authentication +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + first_name VARCHAR(100), + last_name VARCHAR(100), + profile_picture_url TEXT, + email_verified BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + last_login TIMESTAMP WITH TIME ZONE +); + +-- Create index for faster email lookup +CREATE INDEX idx_users_email ON users(email); + +-- Create saved_jobs table to link with users +CREATE TABLE saved_jobs ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + job_id VARCHAR(255) NOT NULL, + saved_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, job_id) +); + +-- Function to automatically update updated_at timestamp +CREATE OR REPLACE FUNCTION update_modified_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- Trigger to automatically update updated_at for users table +CREATE TRIGGER update_users_modtime +BEFORE UPDATE ON users +FOR EACH ROW +EXECUTE FUNCTION update_modified_column(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8c32a76..77fd89e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "tailwindcss": "3.4.1", "tailwindcss-animate": "^1.0.6", "uuid": "^9.0.1", - "zod": "^3.21.4" + "zod": "^3.25.16" }, "devDependencies": { "@types/uuid": "^9.0.8", @@ -6537,9 +6537,10 @@ } }, "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "version": "3.25.16", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.16.tgz", + "integrity": "sha512-3lOav31WLa1MstEvkM0QlcsFjKmJ2TI9IFYlIVpLE3upguhaeiRfPOzqqtisS/Hetk4ri2fLLC3OtW15lS5jxQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 5f77db5..9e19363 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "tailwindcss": "3.4.1", "tailwindcss-animate": "^1.0.6", "uuid": "^9.0.1", - "zod": "^3.21.4" + "zod": "^3.25.16" }, "devDependencies": { "@types/uuid": "^9.0.8", diff --git a/types/user.ts b/types/user.ts new file mode 100644 index 0000000..4ea10bb --- /dev/null +++ b/types/user.ts @@ -0,0 +1,32 @@ +import { z } from 'zod'; + +// User schema for validation +export const UserSchema = z.object({ + id: z.string().uuid(), + email: z.string().email(), + firstName: z.string().optional(), + lastName: z.string().optional(), + profilePictureUrl: z.string().url().optional(), + emailVerified: z.boolean().default(false), + createdAt: z.date(), + updatedAt: z.date(), + lastLogin: z.date().optional(), +}); + +export const UserRegistrationSchema = z.object({ + email: z.string().email(), + password: z.string().min(8, 'Password must be at least 8 characters'), + firstName: z.string().optional(), + lastName: z.string().optional(), +}); + +export const SavedJobSchema = z.object({ + id: z.string().uuid(), + userId: z.string().uuid(), + jobId: z.string(), + savedAt: z.date(), +}); + +export type User = z.infer; +export type UserRegistration = z.infer; +export type SavedJob = z.infer; \ No newline at end of file