Initial commit: tinovisas (visa management app)
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
*.log
|
||||
@@ -0,0 +1,26 @@
|
||||
NODE_ENV=development
|
||||
PORT=4000
|
||||
API_URL=http://localhost:4000
|
||||
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
DB_NAME=tinovisas
|
||||
DB_USER=tinovisas
|
||||
DB_PASSWORD=tinovisas_secret_2024
|
||||
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
|
||||
JWT_SECRET=tinovisas_super_secret_key_change_in_production
|
||||
JWT_EXPIRES_IN=24h
|
||||
|
||||
ENCRYPTION_KEY=tinovisas_encryption_key_32chars!!
|
||||
|
||||
UPLOAD_DIR=/app/uploads
|
||||
MAX_FILE_SIZE=10485760
|
||||
|
||||
PLAYWRIGHT_HEADLESS=true
|
||||
PLAYWRIGHT_TIMEOUT=30000
|
||||
|
||||
DEFAULT_ADMIN_EMAIL=admin@tinovisas.com
|
||||
DEFAULT_ADMIN_PASSWORD=Admin123!
|
||||
@@ -0,0 +1,17 @@
|
||||
FROM node:20-alpine
|
||||
RUN apk add --no-cache \
|
||||
chromium \
|
||||
nss \
|
||||
freetype \
|
||||
freetype-dev \
|
||||
harfbuzz \
|
||||
ca-certificates \
|
||||
ttf-freefont
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/usr/bin/chromium-browser
|
||||
EXPOSE 4000
|
||||
CMD ["npm", "start"]
|
||||
Generated
+2539
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
{"name":"tinovisas-backend","version":"1.0.0","scripts":{"build":"tsc","start":"node dist/server.js","dev":"ts-node-dev --respawn src/server.ts"},"dependencies":{"express":"^4.18.2","cors":"^2.8.5","helmet":"^7.1.0","bcryptjs":"^2.4.3","jsonwebtoken":"^9.0.2","pg":"^8.11.3","redis":"^4.6.10","playwright":"^1.40.1","multer":"^1.4.5-lts.1","express-rate-limit":"^7.1.5","dotenv":"^16.3.1","uuid":"^9.0.1","joi":"^17.11.0","morgan":"^1.10.0","date-fns":"^2.30.0","crypto-js":"^4.2.0"},"devDependencies":{"@types/node":"^20.10.4","@types/express":"^4.17.21","@types/cors":"^2.8.17","@types/bcryptjs":"^2.4.6","@types/jsonwebtoken":"^9.0.5","@types/multer":"^1.4.11","@types/uuid":"^9.0.7","@types/morgan":"^1.9.9","@types/crypto-js":"^4.2.1","@types/pg":"^8.10.9","typescript":"^5.3.3","ts-node-dev":"^2.0.0"}}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { Pool, PoolClient } from "pg";
|
||||
import { env } from "./env";
|
||||
|
||||
let pool: Pool | null = null;
|
||||
|
||||
export const getPool = (): Pool => {
|
||||
if (!pool) {
|
||||
pool = new Pool({
|
||||
host: env.DB_HOST,
|
||||
port: env.DB_PORT,
|
||||
database: env.DB_NAME,
|
||||
user: env.DB_USER,
|
||||
password: env.DB_PASSWORD,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 5000
|
||||
});
|
||||
|
||||
pool.on("error", (err) => {
|
||||
console.error("Unexpected database error:", err);
|
||||
});
|
||||
}
|
||||
return pool;
|
||||
};
|
||||
|
||||
export const connectDB = async (): Promise<void> => {
|
||||
const client = await getPool().connect();
|
||||
try {
|
||||
const result = await client.query("SELECT NOW()");
|
||||
console.log("Database connected:", result.rows[0].now);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
export const query = async (text: string, params?: any[]): Promise<any> => {
|
||||
const client = await getPool().connect();
|
||||
try {
|
||||
const result = await client.query(text, params);
|
||||
return result;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
export const getClient = async (): Promise<PoolClient> => {
|
||||
return getPool().connect();
|
||||
};
|
||||
|
||||
export const initDatabase = async (): Promise<void> => {
|
||||
const client = await getPool().connect();
|
||||
try {
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
first_name VARCHAR(100),
|
||||
last_name VARCHAR(100),
|
||||
role VARCHAR(20) DEFAULT operator CHECK (role IN (admin, operator, viewer)),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
last_login TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS clients (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
first_name VARCHAR(100) NOT NULL,
|
||||
last_name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(255),
|
||||
phone VARCHAR(50),
|
||||
passport_number VARCHAR(100),
|
||||
date_of_birth DATE,
|
||||
nationality VARCHAR(100),
|
||||
priority VARCHAR(20) DEFAULT medium CHECK (priority IN (low, medium, high, urgent)),
|
||||
status VARCHAR(20) DEFAULT active CHECK (status IN (active, inactive, completed, suspended)),
|
||||
notes TEXT,
|
||||
created_by UUID REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS countries (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
code VARCHAR(10) NOT NULL UNIQUE,
|
||||
flag_emoji VARCHAR(10),
|
||||
vfs_url VARCHAR(500),
|
||||
vfs_credentials JSONB,
|
||||
requirements JSONB DEFAULT [],
|
||||
processing_time VARCHAR(50),
|
||||
visa_types JSONB DEFAULT [],
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS client_countries (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
client_id UUID REFERENCES clients(id) ON DELETE CASCADE,
|
||||
country_id UUID REFERENCES countries(id) ON DELETE CASCADE,
|
||||
visa_type VARCHAR(50),
|
||||
status VARCHAR(20) DEFAULT pending CHECK (status IN (pending, in_progress, approved, rejected, completed)),
|
||||
appointment_date TIMESTAMP,
|
||||
application_ref VARCHAR(100),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(client_id, country_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
client_id UUID REFERENCES clients(id) ON DELETE SET NULL,
|
||||
country_id UUID REFERENCES countries(id) ON DELETE SET NULL,
|
||||
type VARCHAR(20) NOT NULL CHECK (type IN (checkup, booking)),
|
||||
status VARCHAR(20) DEFAULT running CHECK (status IN (running, paused, completed, failed, cancelled)),
|
||||
started_at TIMESTAMP DEFAULT NOW(),
|
||||
ended_at TIMESTAMP,
|
||||
logs JSONB DEFAULT [],
|
||||
screenshot_path VARCHAR(500),
|
||||
error_message TEXT,
|
||||
created_by UUID REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflows (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(200) NOT NULL,
|
||||
country_id UUID REFERENCES countries(id) ON DELETE SET NULL,
|
||||
type VARCHAR(20) NOT NULL CHECK (type IN (checkup, booking)),
|
||||
steps JSONB NOT NULL DEFAULT [],
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_by UUID REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
client_id UUID REFERENCES clients(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
file_path VARCHAR(500) NOT NULL,
|
||||
file_type VARCHAR(50),
|
||||
file_size INTEGER,
|
||||
category VARCHAR(50) DEFAULT general,
|
||||
uploaded_by UUID REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
message TEXT,
|
||||
type VARCHAR(20) DEFAULT info CHECK (type IN (info, success, warning, error)),
|
||||
is_read BOOLEAN DEFAULT false,
|
||||
link VARCHAR(500),
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
action VARCHAR(100) NOT NULL,
|
||||
entity_type VARCHAR(50) NOT NULL,
|
||||
entity_id UUID,
|
||||
details JSONB,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_clients_status ON clients(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_clients_priority ON clients(priority);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_type ON sessions(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_logs_user ON audit_logs(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_read ON notifications(is_read);
|
||||
`);
|
||||
console.log("Database tables initialized");
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
export const env = {
|
||||
NODE_ENV: process.env.NODE_ENV || "development",
|
||||
PORT: parseInt(process.env.PORT || "4000"),
|
||||
API_URL: process.env.API_URL || "http://localhost:4000",
|
||||
|
||||
DB_HOST: process.env.DB_HOST || "localhost",
|
||||
DB_PORT: parseInt(process.env.DB_PORT || "5432"),
|
||||
DB_NAME: process.env.DB_NAME || "tinovisas",
|
||||
DB_USER: process.env.DB_USER || "tinovisas",
|
||||
DB_PASSWORD: process.env.DB_PASSWORD || "tinovisas_secret",
|
||||
|
||||
REDIS_HOST: process.env.REDIS_HOST || "localhost",
|
||||
REDIS_PORT: parseInt(process.env.REDIS_PORT || "6379"),
|
||||
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||
|
||||
JWT_SECRET: process.env.JWT_SECRET || "default-secret-change-me",
|
||||
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || "24h",
|
||||
|
||||
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY || "default-encryption-key-32chars!",
|
||||
|
||||
UPLOAD_DIR: process.env.UPLOAD_DIR || "/app/uploads",
|
||||
MAX_FILE_SIZE: parseInt(process.env.MAX_FILE_SIZE || "10485760"),
|
||||
|
||||
PLAYWRIGHT_HEADLESS: process.env.PLAYWRIGHT_HEADLESS === "true",
|
||||
PLAYWRIGHT_TIMEOUT: parseInt(process.env.PLAYWRIGHT_TIMEOUT || "30000"),
|
||||
|
||||
DEFAULT_ADMIN_EMAIL: process.env.DEFAULT_ADMIN_EMAIL || "admin@tinovisas.com",
|
||||
DEFAULT_ADMIN_PASSWORD: process.env.DEFAULT_ADMIN_PASSWORD || "Admin123!"
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { createClient, RedisClientType } from "redis";
|
||||
import { env } from "./env";
|
||||
|
||||
let redisClient: RedisClientType | null = null;
|
||||
|
||||
export const getRedis = (): RedisClientType => {
|
||||
if (!redisClient) {
|
||||
redisClient = createClient({
|
||||
socket: {
|
||||
host: env.REDIS_HOST,
|
||||
port: env.REDIS_PORT
|
||||
},
|
||||
password: env.REDIS_PASSWORD || undefined
|
||||
});
|
||||
|
||||
redisClient.on("error", (err) => {
|
||||
console.error("Redis error:", err);
|
||||
});
|
||||
}
|
||||
return redisClient;
|
||||
};
|
||||
|
||||
export const connectRedis = async (): Promise<void> => {
|
||||
const client = getRedis();
|
||||
await client.connect();
|
||||
console.log("Redis connected");
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Request, Response } from "express";
|
||||
import { getAllUsers, updateUser, deleteUser, findUserById } from "../models/User";
|
||||
import { getAllClients } from "../models/Client";
|
||||
import { getAllSessions } from "../models/Session";
|
||||
import { getAuditLogs } from "../models/AuditLog";
|
||||
import { getAllCountries } from "../models/Country";
|
||||
import { successResponse, errorResponse } from "../utils/response";
|
||||
import { logAction } from "../services/auditService";
|
||||
|
||||
export const getDashboardStats = async (req: any, res: Response) => {
|
||||
try {
|
||||
const { users } = await getAllUsers(1000, 0);
|
||||
const { clients, total: totalClients } = await getAllClients({}, 1000, 0);
|
||||
const { sessions, total: totalSessions } = await getAllSessions({}, 1000, 0);
|
||||
const { countries } = await getAllCountries();
|
||||
|
||||
const stats = {
|
||||
total_users: users.length,
|
||||
total_clients: totalClients,
|
||||
total_sessions: totalSessions,
|
||||
total_countries: countries.length,
|
||||
active_sessions: sessions.filter((s: any) => s.status === "running").length,
|
||||
clients_by_status: {
|
||||
active: clients.filter((c: any) => c.status === "active").length,
|
||||
inactive: clients.filter((c: any) => c.status === "inactive").length,
|
||||
completed: clients.filter((c: any) => c.status === "completed").length
|
||||
},
|
||||
sessions_by_type: {
|
||||
checkup: sessions.filter((s: any) => s.type === "checkup").length,
|
||||
booking: sessions.filter((s: any) => s.type === "booking").length
|
||||
},
|
||||
recent_sessions: sessions.slice(0, 10),
|
||||
users_by_role: {
|
||||
admin: users.filter((u: any) => u.role === "admin").length,
|
||||
operator: users.filter((u: any) => u.role === "operator").length,
|
||||
viewer: users.filter((u: any) => u.role === "viewer").length
|
||||
}
|
||||
};
|
||||
|
||||
await logAction(req, "VIEW", "admin_dashboard", undefined, { stats });
|
||||
return successResponse(res, stats);
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getAdminUsers = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { users, total } = await getAllUsers();
|
||||
return successResponse(res, users, undefined, { total });
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUserRole = async (req: any, res: Response) => {
|
||||
try {
|
||||
const user = await updateUser(req.params.id, { role: req.body.role });
|
||||
await logAction(req, "UPDATE_ROLE", "user", req.params.id, { new_role: req.body.role });
|
||||
return successResponse(res, user, "User role updated");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleUserActive = async (req: any, res: Response) => {
|
||||
try {
|
||||
const user = await findUserById(req.params.id);
|
||||
if (!user) return errorResponse(res, "User not found", 404);
|
||||
|
||||
const updated = await updateUser(req.params.id, { is_active: !user.is_active });
|
||||
await logAction(req, "TOGGLE_ACTIVE", "user", req.params.id, { is_active: !user.is_active });
|
||||
return successResponse(res, updated, `User ${!user.is_active ? "activated" : "deactivated"}`);
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteUserHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
await deleteUser(req.params.id);
|
||||
await logAction(req, "DELETE", "user", req.params.id);
|
||||
return successResponse(res, null, "User deleted");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getSystemSettings = async (_req: Request, res: Response) => {
|
||||
try {
|
||||
return successResponse(res, {
|
||||
app_name: "Tinovisas",
|
||||
version: "1.0.0",
|
||||
features: {
|
||||
checkup: true,
|
||||
booking: true,
|
||||
workflows: true,
|
||||
documents: true,
|
||||
notifications: true
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Request, Response } from "express";
|
||||
import { getAuditLogs } from "../models/AuditLog";
|
||||
import { successResponse, errorResponse } from "../utils/response";
|
||||
|
||||
export const getAuditLogsHandler = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { user_id, entity_type, action, page = "1", limit = "50" } = req.query;
|
||||
const filters: any = {};
|
||||
if (user_id) filters.user_id = user_id;
|
||||
if (entity_type) filters.entity_type = entity_type;
|
||||
if (action) filters.action = action;
|
||||
|
||||
const offset = (parseInt(page as string) - 1) * parseInt(limit as string);
|
||||
const result = await getAuditLogs(filters, parseInt(limit as string), offset);
|
||||
|
||||
return successResponse(res, result.logs, undefined, {
|
||||
page: parseInt(page as string),
|
||||
limit: parseInt(limit as string),
|
||||
total: result.total,
|
||||
totalPages: Math.ceil(result.total / parseInt(limit as string))
|
||||
});
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,192 @@
|
||||
import { Request, Response } from "express";
|
||||
import { findUserByEmail, createUser, updateLastLogin, getAllUsers, updateUser, deleteUser, findUserById } from "../models/User";
|
||||
import { generateToken } from "../utils/jwt";
|
||||
import { hashPassword, comparePassword } from "../utils/password";
|
||||
import { successResponse, errorResponse } from "../utils/response";
|
||||
import { env } from "../config/env";
|
||||
import Joi from "joi";
|
||||
|
||||
const loginSchema = Joi.object({
|
||||
email: Joi.string().email().required(),
|
||||
password: Joi.string().required()
|
||||
});
|
||||
|
||||
const registerSchema = Joi.object({
|
||||
email: Joi.string().email().required(),
|
||||
password: Joi.string().min(6).required(),
|
||||
first_name: Joi.string().allow(""),
|
||||
last_name: Joi.string().allow(""),
|
||||
role: Joi.string().valid("admin", "operator", "viewer").default("operator")
|
||||
});
|
||||
|
||||
export const login = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { error } = loginSchema.validate(req.body);
|
||||
if (error) return errorResponse(res, error.details[0].message, 400);
|
||||
|
||||
const { email, password } = req.body;
|
||||
const user = await findUserByEmail(email);
|
||||
|
||||
if (!user || !user.is_active) {
|
||||
return errorResponse(res, "Invalid credentials", 401);
|
||||
}
|
||||
|
||||
const valid = await comparePassword(password, user.password_hash);
|
||||
if (!valid) return errorResponse(res, "Invalid credentials", 401);
|
||||
|
||||
await updateLastLogin(user.id);
|
||||
|
||||
const token = generateToken({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role
|
||||
});
|
||||
|
||||
return successResponse(res, {
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
role: user.role
|
||||
}
|
||||
}, "Login successful");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const register = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { error } = registerSchema.validate(req.body);
|
||||
if (error) return errorResponse(res, error.details[0].message, 400);
|
||||
|
||||
const { email, password, first_name, last_name, role } = req.body;
|
||||
|
||||
const existing = await findUserByEmail(email);
|
||||
if (existing) return errorResponse(res, "Email already registered", 409);
|
||||
|
||||
const password_hash = await hashPassword(password);
|
||||
const user = await createUser({ email, password_hash, first_name, last_name, role });
|
||||
|
||||
const token = generateToken({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role
|
||||
});
|
||||
|
||||
return successResponse(res, {
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
role: user.role
|
||||
}
|
||||
}, "Registration successful", 201);
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getMe = async (req: any, res: Response) => {
|
||||
try {
|
||||
const user = await findUserById(req.user.userId);
|
||||
if (!user) return errorResponse(res, "User not found", 404);
|
||||
|
||||
return successResponse(res, {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
role: user.role,
|
||||
is_active: user.is_active,
|
||||
last_login: user.last_login,
|
||||
created_at: user.created_at
|
||||
});
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getUsers = async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const { users, total } = await getAllUsers();
|
||||
return successResponse(res, users, undefined, { total });
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUserProfile = async (req: any, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updates = req.body;
|
||||
|
||||
if (updates.password) {
|
||||
updates.password_hash = await hashPassword(updates.password);
|
||||
delete updates.password;
|
||||
}
|
||||
|
||||
const user = await updateUser(id, updates);
|
||||
return successResponse(res, user, "User updated");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteUserAccount = async (req: any, res: Response) => {
|
||||
try {
|
||||
await deleteUser(req.params.id);
|
||||
return successResponse(res, null, "User deleted");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const guestLogin = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const user = await findUserByEmail("guest@tinovisas.com");
|
||||
if (!user) {
|
||||
return errorResponse(res, "Guest account not available", 503);
|
||||
}
|
||||
const token = generateToken({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role
|
||||
});
|
||||
return successResponse(res, {
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
role: user.role
|
||||
}
|
||||
}, "Guest login successful");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const createDefaultAdmin = async (): Promise<void> => {
|
||||
try {
|
||||
const existing = await findUserByEmail(env.DEFAULT_ADMIN_EMAIL);
|
||||
if (!existing) {
|
||||
const password_hash = await hashPassword(env.DEFAULT_ADMIN_PASSWORD);
|
||||
await createUser({
|
||||
email: env.DEFAULT_ADMIN_EMAIL,
|
||||
password_hash,
|
||||
first_name: "Admin",
|
||||
last_name: "User",
|
||||
role: "admin"
|
||||
});
|
||||
console.log("Default admin created:", env.DEFAULT_ADMIN_EMAIL);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create default admin:", error);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Request, Response } from "express";
|
||||
import { getBrowser, createPage, navigateTo, fillForm, clickElement, takeScreenshot, waitForElement } from "../services/playwright";
|
||||
import { createSession, updateSession } from "../models/Session";
|
||||
import { findClientById } from "../models/Client";
|
||||
import { findCountryById } from "../models/Country";
|
||||
import { addLog, isSessionAborted } from "../services/sessionManager";
|
||||
import { sendNotification } from "../services/notificationService";
|
||||
import { successResponse, errorResponse } from "../utils/response";
|
||||
import { logAction } from "../services/auditService";
|
||||
import path from "path";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import fs from "fs";
|
||||
|
||||
const screenshotsDir = "/app/uploads/screenshots";
|
||||
if (!fs.existsSync(screenshotsDir)) fs.mkdirSync(screenshotsDir, { recursive: true });
|
||||
|
||||
export const runBooking = async (req: any, res: Response) => {
|
||||
const { client_id, country_id, preferred_date, visa_type } = req.body;
|
||||
const sessionId = uuidv4();
|
||||
|
||||
try {
|
||||
const session = await createSession({
|
||||
id: sessionId,
|
||||
client_id,
|
||||
country_id,
|
||||
type: "booking",
|
||||
status: "running",
|
||||
created_by: req.user.userId
|
||||
});
|
||||
|
||||
addLog(sessionId, { message: "Booking session started", level: "info" });
|
||||
|
||||
const client = await findClientById(client_id);
|
||||
const country = await findCountryById(country_id);
|
||||
|
||||
if (!country?.vfs_url) {
|
||||
await updateSession(sessionId, { status: "failed", error_message: "No VFS URL configured" });
|
||||
return errorResponse(res, "No VFS URL configured", 400);
|
||||
}
|
||||
|
||||
const browser = await getBrowser();
|
||||
const { page, context } = await createPage(browser);
|
||||
|
||||
try {
|
||||
addLog(sessionId, { message: `Navigating to ${country.vfs_url}`, level: "info" });
|
||||
await navigateTo(page, country.vfs_url);
|
||||
|
||||
const screenshotPath = path.join(screenshotsDir, `${sessionId}_booking.png`);
|
||||
await takeScreenshot(page, screenshotPath);
|
||||
|
||||
// Check if aborted
|
||||
if (isSessionAborted(sessionId)) {
|
||||
await updateSession(sessionId, { status: "cancelled", ended_at: new Date() });
|
||||
return successResponse(res, null, "Booking cancelled");
|
||||
}
|
||||
|
||||
addLog(sessionId, { message: "Booking process simulated (actual VFS implementation required)", level: "info" });
|
||||
|
||||
await updateSession(sessionId, {
|
||||
status: "completed",
|
||||
ended_at: new Date(),
|
||||
logs: [{ message: "Booking process completed", preferred_date, visa_type, timestamp: new Date().toISOString() }]
|
||||
});
|
||||
|
||||
await sendNotification(req.user.userId, "Booking Complete", `Booking attempt for ${country.name} completed.`, "success", `/sessions/${sessionId}`);
|
||||
await logAction(req, "BOOKING", "session", sessionId, { country: country.name, client: `${client?.first_name} ${client?.last_name}`, preferred_date });
|
||||
|
||||
return successResponse(res, { session_id: sessionId, screenshot: screenshotPath }, "Booking process completed");
|
||||
} catch (error: any) {
|
||||
await updateSession(sessionId, { status: "failed", error_message: error.message, ended_at: new Date() });
|
||||
addLog(sessionId, { message: `Error: ${error.message}`, level: "error" });
|
||||
throw error;
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Request, Response } from "express";
|
||||
import { getBrowser, createPage, navigateTo, takeScreenshot, getText, checkForText, waitForElement } from "../services/playwright";
|
||||
import { createSession, updateSession } from "../models/Session";
|
||||
import { findClientById } from "../models/Client";
|
||||
import { findCountryById } from "../models/Country";
|
||||
import { addLog, isSessionAborted } from "../services/sessionManager";
|
||||
import { sendNotification } from "../services/notificationService";
|
||||
import { successResponse, errorResponse } from "../utils/response";
|
||||
import { logAction } from "../services/auditService";
|
||||
import path from "path";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import fs from "fs";
|
||||
|
||||
const screenshotsDir = "/app/uploads/screenshots";
|
||||
if (!fs.existsSync(screenshotsDir)) fs.mkdirSync(screenshotsDir, { recursive: true });
|
||||
|
||||
export const runCheckup = async (req: any, res: Response) => {
|
||||
const { client_id, country_id } = req.body;
|
||||
const sessionId = uuidv4();
|
||||
|
||||
try {
|
||||
// Create session record
|
||||
const session = await createSession({
|
||||
id: sessionId,
|
||||
client_id,
|
||||
country_id,
|
||||
type: "checkup",
|
||||
status: "running",
|
||||
created_by: req.user.userId
|
||||
});
|
||||
|
||||
addLog(sessionId, { message: "Checkup session started", level: "info" });
|
||||
|
||||
// Get client and country info
|
||||
const client = await findClientById(client_id);
|
||||
const country = await findCountryById(country_id);
|
||||
|
||||
if (!country?.vfs_url) {
|
||||
await updateSession(sessionId, { status: "failed", error_message: "No VFS URL configured for this country" });
|
||||
return errorResponse(res, "No VFS URL configured", 400);
|
||||
}
|
||||
|
||||
// Start browser automation
|
||||
const browser = await getBrowser();
|
||||
const { page, context } = await createPage(browser);
|
||||
|
||||
try {
|
||||
addLog(sessionId, { message: `Navigating to ${country.vfs_url}`, level: "info" });
|
||||
await navigateTo(page, country.vfs_url);
|
||||
|
||||
// Take initial screenshot
|
||||
const screenshotPath = path.join(screenshotsDir, `${sessionId}_initial.png`);
|
||||
await takeScreenshot(page, screenshotPath);
|
||||
await updateSession(sessionId, { screenshot_path: screenshotPath });
|
||||
|
||||
addLog(sessionId, { message: "Page loaded successfully", level: "success" });
|
||||
|
||||
// Check for common appointment indicators
|
||||
const hasAppointments = await checkForText(page, "appointment") || await checkForText(page, "slot");
|
||||
addLog(sessionId, { message: `Appointment indicators found: ${hasAppointments}`, level: hasAppointments ? "success" : "warning" });
|
||||
|
||||
// Try to extract available dates info
|
||||
let availableDates: string[] = [];
|
||||
try {
|
||||
const dateElements = await page.locator("[data-date], .date-available, .calendar-day").all();
|
||||
for (const el of dateElements.slice(0, 10)) {
|
||||
const text = await el.textContent();
|
||||
if (text) availableDates.push(text.trim());
|
||||
}
|
||||
} catch {
|
||||
addLog(sessionId, { message: "Could not extract date information", level: "warning" });
|
||||
}
|
||||
|
||||
// Final screenshot
|
||||
const finalScreenshot = path.join(screenshotsDir, `${sessionId}_final.png`);
|
||||
await takeScreenshot(page, finalScreenshot);
|
||||
|
||||
// Complete session
|
||||
await updateSession(sessionId, {
|
||||
status: "completed",
|
||||
ended_at: new Date(),
|
||||
logs: [{ message: "Checkup completed", available_dates: availableDates, timestamp: new Date().toISOString() }]
|
||||
});
|
||||
|
||||
await sendNotification(req.user.userId, "Checkup Complete", `Checkup for ${country.name} completed. ${availableDates.length} dates found.`, "success", `/sessions/${sessionId}`);
|
||||
await logAction(req, "CHECKUP", "session", sessionId, { country: country.name, client: `${client?.first_name} ${client?.last_name}` });
|
||||
|
||||
return successResponse(res, { session_id: sessionId, available_dates: availableDates, screenshots: [screenshotPath, finalScreenshot] }, "Checkup completed");
|
||||
} catch (error: any) {
|
||||
await updateSession(sessionId, { status: "failed", error_message: error.message, ended_at: new Date() });
|
||||
addLog(sessionId, { message: `Error: ${error.message}`, level: "error" });
|
||||
throw error;
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getCheckupStatus = async (req: Request, res: Response) => {
|
||||
try {
|
||||
// Implementation for getting status
|
||||
return successResponse(res, { status: "ok" });
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Request, Response } from "express";
|
||||
import { createClient, findClientById, updateClient, deleteClient, getAllClients, addClientCountry, getClientCountries, updateClientCountry, removeClientCountry } from "../models/Client";
|
||||
import { successResponse, errorResponse } from "../utils/response";
|
||||
import { logAction } from "../services/auditService";
|
||||
import Joi from "joi";
|
||||
|
||||
const clientSchema = Joi.object({
|
||||
first_name: Joi.string().required(),
|
||||
last_name: Joi.string().required(),
|
||||
email: Joi.string().email().allow("", null),
|
||||
phone: Joi.string().allow("", null),
|
||||
passport_number: Joi.string().allow("", null),
|
||||
date_of_birth: Joi.date().allow(null),
|
||||
nationality: Joi.string().allow("", null),
|
||||
priority: Joi.string().valid("low", "medium", "high", "urgent").default("medium"),
|
||||
status: Joi.string().valid("active", "inactive", "completed", "suspended").default("active"),
|
||||
notes: Joi.string().allow("", null)
|
||||
});
|
||||
|
||||
export const createClientHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
const { error } = clientSchema.validate(req.body);
|
||||
if (error) return errorResponse(res, error.details[0].message, 400);
|
||||
|
||||
const client = await createClient({ ...req.body, created_by: req.user.userId });
|
||||
await logAction(req, "CREATE", "client", client.id, { name: `${client.first_name} ${client.last_name}` });
|
||||
|
||||
return successResponse(res, client, "Client created", 201);
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getClients = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { status, priority, search, page = "1", limit = "50" } = req.query;
|
||||
const filters: any = {};
|
||||
if (status) filters.status = status;
|
||||
if (priority) filters.priority = priority;
|
||||
if (search) filters.search = search;
|
||||
|
||||
const offset = (parseInt(page as string) - 1) * parseInt(limit as string);
|
||||
const result = await getAllClients(filters, parseInt(limit as string), offset);
|
||||
|
||||
return successResponse(res, result.clients, undefined, {
|
||||
page: parseInt(page as string),
|
||||
limit: parseInt(limit as string),
|
||||
total: result.total,
|
||||
totalPages: Math.ceil(result.total / parseInt(limit as string))
|
||||
});
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getClient = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const client = await findClientById(req.params.id);
|
||||
if (!client) return errorResponse(res, "Client not found", 404);
|
||||
|
||||
const countries = await getClientCountries(req.params.id);
|
||||
return successResponse(res, { ...client, countries });
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateClientHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
const client = await updateClient(req.params.id, req.body);
|
||||
await logAction(req, "UPDATE", "client", client.id, req.body);
|
||||
return successResponse(res, client, "Client updated");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteClientHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
await deleteClient(req.params.id);
|
||||
await logAction(req, "DELETE", "client", req.params.id);
|
||||
return successResponse(res, null, "Client deleted");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const addCountryToClient = async (req: any, res: Response) => {
|
||||
try {
|
||||
const cc = await addClientCountry({ ...req.body, client_id: req.params.id });
|
||||
return successResponse(res, cc, "Country added to client", 201);
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateClientCountryHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
const cc = await updateClientCountry(req.params.countryId, req.body);
|
||||
return successResponse(res, cc, "Client country updated");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeClientCountryHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
await removeClientCountry(req.params.countryId);
|
||||
return successResponse(res, null, "Country removed from client");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Request, Response } from "express";
|
||||
import { createCountry, findCountryById, updateCountry, deleteCountry, getAllCountries } from "../models/Country";
|
||||
import { successResponse, errorResponse } from "../utils/response";
|
||||
import { logAction } from "../services/auditService";
|
||||
import Joi from "joi";
|
||||
|
||||
const countrySchema = Joi.object({
|
||||
name: Joi.string().required(),
|
||||
code: Joi.string().required(),
|
||||
flag_emoji: Joi.string().allow("", null),
|
||||
vfs_url: Joi.string().uri().allow("", null),
|
||||
vfs_credentials: Joi.object().allow(null),
|
||||
requirements: Joi.array().items(Joi.string()).default([]),
|
||||
processing_time: Joi.string().allow("", null),
|
||||
visa_types: Joi.array().items(Joi.string()).default([]),
|
||||
is_active: Joi.boolean().default(true)
|
||||
});
|
||||
|
||||
export const createCountryHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
const { error } = countrySchema.validate(req.body);
|
||||
if (error) return errorResponse(res, error.details[0].message, 400);
|
||||
|
||||
const country = await createCountry(req.body);
|
||||
await logAction(req, "CREATE", "country", country.id, { name: country.name, code: country.code });
|
||||
return successResponse(res, country, "Country created", 201);
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getCountries = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const activeOnly = req.query.active === "true";
|
||||
const countries = await getAllCountries(activeOnly);
|
||||
return successResponse(res, countries);
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getCountry = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const country = await findCountryById(req.params.id);
|
||||
if (!country) return errorResponse(res, "Country not found", 404);
|
||||
return successResponse(res, country);
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCountryHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
const country = await updateCountry(req.params.id, req.body);
|
||||
await logAction(req, "UPDATE", "country", country.id, req.body);
|
||||
return successResponse(res, country, "Country updated");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteCountryHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
await deleteCountry(req.params.id);
|
||||
await logAction(req, "DELETE", "country", req.params.id);
|
||||
return successResponse(res, null, "Country deleted");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Request, Response } from "express";
|
||||
import { createDocument, findDocumentById, deleteDocument, getDocumentsByClient } from "../models/Document";
|
||||
import { successResponse, errorResponse } from "../utils/response";
|
||||
import { logAction } from "../services/auditService";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { env } from "../config/env";
|
||||
|
||||
export const uploadDocument = async (req: any, res: Response) => {
|
||||
try {
|
||||
if (!req.file) return errorResponse(res, "No file uploaded", 400);
|
||||
|
||||
const { client_id, category = "general" } = req.body;
|
||||
const doc = await createDocument({
|
||||
client_id,
|
||||
name: req.file.originalname,
|
||||
file_path: req.file.filename,
|
||||
file_type: req.file.mimetype,
|
||||
file_size: req.file.size,
|
||||
category,
|
||||
uploaded_by: req.user.userId
|
||||
});
|
||||
|
||||
await logAction(req, "UPLOAD", "document", doc.id, { name: req.file.originalname, client_id });
|
||||
return successResponse(res, doc, "Document uploaded", 201);
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getDocuments = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { client_id } = req.query;
|
||||
if (!client_id) return errorResponse(res, "client_id required", 400);
|
||||
|
||||
const documents = await getDocumentsByClient(client_id as string);
|
||||
return successResponse(res, documents);
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteDocumentHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
const doc = await findDocumentById(req.params.id);
|
||||
if (!doc) return errorResponse(res, "Document not found", 404);
|
||||
|
||||
const filePath = path.join(env.UPLOAD_DIR, doc.file_path);
|
||||
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
||||
|
||||
await deleteDocument(req.params.id);
|
||||
await logAction(req, "DELETE", "document", req.params.id);
|
||||
return successResponse(res, null, "Document deleted");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const downloadDocument = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const doc = await findDocumentById(req.params.id);
|
||||
if (!doc) return errorResponse(res, "Document not found", 404);
|
||||
|
||||
const filePath = path.join(env.UPLOAD_DIR, doc.file_path);
|
||||
if (!fs.existsSync(filePath)) return errorResponse(res, "File not found", 404);
|
||||
|
||||
res.download(filePath, doc.name);
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Request, Response } from "express";
|
||||
import { getNotificationsByUser, markAsRead, markAllAsRead, deleteNotification } from "../models/Notification";
|
||||
import { getUnreadCount } from "../services/notificationService";
|
||||
import { successResponse, errorResponse } from "../utils/response";
|
||||
|
||||
export const getNotifications = async (req: any, res: Response) => {
|
||||
try {
|
||||
const unreadOnly = req.query.unread === "true";
|
||||
const notifications = await getNotificationsByUser(req.user.userId, unreadOnly);
|
||||
return successResponse(res, notifications);
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getUnreadCountHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
const count = await getUnreadCount(req.user.userId);
|
||||
return successResponse(res, { count });
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const markNotificationRead = async (req: any, res: Response) => {
|
||||
try {
|
||||
await markAsRead(req.params.id);
|
||||
return successResponse(res, null, "Notification marked as read");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const markAllNotificationsRead = async (req: any, res: Response) => {
|
||||
try {
|
||||
await markAllAsRead(req.user.userId);
|
||||
return successResponse(res, null, "All notifications marked as read");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteNotificationHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
await deleteNotification(req.params.id);
|
||||
return successResponse(res, null, "Notification deleted");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Request, Response } from "express";
|
||||
import { createSession, findSessionById, updateSession, deleteSession, getAllSessions } from "../models/Session";
|
||||
import { successResponse, errorResponse } from "../utils/response";
|
||||
import { startSession, stopSession, clearSession } from "../services/sessionManager";
|
||||
import { logAction } from "../services/auditService";
|
||||
|
||||
export const createSessionHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
const session = await createSession({ ...req.body, created_by: req.user.userId });
|
||||
startSession(session.id);
|
||||
await logAction(req, "CREATE", "session", session.id, req.body);
|
||||
return successResponse(res, session, "Session created", 201);
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getSessions = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { status, type, client_id, page = "1", limit = "50" } = req.query;
|
||||
const filters: any = {};
|
||||
if (status) filters.status = status;
|
||||
if (type) filters.type = type;
|
||||
if (client_id) filters.client_id = client_id;
|
||||
|
||||
const offset = (parseInt(page as string) - 1) * parseInt(limit as string);
|
||||
const result = await getAllSessions(filters, parseInt(limit as string), offset);
|
||||
|
||||
return successResponse(res, result.sessions, undefined, {
|
||||
page: parseInt(page as string),
|
||||
limit: parseInt(limit as string),
|
||||
total: result.total,
|
||||
totalPages: Math.ceil(result.total / parseInt(limit as string))
|
||||
});
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getSession = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const session = await findSessionById(req.params.id);
|
||||
if (!session) return errorResponse(res, "Session not found", 404);
|
||||
return successResponse(res, session);
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSessionHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
const session = await updateSession(req.params.id, req.body);
|
||||
return successResponse(res, session, "Session updated");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const stopSessionHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
stopSession(req.params.id);
|
||||
await updateSession(req.params.id, { status: "cancelled", ended_at: new Date() });
|
||||
await logAction(req, "STOP", "session", req.params.id);
|
||||
return successResponse(res, null, "Session stopped");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteSessionHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
clearSession(req.params.id);
|
||||
await deleteSession(req.params.id);
|
||||
await logAction(req, "DELETE", "session", req.params.id);
|
||||
return successResponse(res, null, "Session deleted");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Request, Response } from "express";
|
||||
import { getPool } from "../config/database";
|
||||
import { getRedis } from "../config/redis";
|
||||
import { successResponse, errorResponse } from "../utils/response";
|
||||
|
||||
export const getSystemStatus = async (_req: Request, res: Response) => {
|
||||
try {
|
||||
// Check database
|
||||
let dbStatus = "healthy";
|
||||
let dbLatency = 0;
|
||||
try {
|
||||
const start = Date.now();
|
||||
const client = await getPool().connect();
|
||||
await client.query("SELECT 1");
|
||||
client.release();
|
||||
dbLatency = Date.now() - start;
|
||||
} catch {
|
||||
dbStatus = "unhealthy";
|
||||
}
|
||||
|
||||
// Check Redis
|
||||
let redisStatus = "healthy";
|
||||
let redisLatency = 0;
|
||||
try {
|
||||
const start = Date.now();
|
||||
const redis = getRedis();
|
||||
await redis.ping();
|
||||
redisLatency = Date.now() - start;
|
||||
} catch {
|
||||
redisStatus = "unhealthy";
|
||||
}
|
||||
|
||||
// System info
|
||||
const uptime = process.uptime();
|
||||
const memory = process.memoryUsage();
|
||||
|
||||
return successResponse(res, {
|
||||
status: dbStatus === "healthy" && redisStatus === "healthy" ? "healthy" : "degraded",
|
||||
services: {
|
||||
database: { status: dbStatus, latency_ms: dbLatency },
|
||||
redis: { status: redisStatus, latency_ms: redisLatency },
|
||||
api: { status: "healthy", uptime_seconds: uptime }
|
||||
},
|
||||
memory: {
|
||||
used_mb: Math.round(memory.heapUsed / 1024 / 1024),
|
||||
total_mb: Math.round(memory.heapTotal / 1024 / 1024),
|
||||
rss_mb: Math.round(memory.rss / 1024 / 1024)
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Request, Response } from "express";
|
||||
import { createWorkflow, findWorkflowById, updateWorkflow, deleteWorkflow, getAllWorkflows } from "../models/Workflow";
|
||||
import { successResponse, errorResponse } from "../utils/response";
|
||||
import { logAction } from "../services/auditService";
|
||||
import Joi from "joi";
|
||||
|
||||
const workflowSchema = Joi.object({
|
||||
name: Joi.string().required(),
|
||||
country_id: Joi.string().uuid().allow(null),
|
||||
type: Joi.string().valid("checkup", "booking").required(),
|
||||
steps: Joi.array().items(Joi.object()).default([]),
|
||||
is_active: Joi.boolean().default(true)
|
||||
});
|
||||
|
||||
export const createWorkflowHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
const { error } = workflowSchema.validate(req.body);
|
||||
if (error) return errorResponse(res, error.details[0].message, 400);
|
||||
|
||||
const workflow = await createWorkflow({ ...req.body, created_by: req.user.userId });
|
||||
await logAction(req, "CREATE", "workflow", workflow.id, { name: workflow.name });
|
||||
return successResponse(res, workflow, "Workflow created", 201);
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getWorkflows = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { type, country_id } = req.query;
|
||||
const filters: any = {};
|
||||
if (type) filters.type = type;
|
||||
if (country_id) filters.country_id = country_id;
|
||||
|
||||
const workflows = await getAllWorkflows(filters);
|
||||
return successResponse(res, workflows);
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getWorkflow = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const workflow = await findWorkflowById(req.params.id);
|
||||
if (!workflow) return errorResponse(res, "Workflow not found", 404);
|
||||
return successResponse(res, workflow);
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateWorkflowHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
const workflow = await updateWorkflow(req.params.id, req.body);
|
||||
await logAction(req, "UPDATE", "workflow", workflow.id, req.body);
|
||||
return successResponse(res, workflow, "Workflow updated");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteWorkflowHandler = async (req: any, res: Response) => {
|
||||
try {
|
||||
await deleteWorkflow(req.params.id);
|
||||
await logAction(req, "DELETE", "workflow", req.params.id);
|
||||
return successResponse(res, null, "Workflow deleted");
|
||||
} catch (error: any) {
|
||||
return errorResponse(res, error.message, 500);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { env } from "../config/env";
|
||||
import { findUserById } from "../models/User";
|
||||
import { JWTPayload } from "../types";
|
||||
|
||||
export interface AuthenticatedRequest extends Request {
|
||||
user?: JWTPayload;
|
||||
}
|
||||
|
||||
export const authenticate = async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.replace("Bearer ", "");
|
||||
if (!token) {
|
||||
return res.status(401).json({ success: false, message: "Authentication required" });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, env.JWT_SECRET) as JWTPayload;
|
||||
const user = await findUserById(decoded.userId);
|
||||
|
||||
if (!user || !user.is_active) {
|
||||
return res.status(401).json({ success: false, message: "User not found or inactive" });
|
||||
}
|
||||
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(401).json({ success: false, message: "Invalid token" });
|
||||
}
|
||||
};
|
||||
|
||||
export const authorize = (...roles: string[]) => {
|
||||
return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ success: false, message: "Authentication required" });
|
||||
}
|
||||
if (!roles.includes(req.user.role)) {
|
||||
return res.status(403).json({ success: false, message: "Insufficient permissions" });
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
export const errorHandler = (err: any, _req: Request, res: Response, _next: NextFunction) => {
|
||||
console.error("Error:", err);
|
||||
|
||||
if (err.code === "23505") {
|
||||
return res.status(409).json({ success: false, message: "Duplicate entry found" });
|
||||
}
|
||||
|
||||
if (err.code === "23503") {
|
||||
return res.status(400).json({ success: false, message: "Referenced record not found" });
|
||||
}
|
||||
|
||||
const statusCode = err.statusCode || err.status || 500;
|
||||
const message = err.message || "Internal server error";
|
||||
|
||||
res.status(statusCode).json({
|
||||
success: false,
|
||||
message,
|
||||
...(process.env.NODE_ENV === "development" && { stack: err.stack })
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import rateLimit from "express-rate-limit";
|
||||
|
||||
export const rateLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 200, // limit each IP to 200 requests per windowMs
|
||||
message: { success: false, message: "Too many requests, please try again later" },
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false
|
||||
});
|
||||
|
||||
export const strictRateLimiter = rateLimit({
|
||||
windowMs: 5 * 60 * 1000, // 5 minutes
|
||||
max: 10,
|
||||
message: { success: false, message: "Too many attempts, please try again later" }
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import multer from "multer";
|
||||
import path from "path";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { env } from "../config/env";
|
||||
import fs from "fs";
|
||||
|
||||
const uploadDir = env.UPLOAD_DIR;
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (_req, _file, cb) => {
|
||||
cb(null, uploadDir);
|
||||
},
|
||||
filename: (_req, file, cb) => {
|
||||
const uniqueName = `${uuidv4()}${path.extname(file.originalname)}`;
|
||||
cb(null, uniqueName);
|
||||
}
|
||||
});
|
||||
|
||||
export const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: env.MAX_FILE_SIZE },
|
||||
fileFilter: (_req, file, cb) => {
|
||||
const allowedTypes = [".pdf", ".jpg", ".jpeg", ".png", ".doc", ".docx"];
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
if (allowedTypes.includes(ext)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error("Invalid file type. Allowed: PDF, JPG, PNG, DOC, DOCX"));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { query } from "../config/database";
|
||||
|
||||
export const createAuditLog = async (data: any) => {
|
||||
const result = await query(
|
||||
`INSERT INTO audit_logs (user_id, action, entity_type, entity_id, details, ip_address, user_agent)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
|
||||
[data.user_id, data.action, data.entity_type, data.entity_id, data.details ? JSON.stringify(data.details) : null, data.ip_address, data.user_agent]
|
||||
);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const getAuditLogs = async (filters: any = {}, limit: number = 1000, offset: number = 0) => {
|
||||
let sql = `SELECT a.*, u.email as user_email, u.first_name || || u.last_name as user_name
|
||||
FROM audit_logs a
|
||||
LEFT JOIN users u ON a.user_id = u.id
|
||||
WHERE 1=1`;
|
||||
const params: any[] = [];
|
||||
if (filters?.user_id) { sql += ` AND a.user_id = $${params.length + 1}`; params.push(filters.user_id); }
|
||||
if (filters?.entity_type) { sql += ` AND a.entity_type = $${params.length + 1}`; params.push(filters.entity_type); }
|
||||
if (filters?.action) { sql += ` AND a.action = $${params.length + 1}`; params.push(filters.action); }
|
||||
const countResult = await query(`SELECT COUNT(*) FROM (${sql}) AS t`, [...params]);
|
||||
sql += ` ORDER BY a.created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
|
||||
params.push(limit, offset);
|
||||
const result = await query(sql, params);
|
||||
return { logs: result.rows, total: parseInt(countResult.rows[0].count, 10) };
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import { query } from "../config/database";
|
||||
|
||||
export const createClient = async (data: any) => {
|
||||
const result = await query(
|
||||
`INSERT INTO clients (first_name, last_name, email, phone, passport_number, date_of_birth, nationality, priority, status, notes, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *`,
|
||||
[data.first_name, data.last_name, data.email, data.phone, data.passport_number, data.date_of_birth, data.nationality, data.priority || "medium", data.status || "active", data.notes, data.created_by]
|
||||
);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const findClientById = async (id: string) => {
|
||||
const result = await query("SELECT * FROM clients WHERE id = $1", [id]);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const updateClient = async (id: string, data: any) => {
|
||||
const fields = Object.keys(data).filter(k => data[k] !== undefined && k !== "id");
|
||||
const setClause = fields.map((k, i) => `${k} = $${i + 2}`).join(", ");
|
||||
const values = fields.map(k => data[k]);
|
||||
const result = await query(
|
||||
`UPDATE clients SET ${setClause}, updated_at = NOW() WHERE id = $1 RETURNING *`,
|
||||
[id, ...values]
|
||||
);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const deleteClient = async (id: string) => {
|
||||
await query("DELETE FROM clients WHERE id = $1", [id]);
|
||||
};
|
||||
|
||||
export const getAllClients = async (filters: any = {}, limit: number = 1000, offset: number = 0) => {
|
||||
let sql = "SELECT * FROM clients WHERE 1=1";
|
||||
const params: any[] = [];
|
||||
if (filters?.status) { sql += ` AND status = $${params.length + 1}`; params.push(filters.status); }
|
||||
if (filters?.priority) { sql += ` AND priority = $${params.length + 1}`; params.push(filters.priority); }
|
||||
if (filters?.search) { sql += ` AND (first_name ILIKE $${params.length + 1} OR last_name ILIKE $${params.length + 1} OR email ILIKE $${params.length + 1})`; params.push(`%${filters.search}%`); }
|
||||
const countResult = await query(`SELECT COUNT(*) FROM (${sql}) AS t`, [...params]);
|
||||
sql += ` ORDER BY created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
|
||||
params.push(limit, offset);
|
||||
const result = await query(sql, params);
|
||||
return { clients: result.rows, total: parseInt(countResult.rows[0].count, 10) };
|
||||
};
|
||||
|
||||
export const addClientCountry = async (data: any) => {
|
||||
const result = await query(
|
||||
`INSERT INTO client_countries (client_id, country_id, visa_type, status, appointment_date, application_ref, notes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
|
||||
[data.client_id, data.country_id, data.visa_type, data.status || "pending", data.appointment_date, data.application_ref, data.notes]
|
||||
);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const getClientCountries = async (clientId: string) => {
|
||||
const result = await query(
|
||||
`SELECT cc.*, c.name as country_name, c.code as country_code, c.flag_emoji
|
||||
FROM client_countries cc
|
||||
JOIN countries c ON cc.country_id = c.id
|
||||
WHERE cc.client_id = $1`,
|
||||
[clientId]
|
||||
);
|
||||
return result.rows;
|
||||
};
|
||||
|
||||
export const updateClientCountry = async (id: string, data: any) => {
|
||||
const fields = Object.keys(data).filter(k => data[k] !== undefined && k !== "id");
|
||||
const setClause = fields.map((k, i) => `${k} = $${i + 2}`).join(", ");
|
||||
const values = fields.map(k => data[k]);
|
||||
const result = await query(
|
||||
`UPDATE client_countries SET ${setClause}, updated_at = NOW() WHERE id = $1 RETURNING *`,
|
||||
[id, ...values]
|
||||
);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const removeClientCountry = async (id: string) => {
|
||||
await query("DELETE FROM client_countries WHERE id = $1", [id]);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { query } from "../config/database";
|
||||
|
||||
export const createCountry = async (data: any) => {
|
||||
const result = await query(
|
||||
`INSERT INTO countries (name, code, flag_emoji, vfs_url, vfs_credentials, requirements, processing_time, visa_types)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`,
|
||||
[data.name, data.code, data.flag_emoji, data.vfs_url, data.vfs_credentials ? JSON.stringify(data.vfs_credentials) : null, data.requirements ? JSON.stringify(data.requirements) : null, data.processing_time, data.visa_types ? JSON.stringify(data.visa_types) : null]
|
||||
);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const findCountryById = async (id: string) => {
|
||||
const result = await query("SELECT * FROM countries WHERE id = $1", [id]);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const updateCountry = async (id: string, data: any) => {
|
||||
const fields = Object.keys(data).filter(k => data[k] !== undefined && k !== "id");
|
||||
const setClause = fields.map((k, i) => `${k} = $${i + 2}`).join(", ");
|
||||
const values = fields.map(k => data[k]);
|
||||
const result = await query(
|
||||
`UPDATE countries SET ${setClause}, updated_at = NOW() WHERE id = $1 RETURNING *`,
|
||||
[id, ...values]
|
||||
);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const deleteCountry = async (id: string) => {
|
||||
await query("DELETE FROM countries WHERE id = $1", [id]);
|
||||
};
|
||||
|
||||
export const getAllCountries = async (filters: any = {}, limit: number = 1000, offset: number = 0) => {
|
||||
let sql = "SELECT * FROM countries WHERE 1=1";
|
||||
const params: any[] = [];
|
||||
if (filters?.is_active !== undefined) { sql += ` AND is_active = $${params.length + 1}`; params.push(filters.is_active); }
|
||||
if (filters?.search) { sql += ` AND name ILIKE $${params.length + 1}`; params.push(`%${filters.search}%`); }
|
||||
const countResult = await query(`SELECT COUNT(*) FROM (${sql}) AS t`, [...params]);
|
||||
sql += ` ORDER BY name ASC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
|
||||
params.push(limit, offset);
|
||||
const result = await query(sql, params);
|
||||
return { countries: result.rows, total: parseInt(countResult.rows[0].count, 10) };
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { query } from "../config/database";
|
||||
|
||||
export const createDocument = async (data: any) => {
|
||||
const result = await query(
|
||||
`INSERT INTO documents (client_id, name, file_path, file_type, file_size, category, uploaded_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
|
||||
[data.client_id, data.name, data.file_path, data.file_type, data.file_size, data.category || "general", data.uploaded_by]
|
||||
);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const findDocumentById = async (id: string) => {
|
||||
const result = await query("SELECT * FROM documents WHERE id = $1", [id]);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const deleteDocument = async (id: string) => {
|
||||
await query("DELETE FROM documents WHERE id = $1", [id]);
|
||||
};
|
||||
|
||||
export const getDocumentsByClient = async (clientId: string) => {
|
||||
const result = await query(
|
||||
"SELECT * FROM documents WHERE client_id = $1 ORDER BY created_at DESC",
|
||||
[clientId]
|
||||
);
|
||||
return result.rows;
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { query } from "../config/database";
|
||||
|
||||
export const createNotification = async (data: any) => {
|
||||
const result = await query(
|
||||
`INSERT INTO notifications (user_id, title, message, type, link)
|
||||
VALUES ($1, $2, $3, $4, $5) RETURNING *`,
|
||||
[data.user_id, data.title, data.message, data.type || "info", data.link]
|
||||
);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const getNotificationsByUser = async (userId: string, unreadOnly?: boolean) => {
|
||||
let sql = "SELECT * FROM notifications WHERE user_id = $1";
|
||||
const params: any[] = [userId];
|
||||
if (unreadOnly) { sql += " AND is_read = false"; }
|
||||
sql += " ORDER BY created_at DESC";
|
||||
const result = await query(sql, params);
|
||||
return result.rows;
|
||||
};
|
||||
|
||||
export const markAsRead = async (id: string) => {
|
||||
const result = await query(
|
||||
"UPDATE notifications SET is_read = true WHERE id = $1 RETURNING *",
|
||||
[id]
|
||||
);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const markAllAsRead = async (userId: string) => {
|
||||
await query(
|
||||
"UPDATE notifications SET is_read = true WHERE user_id = $1 AND is_read = false",
|
||||
[userId]
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteNotification = async (id: string) => {
|
||||
await query("DELETE FROM notifications WHERE id = $1", [id]);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { query } from "../config/database";
|
||||
|
||||
export const createSession = async (data: any) => {
|
||||
const result = await query(
|
||||
`INSERT INTO sessions (client_id, country_id, type, status, logs, screenshot_path, error_message, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`,
|
||||
[data.client_id, data.country_id, data.type, data.status || "running", data.logs ? JSON.stringify(data.logs) : null, data.screenshot_path, data.error_message, data.created_by]
|
||||
);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const findSessionById = async (id: string) => {
|
||||
const result = await query("SELECT * FROM sessions WHERE id = $1", [id]);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const updateSession = async (id: string, data: any) => {
|
||||
const fields = Object.keys(data).filter(k => data[k] !== undefined && k !== "id");
|
||||
const setClause = fields.map((k, i) => `${k} = $${i + 2}`).join(", ");
|
||||
const values = fields.map(k => data[k]);
|
||||
const result = await query(
|
||||
`UPDATE sessions SET ${setClause}, ended_at = CASE WHEN status IN (completed, failed, cancelled) THEN COALESCE(ended_at, NOW()) ELSE ended_at END WHERE id = $1 RETURNING *`,
|
||||
[id, ...values]
|
||||
);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const deleteSession = async (id: string) => {
|
||||
await query("DELETE FROM sessions WHERE id = $1", [id]);
|
||||
};
|
||||
|
||||
export const getAllSessions = async (filters: any = {}, limit: number = 1000, offset: number = 0) => {
|
||||
let sql = `SELECT s.*, c.first_name || || c.last_name as client_name, co.name as country_name
|
||||
FROM sessions s
|
||||
LEFT JOIN clients c ON s.client_id = c.id
|
||||
LEFT JOIN countries co ON s.country_id = co.id
|
||||
WHERE 1=1`;
|
||||
const params: any[] = [];
|
||||
if (filters?.status) { sql += ` AND s.status = $${params.length + 1}`; params.push(filters.status); }
|
||||
if (filters?.type) { sql += ` AND s.type = $${params.length + 1}`; params.push(filters.type); }
|
||||
if (filters?.client_id) { sql += ` AND s.client_id = $${params.length + 1}`; params.push(filters.client_id); }
|
||||
const countResult = await query(`SELECT COUNT(*) FROM (${sql}) AS t`, [...params]);
|
||||
sql += ` ORDER BY s.created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
|
||||
params.push(limit, offset);
|
||||
const result = await query(sql, params);
|
||||
return { sessions: result.rows, total: parseInt(countResult.rows[0].count, 10) };
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { query } from "../config/database";
|
||||
import { User } from "../types";
|
||||
|
||||
export const createUser = async (userData: Partial<User>): Promise<User> => {
|
||||
const result = await query(
|
||||
"INSERT INTO users (email, password_hash, first_name, last_name, role) VALUES ($1, $2, $3, $4, $5) RETURNING *",
|
||||
[userData.email, userData.password_hash, userData.first_name, userData.last_name, userData.role || "operator"]
|
||||
);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const findUserByEmail = async (email: string): Promise<User | null> => {
|
||||
const result = await query("SELECT * FROM users WHERE email = $1", [email]);
|
||||
return result.rows[0] || null;
|
||||
};
|
||||
|
||||
export const findUserById = async (id: string): Promise<User | null> => {
|
||||
const result = await query("SELECT * FROM users WHERE id = $1", [id]);
|
||||
return result.rows[0] || null;
|
||||
};
|
||||
|
||||
export const updateUser = async (id: string, updates: Partial<User>): Promise<User> => {
|
||||
const fields = Object.keys(updates).filter(k => k !== "id");
|
||||
const values = fields.map(f => updates[f as keyof User]);
|
||||
const setClause = fields.map((f, i) => f + " = $" + (i + 1)).join(", ");
|
||||
const result = await query(
|
||||
"UPDATE users SET " + setClause + ", updated_at = NOW() WHERE id = $" + (fields.length + 1) + " RETURNING *",
|
||||
[...values, id]
|
||||
);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const deleteUser = async (id: string): Promise<void> => {
|
||||
await query("DELETE FROM users WHERE id = $1", [id]);
|
||||
};
|
||||
|
||||
export const getAllUsers = async (limit = 50, offset = 0): Promise<{ users: User[]; total: number }> => {
|
||||
const countResult = await query("SELECT COUNT(*) FROM users");
|
||||
const result = await query("SELECT * FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2", [limit, offset]);
|
||||
return { users: result.rows, total: parseInt(countResult.rows[0].count) };
|
||||
};
|
||||
|
||||
export const updateLastLogin = async (id: string): Promise<void> => {
|
||||
await query("UPDATE users SET last_login = NOW() WHERE id = $1", [id]);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { query } from "../config/database";
|
||||
|
||||
export const createWorkflow = async (data: any) => {
|
||||
const result = await query(
|
||||
`INSERT INTO workflows (name, country_id, type, steps, is_active, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
|
||||
[data.name, data.country_id, data.type, data.steps ? JSON.stringify(data.steps) : null, data.is_active !== false, data.created_by]
|
||||
);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const findWorkflowById = async (id: string) => {
|
||||
const result = await query("SELECT * FROM workflows WHERE id = $1", [id]);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const updateWorkflow = async (id: string, data: any) => {
|
||||
const fields = Object.keys(data).filter(k => data[k] !== undefined);
|
||||
const setClause = fields.map((k, i) => `${k} = $${i + 2}`).join(", ");
|
||||
const values = fields.map(k => data[k]);
|
||||
const result = await query(
|
||||
`UPDATE workflows SET ${setClause}, updated_at = NOW() WHERE id = $1 RETURNING *`,
|
||||
[id, ...values]
|
||||
);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
export const deleteWorkflow = async (id: string) => {
|
||||
await query("DELETE FROM workflows WHERE id = $1", [id]);
|
||||
};
|
||||
|
||||
export const getAllWorkflows = async (filters?: any) => {
|
||||
let sql = `SELECT w.*, c.name as country_name
|
||||
FROM workflows w
|
||||
LEFT JOIN countries c ON w.country_id = c.id
|
||||
WHERE 1=1`;
|
||||
const params: any[] = [];
|
||||
if (filters?.type) { sql += ` AND w.type = $${params.length + 1}`; params.push(filters.type); }
|
||||
if (filters?.country_id) { sql += ` AND w.country_id = $${params.length + 1}`; params.push(filters.country_id); }
|
||||
sql += " ORDER BY w.created_at DESC";
|
||||
const result = await query(sql, params);
|
||||
return result.rows;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Router } from "express";
|
||||
import { getDashboardStats, getAdminUsers, updateUserRole, toggleUserActive, deleteUserHandler, getSystemSettings } from "../controllers/admin";
|
||||
import { authenticate, authorize } from "../middleware/auth";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/dashboard", authenticate, authorize("admin"), getDashboardStats);
|
||||
router.get("/users", authenticate, authorize("admin"), getAdminUsers);
|
||||
router.put("/users/:id/role", authenticate, authorize("admin"), updateUserRole);
|
||||
router.put("/users/:id/toggle", authenticate, authorize("admin"), toggleUserActive);
|
||||
router.delete("/users/:id", authenticate, authorize("admin"), deleteUserHandler);
|
||||
router.get("/settings", authenticate, authorize("admin"), getSystemSettings);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Router } from "express";
|
||||
import { getAuditLogsHandler } from "../controllers/auditLogs";
|
||||
import { authenticate, authorize } from "../middleware/auth";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", authenticate, authorize("admin"), getAuditLogsHandler);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Router } from "express";
|
||||
import { login, register, guestLogin, getMe, getUsers, updateUserProfile, deleteUserAccount } from "../controllers/auth";
|
||||
import { authenticate, authorize } from "../middleware/auth";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/guest", guestLogin);
|
||||
router.post("/login", login);
|
||||
router.post("/register", register);
|
||||
router.get("/me", authenticate, getMe);
|
||||
router.get("/users", authenticate, authorize("admin"), getUsers);
|
||||
router.put("/users/:id", authenticate, authorize("admin"), updateUserProfile);
|
||||
router.delete("/users/:id", authenticate, authorize("admin"), deleteUserAccount);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Router } from "express";
|
||||
import { runBooking } from "../controllers/booking";
|
||||
import { authenticate } from "../middleware/auth";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/run", authenticate, runBooking);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Router } from "express";
|
||||
import { runCheckup, getCheckupStatus } from "../controllers/checkup";
|
||||
import { authenticate } from "../middleware/auth";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/run", authenticate, runCheckup);
|
||||
router.get("/status", authenticate, getCheckupStatus);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Router } from "express";
|
||||
import { createClientHandler, getClients, getClient, updateClientHandler, deleteClientHandler, addCountryToClient, updateClientCountryHandler, removeClientCountryHandler } from "../controllers/clients";
|
||||
import { authenticate } from "../middleware/auth";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/", authenticate, createClientHandler);
|
||||
router.get("/", authenticate, getClients);
|
||||
router.get("/:id", authenticate, getClient);
|
||||
router.put("/:id", authenticate, updateClientHandler);
|
||||
router.delete("/:id", authenticate, deleteClientHandler);
|
||||
router.post("/:id/countries", authenticate, addCountryToClient);
|
||||
router.put("/:id/countries/:countryId", authenticate, updateClientCountryHandler);
|
||||
router.delete("/:id/countries/:countryId", authenticate, removeClientCountryHandler);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Router } from "express";
|
||||
import { createCountryHandler, getCountries, getCountry, updateCountryHandler, deleteCountryHandler } from "../controllers/countries";
|
||||
import { authenticate, authorize } from "../middleware/auth";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/", authenticate, authorize("admin", "operator"), createCountryHandler);
|
||||
router.get("/", authenticate, getCountries);
|
||||
router.get("/:id", authenticate, getCountry);
|
||||
router.put("/:id", authenticate, authorize("admin", "operator"), updateCountryHandler);
|
||||
router.delete("/:id", authenticate, authorize("admin"), deleteCountryHandler);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Router } from "express";
|
||||
import { uploadDocument, getDocuments, deleteDocumentHandler, downloadDocument } from "../controllers/documents";
|
||||
import { authenticate } from "../middleware/auth";
|
||||
import { upload } from "../middleware/upload";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/", authenticate, upload.single("file"), uploadDocument);
|
||||
router.get("/", authenticate, getDocuments);
|
||||
router.get("/:id/download", authenticate, downloadDocument);
|
||||
router.delete("/:id", authenticate, deleteDocumentHandler);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Router } from "express";
|
||||
import { getNotifications, getUnreadCountHandler, markNotificationRead, markAllNotificationsRead, deleteNotificationHandler } from "../controllers/notifications";
|
||||
import { authenticate } from "../middleware/auth";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", authenticate, getNotifications);
|
||||
router.get("/unread-count", authenticate, getUnreadCountHandler);
|
||||
router.put("/:id/read", authenticate, markNotificationRead);
|
||||
router.put("/mark-all-read", authenticate, markAllNotificationsRead);
|
||||
router.delete("/:id", authenticate, deleteNotificationHandler);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Router } from "express";
|
||||
import { createSessionHandler, getSessions, getSession, updateSessionHandler, stopSessionHandler, deleteSessionHandler } from "../controllers/sessions";
|
||||
import { authenticate } from "../middleware/auth";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/", authenticate, createSessionHandler);
|
||||
router.get("/", authenticate, getSessions);
|
||||
router.get("/:id", authenticate, getSession);
|
||||
router.put("/:id", authenticate, updateSessionHandler);
|
||||
router.post("/:id/stop", authenticate, stopSessionHandler);
|
||||
router.delete("/:id", authenticate, deleteSessionHandler);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Router } from "express";
|
||||
import { getSystemStatus } from "../controllers/system";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/status", getSystemStatus);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Router } from "express";
|
||||
import { createWorkflowHandler, getWorkflows, getWorkflow, updateWorkflowHandler, deleteWorkflowHandler } from "../controllers/workflows";
|
||||
import { authenticate } from "../middleware/auth";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/", authenticate, createWorkflowHandler);
|
||||
router.get("/", authenticate, getWorkflows);
|
||||
router.get("/:id", authenticate, getWorkflow);
|
||||
router.put("/:id", authenticate, updateWorkflowHandler);
|
||||
router.delete("/:id", authenticate, deleteWorkflowHandler);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,84 @@
|
||||
import express, { Application } from "express";
|
||||
import cors from "cors";
|
||||
import helmet from "helmet";
|
||||
import morgan from "morgan";
|
||||
import dotenv from "dotenv";
|
||||
import { rateLimiter } from "./middleware/rateLimiter";
|
||||
import { errorHandler } from "./middleware/errorHandler";
|
||||
import { connectDB } from "./config/database";
|
||||
import { connectRedis } from "./config/redis";
|
||||
import authRoutes from "./routes/auth";
|
||||
import clientRoutes from "./routes/clients";
|
||||
import countryRoutes from "./routes/countries";
|
||||
import sessionRoutes from "./routes/sessions";
|
||||
import workflowRoutes from "./routes/workflows";
|
||||
import documentRoutes from "./routes/documents";
|
||||
import adminRoutes from "./routes/admin";
|
||||
import checkupRoutes from "./routes/checkup";
|
||||
import bookingRoutes from "./routes/booking";
|
||||
import notificationRoutes from "./routes/notifications";
|
||||
import auditLogRoutes from "./routes/auditLogs";
|
||||
import systemRoutes from "./routes/system";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app: Application = express();
|
||||
const PORT = process.env.PORT || 4000;
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet());
|
||||
app.use(cors({ origin: process.env.CLIENT_URL || "*", credentials: true }));
|
||||
app.use(rateLimiter);
|
||||
app.use(morgan("combined"));
|
||||
|
||||
// Body parsing
|
||||
app.use(express.json({ limit: "10mb" }));
|
||||
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
|
||||
|
||||
// Static files
|
||||
app.use("/uploads", express.static(process.env.UPLOAD_DIR || "/app/uploads"));
|
||||
|
||||
// Health check
|
||||
app.get("/health", (_req, res) => {
|
||||
res.json({ status: "ok", timestamp: new Date().toISOString(), version: "1.0.0" });
|
||||
});
|
||||
|
||||
// API routes
|
||||
app.use("/api/auth", authRoutes);
|
||||
app.use("/api/clients", clientRoutes);
|
||||
app.use("/api/countries", countryRoutes);
|
||||
app.use("/api/sessions", sessionRoutes);
|
||||
app.use("/api/workflows", workflowRoutes);
|
||||
app.use("/api/documents", documentRoutes);
|
||||
app.use("/api/admin", adminRoutes);
|
||||
app.use("/api/checkup", checkupRoutes);
|
||||
app.use("/api/booking", bookingRoutes);
|
||||
app.use("/api/notifications", notificationRoutes);
|
||||
app.use("/api/audit-logs", auditLogRoutes);
|
||||
app.use("/api/system", systemRoutes);
|
||||
|
||||
// Error handling
|
||||
app.use(errorHandler);
|
||||
|
||||
// 404 handler
|
||||
app.use((_req, res) => {
|
||||
res.status(404).json({ success: false, message: "Route not found" });
|
||||
});
|
||||
|
||||
// Start server
|
||||
const startServer = async () => {
|
||||
try {
|
||||
await connectDB();
|
||||
await connectRedis();
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Tinovisas API running on port ${PORT}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to start server:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
startServer();
|
||||
|
||||
export default app;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createAuditLog } from "../models/AuditLog";
|
||||
import { AuthenticatedRequest } from "../middleware/auth";
|
||||
import { logAudit } from "../utils/logger";
|
||||
|
||||
export const logAction = async (
|
||||
req: AuthenticatedRequest,
|
||||
action: string,
|
||||
entityType: string,
|
||||
entityId?: string,
|
||||
details?: any
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const userId = req.user?.userId;
|
||||
const ip = req.ip || req.socket.remoteAddress || "unknown";
|
||||
const userAgent = req.headers["user-agent"] || "unknown";
|
||||
|
||||
await createAuditLog({
|
||||
user_id: userId,
|
||||
action,
|
||||
entity_type: entityType,
|
||||
entity_id: entityId,
|
||||
details,
|
||||
ip_address: ip,
|
||||
user_agent: userAgent
|
||||
});
|
||||
|
||||
if (userId) {
|
||||
logAudit(action, userId, { entityType, entityId, details });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create audit log:", error);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { createNotification, getNotificationsByUser } from "../models/Notification";
|
||||
import { getRedis } from "../config/redis";
|
||||
import { logInfo } from "../utils/logger";
|
||||
|
||||
export const sendNotification = async (userId: string, title: string, message: string, type = "info", link?: string): Promise<void> => {
|
||||
try {
|
||||
await createNotification({ user_id: userId, title, message, type, link });
|
||||
|
||||
// Cache unread count
|
||||
const redis = getRedis();
|
||||
const key = `notifications:unread:${userId}`;
|
||||
await redis.incr(key);
|
||||
await redis.expire(key, 3600);
|
||||
|
||||
logInfo(`Notification sent to user ${userId}: ${title}`);
|
||||
} catch (error) {
|
||||
console.error("Failed to send notification:", error);
|
||||
}
|
||||
};
|
||||
|
||||
export const getUnreadCount = async (userId: string): Promise<number> => {
|
||||
try {
|
||||
const redis = getRedis();
|
||||
const cached = await redis.get(`notifications:unread:${userId}`);
|
||||
if (cached) return parseInt(cached);
|
||||
|
||||
const notifications = await getNotificationsByUser(userId, true);
|
||||
const count = notifications.length;
|
||||
await redis.setEx(`notifications:unread:${userId}`, 3600, count.toString());
|
||||
return count;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const broadcastToAdmins = async (title: string, message: string, type = "info"): Promise<void> => {
|
||||
// Implementation would query all admin users and send to each
|
||||
logInfo(`Admin broadcast: ${title}`);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { chromium, Browser, Page, BrowserContext } from "playwright";
|
||||
import { env } from "../config/env";
|
||||
import { logInfo, logError } from "../utils/logger";
|
||||
|
||||
let browserInstance: Browser | null = null;
|
||||
|
||||
export const getBrowser = async (): Promise<Browser> => {
|
||||
if (!browserInstance) {
|
||||
browserInstance = await chromium.launch({
|
||||
headless: env.PLAYWRIGHT_HEADLESS,
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]
|
||||
});
|
||||
logInfo("Playwright browser launched");
|
||||
}
|
||||
return browserInstance;
|
||||
};
|
||||
|
||||
export const closeBrowser = async (): Promise<void> => {
|
||||
if (browserInstance) {
|
||||
await browserInstance.close();
|
||||
browserInstance = null;
|
||||
logInfo("Playwright browser closed");
|
||||
}
|
||||
};
|
||||
|
||||
export const createPage = async (browser: Browser): Promise<{ page: Page; context: BrowserContext }> => {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
});
|
||||
const page = await context.newPage();
|
||||
page.setDefaultTimeout(env.PLAYWRIGHT_TIMEOUT);
|
||||
return { page, context };
|
||||
};
|
||||
|
||||
export const takeScreenshot = async (page: Page, path: string): Promise<string> => {
|
||||
await page.screenshot({ path, fullPage: true });
|
||||
logInfo(`Screenshot saved: ${path}`);
|
||||
return path;
|
||||
};
|
||||
|
||||
export const navigateTo = async (page: Page, url: string): Promise<void> => {
|
||||
logInfo(`Navigating to: ${url}`);
|
||||
await page.goto(url, { waitUntil: "networkidle" });
|
||||
};
|
||||
|
||||
export const fillForm = async (page: Page, fields: { selector: string; value: string }[]): Promise<void> => {
|
||||
for (const field of fields) {
|
||||
await page.waitForSelector(field.selector, { state: "visible" });
|
||||
await page.fill(field.selector, field.value);
|
||||
}
|
||||
};
|
||||
|
||||
export const clickElement = async (page: Page, selector: string): Promise<void> => {
|
||||
await page.waitForSelector(selector, { state: "visible" });
|
||||
await page.click(selector);
|
||||
};
|
||||
|
||||
export const getText = async (page: Page, selector: string): Promise<string | null> => {
|
||||
await page.waitForSelector(selector);
|
||||
return page.textContent(selector);
|
||||
};
|
||||
|
||||
export const waitForElement = async (page: Page, selector: string, timeout?: number): Promise<void> => {
|
||||
await page.waitForSelector(selector, { state: "visible", timeout: timeout || env.PLAYWRIGHT_TIMEOUT });
|
||||
};
|
||||
|
||||
export const checkForText = async (page: Page, text: string): Promise<boolean> => {
|
||||
const body = await page.textContent("body");
|
||||
return body ? body.includes(text) : false;
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { updateSession } from "../models/Session";
|
||||
import { logInfo, logError } from "../utils/logger";
|
||||
|
||||
const activeSessions = new Map<string, { abort: boolean; logs: any[] }>();
|
||||
|
||||
export const startSession = (sessionId: string): void => {
|
||||
activeSessions.set(sessionId, { abort: false, logs: [] });
|
||||
logInfo(`Session started: ${sessionId}`);
|
||||
};
|
||||
|
||||
export const stopSession = (sessionId: string): void => {
|
||||
const session = activeSessions.get(sessionId);
|
||||
if (session) {
|
||||
session.abort = true;
|
||||
logInfo(`Session stopped: ${sessionId}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const isSessionAborted = (sessionId: string): boolean => {
|
||||
return activeSessions.get(sessionId)?.abort || false;
|
||||
};
|
||||
|
||||
export const addLog = (sessionId: string, log: { message: string; level?: string; timestamp?: string }): void => {
|
||||
const session = activeSessions.get(sessionId);
|
||||
if (session) {
|
||||
const entry = {
|
||||
message: log.message,
|
||||
level: log.level || "info",
|
||||
timestamp: log.timestamp || new Date().toISOString()
|
||||
};
|
||||
session.logs.push(entry);
|
||||
updateSession(sessionId, { logs: session.logs }).catch((err: any) => logError("Failed to update session logs", err));
|
||||
}
|
||||
};
|
||||
|
||||
export const getSessionLogs = (sessionId: string): any[] => {
|
||||
return activeSessions.get(sessionId)?.logs || [];
|
||||
};
|
||||
|
||||
export const clearSession = (sessionId: string): void => {
|
||||
activeSessions.delete(sessionId);
|
||||
};
|
||||
|
||||
export const getAllActiveSessions = (): string[] => {
|
||||
return Array.from(activeSessions.keys());
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
password_hash: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
role: "admin" | "operator" | "viewer";
|
||||
is_active: boolean;
|
||||
last_login?: Date;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface Client {
|
||||
id: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
passport_number?: string;
|
||||
date_of_birth?: Date;
|
||||
nationality?: string;
|
||||
priority: "low" | "medium" | "high" | "urgent";
|
||||
status: "active" | "inactive" | "completed" | "suspended";
|
||||
notes?: string;
|
||||
created_by?: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface Country {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
flag_emoji?: string;
|
||||
vfs_url?: string;
|
||||
vfs_credentials?: any;
|
||||
requirements?: any[];
|
||||
processing_time?: string;
|
||||
visa_types?: string[];
|
||||
is_active: boolean;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface ClientCountry {
|
||||
id: string;
|
||||
client_id: string;
|
||||
country_id: string;
|
||||
visa_type?: string;
|
||||
status: "pending" | "in_progress" | "approved" | "rejected" | "completed";
|
||||
appointment_date?: Date;
|
||||
application_ref?: string;
|
||||
notes?: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
id: string;
|
||||
client_id?: string;
|
||||
country_id?: string;
|
||||
type: "checkup" | "booking";
|
||||
status: "running" | "paused" | "completed" | "failed" | "cancelled";
|
||||
started_at: Date;
|
||||
ended_at?: Date;
|
||||
logs?: any[];
|
||||
screenshot_path?: string;
|
||||
error_message?: string;
|
||||
created_by?: string;
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
export interface Workflow {
|
||||
id: string;
|
||||
name: string;
|
||||
country_id?: string;
|
||||
type: "checkup" | "booking";
|
||||
steps: any[];
|
||||
is_active: boolean;
|
||||
created_by?: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface Document {
|
||||
id: string;
|
||||
client_id: string;
|
||||
name: string;
|
||||
file_path: string;
|
||||
file_type?: string;
|
||||
file_size?: number;
|
||||
category?: string;
|
||||
uploaded_by?: string;
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
user_id: string;
|
||||
title: string;
|
||||
message?: string;
|
||||
type: "info" | "success" | "warning" | "error";
|
||||
is_read: boolean;
|
||||
link?: string;
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
export interface AuditLog {
|
||||
id: string;
|
||||
user_id?: string;
|
||||
action: string;
|
||||
entity_type: string;
|
||||
entity_id?: string;
|
||||
details?: any;
|
||||
ip_address?: string;
|
||||
user_agent?: string;
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
export interface JWTPayload {
|
||||
userId: string;
|
||||
email: string;
|
||||
role: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
meta?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
total?: number;
|
||||
totalPages?: number;
|
||||
};
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
declare module "pg" {
|
||||
export class Pool {
|
||||
constructor(config?: any);
|
||||
connect(): Promise<PoolClient>;
|
||||
query(text: string, params?: any[]): Promise<any>;
|
||||
on(event: string, callback: (...args: any[]) => void): void;
|
||||
}
|
||||
export class PoolClient {
|
||||
query(text: string, params?: any[]): Promise<any>;
|
||||
release(): void;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
import { env } from "../config/env";
|
||||
import { JWTPayload } from "../types";
|
||||
|
||||
export const generateToken = (payload: Omit<JWTPayload, "iat" | "exp">): string => {
|
||||
return jwt.sign(payload, env.JWT_SECRET as jwt.Secret, { expiresIn: env.JWT_EXPIRES_IN as any });
|
||||
};
|
||||
|
||||
export const verifyToken = (token: string): JWTPayload => {
|
||||
return jwt.verify(token, env.JWT_SECRET as jwt.Secret) as JWTPayload;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
export const logInfo = (message: string, meta?: any) => {
|
||||
console.log(`[INFO] ${new Date().toISOString()} ${message}`, meta ? JSON.stringify(meta) : "");
|
||||
};
|
||||
|
||||
export const logError = (message: string, error?: any) => {
|
||||
console.error(`[ERROR] ${new Date().toISOString()} ${message}`, error);
|
||||
};
|
||||
|
||||
export const logAudit = (action: string, userId: string, details: any) => {
|
||||
console.log(`[AUDIT] ${new Date().toISOString()} ${action} user=${userId}`, JSON.stringify(details));
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
export const hashPassword = async (password: string): Promise<string> => {
|
||||
return bcrypt.hash(password, 12);
|
||||
};
|
||||
|
||||
export const comparePassword = async (password: string, hash: string): Promise<boolean> => {
|
||||
return bcrypt.compare(password, hash);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Response } from "express";
|
||||
|
||||
export const successResponse = <T>(res: Response, data: T, message?: string, meta?: any) => {
|
||||
return res.json({
|
||||
success: true,
|
||||
data,
|
||||
message,
|
||||
meta
|
||||
});
|
||||
};
|
||||
|
||||
export const errorResponse = (res: Response, message: string, statusCode = 400) => {
|
||||
return res.status(statusCode).json({
|
||||
success: false,
|
||||
message
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": false,
|
||||
"strictNullChecks": false,
|
||||
"noImplicitReturns": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user