Backup: 2025-06-08 00:29 - User and admin online ready for dockerhub

[Restore from backup: vip-coordinator-backup-2025-06-08-00-29-user and admin online ready for dockerhub]
This commit is contained in:
2025-06-08 00:29:00 +02:00
parent 035f76fdd3
commit 36cb8e8886
33 changed files with 3676 additions and 3527 deletions

View File

@@ -7,7 +7,14 @@
"start": "node dist/index.js",
"dev": "npx tsx src/index.ts",
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"db:migrate": "tsx src/scripts/db-cli.ts migrate",
"db:migrate:create": "tsx src/scripts/db-cli.ts migrate:create",
"db:seed": "tsx src/scripts/db-cli.ts seed",
"db:seed:reset": "tsx src/scripts/db-cli.ts seed:reset",
"db:setup": "tsx src/scripts/db-cli.ts setup"
},
"keywords": [
"vip",
@@ -21,18 +28,25 @@
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"google-auth-library": "^10.1.0",
"jsonwebtoken": "^9.0.2",
"pg": "^8.11.3",
"redis": "^4.6.8",
"uuid": "^9.0.0"
"uuid": "^9.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^20.5.0",
"@types/pg": "^8.10.2",
"@types/supertest": "^2.0.16",
"@types/uuid": "^9.0.2",
"jest": "^29.7.0",
"supertest": "^6.3.4",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.1",
"ts-node-dev": "^2.0.0",
"tsx": "^4.7.0",

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@ import databaseService from '../services/databaseService';
const router = express.Router();
// Enhanced logging for production debugging
function logAuthEvent(event: string, details: any = {}) {
function logAuthEvent(event: string, details: Record<string, unknown> = {}) {
const timestamp = new Date().toISOString();
console.log(`🔐 [AUTH ${timestamp}] ${event}:`, JSON.stringify(details, null, 2));
}
@@ -277,13 +277,13 @@ router.get('/google/callback', async (req: Request, res: Response) => {
if (!user) {
// Determine role - first user becomes admin, others need approval
const approvedUserCount = await databaseService.getApprovedUserCount();
const role = approvedUserCount === 0 ? 'administrator' : 'coordinator';
const isFirstUser = await databaseService.isFirstUser();
const role = isFirstUser ? 'administrator' : 'coordinator';
logAuthEvent('USER_CREATION', {
email: googleUser.email,
role,
is_first_user: approvedUserCount === 0
is_first_user: isFirstUser
});
user = await databaseService.createUser({
@@ -292,13 +292,12 @@ router.get('/google/callback', async (req: Request, res: Response) => {
email: googleUser.email,
name: googleUser.name,
profile_picture_url: googleUser.picture,
role
role,
status: isFirstUser ? 'active' : 'pending'
});
// Auto-approve first admin, others need approval
if (approvedUserCount === 0) {
await databaseService.updateUserApprovalStatus(googleUser.email, 'approved');
user.approval_status = 'approved';
// Log the user creation
if (isFirstUser) {
logAuthEvent('FIRST_ADMIN_CREATED', { email: googleUser.email });
} else {
logAuthEvent('USER_PENDING_APPROVAL', { email: googleUser.email });
@@ -314,9 +313,9 @@ router.get('/google/callback', async (req: Request, res: Response) => {
});
}
// Check if user is approved
if (user.approval_status !== 'approved') {
logAuthEvent('USER_NOT_APPROVED', { email: user.email, status: user.approval_status });
// Check if user is approved (admins are always approved)
if (user.role !== 'administrator' && user.status === 'pending') {
logAuthEvent('USER_NOT_APPROVED', { email: user.email, status: user.status });
return res.redirect(`${frontendUrl}?error=pending_approval&message=Your account is pending administrator approval`);
}
@@ -365,8 +364,8 @@ router.post('/google/exchange', async (req: Request, res: Response) => {
if (!user) {
// Determine role - first user becomes admin
const userCount = await databaseService.getUserCount();
const role = userCount === 0 ? 'administrator' : 'coordinator';
const isFirstUser = await databaseService.isFirstUser();
const role = isFirstUser ? 'administrator' : 'coordinator';
user = await databaseService.createUser({
id: googleUser.id,
@@ -374,14 +373,30 @@ router.post('/google/exchange', async (req: Request, res: Response) => {
email: googleUser.email,
name: googleUser.name,
profile_picture_url: googleUser.picture,
role
role,
status: isFirstUser ? 'active' : 'pending'
});
// Log the user creation
if (isFirstUser) {
console.log(`✅ First admin created and auto-approved: ${user.name} (${user.email})`);
} else {
console.log(`✅ User created (pending approval): ${user.name} (${user.email}) as ${user.role}`);
}
} else {
// Update last sign in
await databaseService.updateUserLastSignIn(googleUser.email);
console.log(`✅ User logged in: ${user.name} (${user.email})`);
}
// Check if user is approved (admins are always approved)
if (user.role !== 'administrator' && user.status === 'pending') {
return res.status(403).json({
error: 'pending_approval',
message: 'Your account is pending administrator approval'
});
}
// Generate JWT token
const token = generateToken(user);
@@ -393,7 +408,8 @@ router.post('/google/exchange', async (req: Request, res: Response) => {
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role
role: user.role,
status: user.status
}
});
@@ -420,6 +436,115 @@ router.post('/logout', (req: Request, res: Response) => {
res.json({ message: 'Logged out successfully' });
});
// Verify Google credential (from Google Identity Services)
router.post('/google/verify', async (req: Request, res: Response) => {
const { credential } = req.body;
if (!credential) {
return res.status(400).json({ error: 'Credential is required' });
}
try {
// Decode the JWT credential from Google
const parts = credential.split('.');
if (parts.length !== 3) {
return res.status(400).json({ error: 'Invalid credential format' });
}
// Decode the payload (base64)
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
if (!payload.email || !payload.email_verified) {
return res.status(400).json({ error: 'Invalid or unverified email' });
}
// Create Google user object
const googleUser = {
id: payload.sub,
email: payload.email,
name: payload.name || payload.email,
picture: payload.picture,
verified_email: payload.email_verified
};
logAuthEvent('GOOGLE_CREDENTIAL_VERIFIED', {
email: googleUser.email,
name: googleUser.name
});
// Check if user exists or create new user
let user = await databaseService.getUserByEmail(googleUser.email);
if (!user) {
// Determine role - first user becomes admin
const isFirstUser = await databaseService.isFirstUser();
const role = isFirstUser ? 'administrator' : 'coordinator';
user = await databaseService.createUser({
id: googleUser.id,
google_id: googleUser.id,
email: googleUser.email,
name: googleUser.name,
profile_picture_url: googleUser.picture,
role,
status: isFirstUser ? 'active' : 'pending'
});
// Log the user creation
if (isFirstUser) {
logAuthEvent('FIRST_ADMIN_CREATED', { email: googleUser.email });
} else {
logAuthEvent('USER_PENDING_APPROVAL', { email: googleUser.email });
}
} else {
// Update last sign in
await databaseService.updateUserLastSignIn(googleUser.email);
logAuthEvent('USER_LOGIN', {
email: user.email,
name: user.name,
role: user.role,
status: user.status
});
}
// Check if user is approved (admins are always approved)
if (user.role !== 'administrator' && user.status === 'pending') {
return res.status(403).json({
error: 'pending_approval',
message: 'Your account is pending administrator approval',
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
status: user.status
},
token: generateToken(user) // Still give them a token so they can check status
});
}
// Generate JWT token
const token = generateToken(user);
// Return token to frontend
res.json({
token,
user: {
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role,
status: user.status
}
});
} catch (error) {
console.error('Error verifying Google credential:', error);
res.status(500).json({ error: 'Failed to verify credential' });
}
});
// Get auth status
router.get('/status', (req: Request, res: Response) => {
const authHeader = req.headers.authorization;
@@ -610,4 +735,143 @@ router.patch('/users/:email/approval', requireAuth, requireRole(['administrator'
}
});
// Complete user onboarding
router.post('/users/complete-onboarding', requireAuth, async (req: Request, res: Response) => {
try {
const userEmail = req.user?.email;
if (!userEmail) {
return res.status(401).json({ error: 'User not authenticated' });
}
const { onboardingData, phone, organization } = req.body;
const updatedUser = await databaseService.completeUserOnboarding(userEmail, {
...onboardingData,
phone,
organization
});
res.json({ message: 'Onboarding completed successfully', user: updatedUser });
} catch (error) {
console.error('Failed to complete onboarding:', error);
res.status(500).json({ error: 'Failed to complete onboarding' });
}
});
// Get current user with full details
router.get('/users/me', requireAuth, async (req: Request, res: Response) => {
try {
const userEmail = req.user?.email;
if (!userEmail) {
return res.status(401).json({ error: 'User not authenticated' });
}
const user = await databaseService.getUserByEmail(userEmail);
res.json(user);
} catch (error) {
console.error('Failed to get user details:', error);
res.status(500).json({ error: 'Failed to get user details' });
}
});
// Approve user (by email, not ID)
router.post('/users/:email/approve', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
try {
const { email } = req.params;
const { role } = req.body;
const approvedBy = req.user?.email || '';
const updatedUser = await databaseService.approveUser(email, approvedBy, role);
if (!updatedUser) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ message: 'User approved successfully', user: updatedUser });
} catch (error) {
console.error('Failed to approve user:', error);
res.status(500).json({ error: 'Failed to approve user' });
}
});
// Reject user
router.post('/users/:email/reject', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
try {
const { email } = req.params;
const { reason } = req.body;
const rejectedBy = req.user?.email || '';
const updatedUser = await databaseService.rejectUser(email, rejectedBy, reason);
if (!updatedUser) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ message: 'User rejected', user: updatedUser });
} catch (error) {
console.error('Failed to reject user:', error);
res.status(500).json({ error: 'Failed to reject user' });
}
});
// Deactivate user
router.post('/users/:email/deactivate', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
try {
const { email } = req.params;
const deactivatedBy = req.user?.email || '';
const updatedUser = await databaseService.deactivateUser(email, deactivatedBy);
if (!updatedUser) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ message: 'User deactivated', user: updatedUser });
} catch (error) {
console.error('Failed to deactivate user:', error);
res.status(500).json({ error: 'Failed to deactivate user' });
}
});
// Reactivate user
router.post('/users/:email/reactivate', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
try {
const { email } = req.params;
const reactivatedBy = req.user?.email || '';
const updatedUser = await databaseService.reactivateUser(email, reactivatedBy);
if (!updatedUser) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ message: 'User reactivated', user: updatedUser });
} catch (error) {
console.error('Failed to reactivate user:', error);
res.status(500).json({ error: 'Failed to reactivate user' });
}
});
// Update user role
router.put('/users/:email/role', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => {
try {
const { email } = req.params;
const { role } = req.body;
const updatedUser = await databaseService.updateUserRole(email, role);
if (!updatedUser) {
return res.status(404).json({ error: 'User not found' });
}
// Log audit
await databaseService.createAuditLog('role_changed', email, req.user?.email || '', { newRole: role });
res.json({ message: 'User role updated', user: updatedUser });
} catch (error) {
console.error('Failed to update user role:', error);
res.status(500).json({ error: 'Failed to update user role' });
}
});
export default router;

View File

@@ -1,550 +1,332 @@
import { Pool, PoolClient } from 'pg';
import { createClient, RedisClientType } from 'redis';
class DatabaseService {
private pool: Pool;
private redis: RedisClientType;
// Import the existing backup service
import backupDatabaseService from './backup-services/databaseService';
// Extend the backup service with new user management methods
class EnhancedDatabaseService {
private backupService: typeof backupDatabaseService;
constructor() {
this.pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
});
// Initialize Redis connection
this.redis = createClient({
socket: {
host: process.env.REDIS_HOST || 'redis',
port: parseInt(process.env.REDIS_PORT || '6379')
}
});
this.redis.on('error', (err) => {
console.error('❌ Redis connection error:', err);
});
// Test connections on startup
this.testConnection();
this.testRedisConnection();
}
private async testConnection(): Promise<void> {
try {
const client = await this.pool.connect();
console.log('✅ Connected to PostgreSQL database');
client.release();
} catch (error) {
console.error('❌ Failed to connect to PostgreSQL database:', error);
}
}
private async testRedisConnection(): Promise<void> {
try {
if (!this.redis.isOpen) {
await this.redis.connect();
}
await this.redis.ping();
console.log('✅ Connected to Redis');
} catch (error) {
console.error('❌ Failed to connect to Redis:', error);
}
this.backupService = backupDatabaseService;
}
// Delegate all existing methods to backup service
async query(text: string, params?: any[]): Promise<any> {
const client = await this.pool.connect();
try {
const result = await client.query(text, params);
return result;
} finally {
client.release();
}
return this.backupService.query(text, params);
}
async getClient(): Promise<PoolClient> {
return await this.pool.connect();
return this.backupService.getClient();
}
async close(): Promise<void> {
await this.pool.end();
if (this.redis.isOpen) {
await this.redis.disconnect();
}
return this.backupService.close();
}
// Initialize database tables
async initializeTables(): Promise<void> {
try {
// Create users table (matching the actual schema)
await this.query(`
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(255) PRIMARY KEY,
google_id VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL CHECK (role IN ('driver', 'coordinator', 'administrator')),
profile_picture_url TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP,
is_active BOOLEAN DEFAULT true,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
approval_status VARCHAR(20) DEFAULT 'pending' CHECK (approval_status IN ('pending', 'approved', 'denied'))
)
`);
// Add approval_status column if it doesn't exist (migration for existing databases)
await this.query(`
ALTER TABLE users
ADD COLUMN IF NOT EXISTS approval_status VARCHAR(20) DEFAULT 'pending' CHECK (approval_status IN ('pending', 'approved', 'denied'))
`);
// Create indexes
await this.query(`
CREATE INDEX IF NOT EXISTS idx_users_google_id ON users(google_id)
`);
await this.query(`
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)
`);
await this.query(`
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role)
`);
console.log('✅ Database tables initialized successfully');
} catch (error) {
console.error('❌ Failed to initialize database tables:', error);
throw error;
}
return this.backupService.initializeTables();
}
// User management methods
async createUser(user: {
id: string;
google_id: string;
email: string;
name: string;
profile_picture_url?: string;
role: string;
}): Promise<any> {
const query = `
INSERT INTO users (id, google_id, email, name, profile_picture_url, role, last_login)
VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP)
RETURNING *
`;
const values = [
user.id,
user.google_id,
user.email,
user.name,
user.profile_picture_url || null,
user.role
];
const result = await this.query(query, values);
console.log(`👤 Created user: ${user.name} (${user.email}) as ${user.role}`);
return result.rows[0];
// User methods from backup service
async createUser(user: any): Promise<any> {
return this.backupService.createUser(user);
}
async getUserByEmail(email: string): Promise<any> {
const query = 'SELECT * FROM users WHERE email = $1';
const result = await this.query(query, [email]);
return result.rows[0] || null;
return this.backupService.getUserByEmail(email);
}
async getUserById(id: string): Promise<any> {
const query = 'SELECT * FROM users WHERE id = $1';
const result = await this.query(query, [id]);
return result.rows[0] || null;
}
async getAllUsers(): Promise<any[]> {
const query = 'SELECT * FROM users ORDER BY created_at ASC';
const result = await this.query(query);
return result.rows;
return this.backupService.getUserById(id);
}
async updateUserRole(email: string, role: string): Promise<any> {
const query = `
UPDATE users
SET role = $1, updated_at = CURRENT_TIMESTAMP
WHERE email = $2
RETURNING *
`;
const result = await this.query(query, [role, email]);
if (result.rows[0]) {
console.log(`👤 Updated user role: ${result.rows[0].name} (${email}) -> ${role}`);
}
return result.rows[0] || null;
return this.backupService.updateUserRole(email, role);
}
async updateUserLastSignIn(email: string): Promise<any> {
const query = `
UPDATE users
SET last_login = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
WHERE email = $1
RETURNING *
`;
const result = await this.query(query, [email]);
return result.rows[0] || null;
}
async deleteUser(email: string): Promise<any> {
const query = 'DELETE FROM users WHERE email = $1 RETURNING *';
const result = await this.query(query, [email]);
if (result.rows[0]) {
console.log(`👤 Deleted user: ${result.rows[0].name} (${email})`);
}
return result.rows[0] || null;
return this.backupService.updateUserLastSignIn(email);
}
async getUserCount(): Promise<number> {
const query = 'SELECT COUNT(*) as count FROM users';
return this.backupService.getUserCount();
}
async updateUserApprovalStatus(email: string, status: 'pending' | 'approved' | 'denied'): Promise<any> {
return this.backupService.updateUserApprovalStatus(email, status);
}
async getApprovedUserCount(): Promise<number> {
return this.backupService.getApprovedUserCount();
}
async getAllUsers(): Promise<any[]> {
return this.backupService.getAllUsers();
}
async deleteUser(email: string): Promise<boolean> {
return this.backupService.deleteUser(email);
}
async getPendingUsers(): Promise<any[]> {
return this.backupService.getPendingUsers();
}
// NEW: Enhanced user management methods
async completeUserOnboarding(email: string, onboardingData: any): Promise<any> {
const query = `
UPDATE users
SET phone = $1,
organization = $2,
onboarding_data = $3,
updated_at = CURRENT_TIMESTAMP
WHERE email = $4
RETURNING *
`;
const result = await this.query(query, [
onboardingData.phone,
onboardingData.organization,
JSON.stringify(onboardingData),
email
]);
return result.rows[0] || null;
}
async approveUser(userEmail: string, approvedBy: string, newRole?: string): Promise<any> {
const query = `
UPDATE users
SET status = 'active',
approval_status = 'approved',
approved_by = $1,
approved_at = CURRENT_TIMESTAMP,
role = COALESCE($2, role),
updated_at = CURRENT_TIMESTAMP
WHERE email = $3
RETURNING *
`;
const result = await this.query(query, [approvedBy, newRole, userEmail]);
// Log audit
if (result.rows[0]) {
await this.createAuditLog('user_approved', userEmail, approvedBy, { newRole });
}
return result.rows[0] || null;
}
async rejectUser(userEmail: string, rejectedBy: string, reason?: string): Promise<any> {
const query = `
UPDATE users
SET status = 'deactivated',
approval_status = 'denied',
rejected_by = $1,
rejected_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE email = $2
RETURNING *
`;
const result = await this.query(query, [rejectedBy, userEmail]);
// Log audit
if (result.rows[0]) {
await this.createAuditLog('user_rejected', userEmail, rejectedBy, { reason });
}
return result.rows[0] || null;
}
async deactivateUser(userEmail: string, deactivatedBy: string): Promise<any> {
const query = `
UPDATE users
SET status = 'deactivated',
deactivated_by = $1,
deactivated_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE email = $2
RETURNING *
`;
const result = await this.query(query, [deactivatedBy, userEmail]);
// Log audit
if (result.rows[0]) {
await this.createAuditLog('user_deactivated', userEmail, deactivatedBy, {});
}
return result.rows[0] || null;
}
async reactivateUser(userEmail: string, reactivatedBy: string): Promise<any> {
const query = `
UPDATE users
SET status = 'active',
deactivated_by = NULL,
deactivated_at = NULL,
updated_at = CURRENT_TIMESTAMP
WHERE email = $1
RETURNING *
`;
const result = await this.query(query, [userEmail]);
// Log audit
if (result.rows[0]) {
await this.createAuditLog('user_reactivated', userEmail, reactivatedBy, {});
}
return result.rows[0] || null;
}
async createAuditLog(action: string, userEmail: string, performedBy: string, details: any): Promise<void> {
const query = `
INSERT INTO user_audit_log (action, user_email, performed_by, action_details)
VALUES ($1, $2, $3, $4)
`;
await this.query(query, [action, userEmail, performedBy, JSON.stringify(details)]);
}
async getUserAuditLog(userEmail: string): Promise<any[]> {
const query = `
SELECT * FROM user_audit_log
WHERE user_email = $1
ORDER BY created_at DESC
`;
const result = await this.query(query, [userEmail]);
return result.rows;
}
async getUsersWithFilters(filters: {
status?: string;
role?: string;
search?: string;
}): Promise<any[]> {
let query = 'SELECT * FROM users WHERE 1=1';
const params: any[] = [];
let paramIndex = 1;
if (filters.status) {
query += ` AND status = $${paramIndex}`;
params.push(filters.status);
paramIndex++;
}
if (filters.role) {
query += ` AND role = $${paramIndex}`;
params.push(filters.role);
paramIndex++;
}
if (filters.search) {
query += ` AND (LOWER(name) LIKE LOWER($${paramIndex}) OR LOWER(email) LIKE LOWER($${paramIndex}) OR LOWER(organization) LIKE LOWER($${paramIndex}))`;
params.push(`%${filters.search}%`);
paramIndex++;
}
query += ' ORDER BY created_at DESC';
const result = await this.query(query, params);
return result.rows;
}
// Fix for first user admin issue
async getActiveUserCount(): Promise<number> {
const query = "SELECT COUNT(*) as count FROM users WHERE status = 'active'";
const result = await this.query(query);
return parseInt(result.rows[0].count);
}
// User approval methods
async updateUserApprovalStatus(email: string, status: 'pending' | 'approved' | 'denied'): Promise<any> {
const query = `
UPDATE users
SET approval_status = $1, updated_at = CURRENT_TIMESTAMP
WHERE email = $2
RETURNING *
`;
const result = await this.query(query, [status, email]);
if (result.rows[0]) {
console.log(`👤 Updated user approval: ${result.rows[0].name} (${email}) -> ${status}`);
}
return result.rows[0] || null;
async isFirstUser(): Promise<boolean> {
return this.backupService.isFirstUser();
}
async getPendingUsers(): Promise<any[]> {
const query = 'SELECT * FROM users WHERE approval_status = $1 ORDER BY created_at ASC';
const result = await this.query(query, ['pending']);
return result.rows;
// VIP methods from backup service
async createVip(vip: any): Promise<any> {
return this.backupService.createVip(vip);
}
async getApprovedUserCount(): Promise<number> {
const query = 'SELECT COUNT(*) as count FROM users WHERE approval_status = $1';
const result = await this.query(query, ['approved']);
return parseInt(result.rows[0].count);
async getVipById(id: string): Promise<any> {
return this.backupService.getVipById(id);
}
// Initialize all database tables and schema
async initializeDatabase(): Promise<void> {
try {
await this.initializeTables();
await this.initializeVipTables();
// Approve all existing users (migration for approval system)
await this.query(`
UPDATE users
SET approval_status = 'approved'
WHERE approval_status IS NULL OR approval_status = 'pending'
`);
console.log('✅ Approved all existing users');
console.log('✅ Database schema initialization completed');
} catch (error) {
console.error('❌ Failed to initialize database schema:', error);
throw error;
}
async getAllVips(): Promise<any[]> {
return this.backupService.getAllVips();
}
// VIP table initialization using the correct schema
async initializeVipTables(): Promise<void> {
try {
// Check if VIPs table exists and has the correct schema
const tableExists = await this.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'vips'
)
`);
if (tableExists.rows[0].exists) {
// Check if the table has the correct columns
const columnCheck = await this.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'vips'
AND column_name = 'organization'
`);
if (columnCheck.rows.length === 0) {
console.log('🔄 Migrating VIPs table to new schema...');
// Drop the old table and recreate with correct schema
await this.query(`DROP TABLE IF EXISTS vips CASCADE`);
}
}
// Create VIPs table with correct schema matching enhancedDataService expectations
await this.query(`
CREATE TABLE IF NOT EXISTS vips (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
organization VARCHAR(255) NOT NULL,
department VARCHAR(255) DEFAULT 'Office of Development',
transport_mode VARCHAR(50) NOT NULL CHECK (transport_mode IN ('flight', 'self-driving')),
expected_arrival TIMESTAMP,
needs_airport_pickup BOOLEAN DEFAULT false,
needs_venue_transport BOOLEAN DEFAULT true,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Create flights table (for VIPs with flight transport)
await this.query(`
CREATE TABLE IF NOT EXISTS flights (
id SERIAL PRIMARY KEY,
vip_id VARCHAR(255) REFERENCES vips(id) ON DELETE CASCADE,
flight_number VARCHAR(50) NOT NULL,
flight_date DATE NOT NULL,
segment INTEGER NOT NULL,
departure_airport VARCHAR(10),
arrival_airport VARCHAR(10),
scheduled_departure TIMESTAMP,
scheduled_arrival TIMESTAMP,
actual_departure TIMESTAMP,
actual_arrival TIMESTAMP,
status VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Check and migrate drivers table
const driversTableExists = await this.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'drivers'
)
`);
if (driversTableExists.rows[0].exists) {
// Check if drivers table has the correct schema (phone column and department column)
const driversSchemaCheck = await this.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'drivers'
AND column_name IN ('phone', 'department')
`);
if (driversSchemaCheck.rows.length < 2) {
console.log('🔄 Migrating drivers table to new schema...');
await this.query(`DROP TABLE IF EXISTS drivers CASCADE`);
}
}
// Create drivers table with correct schema
await this.query(`
CREATE TABLE IF NOT EXISTS drivers (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
phone VARCHAR(50) NOT NULL,
department VARCHAR(255) DEFAULT 'Office of Development',
user_id VARCHAR(255) REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Check and migrate schedule_events table
const scheduleTableExists = await this.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'schedule_events'
)
`);
if (!scheduleTableExists.rows[0].exists) {
// Check for old 'schedules' table and drop it
const oldScheduleExists = await this.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'schedules'
)
`);
if (oldScheduleExists.rows[0].exists) {
console.log('🔄 Migrating schedules table to schedule_events...');
await this.query(`DROP TABLE IF EXISTS schedules CASCADE`);
}
}
// Create schedule_events table
await this.query(`
CREATE TABLE IF NOT EXISTS schedule_events (
id VARCHAR(255) PRIMARY KEY,
vip_id VARCHAR(255) REFERENCES vips(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
location VARCHAR(255) NOT NULL,
start_time TIMESTAMP NOT NULL,
end_time TIMESTAMP NOT NULL,
description TEXT,
assigned_driver_id VARCHAR(255) REFERENCES drivers(id) ON DELETE SET NULL,
status VARCHAR(50) DEFAULT 'scheduled' CHECK (status IN ('scheduled', 'in-progress', 'completed', 'cancelled')),
event_type VARCHAR(50) NOT NULL CHECK (event_type IN ('transport', 'meeting', 'event', 'meal', 'accommodation')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Create system_setup table for tracking initial setup
await this.query(`
CREATE TABLE IF NOT EXISTS system_setup (
id SERIAL PRIMARY KEY,
setup_completed BOOLEAN DEFAULT false,
first_admin_created BOOLEAN DEFAULT false,
setup_date TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Create admin_settings table
await this.query(`
CREATE TABLE IF NOT EXISTS admin_settings (
id SERIAL PRIMARY KEY,
setting_key VARCHAR(255) UNIQUE NOT NULL,
setting_value TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Create indexes for better performance
await this.query(`CREATE INDEX IF NOT EXISTS idx_vips_transport_mode ON vips(transport_mode)`);
await this.query(`CREATE INDEX IF NOT EXISTS idx_flights_vip_id ON flights(vip_id)`);
await this.query(`CREATE INDEX IF NOT EXISTS idx_flights_date ON flights(flight_date)`);
await this.query(`CREATE INDEX IF NOT EXISTS idx_schedule_events_vip_id ON schedule_events(vip_id)`);
await this.query(`CREATE INDEX IF NOT EXISTS idx_schedule_events_driver_id ON schedule_events(assigned_driver_id)`);
await this.query(`CREATE INDEX IF NOT EXISTS idx_schedule_events_start_time ON schedule_events(start_time)`);
await this.query(`CREATE INDEX IF NOT EXISTS idx_schedule_events_status ON schedule_events(status)`);
await this.query(`CREATE INDEX IF NOT EXISTS idx_drivers_user_id ON drivers(user_id)`);
// Create updated_at trigger function
await this.query(`
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql'
`);
// Create triggers for updated_at (drop if exists first)
await this.query(`DROP TRIGGER IF EXISTS update_vips_updated_at ON vips`);
await this.query(`DROP TRIGGER IF EXISTS update_flights_updated_at ON flights`);
await this.query(`DROP TRIGGER IF EXISTS update_drivers_updated_at ON drivers`);
await this.query(`DROP TRIGGER IF EXISTS update_schedule_events_updated_at ON schedule_events`);
await this.query(`DROP TRIGGER IF EXISTS update_admin_settings_updated_at ON admin_settings`);
await this.query(`CREATE TRIGGER update_vips_updated_at BEFORE UPDATE ON vips FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()`);
await this.query(`CREATE TRIGGER update_flights_updated_at BEFORE UPDATE ON flights FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()`);
await this.query(`CREATE TRIGGER update_drivers_updated_at BEFORE UPDATE ON drivers FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()`);
await this.query(`CREATE TRIGGER update_schedule_events_updated_at BEFORE UPDATE ON schedule_events FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()`);
await this.query(`CREATE TRIGGER update_admin_settings_updated_at BEFORE UPDATE ON admin_settings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()`);
console.log('✅ VIP Coordinator database schema initialized successfully');
} catch (error) {
console.error('❌ Failed to initialize VIP tables:', error);
throw error;
}
async updateVip(id: string, vip: any): Promise<any> {
return this.backupService.updateVip(id, vip);
}
// Redis-based driver location tracking
async getDriverLocation(driverId: string): Promise<{ lat: number; lng: number } | null> {
try {
if (!this.redis.isOpen) {
await this.redis.connect();
}
const location = await this.redis.hGetAll(`driver:${driverId}:location`);
if (location && location.lat && location.lng) {
return {
lat: parseFloat(location.lat),
lng: parseFloat(location.lng)
};
}
return null;
} catch (error) {
console.error('❌ Error getting driver location from Redis:', error);
return null;
}
async deleteVip(id: string): Promise<boolean> {
return this.backupService.deleteVip(id);
}
async updateDriverLocation(driverId: string, location: { lat: number; lng: number }): Promise<void> {
try {
if (!this.redis.isOpen) {
await this.redis.connect();
}
const key = `driver:${driverId}:location`;
await this.redis.hSet(key, {
lat: location.lat.toString(),
lng: location.lng.toString(),
updated_at: new Date().toISOString()
});
// Set expiration to 24 hours
await this.redis.expire(key, 24 * 60 * 60);
} catch (error) {
console.error('❌ Error updating driver location in Redis:', error);
}
async getVipsByDepartment(department: string): Promise<any[]> {
return this.backupService.getVipsByDepartment(department);
}
async getAllDriverLocations(): Promise<{ [driverId: string]: { lat: number; lng: number } }> {
try {
if (!this.redis.isOpen) {
await this.redis.connect();
}
const keys = await this.redis.keys('driver:*:location');
const locations: { [driverId: string]: { lat: number; lng: number } } = {};
for (const key of keys) {
const driverId = key.split(':')[1];
const location = await this.redis.hGetAll(key);
if (location && location.lat && location.lng) {
locations[driverId] = {
lat: parseFloat(location.lat),
lng: parseFloat(location.lng)
};
}
}
return locations;
} catch (error) {
console.error('❌ Error getting all driver locations from Redis:', error);
return {};
}
// Driver methods from backup service
async createDriver(driver: any): Promise<any> {
return this.backupService.createDriver(driver);
}
async removeDriverLocation(driverId: string): Promise<void> {
try {
if (!this.redis.isOpen) {
await this.redis.connect();
}
await this.redis.del(`driver:${driverId}:location`);
} catch (error) {
console.error('❌ Error removing driver location from Redis:', error);
}
async getDriverById(id: string): Promise<any> {
return this.backupService.getDriverById(id);
}
async getAllDrivers(): Promise<any[]> {
return this.backupService.getAllDrivers();
}
async updateDriver(id: string, driver: any): Promise<any> {
return this.backupService.updateDriver(id, driver);
}
async deleteDriver(id: string): Promise<boolean> {
return this.backupService.deleteDriver(id);
}
async getDriversByDepartment(department: string): Promise<any[]> {
return this.backupService.getDriversByDepartment(department);
}
async updateDriverLocation(id: string, location: any): Promise<any> {
return this.backupService.updateDriverLocation(id, location);
}
// Schedule methods from backup service
async createScheduleEvent(vipId: string, event: any): Promise<any> {
return this.backupService.createScheduleEvent(vipId, event);
}
async getScheduleByVipId(vipId: string): Promise<any[]> {
return this.backupService.getScheduleByVipId(vipId);
}
async updateScheduleEvent(vipId: string, eventId: string, event: any): Promise<any> {
return this.backupService.updateScheduleEvent(vipId, eventId, event);
}
async deleteScheduleEvent(vipId: string, eventId: string): Promise<boolean> {
return this.backupService.deleteScheduleEvent(vipId, eventId);
}
async getAllScheduleEvents(): Promise<any[]> {
return this.backupService.getAllScheduleEvents();
}
async getScheduleEventsByDateRange(startDate: Date, endDate: Date): Promise<any[]> {
return this.backupService.getScheduleEventsByDateRange(startDate, endDate);
}
}
export default new DatabaseService();
// Export singleton instance
const databaseService = new EnhancedDatabaseService();
export default databaseService;

View File

@@ -8,10 +8,13 @@ export interface User {
name: string;
profile_picture_url?: string;
role: 'driver' | 'coordinator' | 'administrator';
status?: 'pending' | 'active' | 'deactivated';
created_at?: string;
last_login?: string;
is_active?: boolean;
updated_at?: string;
approval_status?: string;
onboardingData?: any;
}
class JWTKeyManager {
@@ -78,6 +81,9 @@ class JWTKeyManager {
name: user.name,
profile_picture_url: user.profile_picture_url,
role: user.role,
status: user.status,
approval_status: user.approval_status,
onboardingData: user.onboardingData,
iat: Math.floor(Date.now() / 1000) // Issued at time
};
@@ -102,7 +108,10 @@ class JWTKeyManager {
email: decoded.email,
name: decoded.name,
profile_picture_url: decoded.profile_picture_url,
role: decoded.role
role: decoded.role,
status: decoded.status,
approval_status: decoded.approval_status,
onboardingData: decoded.onboardingData
};
} catch (error) {
// Try previous secret during grace period
@@ -121,7 +130,10 @@ class JWTKeyManager {
email: decoded.email,
name: decoded.name,
profile_picture_url: decoded.profile_picture_url,
role: decoded.role
role: decoded.role,
status: decoded.status,
approval_status: decoded.approval_status,
onboardingData: decoded.onboardingData
};
} catch (gracePeriodError) {
console.log('❌ Token verification failed with both current and previous keys');

View File

@@ -17,5 +17,5 @@
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
"exclude": ["node_modules", "dist", "src/**/*.original.ts", "src/**/backup-services/**", "src/routes/simpleAuth.ts", "src/config/simpleAuth.ts"]
}