(Feat): Initial Commit

This commit is contained in:
2025-11-27 22:50:08 +00:00
commit 00f9ed128b
79 changed files with 17413 additions and 0 deletions

24
backend-deno/.env Normal file
View File

@@ -0,0 +1,24 @@
# Database Configuration
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=admin123
DB_NAME=work_allocation
DB_PORT=3306
# JWT Configuration - CHANGE IN PRODUCTION!
JWT_SECRET=work_alloc_jwt_secret_key_change_in_production_2024
JWT_EXPIRES_IN=7d
# Server Configuration
PORT=3000
# Security Configuration
BCRYPT_ROUNDS=12
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100
# CORS Configuration (comma-separated for multiple origins)
CORS_ORIGIN=http://localhost:5173
# Environment
NODE_ENV=development

210
backend-deno/README.md Normal file
View File

@@ -0,0 +1,210 @@
# Work Allocation Backend - Deno TypeScript
A secure, type-safe backend for the Work Allocation System built with Deno and TypeScript.
## Features
### Security Improvements
- **Strong Password Hashing**: bcrypt with configurable rounds (default: 12)
- **JWT Authentication**: Secure token-based authentication with HMAC-SHA256
- **Rate Limiting**: Configurable request rate limiting to prevent abuse
- **CORS Protection**: Configurable cross-origin resource sharing
- **Security Headers**: X-Frame-Options, X-Content-Type-Options, XSS protection
- **Input Sanitization**: Protection against XSS and injection attacks
- **Strict TypeScript**: Full type safety with strict compiler options
### Technical Stack
- **Runtime**: Deno 2.x
- **Framework**: Oak (Deno's Express-like framework)
- **Database**: MySQL 8.0 with mysql2 driver
- **Authentication**: JWT with djwt library
- **Password Hashing**: bcrypt
## Prerequisites
- [Deno](https://deno.land/) 2.0 or higher
- MySQL 8.0 (via Docker or local installation)
## Installation
1. **Install Deno** (if not already installed):
```bash
curl -fsSL https://deno.land/install.sh | sh
```
2. **Configure environment**:
```bash
cp .env.example .env
# Edit .env with your database credentials
```
3. **Start the database** (if using Docker):
```bash
cd .. && docker-compose up -d
```
## Running the Server
### Development Mode (with auto-reload)
```bash
deno task dev
```
### Production Mode
```bash
deno task start
```
### Seed the Database
```bash
deno task seed
```
## API Endpoints
### Authentication
- `POST /api/auth/login` - User login
- `GET /api/auth/me` - Get current user
- `POST /api/auth/change-password` - Change password
### Users
- `GET /api/users` - List users (filtered by role)
- `GET /api/users/:id` - Get user by ID
- `POST /api/users` - Create user (Admin/Supervisor)
- `PUT /api/users/:id` - Update user (Admin/Supervisor)
- `DELETE /api/users/:id` - Delete user (Admin/Supervisor)
### Departments
- `GET /api/departments` - List departments
- `GET /api/departments/:id` - Get department
- `GET /api/departments/:id/sub-departments` - Get sub-departments
- `POST /api/departments` - Create department (SuperAdmin)
- `POST /api/departments/:id/sub-departments` - Create sub-department (SuperAdmin)
### Work Allocations
- `GET /api/work-allocations` - List allocations (role-filtered)
- `GET /api/work-allocations/:id` - Get allocation
- `POST /api/work-allocations` - Create allocation (Supervisor/Admin)
- `PUT /api/work-allocations/:id/status` - Update status (Supervisor/Admin)
- `DELETE /api/work-allocations/:id` - Delete allocation (Supervisor/Admin)
### Attendance
- `GET /api/attendance` - List attendance records
- `GET /api/attendance/:id` - Get attendance record
- `POST /api/attendance/check-in` - Check in employee (Supervisor/Admin)
- `POST /api/attendance/check-out` - Check out employee (Supervisor/Admin)
- `GET /api/attendance/summary/stats` - Get attendance summary
### Contractor Rates
- `GET /api/contractor-rates` - List rates
- `GET /api/contractor-rates/contractor/:id/current` - Get current rate
- `POST /api/contractor-rates` - Set rate (Supervisor/Admin)
- `PUT /api/contractor-rates/:id` - Update rate (Supervisor/Admin)
- `DELETE /api/contractor-rates/:id` - Delete rate (Supervisor/Admin)
### Health Check
- `GET /health` - Server health status
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `PORT` | Server port | 3000 |
| `DB_HOST` | Database host | localhost |
| `DB_USER` | Database user | root |
| `DB_PASSWORD` | Database password | admin123 |
| `DB_NAME` | Database name | work_allocation |
| `DB_PORT` | Database port | 3306 |
| `JWT_SECRET` | JWT signing secret | (change in production!) |
| `JWT_EXPIRES_IN` | Token expiration | 7d |
| `BCRYPT_ROUNDS` | Password hash rounds | 12 |
| `RATE_LIMIT_WINDOW_MS` | Rate limit window | 900000 (15 min) |
| `RATE_LIMIT_MAX_REQUESTS` | Max requests per window | 100 |
| `CORS_ORIGIN` | Allowed CORS origins | <http://localhost:5173> |
| `NODE_ENV` | Environment | development |
## Security Best Practices
### For Production
1. **Change JWT Secret**: Use a strong, random secret
```bash
JWT_SECRET=$(openssl rand -base64 64)
```
2. **Enable HTTPS**: Use a reverse proxy (nginx) with SSL
3. **Set Production Environment**:
```bash
NODE_ENV=production
```
4. **Increase bcrypt rounds** (if performance allows):
```bash
BCRYPT_ROUNDS=14
```
5. **Configure CORS** for your domain:
```bash
CORS_ORIGIN=https://yourdomain.com
```
## Project Structure
```bash
backend-deno/
├── config/
│ ├── database.ts # Database connection pool
│ └── env.ts # Environment configuration
├── middleware/
│ ├── auth.ts # JWT authentication & authorization
│ └── security.ts # Security middleware (CORS, rate limit, etc.)
├── routes/
│ ├── auth.ts # Authentication routes
│ ├── users.ts # User management routes
│ ├── departments.ts # Department routes
│ ├── work-allocations.ts
│ ├── attendance.ts
│ └── contractor-rates.ts
├── scripts/
│ └── seed.ts # Database seeding script
├── types/
│ └── index.ts # TypeScript type definitions
├── main.ts # Application entry point
├── deno.json # Deno configuration
└── .env # Environment variables
```
## Differences from Node.js Backend
| Feature | Node.js | Deno |
|---------|---------|------|
| Runtime | Node.js | Deno |
| Package Manager | npm | Built-in (JSR/npm) |
| TypeScript | Requires compilation | Native support |
| Security | Manual setup | Secure by default |
| Permissions | Full access | Explicit permissions |
| Framework | Express | Oak |
## License
MIT

View File

@@ -0,0 +1,79 @@
import { createPool, Pool } from "mysql2/promise";
import { load } from "@std/dotenv";
// Load environment variables
await load({ export: true });
const config = {
host: Deno.env.get("DB_HOST") || "localhost",
user: Deno.env.get("DB_USER") || "root",
password: Deno.env.get("DB_PASSWORD") || "admin123",
database: Deno.env.get("DB_NAME") || "work_allocation",
port: parseInt(Deno.env.get("DB_PORT") || "3306"),
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
enableKeepAlive: true,
keepAliveInitialDelay: 0,
};
class Database {
private pool: Pool | null = null;
private static instance: Database;
private constructor() {}
static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
async connect(): Promise<Pool> {
if (!this.pool) {
this.pool = createPool(config);
// Test connection
try {
const connection = await this.pool.getConnection();
console.log("✅ Database connected successfully");
connection.release();
} catch (error) {
console.error("❌ Database connection failed:", (error as Error).message);
throw error;
}
}
return this.pool;
}
async getPool(): Promise<Pool> {
if (!this.pool) {
return await this.connect();
}
return this.pool;
}
async query<T>(sql: string, params?: unknown[]): Promise<T> {
const pool = await this.getPool();
const [rows] = await pool.query(sql, params);
return rows as T;
}
async execute(sql: string, params?: unknown[]): Promise<{ insertId: number; affectedRows: number }> {
const pool = await this.getPool();
const [result] = await pool.execute(sql, params);
return result as { insertId: number; affectedRows: number };
}
async close(): Promise<void> {
if (this.pool) {
await this.pool.end();
this.pool = null;
console.log("Database connection closed");
}
}
}
export const db = Database.getInstance();
export default db;

View File

@@ -0,0 +1,40 @@
import { load } from "@std/dotenv";
await load({ export: true });
export const config = {
// Server
PORT: parseInt(Deno.env.get("PORT") || "3000"),
// Database
DB_HOST: Deno.env.get("DB_HOST") || "localhost",
DB_USER: Deno.env.get("DB_USER") || "root",
DB_PASSWORD: Deno.env.get("DB_PASSWORD") || "admin123",
DB_NAME: Deno.env.get("DB_NAME") || "work_allocation",
DB_PORT: parseInt(Deno.env.get("DB_PORT") || "3306"),
// JWT - Security: Use strong secret in production
JWT_SECRET: Deno.env.get("JWT_SECRET") || "work_alloc_jwt_secret_key_change_in_production_2024",
JWT_EXPIRES_IN: Deno.env.get("JWT_EXPIRES_IN") || "7d",
// Security settings
BCRYPT_ROUNDS: parseInt(Deno.env.get("BCRYPT_ROUNDS") || "12"),
RATE_LIMIT_WINDOW_MS: parseInt(Deno.env.get("RATE_LIMIT_WINDOW_MS") || "900000"), // 15 minutes
RATE_LIMIT_MAX_REQUESTS: parseInt(Deno.env.get("RATE_LIMIT_MAX_REQUESTS") || "100"),
// CORS
CORS_ORIGIN: Deno.env.get("CORS_ORIGIN") || "http://localhost:5173",
// Environment
NODE_ENV: Deno.env.get("NODE_ENV") || "development",
isDevelopment(): boolean {
return this.NODE_ENV === "development";
},
isProduction(): boolean {
return this.NODE_ENV === "production";
}
};
export default config;

19
backend-deno/deno.json Normal file
View File

@@ -0,0 +1,19 @@
{
"tasks": {
"dev": "deno run --watch --allow-net --allow-env --allow-read main.ts",
"start": "deno run --allow-net --allow-env --allow-read main.ts",
"seed": "deno run --allow-net --allow-env --allow-read scripts/seed.ts"
},
"imports": {
"@oak/oak": "jsr:@oak/oak@^17.1.4",
"@std/dotenv": "jsr:@std/dotenv@^0.225.3",
"mysql2": "npm:mysql2@^3.11.0",
"bcrypt": "https://deno.land/x/bcrypt@v0.4.1/mod.ts",
"djwt": "https://deno.land/x/djwt@v3.0.2/mod.ts"
},
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true
}
}

163
backend-deno/deno.lock generated Normal file
View File

@@ -0,0 +1,163 @@
{
"version": "5",
"specifiers": {
"jsr:@oak/commons@1": "1.0.1",
"jsr:@oak/oak@^17.1.4": "17.2.0",
"jsr:@std/assert@1": "1.0.16",
"jsr:@std/bytes@1": "1.0.6",
"jsr:@std/crypto@1": "1.0.5",
"jsr:@std/dotenv@~0.225.3": "0.225.5",
"jsr:@std/encoding@1": "1.0.10",
"jsr:@std/encoding@^1.0.10": "1.0.10",
"jsr:@std/http@1": "1.0.22",
"jsr:@std/internal@^1.0.12": "1.0.12",
"jsr:@std/media-types@1": "1.1.0",
"jsr:@std/path@1": "1.1.3",
"npm:mysql2@^3.11.0": "3.15.3",
"npm:path-to-regexp@^6.3.0": "6.3.0"
},
"jsr": {
"@oak/commons@1.0.1": {
"integrity": "889ff210f0b4292591721be07244ecb1b5c118742f5273c70cf30d7cd4184d0c",
"dependencies": [
"jsr:@std/assert",
"jsr:@std/bytes",
"jsr:@std/crypto",
"jsr:@std/encoding@1",
"jsr:@std/http",
"jsr:@std/media-types"
]
},
"@oak/oak@17.2.0": {
"integrity": "938537a92fc7922a46a9984696c65fb189c9baad164416ac3e336768a9ff0cd1",
"dependencies": [
"jsr:@oak/commons",
"jsr:@std/assert",
"jsr:@std/bytes",
"jsr:@std/http",
"jsr:@std/media-types",
"jsr:@std/path",
"npm:path-to-regexp"
]
},
"@std/assert@1.0.16": {
"integrity": "6a7272ed1eaa77defe76e5ff63ca705d9c495077e2d5fd0126d2b53fc5bd6532"
},
"@std/bytes@1.0.6": {
"integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a"
},
"@std/crypto@1.0.5": {
"integrity": "0dcfbb319fe0bba1bd3af904ceb4f948cde1b92979ec1614528380ed308a3b40"
},
"@std/dotenv@0.225.5": {
"integrity": "9ce6f9d0ec3311f74a32535aa1b8c62ed88b1ab91b7f0815797d77a6f60c922f"
},
"@std/encoding@1.0.10": {
"integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1"
},
"@std/http@1.0.22": {
"integrity": "53f0bb70e23a2eec3e17c4240a85bb23d185b2e20635adb37ce0f03cc4ca012a",
"dependencies": [
"jsr:@std/encoding@^1.0.10"
]
},
"@std/internal@1.0.12": {
"integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
},
"@std/media-types@1.1.0": {
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
},
"@std/path@1.1.3": {
"integrity": "b015962d82a5e6daea980c32b82d2c40142149639968549c649031a230b1afb3",
"dependencies": [
"jsr:@std/internal"
]
}
},
"npm": {
"aws-ssl-profiles@1.1.2": {
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="
},
"denque@2.1.0": {
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="
},
"generate-function@2.3.1": {
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
"dependencies": [
"is-property"
]
},
"iconv-lite@0.7.0": {
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"dependencies": [
"safer-buffer"
]
},
"is-property@1.0.2": {
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="
},
"long@5.3.2": {
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="
},
"lru-cache@7.18.3": {
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="
},
"lru.min@1.1.3": {
"integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q=="
},
"mysql2@3.15.3": {
"integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==",
"dependencies": [
"aws-ssl-profiles",
"denque",
"generate-function",
"iconv-lite",
"long",
"lru.min",
"named-placeholders",
"seq-queue",
"sqlstring"
]
},
"named-placeholders@1.1.3": {
"integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==",
"dependencies": [
"lru-cache"
]
},
"path-to-regexp@6.3.0": {
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="
},
"safer-buffer@2.1.2": {
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"seq-queue@0.0.5": {
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
},
"sqlstring@2.3.3": {
"integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="
}
},
"remote": {
"https://deno.land/std@0.221.0/encoding/_util.ts": "beacef316c1255da9bc8e95afb1fa56ed69baef919c88dc06ae6cb7a6103d376",
"https://deno.land/std@0.221.0/encoding/base64.ts": "8ccae67a1227b875340a8582ff707f37b131df435b07080d3bb58e07f5f97807",
"https://deno.land/std@0.221.0/encoding/base64url.ts": "9cc46cf510436be63ac00ebf97a7de1993e603ca58e1853b344bf90d80ea9945",
"https://deno.land/x/bcrypt@v0.4.1/mod.ts": "ff09bdae282583cf5f7d87efe37ddcecef7f14f6d12e8b8066a3058db8c6c2f7",
"https://deno.land/x/bcrypt@v0.4.1/src/bcrypt/base64.ts": "b8266450a4f1eb6960f60f2f7986afc4dde6b45bd2d7ee7ba10789e67e17b9f7",
"https://deno.land/x/bcrypt@v0.4.1/src/bcrypt/bcrypt.ts": "ec221648cc6453ea5e3803bc817c01157dada06aa6f7a0ba6b9f87aae32b21e2",
"https://deno.land/x/bcrypt@v0.4.1/src/main.ts": "08d201b289c8d9c46f8839c69cd6625b213863db29775c7a200afc3b540e64f8",
"https://deno.land/x/bcrypt@v0.4.1/src/worker.ts": "5a73bdfee9c9e622f47c9733d374b627dce52fb3ec1e74c8226698b3fc57ffac",
"https://deno.land/x/djwt@v3.0.2/algorithm.ts": "b1c6645f9dbd6e6c47c123a3b18c28b956f91c65ed17f5b6d5d968fc3750542b",
"https://deno.land/x/djwt@v3.0.2/deps.ts": "a7954fe567f2097b4f6aca11d091b6df658e485a817ac4dee47257ed5c28fd6e",
"https://deno.land/x/djwt@v3.0.2/mod.ts": "962d8f2c4d6a4db111f45d777b152356aec31ba7db0ca664601175a422629857",
"https://deno.land/x/djwt@v3.0.2/signature.ts": "16238fbf558267c85dd6c0178045f006c8b914a7301db87149f3318326569272",
"https://deno.land/x/djwt@v3.0.2/util.ts": "5cb264d2125c553678e11446bcfa0494025d120e3f59d0a3ab38f6800def697d"
},
"workspace": {
"dependencies": [
"jsr:@oak/oak@^17.1.4",
"jsr:@std/dotenv@~0.225.3",
"npm:mysql2@^3.11.0"
]
}
}

107
backend-deno/main.ts Normal file
View File

@@ -0,0 +1,107 @@
import { Application, Router } from "@oak/oak";
import { config } from "./config/env.ts";
import { db } from "./config/database.ts";
import { cors, securityHeaders, requestLogger, rateLimit } from "./middleware/security.ts";
// Import routes
import authRoutes from "./routes/auth.ts";
import userRoutes from "./routes/users.ts";
import departmentRoutes from "./routes/departments.ts";
import workAllocationRoutes from "./routes/work-allocations.ts";
import attendanceRoutes from "./routes/attendance.ts";
import contractorRateRoutes from "./routes/contractor-rates.ts";
// Initialize database connection
await db.connect();
// Create Oak application
const app = new Application();
// Global error handler
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
const error = err as Error & { status?: number };
console.error("Error:", error.message);
ctx.response.status = error.status || 500;
ctx.response.body = {
error: config.isDevelopment() ? error.message : "Internal server error",
};
}
});
// Apply security middleware
app.use(cors);
app.use(securityHeaders);
app.use(requestLogger);
// Rate limiting (only in production or if enabled)
if (config.isProduction()) {
app.use(rateLimit);
}
// Create main router
const router = new Router();
// Health check endpoint
router.get("/health", (ctx) => {
ctx.response.body = {
status: "ok",
timestamp: new Date().toISOString(),
version: "2.0.0-deno",
runtime: "Deno",
};
});
// Mount API routes
router.use("/api/auth", authRoutes.routes(), authRoutes.allowedMethods());
router.use("/api/users", userRoutes.routes(), userRoutes.allowedMethods());
router.use("/api/departments", departmentRoutes.routes(), departmentRoutes.allowedMethods());
router.use("/api/work-allocations", workAllocationRoutes.routes(), workAllocationRoutes.allowedMethods());
router.use("/api/attendance", attendanceRoutes.routes(), attendanceRoutes.allowedMethods());
router.use("/api/contractor-rates", contractorRateRoutes.routes(), contractorRateRoutes.allowedMethods());
// Apply routes
app.use(router.routes());
app.use(router.allowedMethods());
// 404 handler
app.use((ctx) => {
ctx.response.status = 404;
ctx.response.body = { error: "Route not found" };
});
// Graceful shutdown
const controller = new AbortController();
Deno.addSignalListener("SIGINT", () => {
console.log("\n🛑 Shutting down gracefully...");
controller.abort();
db.close();
Deno.exit(0);
});
Deno.addSignalListener("SIGTERM", () => {
console.log("\n🛑 Shutting down gracefully...");
controller.abort();
db.close();
Deno.exit(0);
});
// Start server
console.log(`
╔════════════════════════════════════════════════════════════╗
║ Work Allocation System - Deno Backend v2.0.0 ║
╠════════════════════════════════════════════════════════════╣
║ 🦕 Runtime: Deno ${Deno.version.deno}
║ 🔒 TypeScript with strict type checking ║
║ 🛡️ Security: Rate limiting, CORS, XSS protection ║
╚════════════════════════════════════════════════════════════╝
`);
console.log(`🚀 Server running on http://localhost:${config.PORT}`);
console.log(`📊 Health check: http://localhost:${config.PORT}/health`);
console.log(`🔧 Environment: ${config.NODE_ENV}`);
await app.listen({ port: config.PORT, signal: controller.signal });

View File

@@ -0,0 +1,102 @@
import { Context, Next } from "@oak/oak";
import { verify, create, getNumericDate } from "djwt";
import { config } from "../config/env.ts";
import type { JWTPayload, UserRole } from "../types/index.ts";
// Create crypto key from secret
const encoder = new TextEncoder();
const keyData = encoder.encode(config.JWT_SECRET);
const cryptoKey = await crypto.subtle.importKey(
"raw",
keyData,
{ name: "HMAC", hash: "SHA-256" },
false,
["sign", "verify"]
);
// Generate JWT token
export async function generateToken(payload: Omit<JWTPayload, "exp" | "iat">): Promise<string> {
const expiresIn = config.JWT_EXPIRES_IN;
let expSeconds = 7 * 24 * 60 * 60; // Default 7 days
if (expiresIn.endsWith("d")) {
expSeconds = parseInt(expiresIn) * 24 * 60 * 60;
} else if (expiresIn.endsWith("h")) {
expSeconds = parseInt(expiresIn) * 60 * 60;
} else if (expiresIn.endsWith("m")) {
expSeconds = parseInt(expiresIn) * 60;
}
const token = await create(
{ alg: "HS256", typ: "JWT" },
{
...payload,
exp: getNumericDate(expSeconds),
iat: getNumericDate(0),
},
cryptoKey
);
return token;
}
// Verify JWT token
export async function verifyToken(token: string): Promise<JWTPayload | null> {
try {
const payload = await verify(token, cryptoKey);
return payload as unknown as JWTPayload;
} catch {
return null;
}
}
// Authentication middleware
export async function authenticateToken(ctx: Context, next: Next): Promise<void> {
const authHeader = ctx.request.headers.get("Authorization");
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
if (!token) {
ctx.response.status = 401;
ctx.response.body = { error: "Access token required" };
return;
}
const payload = await verifyToken(token);
if (!payload) {
ctx.response.status = 403;
ctx.response.body = { error: "Invalid or expired token" };
return;
}
// Attach user to context state
ctx.state.user = payload;
await next();
}
// Authorization middleware factory
export function authorize(...roles: UserRole[]) {
return async (ctx: Context, next: Next): Promise<void> => {
const user = ctx.state.user as JWTPayload | undefined;
if (!user) {
ctx.response.status = 401;
ctx.response.body = { error: "Unauthorized" };
return;
}
if (!roles.includes(user.role)) {
ctx.response.status = 403;
ctx.response.body = { error: "Insufficient permissions" };
return;
}
await next();
};
}
// Get current user from context
export function getCurrentUser(ctx: Context): JWTPayload {
return ctx.state.user as JWTPayload;
}

View File

@@ -0,0 +1,152 @@
import { Context, Next } from "@oak/oak";
import { config } from "../config/env.ts";
// Rate limiting store (in-memory, use Redis in production)
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
// Rate limiting middleware
export async function rateLimit(ctx: Context, next: Next): Promise<void> {
const ip = ctx.request.ip || "unknown";
const now = Date.now();
const windowMs = config.RATE_LIMIT_WINDOW_MS;
const maxRequests = config.RATE_LIMIT_MAX_REQUESTS;
const record = rateLimitStore.get(ip);
if (!record || now > record.resetTime) {
rateLimitStore.set(ip, { count: 1, resetTime: now + windowMs });
} else {
record.count++;
if (record.count > maxRequests) {
ctx.response.status = 429;
ctx.response.body = {
error: "Too many requests",
retryAfter: Math.ceil((record.resetTime - now) / 1000)
};
return;
}
}
await next();
}
// Security headers middleware
export async function securityHeaders(ctx: Context, next: Next): Promise<void> {
await next();
// Prevent clickjacking
ctx.response.headers.set("X-Frame-Options", "DENY");
// Prevent MIME type sniffing
ctx.response.headers.set("X-Content-Type-Options", "nosniff");
// XSS protection
ctx.response.headers.set("X-XSS-Protection", "1; mode=block");
// Referrer policy
ctx.response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
// Content Security Policy
ctx.response.headers.set(
"Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';"
);
// Strict Transport Security (only in production with HTTPS)
if (config.isProduction()) {
ctx.response.headers.set(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains"
);
}
}
// CORS middleware
export async function cors(ctx: Context, next: Next): Promise<void> {
const origin = ctx.request.headers.get("Origin");
const allowedOrigins = config.CORS_ORIGIN.split(",").map(o => o.trim());
// Check if origin is allowed
if (origin && (allowedOrigins.includes(origin) || allowedOrigins.includes("*"))) {
ctx.response.headers.set("Access-Control-Allow-Origin", origin);
} else if (config.isDevelopment()) {
// Allow all origins in development
ctx.response.headers.set("Access-Control-Allow-Origin", origin || "*");
}
ctx.response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
ctx.response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
ctx.response.headers.set("Access-Control-Allow-Credentials", "true");
ctx.response.headers.set("Access-Control-Max-Age", "86400");
// Handle preflight requests
if (ctx.request.method === "OPTIONS") {
ctx.response.status = 204;
return;
}
await next();
}
// Request logging middleware
export async function requestLogger(ctx: Context, next: Next): Promise<void> {
const start = Date.now();
const { method, url } = ctx.request;
await next();
const ms = Date.now() - start;
const status = ctx.response.status;
// Color code based on status
let statusColor = "\x1b[32m"; // Green for 2xx
if (status >= 400 && status < 500) statusColor = "\x1b[33m"; // Yellow for 4xx
if (status >= 500) statusColor = "\x1b[31m"; // Red for 5xx
console.log(
`${new Date().toISOString()} - ${method} ${url.pathname} ${statusColor}${status}\x1b[0m ${ms}ms`
);
}
// Input sanitization helper
export function sanitizeInput(input: string): string {
return input
.replace(/[<>]/g, "") // Remove angle brackets
.replace(/javascript:/gi, "") // Remove javascript: protocol
.replace(/on\w+=/gi, "") // Remove event handlers
.trim();
}
// Validate email format
export function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// Validate password strength
export function isStrongPassword(password: string): { valid: boolean; message?: string } {
if (password.length < 8) {
return { valid: false, message: "Password must be at least 8 characters long" };
}
if (!/[A-Z]/.test(password)) {
return { valid: false, message: "Password must contain at least one uppercase letter" };
}
if (!/[a-z]/.test(password)) {
return { valid: false, message: "Password must contain at least one lowercase letter" };
}
if (!/[0-9]/.test(password)) {
return { valid: false, message: "Password must contain at least one number" };
}
return { valid: true };
}
// Clean up old rate limit entries periodically
setInterval(() => {
const now = Date.now();
for (const [ip, record] of rateLimitStore.entries()) {
if (now > record.resetTime) {
rateLimitStore.delete(ip);
}
}
}, 60000); // Clean up every minute

View File

@@ -0,0 +1,293 @@
import { Router } from "@oak/oak";
import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
import type { Attendance, CheckInOutRequest, User } from "../types/index.ts";
const router = new Router();
// Get all attendance records
router.get("/", authenticateToken, async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const params = ctx.request.url.searchParams;
const employeeId = params.get("employeeId");
const startDate = params.get("startDate");
const endDate = params.get("endDate");
const status = params.get("status");
let query = `
SELECT a.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
d.name as department_name,
c.name as contractor_name
FROM attendance a
JOIN users e ON a.employee_id = e.id
JOIN users s ON a.supervisor_id = s.id
LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id
WHERE 1=1
`;
const queryParams: unknown[] = [];
// Role-based filtering
if (currentUser.role === "Supervisor") {
query += " AND a.supervisor_id = ?";
queryParams.push(currentUser.id);
} else if (currentUser.role === "Employee") {
query += " AND a.employee_id = ?";
queryParams.push(currentUser.id);
}
if (employeeId) {
query += " AND a.employee_id = ?";
queryParams.push(employeeId);
}
if (startDate) {
query += " AND a.work_date >= ?";
queryParams.push(startDate);
}
if (endDate) {
query += " AND a.work_date <= ?";
queryParams.push(endDate);
}
if (status) {
query += " AND a.status = ?";
queryParams.push(status);
}
query += " ORDER BY a.work_date DESC, a.check_in_time DESC";
const records = await db.query<Attendance[]>(query, queryParams);
ctx.response.body = records;
} catch (error) {
console.error("Get attendance error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Get attendance by ID
router.get("/:id", authenticateToken, async (ctx) => {
try {
const attendanceId = ctx.params.id;
const records = await db.query<Attendance[]>(
`SELECT a.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
d.name as department_name,
c.name as contractor_name
FROM attendance a
JOIN users e ON a.employee_id = e.id
JOIN users s ON a.supervisor_id = s.id
LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`,
[attendanceId]
);
if (records.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Attendance record not found" };
return;
}
ctx.response.body = records[0];
} catch (error) {
console.error("Get attendance error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Check in employee (Supervisor or SuperAdmin)
router.post("/check-in", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CheckInOutRequest;
const { employeeId, workDate } = body;
if (!employeeId || !workDate) {
ctx.response.status = 400;
ctx.response.body = { error: "Employee ID and work date required" };
return;
}
// Verify employee exists
let employeeQuery = "SELECT * FROM users WHERE id = ? AND role = ?";
const employeeParams: unknown[] = [employeeId, "Employee"];
if (currentUser.role === "Supervisor") {
employeeQuery += " AND department_id = ?";
employeeParams.push(currentUser.departmentId);
}
const employees = await db.query<User[]>(employeeQuery, employeeParams);
if (employees.length === 0) {
ctx.response.status = 403;
ctx.response.body = { error: "Employee not found or not in your department" };
return;
}
// Check if already checked in today
const existing = await db.query<Attendance[]>(
"SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?",
[employeeId, workDate, "CheckedIn"]
);
if (existing.length > 0) {
ctx.response.status = 400;
ctx.response.body = { error: "Employee already checked in today" };
return;
}
const checkInTime = new Date().toISOString().slice(0, 19).replace("T", " ");
const result = await db.execute(
"INSERT INTO attendance (employee_id, supervisor_id, check_in_time, work_date, status) VALUES (?, ?, ?, ?, ?)",
[employeeId, currentUser.id, checkInTime, workDate, "CheckedIn"]
);
const newRecord = await db.query<Attendance[]>(
`SELECT a.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
d.name as department_name,
c.name as contractor_name
FROM attendance a
JOIN users e ON a.employee_id = e.id
JOIN users s ON a.supervisor_id = s.id
LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`,
[result.insertId]
);
ctx.response.status = 201;
ctx.response.body = newRecord[0];
} catch (error) {
console.error("Check in error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Check out employee (Supervisor or SuperAdmin)
router.post("/check-out", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CheckInOutRequest;
const { employeeId, workDate } = body;
if (!employeeId || !workDate) {
ctx.response.status = 400;
ctx.response.body = { error: "Employee ID and work date required" };
return;
}
// Find the check-in record
let query = "SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?";
const params: unknown[] = [employeeId, workDate, "CheckedIn"];
if (currentUser.role === "Supervisor") {
query += " AND supervisor_id = ?";
params.push(currentUser.id);
}
const records = await db.query<Attendance[]>(query, params);
if (records.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "No check-in record found for today" };
return;
}
const checkOutTime = new Date().toISOString().slice(0, 19).replace("T", " ");
await db.execute(
"UPDATE attendance SET check_out_time = ?, status = ? WHERE id = ?",
[checkOutTime, "CheckedOut", records[0].id]
);
const updatedRecord = await db.query<Attendance[]>(
`SELECT a.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
d.name as department_name,
c.name as contractor_name
FROM attendance a
JOIN users e ON a.employee_id = e.id
JOIN users s ON a.supervisor_id = s.id
LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`,
[records[0].id]
);
ctx.response.body = updatedRecord[0];
} catch (error) {
console.error("Check out error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Get attendance summary
router.get("/summary/stats", authenticateToken, async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const params = ctx.request.url.searchParams;
const startDate = params.get("startDate");
const endDate = params.get("endDate");
const departmentId = params.get("departmentId");
let query = `
SELECT
COUNT(DISTINCT a.employee_id) as total_employees,
COUNT(DISTINCT CASE WHEN a.status = 'CheckedIn' THEN a.employee_id END) as checked_in,
COUNT(DISTINCT CASE WHEN a.status = 'CheckedOut' THEN a.employee_id END) as checked_out,
d.name as department_name
FROM attendance a
JOIN users e ON a.employee_id = e.id
LEFT JOIN departments d ON e.department_id = d.id
WHERE 1=1
`;
const queryParams: unknown[] = [];
if (currentUser.role === "Supervisor") {
query += " AND a.supervisor_id = ?";
queryParams.push(currentUser.id);
}
if (startDate) {
query += " AND a.work_date >= ?";
queryParams.push(startDate);
}
if (endDate) {
query += " AND a.work_date <= ?";
queryParams.push(endDate);
}
if (departmentId) {
query += " AND e.department_id = ?";
queryParams.push(departmentId);
}
query += " GROUP BY d.id, d.name";
const summary = await db.query(query, queryParams);
ctx.response.body = summary;
} catch (error) {
console.error("Get attendance summary error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
export default router;

163
backend-deno/routes/auth.ts Normal file
View File

@@ -0,0 +1,163 @@
import { Router } from "@oak/oak";
import { hash, compare } from "bcrypt";
import { db } from "../config/database.ts";
import { config } from "../config/env.ts";
import { authenticateToken, generateToken, getCurrentUser } from "../middleware/auth.ts";
import { sanitizeInput, isValidEmail, isStrongPassword } from "../middleware/security.ts";
import type { User, LoginRequest, ChangePasswordRequest } from "../types/index.ts";
const router = new Router();
// Login
router.post("/login", async (ctx) => {
try {
const body = await ctx.request.body.json() as LoginRequest;
const { username, password } = body;
// Input validation
if (!username || !password) {
ctx.response.status = 400;
ctx.response.body = { error: "Username and password required" };
return;
}
// Sanitize input
const sanitizedUsername = sanitizeInput(username);
// Query user
const users = await db.query<User[]>(
"SELECT * FROM users WHERE username = ? AND is_active = TRUE",
[sanitizedUsername]
);
if (users.length === 0) {
// Use generic message to prevent user enumeration
ctx.response.status = 401;
ctx.response.body = { error: "Invalid credentials" };
return;
}
const user = users[0];
// Verify password
const validPassword = await compare(password, user.password!);
if (!validPassword) {
ctx.response.status = 401;
ctx.response.body = { error: "Invalid credentials" };
return;
}
// Generate JWT token
const token = await generateToken({
id: user.id,
username: user.username,
role: user.role,
departmentId: user.department_id,
});
// Return user data without password
const { password: _, ...userWithoutPassword } = user;
ctx.response.body = {
token,
user: userWithoutPassword,
};
} catch (error) {
console.error("Login error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Get current user
router.get("/me", authenticateToken, async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const users = await db.query<User[]>(
"SELECT id, username, name, email, role, department_id, contractor_id, is_active FROM users WHERE id = ?",
[currentUser.id]
);
if (users.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "User not found" };
return;
}
ctx.response.body = users[0];
} catch (error) {
console.error("Get user error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Change password
router.post("/change-password", authenticateToken, async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as ChangePasswordRequest;
const { currentPassword, newPassword } = body;
// Input validation
if (!currentPassword || !newPassword) {
ctx.response.status = 400;
ctx.response.body = { error: "Current and new password required" };
return;
}
// Validate new password strength (only enforce in production or if explicitly enabled)
if (config.isProduction()) {
const passwordCheck = isStrongPassword(newPassword);
if (!passwordCheck.valid) {
ctx.response.status = 400;
ctx.response.body = { error: passwordCheck.message };
return;
}
} else if (newPassword.length < 6) {
ctx.response.status = 400;
ctx.response.body = { error: "Password must be at least 6 characters" };
return;
}
// Get current password hash
const users = await db.query<User[]>(
"SELECT password FROM users WHERE id = ?",
[currentUser.id]
);
if (users.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "User not found" };
return;
}
// Verify current password
const validPassword = await compare(currentPassword, users[0].password!);
if (!validPassword) {
ctx.response.status = 401;
ctx.response.body = { error: "Current password is incorrect" };
return;
}
// Hash new password with configured rounds
const hashedPassword = await hash(newPassword, config.BCRYPT_ROUNDS);
// Update password
await db.execute(
"UPDATE users SET password = ? WHERE id = ?",
[hashedPassword, currentUser.id]
);
ctx.response.body = { message: "Password changed successfully" };
} catch (error) {
console.error("Change password error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
export default router;

View File

@@ -0,0 +1,241 @@
import { Router } from "@oak/oak";
import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
import { sanitizeInput } from "../middleware/security.ts";
import type { ContractorRate, CreateContractorRateRequest, User } from "../types/index.ts";
const router = new Router();
// Get contractor rates
router.get("/", authenticateToken, async (ctx) => {
try {
const params = ctx.request.url.searchParams;
const contractorId = params.get("contractorId");
const subDepartmentId = params.get("subDepartmentId");
let query = `
SELECT cr.*,
u.name as contractor_name, u.username as contractor_username,
sd.name as sub_department_name,
d.name as department_name
FROM contractor_rates cr
JOIN users u ON cr.contractor_id = u.id
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
LEFT JOIN departments d ON sd.department_id = d.id
WHERE 1=1
`;
const queryParams: unknown[] = [];
if (contractorId) {
query += " AND cr.contractor_id = ?";
queryParams.push(contractorId);
}
if (subDepartmentId) {
query += " AND cr.sub_department_id = ?";
queryParams.push(subDepartmentId);
}
query += " ORDER BY cr.effective_date DESC, cr.created_at DESC";
const rates = await db.query<ContractorRate[]>(query, queryParams);
ctx.response.body = rates;
} catch (error) {
console.error("Get contractor rates error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Get current rate for a contractor + sub-department combination
router.get("/contractor/:contractorId/current", authenticateToken, async (ctx) => {
try {
const contractorId = ctx.params.contractorId;
const params = ctx.request.url.searchParams;
const subDepartmentId = params.get("subDepartmentId");
let query = `
SELECT cr.*,
u.name as contractor_name, u.username as contractor_username,
sd.name as sub_department_name
FROM contractor_rates cr
JOIN users u ON cr.contractor_id = u.id
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
WHERE cr.contractor_id = ?
`;
const queryParams: unknown[] = [contractorId];
if (subDepartmentId) {
query += " AND cr.sub_department_id = ?";
queryParams.push(subDepartmentId);
}
query += " ORDER BY cr.effective_date DESC LIMIT 1";
const rates = await db.query<ContractorRate[]>(query, queryParams);
if (rates.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "No rate found for contractor" };
return;
}
ctx.response.body = rates[0];
} catch (error) {
console.error("Get current rate error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Set contractor rate (Supervisor or SuperAdmin)
router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CreateContractorRateRequest;
const { contractorId, subDepartmentId, activity, rate, effectiveDate } = body;
if (!contractorId || !rate || !effectiveDate) {
ctx.response.status = 400;
ctx.response.body = { error: "Missing required fields (contractorId, rate, effectiveDate)" };
return;
}
// Verify contractor exists
const contractors = await db.query<User[]>(
"SELECT * FROM users WHERE id = ? AND role = ?",
[contractorId, "Contractor"]
);
if (contractors.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Contractor not found" };
return;
}
// Supervisors can only set rates for contractors in their department
if (currentUser.role === "Supervisor" && contractors[0].department_id !== currentUser.departmentId) {
ctx.response.status = 403;
ctx.response.body = { error: "Contractor not in your department" };
return;
}
const sanitizedActivity = activity ? sanitizeInput(activity) : null;
const result = await db.execute(
"INSERT INTO contractor_rates (contractor_id, sub_department_id, activity, rate, effective_date) VALUES (?, ?, ?, ?, ?)",
[contractorId, subDepartmentId || null, sanitizedActivity, rate, effectiveDate]
);
const newRate = await db.query<ContractorRate[]>(
`SELECT cr.*,
u.name as contractor_name, u.username as contractor_username,
sd.name as sub_department_name
FROM contractor_rates cr
JOIN users u ON cr.contractor_id = u.id
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
WHERE cr.id = ?`,
[result.insertId]
);
ctx.response.status = 201;
ctx.response.body = newRate[0];
} catch (error) {
console.error("Set contractor rate error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Update contractor rate
router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const rateId = ctx.params.id;
const body = await ctx.request.body.json() as { rate?: number; activity?: string; effectiveDate?: string };
const { rate, activity, effectiveDate } = body;
const existing = await db.query<ContractorRate[]>(
"SELECT * FROM contractor_rates WHERE id = ?",
[rateId]
);
if (existing.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Rate not found" };
return;
}
const updates: string[] = [];
const params: unknown[] = [];
if (rate !== undefined) {
updates.push("rate = ?");
params.push(rate);
}
if (activity !== undefined) {
updates.push("activity = ?");
params.push(sanitizeInput(activity));
}
if (effectiveDate !== undefined) {
updates.push("effective_date = ?");
params.push(effectiveDate);
}
if (updates.length === 0) {
ctx.response.status = 400;
ctx.response.body = { error: "No fields to update" };
return;
}
params.push(rateId);
await db.execute(
`UPDATE contractor_rates SET ${updates.join(", ")} WHERE id = ?`,
params
);
const updatedRate = await db.query<ContractorRate[]>(
`SELECT cr.*,
u.name as contractor_name, u.username as contractor_username,
sd.name as sub_department_name
FROM contractor_rates cr
JOIN users u ON cr.contractor_id = u.id
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
WHERE cr.id = ?`,
[rateId]
);
ctx.response.body = updatedRate[0];
} catch (error) {
console.error("Update contractor rate error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Delete contractor rate
router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const rateId = ctx.params.id;
const existing = await db.query<ContractorRate[]>(
"SELECT * FROM contractor_rates WHERE id = ?",
[rateId]
);
if (existing.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Rate not found" };
return;
}
await db.execute("DELETE FROM contractor_rates WHERE id = ?", [rateId]);
ctx.response.body = { message: "Rate deleted successfully" };
} catch (error) {
console.error("Delete contractor rate error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
export default router;

View File

@@ -0,0 +1,139 @@
import { Router } from "@oak/oak";
import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
import { sanitizeInput } from "../middleware/security.ts";
import type { Department, SubDepartment } from "../types/index.ts";
const router = new Router();
// Get all departments
router.get("/", authenticateToken, async (ctx) => {
try {
const departments = await db.query<Department[]>(
"SELECT * FROM departments ORDER BY name"
);
ctx.response.body = departments;
} catch (error) {
console.error("Get departments error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Get department by ID
router.get("/:id", authenticateToken, async (ctx) => {
try {
const deptId = ctx.params.id;
const departments = await db.query<Department[]>(
"SELECT * FROM departments WHERE id = ?",
[deptId]
);
if (departments.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Department not found" };
return;
}
ctx.response.body = departments[0];
} catch (error) {
console.error("Get department error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Get sub-departments by department ID
router.get("/:id/sub-departments", authenticateToken, async (ctx) => {
try {
const deptId = ctx.params.id;
const subDepartments = await db.query<SubDepartment[]>(
"SELECT * FROM sub_departments WHERE department_id = ? ORDER BY name",
[deptId]
);
ctx.response.body = subDepartments;
} catch (error) {
console.error("Get sub-departments error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Create department (SuperAdmin only)
router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
try {
const body = await ctx.request.body.json() as { name: string };
const { name } = body;
if (!name) {
ctx.response.status = 400;
ctx.response.body = { error: "Department name required" };
return;
}
const sanitizedName = sanitizeInput(name);
const result = await db.execute(
"INSERT INTO departments (name) VALUES (?)",
[sanitizedName]
);
const newDepartment = await db.query<Department[]>(
"SELECT * FROM departments WHERE id = ?",
[result.insertId]
);
ctx.response.status = 201;
ctx.response.body = newDepartment[0];
} catch (error) {
const err = error as { code?: string };
if (err.code === "ER_DUP_ENTRY") {
ctx.response.status = 400;
ctx.response.body = { error: "Department already exists" };
return;
}
console.error("Create department error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Create sub-department (SuperAdmin only)
router.post("/:id/sub-departments", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
try {
const deptId = ctx.params.id;
const body = await ctx.request.body.json() as { name: string; primaryActivity: string };
const { name, primaryActivity } = body;
if (!name || !primaryActivity) {
ctx.response.status = 400;
ctx.response.body = { error: "Name and primary activity required" };
return;
}
const sanitizedName = sanitizeInput(name);
const sanitizedActivity = sanitizeInput(primaryActivity);
const result = await db.execute(
"INSERT INTO sub_departments (department_id, name, primary_activity) VALUES (?, ?, ?)",
[deptId, sanitizedName, sanitizedActivity]
);
const newSubDepartment = await db.query<SubDepartment[]>(
"SELECT * FROM sub_departments WHERE id = ?",
[result.insertId]
);
ctx.response.status = 201;
ctx.response.body = newSubDepartment[0];
} catch (error) {
console.error("Create sub-department error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
export default router;

View File

@@ -0,0 +1,312 @@
import { Router } from "@oak/oak";
import { hash } from "bcrypt";
import { db } from "../config/database.ts";
import { config } from "../config/env.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
import { sanitizeInput, isValidEmail } from "../middleware/security.ts";
import type { User, CreateUserRequest, UpdateUserRequest } from "../types/index.ts";
const router = new Router();
// Get all users (with filters)
router.get("/", authenticateToken, async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const params = ctx.request.url.searchParams;
const role = params.get("role");
const departmentId = params.get("departmentId");
let query = `
SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
u.contractor_id, u.is_active, u.created_at,
d.name as department_name,
c.name as contractor_name
FROM users u
LEFT JOIN departments d ON u.department_id = d.id
LEFT JOIN users c ON u.contractor_id = c.id
WHERE 1=1
`;
const queryParams: unknown[] = [];
// Supervisors can only see users in their department
if (currentUser.role === "Supervisor") {
query += " AND u.department_id = ?";
queryParams.push(currentUser.departmentId);
}
if (role) {
query += " AND u.role = ?";
queryParams.push(role);
}
if (departmentId) {
query += " AND u.department_id = ?";
queryParams.push(departmentId);
}
query += " ORDER BY u.created_at DESC";
const users = await db.query<User[]>(query, queryParams);
ctx.response.body = users;
} catch (error) {
console.error("Get users error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Get user by ID
router.get("/:id", authenticateToken, async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const userId = ctx.params.id;
const users = await db.query<User[]>(
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
u.contractor_id, u.is_active, u.created_at,
d.name as department_name,
c.name as contractor_name
FROM users u
LEFT JOIN departments d ON u.department_id = d.id
LEFT JOIN users c ON u.contractor_id = c.id
WHERE u.id = ?`,
[userId]
);
if (users.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "User not found" };
return;
}
// Supervisors can only view users in their department
if (currentUser.role === "Supervisor" && users[0].department_id !== currentUser.departmentId) {
ctx.response.status = 403;
ctx.response.body = { error: "Access denied" };
return;
}
ctx.response.body = users[0];
} catch (error) {
console.error("Get user error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Create user
router.post("/", authenticateToken, authorize("SuperAdmin", "Supervisor"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CreateUserRequest;
const { username, name, email, password, role, departmentId, contractorId } = body;
// Input validation
if (!username || !name || !email || !password || !role) {
ctx.response.status = 400;
ctx.response.body = { error: "Missing required fields" };
return;
}
// Sanitize inputs
const sanitizedUsername = sanitizeInput(username);
const sanitizedName = sanitizeInput(name);
const sanitizedEmail = sanitizeInput(email);
// Validate email
if (!isValidEmail(sanitizedEmail)) {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid email format" };
return;
}
// Supervisors can only create users in their department
if (currentUser.role === "Supervisor") {
if (departmentId !== currentUser.departmentId) {
ctx.response.status = 403;
ctx.response.body = { error: "Can only create users in your department" };
return;
}
if (role === "SuperAdmin" || role === "Supervisor") {
ctx.response.status = 403;
ctx.response.body = { error: "Cannot create admin or supervisor users" };
return;
}
}
// Hash password
const hashedPassword = await hash(password, config.BCRYPT_ROUNDS);
const result = await db.execute(
"INSERT INTO users (username, name, email, password, role, department_id, contractor_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
[sanitizedUsername, sanitizedName, sanitizedEmail, hashedPassword, role, departmentId || null, contractorId || null]
);
const newUser = await db.query<User[]>(
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
u.contractor_id, u.is_active, u.created_at,
d.name as department_name,
c.name as contractor_name
FROM users u
LEFT JOIN departments d ON u.department_id = d.id
LEFT JOIN users c ON u.contractor_id = c.id
WHERE u.id = ?`,
[result.insertId]
);
ctx.response.status = 201;
ctx.response.body = newUser[0];
} catch (error) {
const err = error as { code?: string };
if (err.code === "ER_DUP_ENTRY") {
ctx.response.status = 400;
ctx.response.body = { error: "Username or email already exists" };
return;
}
console.error("Create user error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Update user
router.put("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const userId = ctx.params.id;
const body = await ctx.request.body.json() as UpdateUserRequest;
const { name, email, role, departmentId, contractorId, isActive } = body;
// Check if user exists
const existingUsers = await db.query<User[]>(
"SELECT * FROM users WHERE id = ?",
[userId]
);
if (existingUsers.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "User not found" };
return;
}
// Supervisors can only update users in their department
if (currentUser.role === "Supervisor") {
if (existingUsers[0].department_id !== currentUser.departmentId) {
ctx.response.status = 403;
ctx.response.body = { error: "Can only update users in your department" };
return;
}
if (role === "SuperAdmin" || role === "Supervisor") {
ctx.response.status = 403;
ctx.response.body = { error: "Cannot modify admin or supervisor roles" };
return;
}
}
const updates: string[] = [];
const params: unknown[] = [];
if (name !== undefined) {
updates.push("name = ?");
params.push(sanitizeInput(name));
}
if (email !== undefined) {
if (!isValidEmail(email)) {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid email format" };
return;
}
updates.push("email = ?");
params.push(sanitizeInput(email));
}
if (role !== undefined) {
updates.push("role = ?");
params.push(role);
}
if (departmentId !== undefined) {
updates.push("department_id = ?");
params.push(departmentId);
}
if (contractorId !== undefined) {
updates.push("contractor_id = ?");
params.push(contractorId);
}
if (isActive !== undefined) {
updates.push("is_active = ?");
params.push(isActive);
}
if (updates.length === 0) {
ctx.response.status = 400;
ctx.response.body = { error: "No fields to update" };
return;
}
params.push(userId);
await db.execute(
`UPDATE users SET ${updates.join(", ")} WHERE id = ?`,
params
);
const updatedUser = await db.query<User[]>(
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
u.contractor_id, u.is_active, u.created_at,
d.name as department_name,
c.name as contractor_name
FROM users u
LEFT JOIN departments d ON u.department_id = d.id
LEFT JOIN users c ON u.contractor_id = c.id
WHERE u.id = ?`,
[userId]
);
ctx.response.body = updatedUser[0];
} catch (error) {
console.error("Update user error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Delete user
router.delete("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const userId = ctx.params.id;
const users = await db.query<User[]>(
"SELECT * FROM users WHERE id = ?",
[userId]
);
if (users.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "User not found" };
return;
}
// Supervisors can only delete users in their department
if (currentUser.role === "Supervisor") {
if (users[0].department_id !== currentUser.departmentId) {
ctx.response.status = 403;
ctx.response.body = { error: "Can only delete users in your department" };
return;
}
if (users[0].role === "SuperAdmin" || users[0].role === "Supervisor") {
ctx.response.status = 403;
ctx.response.body = { error: "Cannot delete admin or supervisor users" };
return;
}
}
await db.execute("DELETE FROM users WHERE id = ?", [userId]);
ctx.response.body = { message: "User deleted successfully" };
} catch (error) {
console.error("Delete user error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
export default router;

View File

@@ -0,0 +1,278 @@
import { Router } from "@oak/oak";
import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
import { sanitizeInput } from "../middleware/security.ts";
import type { WorkAllocation, CreateWorkAllocationRequest, ContractorRate } from "../types/index.ts";
const router = new Router();
// Get all work allocations
router.get("/", authenticateToken, async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const params = ctx.request.url.searchParams;
const employeeId = params.get("employeeId");
const status = params.get("status");
const departmentId = params.get("departmentId");
let query = `
SELECT wa.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
c.name as contractor_name,
sd.name as sub_department_name,
d.name as department_name
FROM work_allocations wa
JOIN users e ON wa.employee_id = e.id
JOIN users s ON wa.supervisor_id = s.id
JOIN users c ON wa.contractor_id = c.id
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
LEFT JOIN departments d ON e.department_id = d.id
WHERE 1=1
`;
const queryParams: unknown[] = [];
// Role-based filtering
if (currentUser.role === "Supervisor") {
query += " AND wa.supervisor_id = ?";
queryParams.push(currentUser.id);
} else if (currentUser.role === "Employee") {
query += " AND wa.employee_id = ?";
queryParams.push(currentUser.id);
} else if (currentUser.role === "Contractor") {
query += " AND wa.contractor_id = ?";
queryParams.push(currentUser.id);
}
if (employeeId) {
query += " AND wa.employee_id = ?";
queryParams.push(employeeId);
}
if (status) {
query += " AND wa.status = ?";
queryParams.push(status);
}
if (departmentId) {
query += " AND e.department_id = ?";
queryParams.push(departmentId);
}
query += " ORDER BY wa.assigned_date DESC, wa.created_at DESC";
const allocations = await db.query<WorkAllocation[]>(query, queryParams);
ctx.response.body = allocations;
} catch (error) {
console.error("Get work allocations error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Get work allocation by ID
router.get("/:id", authenticateToken, async (ctx) => {
try {
const allocationId = ctx.params.id;
const allocations = await db.query<WorkAllocation[]>(
`SELECT wa.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
c.name as contractor_name,
sd.name as sub_department_name,
d.name as department_name
FROM work_allocations wa
JOIN users e ON wa.employee_id = e.id
JOIN users s ON wa.supervisor_id = s.id
JOIN users c ON wa.contractor_id = c.id
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
LEFT JOIN departments d ON e.department_id = d.id
WHERE wa.id = ?`,
[allocationId]
);
if (allocations.length === 0) {
ctx.response.status = 404;
ctx.response.body = { error: "Work allocation not found" };
return;
}
ctx.response.body = allocations[0];
} catch (error) {
console.error("Get work allocation error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Create work allocation (Supervisor or SuperAdmin)
router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CreateWorkAllocationRequest;
const { employeeId, contractorId, subDepartmentId, activity, description, assignedDate, rate, units, totalAmount, departmentId } = body;
if (!employeeId || !contractorId || !assignedDate) {
ctx.response.status = 400;
ctx.response.body = { error: "Missing required fields" };
return;
}
// Verify employee exists
let employeeQuery = "SELECT * FROM users WHERE id = ?";
const employeeParams: unknown[] = [employeeId];
if (currentUser.role === "Supervisor") {
employeeQuery += " AND department_id = ?";
employeeParams.push(currentUser.departmentId);
}
const employees = await db.query<{ id: number }[]>(employeeQuery, employeeParams);
if (employees.length === 0) {
ctx.response.status = 403;
ctx.response.body = { error: "Employee not found or not in your department" };
return;
}
// Use provided rate or get contractor's current rate
let finalRate = rate;
if (!finalRate) {
const rates = await db.query<ContractorRate[]>(
"SELECT rate FROM contractor_rates WHERE contractor_id = ? ORDER BY effective_date DESC LIMIT 1",
[contractorId]
);
finalRate = rates.length > 0 ? rates[0].rate : null;
}
const sanitizedActivity = activity ? sanitizeInput(activity) : null;
const sanitizedDescription = description ? sanitizeInput(description) : null;
const result = await db.execute(
`INSERT INTO work_allocations
(employee_id, supervisor_id, contractor_id, sub_department_id, activity, description, assigned_date, rate, units, total_amount)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[employeeId, currentUser.id, contractorId, subDepartmentId || null, sanitizedActivity, sanitizedDescription, assignedDate, finalRate, units || null, totalAmount || null]
);
const newAllocation = await db.query<WorkAllocation[]>(
`SELECT wa.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
c.name as contractor_name,
sd.name as sub_department_name,
d.name as department_name
FROM work_allocations wa
JOIN users e ON wa.employee_id = e.id
JOIN users s ON wa.supervisor_id = s.id
JOIN users c ON wa.contractor_id = c.id
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
LEFT JOIN departments d ON e.department_id = d.id
WHERE wa.id = ?`,
[result.insertId]
);
ctx.response.status = 201;
ctx.response.body = newAllocation[0];
} catch (error) {
console.error("Create work allocation error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Update work allocation status (Supervisor or SuperAdmin)
router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const allocationId = ctx.params.id;
const body = await ctx.request.body.json() as { status: string; completionDate?: string };
const { status, completionDate } = body;
if (!status) {
ctx.response.status = 400;
ctx.response.body = { error: "Status required" };
return;
}
// Verify allocation exists and user has access
let query = "SELECT * FROM work_allocations WHERE id = ?";
const params: unknown[] = [allocationId];
if (currentUser.role === "Supervisor") {
query += " AND supervisor_id = ?";
params.push(currentUser.id);
}
const allocations = await db.query<WorkAllocation[]>(query, params);
if (allocations.length === 0) {
ctx.response.status = 403;
ctx.response.body = { error: "Work allocation not found or access denied" };
return;
}
await db.execute(
"UPDATE work_allocations SET status = ?, completion_date = ? WHERE id = ?",
[status, completionDate || null, allocationId]
);
const updatedAllocation = await db.query<WorkAllocation[]>(
`SELECT wa.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
c.name as contractor_name,
sd.name as sub_department_name,
d.name as department_name
FROM work_allocations wa
JOIN users e ON wa.employee_id = e.id
JOIN users s ON wa.supervisor_id = s.id
JOIN users c ON wa.contractor_id = c.id
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
LEFT JOIN departments d ON e.department_id = d.id
WHERE wa.id = ?`,
[allocationId]
);
ctx.response.body = updatedAllocation[0];
} catch (error) {
console.error("Update work allocation error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Delete work allocation (Supervisor or SuperAdmin)
router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const allocationId = ctx.params.id;
// Verify allocation exists and user has access
let query = "SELECT * FROM work_allocations WHERE id = ?";
const params: unknown[] = [allocationId];
if (currentUser.role === "Supervisor") {
query += " AND supervisor_id = ?";
params.push(currentUser.id);
}
const allocations = await db.query<WorkAllocation[]>(query, params);
if (allocations.length === 0) {
ctx.response.status = 403;
ctx.response.body = { error: "Work allocation not found or access denied" };
return;
}
await db.execute("DELETE FROM work_allocations WHERE id = ?", [allocationId]);
ctx.response.body = { message: "Work allocation deleted successfully" };
} catch (error) {
console.error("Delete work allocation error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
export default router;

View File

@@ -0,0 +1,247 @@
import { hash } from "bcrypt";
import { db } from "../config/database.ts";
import { config } from "../config/env.ts";
async function seedDatabase() {
try {
console.log("🔌 Connecting to database...");
await db.connect();
console.log("✅ Connected to database\n");
// 1. Seed Departments
console.log("📁 Seeding departments...");
const existingDepts = await db.query<{ count: number }[]>(
"SELECT COUNT(*) as count FROM departments"
);
if (existingDepts[0].count === 0) {
await db.execute(`
INSERT INTO departments (name) VALUES
('Tudki'),
('Dana'),
('Groundnut')
`);
console.log(" ✅ Departments created");
} else {
console.log(" Departments already exist");
}
// 2. Seed Sub-departments for Groundnut
console.log("📂 Seeding sub-departments...");
const groundnutDept = await db.query<{ id: number }[]>(
"SELECT id FROM departments WHERE name = ?",
["Groundnut"]
);
let groundnutId: number | null = null;
if (groundnutDept.length > 0) {
groundnutId = groundnutDept[0].id;
const existingSubDepts = await db.query<{ count: number }[]>(
"SELECT COUNT(*) as count FROM sub_departments WHERE department_id = ?",
[groundnutId]
);
if (existingSubDepts[0].count === 0) {
const subDepts = [
"Mufali Aavak Katai",
"Mufali Aavak Dhang",
"Dhang Se Katai",
"Guthli Bori Silai Dhang",
"Guthali dada Pala Tulai Silai Dhang",
"Mufali Patthar Bori silai dhang",
"Mufali Patthar Bori Utrai",
"Bardana Bandal Loading Unloading",
"Bardana Gatthi Loading",
"Black Dana Loading/Unloading",
"Pre Cleaning",
"Destoner",
"Water",
"Decordicater",
"Round Chalna",
"Cleaning",
"Round Chalna No.1"
];
for (const name of subDepts) {
await db.execute(
"INSERT INTO sub_departments (department_id, name, primary_activity) VALUES (?, ?, ?)",
[groundnutId, name, "Loading/Unloading"]
);
}
console.log(" ✅ Sub-departments created");
} else {
console.log(" Sub-departments already exist");
}
}
// 3. Seed SuperAdmin
console.log("👤 Seeding SuperAdmin user...");
const existingAdmin = await db.query<{ id: number }[]>(
"SELECT id FROM users WHERE username = ?",
["admin"]
);
const adminPassword = await hash("admin123", config.BCRYPT_ROUNDS);
if (existingAdmin.length > 0) {
await db.execute(
"UPDATE users SET password = ?, is_active = TRUE WHERE username = ?",
[adminPassword, "admin"]
);
console.log(" ✅ SuperAdmin password updated");
} else {
await db.execute(
"INSERT INTO users (username, name, email, password, role, is_active) VALUES (?, ?, ?, ?, ?, ?)",
["admin", "Super Admin", "admin@workallocate.com", adminPassword, "SuperAdmin", true]
);
console.log(" ✅ SuperAdmin created");
}
// 4. Seed Sample Supervisors
console.log("👥 Seeding sample supervisors...");
const tudkiDept = await db.query<{ id: number }[]>(
"SELECT id FROM departments WHERE name = ?",
["Tudki"]
);
const danaDept = await db.query<{ id: number }[]>(
"SELECT id FROM departments WHERE name = ?",
["Dana"]
);
const supervisorPassword = await hash("supervisor123", config.BCRYPT_ROUNDS);
const supervisors = [
{ username: "supervisor_tudki", name: "Tudki Supervisor", email: "supervisor.tudki@workallocate.com", deptId: tudkiDept[0]?.id },
{ username: "supervisor_dana", name: "Dana Supervisor", email: "supervisor.dana@workallocate.com", deptId: danaDept[0]?.id },
{ username: "supervisor_groundnut", name: "Groundnut Supervisor", email: "supervisor.groundnut@workallocate.com", deptId: groundnutId }
];
for (const sup of supervisors) {
if (sup.deptId) {
const existing = await db.query<{ id: number }[]>(
"SELECT id FROM users WHERE username = ?",
[sup.username]
);
if (existing.length === 0) {
await db.execute(
"INSERT INTO users (username, name, email, password, role, department_id, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)",
[sup.username, sup.name, sup.email, supervisorPassword, "Supervisor", sup.deptId, true]
);
console.log(`${sup.name} created`);
} else {
console.log(` ${sup.name} already exists`);
}
}
}
// 5. Seed Sample Contractors
console.log("🏗️ Seeding sample contractors...");
const contractorPassword = await hash("contractor123", config.BCRYPT_ROUNDS);
const contractors = [
{ username: "contractor1", name: "Contractor One", email: "contractor1@workallocate.com", deptId: groundnutId },
{ username: "contractor2", name: "Contractor Two", email: "contractor2@workallocate.com", deptId: groundnutId }
];
for (const con of contractors) {
const existing = await db.query<{ id: number }[]>(
"SELECT id FROM users WHERE username = ?",
[con.username]
);
if (existing.length === 0) {
await db.execute(
"INSERT INTO users (username, name, email, password, role, department_id, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)",
[con.username, con.name, con.email, contractorPassword, "Contractor", con.deptId, true]
);
console.log(`${con.name} created`);
} else {
console.log(` ${con.name} already exists`);
}
}
// 6. Seed Sample Employees
console.log("👷 Seeding sample employees...");
const contractor1 = await db.query<{ id: number }[]>(
"SELECT id FROM users WHERE username = ?",
["contractor1"]
);
const employeePassword = await hash("employee123", config.BCRYPT_ROUNDS);
if (contractor1.length > 0) {
const employees = [
{ username: "employee1", name: "Employee One", email: "employee1@workallocate.com" },
{ username: "employee2", name: "Employee Two", email: "employee2@workallocate.com" },
{ username: "employee3", name: "Employee Three", email: "employee3@workallocate.com" }
];
for (const emp of employees) {
const existing = await db.query<{ id: number }[]>(
"SELECT id FROM users WHERE username = ?",
[emp.username]
);
if (existing.length === 0) {
await db.execute(
"INSERT INTO users (username, name, email, password, role, department_id, contractor_id, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
[emp.username, emp.name, emp.email, employeePassword, "Employee", groundnutId, contractor1[0].id, true]
);
console.log(`${emp.name} created`);
} else {
console.log(` ${emp.name} already exists`);
}
}
}
// 7. Seed Contractor Rates
console.log("💰 Seeding contractor rates...");
if (contractor1.length > 0) {
const existingRate = await db.query<{ id: number }[]>(
"SELECT id FROM contractor_rates WHERE contractor_id = ?",
[contractor1[0].id]
);
if (existingRate.length === 0) {
const today = new Date().toISOString().split("T")[0];
await db.execute(
"INSERT INTO contractor_rates (contractor_id, rate, effective_date) VALUES (?, ?, ?)",
[contractor1[0].id, 500.00, today]
);
console.log(" ✅ Contractor rates created");
} else {
console.log(" Contractor rates already exist");
}
}
console.log(`
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ Database seeding completed successfully!
🔑 Default Login Credentials:
SuperAdmin:
Username: admin
Password: admin123
Supervisor (Groundnut):
Username: supervisor_groundnut
Password: supervisor123
Contractor:
Username: contractor1
Password: contractor123
Employee:
Username: employee1
Password: employee123
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
`);
} catch (error) {
console.error("❌ Error seeding database:", (error as Error).message);
Deno.exit(1);
} finally {
await db.close();
}
}
await seedDatabase();

170
backend-deno/types/index.ts Normal file
View File

@@ -0,0 +1,170 @@
// User types
export type UserRole = "SuperAdmin" | "Supervisor" | "Contractor" | "Employee";
export interface User {
id: number;
username: string;
name: string;
email: string;
password?: string;
role: UserRole;
department_id: number | null;
contractor_id: number | null;
is_active: boolean;
created_at: Date;
department_name?: string;
contractor_name?: string;
}
export interface JWTPayload {
id: number;
username: string;
role: UserRole;
departmentId: number | null;
exp: number;
iat: number;
}
// Department types
export interface Department {
id: number;
name: string;
created_at: Date;
}
export interface SubDepartment {
id: number;
department_id: number;
name: string;
primary_activity: string;
created_at: Date;
}
// Work allocation types
export type AllocationStatus = "Pending" | "InProgress" | "Completed" | "Cancelled";
export interface WorkAllocation {
id: number;
employee_id: number;
supervisor_id: number;
contractor_id: number;
sub_department_id: number | null;
activity: string | null;
description: string | null;
assigned_date: Date;
completion_date: Date | null;
status: AllocationStatus;
rate: number | null;
units: number | null;
total_amount: number | null;
created_at: Date;
employee_name?: string;
supervisor_name?: string;
contractor_name?: string;
sub_department_name?: string;
department_name?: string;
}
// Attendance types
export type AttendanceStatus = "CheckedIn" | "CheckedOut" | "Absent";
export interface Attendance {
id: number;
employee_id: number;
supervisor_id: number;
check_in_time: Date | null;
check_out_time: Date | null;
work_date: Date;
status: AttendanceStatus;
created_at: Date;
employee_name?: string;
supervisor_name?: string;
department_name?: string;
contractor_name?: string;
}
// Contractor rate types
export interface ContractorRate {
id: number;
contractor_id: number;
sub_department_id: number | null;
activity: string | null;
rate: number;
effective_date: Date;
created_at: Date;
contractor_name?: string;
sub_department_name?: string;
department_name?: string;
}
// API response types
export interface ApiError {
error: string;
details?: string;
}
export interface ApiSuccess<T> {
data: T;
message?: string;
}
export interface LoginResponse {
token: string;
user: Omit<User, "password">;
}
// Request body types
export interface LoginRequest {
username: string;
password: string;
}
export interface CreateUserRequest {
username: string;
name: string;
email: string;
password: string;
role: UserRole;
departmentId?: number | null;
contractorId?: number | null;
}
export interface UpdateUserRequest {
name?: string;
email?: string;
role?: UserRole;
departmentId?: number | null;
contractorId?: number | null;
isActive?: boolean;
}
export interface ChangePasswordRequest {
currentPassword: string;
newPassword: string;
}
export interface CreateWorkAllocationRequest {
employeeId: number;
contractorId: number;
subDepartmentId?: number | null;
activity?: string | null;
description?: string | null;
assignedDate: string;
rate?: number | null;
units?: number | null;
totalAmount?: number | null;
departmentId?: number | null;
}
export interface CheckInOutRequest {
employeeId: number;
workDate: string;
}
export interface CreateContractorRateRequest {
contractorId: number;
subDepartmentId?: number | null;
activity?: string | null;
rate: number;
effectiveDate: string;
}