api #1

Merged
prao merged 1 commits from feature/leave-agentapp into playtest 2025-11-27 14:31:52 +00:00
7 changed files with 420 additions and 33 deletions

219
docs/leave-agent-design.md Normal file
View File

@@ -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);
```

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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)',

39
src/types/index.ts Normal file
View File

@@ -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;
};
}

21
tsconfig.json Normal file
View File

@@ -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"]
}

View File

@@ -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"