From 1ecaf8ff95a1732e7bebc459c3d422d9666bef74 Mon Sep 17 00:00:00 2001 From: purvarao Date: Thu, 27 Nov 2025 19:14:46 +0530 Subject: [PATCH] api --- docs/leave-agent-design.md | 219 +++++++++++++++++++++++++ src/index.ts | 82 ++++++++- src/services/db-service.js | 20 +-- src/swagger/endpoints/auth-validate.js | 44 +++++ src/types/index.ts | 39 +++++ tsconfig.json | 21 +++ wrangler.toml | 28 ++-- 7 files changed, 420 insertions(+), 33 deletions(-) create mode 100644 docs/leave-agent-design.md create mode 100644 src/types/index.ts create mode 100644 tsconfig.json diff --git a/docs/leave-agent-design.md b/docs/leave-agent-design.md new file mode 100644 index 0000000..bda23a0 --- /dev/null +++ b/docs/leave-agent-design.md @@ -0,0 +1,219 @@ +# Leave Agent - Database Design + +## Database Schema + +### 1. Employees +Stores employee information and their leave balances. + +```sql +CREATE TABLE employees ( + employee_id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +### 2. Leave Types +Defines different types of leaves available in the system. + +```sql +CREATE TABLE leave_types ( + leave_types_id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, -- e.g., "Annual Leave", "Sick Leave" + description TEXT, + max_days INTEGER NOT NULL, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +### 3. Leave Balances +Tracks the number of leave days available for each employee by leave type. + +```sql +CREATE TABLE leave_balances ( + leave_balances_id INTEGER PRIMARY KEY AUTOINCREMENT, + employee_id INTEGER NOT NULL, + leave_type_id INTEGER NOT NULL, + balance_days REAL NOT NULL, + year INTEGER NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE CASCADE, + FOREIGN KEY (leave_type_id) REFERENCES leave_types(leave_types_id) ON DELETE CASCADE, + UNIQUE(employee_id, leave_type_id, year) +); +``` + +### 4. Leave Requests +Tracks all leave requests made by employees. + +```sql +CREATE TABLE leave_requests ( + leave_requests_id INTEGER PRIMARY KEY AUTOINCREMENT, + employee_id INTEGER NOT NULL, + leave_type_id INTEGER NOT NULL, + start_date DATE NOT NULL, + end_date DATE NOT NULL, + reason TEXT, + status TEXT NOT NULL, -- 'pending', 'approved', 'rejected', 'cancelled' + business_days REAL NOT NULL, + notes TEXT, + approved_by INTEGER, + approved_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE CASCADE, + FOREIGN KEY (leave_type_id) REFERENCES leave_types(leave_types_id) ON DELETE CASCADE, + FOREIGN KEY (approved_by) REFERENCES employees(employee_id) ON DELETE SET NULL +); +``` + +### 5. Holidays +Stores public holidays. + +```sql +CREATE TABLE holidays ( + holidays_id INTEGER PRIMARY KEY AUTOINCREMENT, + date DATE NOT NULL, + name TEXT NOT NULL, + description TEXT, + is_recurring BOOLEAN DEFAULT false, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(date, name) +); +``` + +### 6. Team Events +Stores company events and team activities. + +```sql +CREATE TABLE team_events ( + team_events_id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + start_date DATE NOT NULL, + end_date DATE, + is_recurring BOOLEAN DEFAULT false, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +## Entity Relationship Diagram + +```mermaid +erDiagram + employees ||--o{ leave_balances : has + employees ||--o{ leave_requests : makes + leave_types ||--o{ leave_balances : has + leave_types ||--o{ leave_requests : has + employees ||--o{ leave_requests : approves + + employees { + int employee_id PK + string name + string email + timestamp created_at + timestamp updated_at + } + + leave_types { + int leave_types_id PK + string name + string description + int max_days + boolean is_active + timestamp created_at + } + + leave_balances { + int leave_balances_id PK + int employee_id FK + int leave_type_id FK + real balance_days + int year + timestamp created_at + timestamp updated_at + } + + leave_requests { + int leave_requests_id PK + int employee_id FK + int leave_type_id FK + date start_date + date end_date + string reason + string status + real business_days + string notes + int approved_by FK + timestamp approved_at + timestamp created_at + timestamp updated_at + } + + holidays { + int holidays_id PK + date date + string name + string description + boolean is_recurring + timestamp created_at + } + + team_events { + int team_events_id PK + string name + string description + date start_date + date end_date + boolean is_recurring + timestamp created_at + timestamp updated_at + } +``` + +## Initial Data Setup + +### Default Leave Types +```sql +INSERT INTO leave_types (name, description, max_days, is_active) VALUES +('Annual Leave', 'Paid time off for vacation or personal reasons', 24, true), +('Sick Leave', 'Paid time off for illness or medical appointments', 12, true), +('Maternity Leave', 'Paid leave for new mothers', 182, true), +('Paternity Leave', 'Paid leave for new fathers', 14, true), +('Bereavement Leave', 'Paid time off following the death of a family member', 5, true), +('Marriage Leave', 'Paid time off for employee''s own marriage', 5, true); +``` + +### Sample Holidays (2025) +```sql +INSERT INTO holidays (date, name, description, is_recurring) VALUES +('2025-01-01', 'New Year''s Day', 'First day of the year', true), +('2025-01-26', 'Republic Day', 'Indian Republic Day', true), +('2025-03-17', 'Holi', 'Festival of Colors', true), +('2025-04-14', 'Ambedkar Jayanti', 'Birth anniversary of Dr. B.R. Ambedkar', true), +('2025-08-15', 'Independence Day', 'Indian Independence Day', true), +('2025-10-02', 'Gandhi Jayanti', 'Birth anniversary of Mahatma Gandhi', true), +('2025-12-25', 'Christmas Day', 'Christmas holiday', true); +``` + +## Indexes + +For better query performance, the following indexes are recommended: + +```sql +-- Indexes for leave_requests +CREATE INDEX idx_leave_requests_employee_id ON leave_requests(employee_id); +CREATE INDEX idx_leave_requests_status ON leave_requests(status); +CREATE INDEX idx_leave_requests_dates ON leave_requests(start_date, end_date); + +-- Indexes for leave_balances +CREATE INDEX idx_leave_balances_employee ON leave_balances(employee_id, year); + +``` + + diff --git a/src/index.ts b/src/index.ts index b3c5850..5a979d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import { Hono } from 'hono'; import { cors } from 'hono/cors'; import { getCookie } from 'hono/cookie'; +import { Context } from 'hono'; import { decryptAuthCookie } from './services/decrypt-service'; import { handleSwaggerRequest } from './swagger-ui'; import { authMiddleware } from './middleware/auth'; @@ -20,7 +21,7 @@ app.use('*', cors()); // Create a group for protected routes with auth middleware // Exclude Swagger docs routes from auth middleware -app.use('/api/cf-template/*', async (c, next) => { +app.use('/api/leave-agent/*', async (c, next) => { const path = new URL(c.req.url).pathname; // Skip auth for Swagger docs routes @@ -33,7 +34,7 @@ app.use('/api/cf-template/*', async (c, next) => { }); // Add auth validation endpoint (GET method) -app.get('/api/cf-template/auth/validate', async (c) => { +app.get('/api/leave-agent/auth/validate', async (c) => { try { // Get auth from query parameter const authToken = c.req.query('auth'); @@ -58,7 +59,7 @@ app.get('/api/cf-template/auth/validate', async (c) => { }); // Add auth validation endpoint (POST method) -app.post('/api/cf-template/auth/validate', async (c) => { +app.post('/api/leave-agent/auth/validate', async (c) => { try { // Get auth from request body const body = await c.req.json(); @@ -83,8 +84,71 @@ app.post('/api/cf-template/auth/validate', async (c) => { } }); +// Define types for the user data +interface UserData { + id: number; + uid: string; + email: string; + firstname?: string; + lastname?: string; + company_name?: string; + created_at: string; + updated_at: string; +} + +// Define types for the context +interface UserContext extends Context { + get: (key: string) => string | undefined; + env: { + CF_TEMPLATE_DB?: any; // Replace 'any' with proper D1Database type if available + }; +} + +// Get current user's profile with leave balances +app.get('/api/leave-agent/employees/me', async (c: UserContext) => { + try { + // Get user ID from the auth middleware + const userId = c.get('userId'); + + if (!userId) { + return c.json({ error: 'User not authenticated' }, 401); + } + + // Get user data from the database + const userData = await getUserByUid(userId, c.env); + + if (!userData) { + return c.json({ error: 'User not found' }, 404); + } + + // TODO: Replace with actual leave balance calculation from your database + // This is a mock implementation + const leaveBalances = { + "Annual Leave": 24, + "Maternity Leave": 182, + "Paternity Leave": 0, + "Bereavement Leave": 5, + "Marriage Leave": 5 + }; + + // Format the response + const response = { + id: userData.id, + name: `${userData.firstname || ''} ${userData.lastname || ''}`.trim(), + email: userData.email, + color: '#3b82f6', // Default color, can be customized per user + leaveBalances: leaveBalances + }; + + return c.json(response); + } catch (error) { + console.error('Error fetching user profile:', error); + return c.json({ error: 'Failed to fetch user profile' }, 500); + } +}); + // Add endpoint to decrypt and save cookie data to D1 database -app.post('/api/cf-template/auth/decrypt-and-save', async (c) => { +app.post('/api/leave-agent/auth/decrypt-and-save', async (c) => { try { // Get auth token from request body const body = await c.req.json(); @@ -125,15 +189,15 @@ app.post('/api/cf-template/auth/decrypt-and-save', async (c) => { }); // Add health check endpoint -app.get('/api/cf-template/health', (c) => { +app.get('/api/leave-agent/health', (c) => { return c.json({ status: 'ok' }); }); // Add Swagger UI routes -app.get('/api/cf-template/docs', (c) => handleSwaggerRequest(c.req.raw, '/api/cf-template')); -app.get('/api/cf-template/docs/', (c) => handleSwaggerRequest(c.req.raw, '/api/cf-template')); -app.get('/api/cf-template/swagger.json', (c) => handleSwaggerRequest(c.req.raw, '/api/cf-template')); -app.get('/api/cf-template/openapi.json', (c) => handleSwaggerRequest(c.req.raw, '/api/cf-template')); +app.get('/api/leave-agent/docs', (c) => handleSwaggerRequest(c.req.raw, '/api/leave-agent')); +app.get('/api/leave-agent/docs/', (c) => handleSwaggerRequest(c.req.raw, '/api/leave-agent')); +app.get('/api/leave-agent/swagger.json', (c) => handleSwaggerRequest(c.req.raw, '/api/leave-agent')); +app.get('/api/leave-agent/openapi.json', (c) => handleSwaggerRequest(c.req.raw, '/api/leave-agent')); // Export the app export default { diff --git a/src/services/db-service.js b/src/services/db-service.js index e25dc1a..2d7b69d 100644 --- a/src/services/db-service.js +++ b/src/services/db-service.js @@ -11,7 +11,7 @@ import { saveUserDataLocal, getUserByUidLocal, initLocalDb } from './local-db-se */ async function initializeDatabase(env) { try { - if (!env.CF_TEMPLATE_DB) { + if (!env.LEAVE_AGENT_DB) { console.log('D1 database binding not found, skipping initialization'); return false; } @@ -19,11 +19,11 @@ async function initializeDatabase(env) { // Create user_data table if it doesn't exist try { // Use a single-line SQL statement to avoid parsing issues with D1 - await env.CF_TEMPLATE_DB.exec(`CREATE TABLE IF NOT EXISTS user_data (id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE NOT NULL, email TEXT NOT NULL, firebase_uid TEXT, firstname TEXT, lastname TEXT, company_name TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);`); + await env.LEAVE_AGENT_DB.exec(`CREATE TABLE IF NOT EXISTS user_data (id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE NOT NULL, email TEXT NOT NULL, firebase_uid TEXT, firstname TEXT, lastname TEXT, company_name TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);`); // Create indexes in a single-line format as well - await env.CF_TEMPLATE_DB.exec(`CREATE INDEX IF NOT EXISTS idx_user_data_uid ON user_data(uid);`); - await env.CF_TEMPLATE_DB.exec(`CREATE INDEX IF NOT EXISTS idx_user_data_email ON user_data(email);`); + await env.LEAVE_AGENT_DB.exec(`CREATE INDEX IF NOT EXISTS idx_user_data_uid ON user_data(uid);`); + await env.LEAVE_AGENT_DB.exec(`CREATE INDEX IF NOT EXISTS idx_user_data_email ON user_data(email);`); console.log('Database schema initialized successfully'); } catch (dbError) { @@ -50,7 +50,7 @@ export async function saveUserData(userData, env) { // Initialize database schema if needed await initializeDatabase(env); // Check if required database binding exists - if (!env.CF_TEMPLATE_DB) { + if (!env.LEAVE_AGENT_DB) { console.log('D1 database binding not found, using local database'); // Use our local database implementation return await saveUserDataLocal(userData); @@ -66,7 +66,7 @@ export async function saveUserData(userData, env) { try { // Check if user already exists - const existingUser = await env.CF_TEMPLATE_DB.prepare( + const existingUser = await env.LEAVE_AGENT_DB.prepare( 'SELECT id FROM user_data WHERE uid = ?' ).bind(uid).first(); @@ -74,7 +74,7 @@ export async function saveUserData(userData, env) { if (existingUser) { // Update existing user - result = await env.CF_TEMPLATE_DB.prepare(` + result = await env.LEAVE_AGENT_DB.prepare(` UPDATE user_data SET email = ?, firebase_uid = ?, firstname = ?, lastname = ?, company_name = ?, updated_at = CURRENT_TIMESTAMP WHERE uid = ? @@ -88,7 +88,7 @@ export async function saveUserData(userData, env) { }; } else { // Insert new user - result = await env.CF_TEMPLATE_DB.prepare(` + result = await env.LEAVE_AGENT_DB.prepare(` INSERT INTO user_data (uid, email, firebase_uid, firstname, lastname, company_name) VALUES (?, ?, ?, ?, ?, ?) `).bind(uid, email, firebase_uid, firstname, lastname, company_name).run(); @@ -128,13 +128,13 @@ export async function getUserByUid(uid, env) { try { // Initialize database schema if needed await initializeDatabase(env); - if (!env.CF_TEMPLATE_DB) { + if (!env.LEAVE_AGENT_DB) { console.log('D1 database binding not found, using local database'); // Use our local database implementation return await getUserByUidLocal(uid); } - const user = await env.CF_TEMPLATE_DB.prepare( + const user = await env.LEAVE_AGENT_DB.prepare( 'SELECT * FROM user_data WHERE uid = ?' ).bind(uid).first(); diff --git a/src/swagger/endpoints/auth-validate.js b/src/swagger/endpoints/auth-validate.js index 845cbd2..8839232 100644 --- a/src/swagger/endpoints/auth-validate.js +++ b/src/swagger/endpoints/auth-validate.js @@ -3,6 +3,50 @@ */ export const authValidateEndpoint = { + '/employees/me': { + get: { + summary: 'Get current employee profile', + description: 'Retrieves details and leave balances for the authenticated user', + security: [ + { ApiKeyAuth: [] }, + { BearerAuth: [] } + ], + responses: { + '200': { + description: 'Successfully retrieved employee profile', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + id: { type: 'number', example: 1 }, + name: { type: 'string', example: 'Purva Rao' }, + email: { type: 'string', example: 'purva.rao@humanizeiq.ai' }, + color: { type: 'string', example: '#3b82f6' }, + leaveBalances: { + type: 'object', + properties: { + 'Annual Leave': { type: 'number', example: 24 }, + 'Maternity Leave': { type: 'number', example: 182 }, + 'Paternity Leave': { type: 'number', example: 0 }, + 'Bereavement Leave': { type: 'number', example: 5 }, + 'Marriage Leave': { type: 'number', example: 5 } + } + } + } + } + } + } + }, + '401': { + description: 'Unauthorized - Invalid or missing authentication token' + }, + '500': { + description: 'Internal server error' + } + } + } + }, '/auth/validate': { get: { summary: 'Validate authentication token (GET)', diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..2a375d0 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,39 @@ +import { Context } from 'hono'; + +// Define the structure of user data from the database +export interface UserData { + id: number; + uid: string; + email: string; + firebase_uid?: string; + firstname?: string; + lastname?: string; + company_name?: string; + created_at: string; + updated_at: string; +} + +// Define the structure of the environment variables +export interface Env { + CF_TEMPLATE_DB?: D1Database; // D1Database type from @cloudflare/workers-types + DECRYPT_API_URL?: string; + DECRYPT_API_KEY?: string; +} + +// Extend the Hono context with our custom types +export interface CustomContext extends Context { + env: Env; + get: (key: string) => string | undefined; + set: (key: string, value: any) => void; +} + +// Response type for the user profile endpoint +export interface UserProfileResponse { + id: number; + name: string; + email: string; + color: string; + leaveBalances: { + [key: string]: number; + }; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b9b7759 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "outDir": "dist", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/wrangler.toml b/wrangler.toml index c200384..8bcc895 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -5,25 +5,25 @@ compatibility_date = "2025-03-10" compatibility_flags = ["nodejs_compat"] [vars] -name = 'cf-template-local' +name = 'leave-agent-local' CM_BASE_URL = "https://cm.dev.svchub.com" DECRYPT_API_URL = "https://www.dev.humanizeiq.ai/api/dashboard-backend" DECRYPT_API_KEY = "XMCKhjOTTPJJzoSiNTmRTcQauvtGKRnUZNFlhWpjhTEaIdIeIdLFM" d1_databases= [ - { binding = "CF_TEMPLATE_DB", database_name = "cf-template_test-db", database_id = "c020574a-5623-407b-be0c-cd192bab9545" } + { binding = "LEAVE_AGENT_DB", database_name = "leave-agent_test-db", database_id = "c020574a-5623-407b-be0c-cd192bab9545" } ] # Playtest Environment [env.playtest] -name = "cf-template-playtest" +name = "leave-agent-playtest" routes = [ - { pattern = "cf-template.playtest.svchub.com", custom_domain = true } + { pattern = "leave-agent.playtest.svchub.com", custom_domain = true } ] d1_databases = [ - { binding = "CF_TEMPLATE_DB", database_name = "cf-template_playtest", database_id = "fd847b4b-e1e9-442e-9756-23c4540476e7" } + { binding = "LEAVE_AGENT_DB", database_name = "leave-agent_playtest", database_id = "fd847b4b-e1e9-442e-9756-23c4540476e7" } ] r2_buckets = [ - { binding = "CF_TEMPLATE_BUCKET", bucket_name = "cf-template-playtest" } + { binding = "LEAVE_AGENT_BUCKET", bucket_name = "leave-agent-playtest" } ] [env.playtest.vars] WORKER_ENV = "playtest" @@ -32,15 +32,15 @@ DECRYPT_API_URL = "https://www.dev.humanizeiq.ai/api/dashboard-backend" # Dev Environment [env.development] -name = "cf-template-dev" +name = "leave-agent-dev" routes = [ - { pattern = "cf-template.dev.svchub.com", custom_domain = true } + { pattern = "leave-agent.dev.svchub.com", custom_domain = true } ] d1_databases = [ - { binding = "CF_TEMPLATE_DB", database_name = "cf-template_dev", database_id = "08a710a9-27ae-4886-9cab-c0a7ab204de9" } + { binding = "LEAVE_AGENT_DB", database_name = "leave-agent_dev", database_id = "08a710a9-27ae-4886-9cab-c0a7ab204de9" } ] r2_buckets = [ - { binding = "CF_TEMPLATE_BUCKET", bucket_name = "cf-template-dev" } + { binding = "LEAVE_AGENT_BUCKET", bucket_name = "leave-agent-dev" } ] [env.development.vars] WORKER_ENV = "development" @@ -49,15 +49,15 @@ DECRYPT_API_URL = "https://www.dev.humanizeiq.ai/api/dashboard-backend" # Production Environment [env.production] -name = "cf-template-prod" +name = " leave-agent-prod" routes = [ - { pattern = "cf-template.prod.svchub.com", custom_domain = true } + { pattern = "leave-agent.prod.svchub.com", custom_domain = true } ] d1_databases = [ - { binding = "CF_TEMPLATE_DB", database_name = "cf-template_prod", database_id = "2983ec1a-3c9c-451d-b85e-5a52ff9b114a" } + { binding = "LEAVE_AGENT_DB", database_name = "leave-agent_prod", database_id = "2983ec1a-3c9c-451d-b85e-5a52ff9b114a" } ] r2_buckets = [ - { binding = "CF_TEMPLATE_BUCKET", bucket_name = "cf-template-prod" } + { binding = "LEAVE_AGENT_BUCKET", bucket_name = "leave-agent-prod" } ] [env.production.vars] WORKER_ENV = "production"