the beginning of the idiots
This commit is contained in:
13
qwen/nodejs/.dockerignore
Normal file
13
qwen/nodejs/.dockerignore
Normal file
@@ -0,0 +1,13 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
.env
|
||||
.nyc_output
|
||||
coverage
|
||||
.nyc_output
|
||||
.coverage
|
||||
.coverage/
|
||||
.vscode
|
||||
.DS_Store
|
||||
9
qwen/nodejs/.env
Normal file
9
qwen/nodejs/.env
Normal file
@@ -0,0 +1,9 @@
|
||||
NODE_ENV=development
|
||||
PORT=19000
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=moh_portal
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=postgres
|
||||
JWT_SECRET=secret_key_for_jwt_tokens
|
||||
SESSION_SECRET=secret_key_for_session
|
||||
85
qwen/nodejs/AGENTS.md
Normal file
85
qwen/nodejs/AGENTS.md
Normal file
@@ -0,0 +1,85 @@
|
||||
Do not perform any operations on the host other than git and docker / docker compose operations
|
||||
|
||||
Utilize docker containers for all work done in this repository.
|
||||
Utilize a docker artifact related name prefix of <codingagent>-<language>-<function> to make it easy to manage all the docker artifacts.
|
||||
|
||||
Only expose the main app web interface over the network. All other ports should remain on a per stack docker network.
|
||||
|
||||
Here are the port assignments for the containers
|
||||
|
||||
|
||||
gemini/go 12000
|
||||
gemini/hack 13000
|
||||
gemini/nodejs 14000
|
||||
gemini/php 15000
|
||||
gemini/python 16000
|
||||
|
||||
qwen/go 17000
|
||||
qwen//hack 18000
|
||||
qwen/nodejs 19000
|
||||
qwen/php 20000
|
||||
qwen/python 21000
|
||||
|
||||
copilot/go 22000
|
||||
copilot/gemini/hack 23000
|
||||
copilot/nodejs 24000
|
||||
copilot/php 25000
|
||||
copilot/python 26000
|
||||
|
||||
The purpose of this repository is to test three coding agents:
|
||||
|
||||
qwen
|
||||
copilot
|
||||
gemini
|
||||
|
||||
and five programming languages:
|
||||
|
||||
go
|
||||
hack
|
||||
nodejs
|
||||
php
|
||||
python
|
||||
|
||||
against the following programming test:
|
||||
|
||||
I have purchased the domain name MerchantsOfHope.org and its intened to be the consulting/contracting arm of TSYS Group.
|
||||
It will need to handle:
|
||||
|
||||
- Multiple independent tennants (TSYS Group has dozens and dozens of lines of business, all fully isolated from each other)
|
||||
- OIDC and social media login
|
||||
|
||||
It will need to handle all functionality of a recuriting platform:
|
||||
|
||||
- Job seekers browsing postions and posting resumes/going through the application process
|
||||
- Job providrrs managing the lifecycle of positions and applications
|
||||
|
||||
This should be pretty simple and off the shelf, bog standard type workflows.
|
||||
|
||||
Presume USA law compliance only.
|
||||
|
||||
No need for anything other than English to be supported.
|
||||
|
||||
Accessibility is critical, we have a number of US Government contracts and they mandate accessibility.
|
||||
|
||||
Also we need to be compliant with PCI, GDPR, SOC, FedRamp etc.
|
||||
|
||||
|
||||
Use the name of the directory you are in to determine the programming language to use.
|
||||
|
||||
Do not create any artifacts outside of the directory you are in now.
|
||||
|
||||
You may manage the contents of this directory as you see fit.
|
||||
|
||||
Please keep it well organized.
|
||||
|
||||
Follow Test Driven Development for all your work.
|
||||
|
||||
Create and maintain a docker-compose.yml file with your service dependenices
|
||||
|
||||
Ship this application as a docker container.
|
||||
|
||||
This will eventually be deployed into a k8s cluster , so make sure to take that into account.
|
||||
|
||||
Also follow all best common practices for security, QA, engineering, SRE/devops etc.
|
||||
|
||||
Make it happen.
|
||||
25
qwen/nodejs/Dockerfile
Normal file
25
qwen/nodejs/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
# Use Node.js 18 LTS as the base image
|
||||
FROM node:18-alpine
|
||||
|
||||
# Set working directory in the container
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json and package-lock.json (if available)
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci --only=production
|
||||
|
||||
# Copy the rest of the application code
|
||||
COPY . .
|
||||
|
||||
# Create a non-root user and switch to it
|
||||
RUN addgroup -g 1001 -S nodejs
|
||||
RUN adduser -S nextjs -u 1001
|
||||
USER nextjs
|
||||
|
||||
# Expose the port the app runs on
|
||||
EXPOSE 19000
|
||||
|
||||
# Define the command to run the application
|
||||
CMD ["npm", "start"]
|
||||
78
qwen/nodejs/controllers/authController.js
Normal file
78
qwen/nodejs/controllers/authController.js
Normal file
@@ -0,0 +1,78 @@
|
||||
// controllers/authController.js
|
||||
const authService = require('../services/authService');
|
||||
|
||||
const login = async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({ error: 'Email and password are required' });
|
||||
}
|
||||
|
||||
const result = await authService.login(email, password);
|
||||
|
||||
if (result.error) {
|
||||
return res.status(401).json({ error: result.error });
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
message: 'Login successful',
|
||||
user: result.user,
|
||||
token: result.token
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
const register = async (req, res) => {
|
||||
try {
|
||||
const { email, password, firstName, lastName, userType } = req.body;
|
||||
|
||||
if (!email || !password || !firstName || !lastName || !userType) {
|
||||
return res.status(400).json({ error: 'All fields are required' });
|
||||
}
|
||||
|
||||
const result = await authService.register(email, password, firstName, lastName, userType, req.tenantId);
|
||||
|
||||
if (result.error) {
|
||||
return res.status(400).json({ error: result.error });
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Registration successful',
|
||||
user: result.user
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
const logout = async (req, res) => {
|
||||
try {
|
||||
// In a real implementation, you might invalidate the JWT token
|
||||
res.status(200).json({ message: 'Logout successful' });
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
const getCurrentUser = async (req, res) => {
|
||||
try {
|
||||
// This would use middleware to verify JWT and extract user info
|
||||
res.status(200).json({ user: req.user });
|
||||
} catch (error) {
|
||||
console.error('Get current user error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
getCurrentUser
|
||||
};
|
||||
154
qwen/nodejs/controllers/tenantController.js
Normal file
154
qwen/nodejs/controllers/tenantController.js
Normal file
@@ -0,0 +1,154 @@
|
||||
// controllers/tenantController.js
|
||||
// Controller for tenant-related operations
|
||||
|
||||
// Mock tenant storage - this would be a database in production
|
||||
const tenants = [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default Tenant',
|
||||
subdomain: 'default',
|
||||
settings: {
|
||||
allowedDomains: ['localhost', 'merchants-of-hope.org'],
|
||||
features: ['job-posting', 'resume-uploading', 'application-tracking']
|
||||
},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
];
|
||||
|
||||
const getTenant = async (req, res) => {
|
||||
try {
|
||||
const { tenantId } = req.params;
|
||||
|
||||
// Find the requested tenant
|
||||
const tenant = tenants.find(t => t.id === tenantId || t.subdomain === tenantId);
|
||||
|
||||
if (!tenant) {
|
||||
return res.status(404).json({ error: 'Tenant not found' });
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
tenant: {
|
||||
id: tenant.id,
|
||||
name: tenant.name,
|
||||
subdomain: tenant.subdomain,
|
||||
settings: tenant.settings,
|
||||
createdAt: tenant.createdAt,
|
||||
updatedAt: tenant.updatedAt
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get tenant error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
const createTenant = async (req, res) => {
|
||||
try {
|
||||
const { name, subdomain, settings } = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!name || !subdomain) {
|
||||
return res.status(400).json({ error: 'Name and subdomain are required' });
|
||||
}
|
||||
|
||||
// Check if tenant already exists
|
||||
const existingTenant = tenants.find(t => t.subdomain === subdomain || t.name === name);
|
||||
if (existingTenant) {
|
||||
return res.status(409).json({ error: 'Tenant with this name or subdomain already exists' });
|
||||
}
|
||||
|
||||
// Create new tenant
|
||||
const newTenant = {
|
||||
id: require('uuid').v4(),
|
||||
name,
|
||||
subdomain,
|
||||
settings: settings || {},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
tenants.push(newTenant);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Tenant created successfully',
|
||||
tenant: {
|
||||
id: newTenant.id,
|
||||
name: newTenant.name,
|
||||
subdomain: newTenant.subdomain,
|
||||
settings: newTenant.settings
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Create tenant error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
const updateTenant = async (req, res) => {
|
||||
try {
|
||||
const { tenantId } = req.params;
|
||||
const { name, settings } = req.body;
|
||||
|
||||
// Find the tenant to update
|
||||
const tenantIndex = tenants.findIndex(t => t.id === tenantId || t.subdomain === tenantId);
|
||||
|
||||
if (tenantIndex === -1) {
|
||||
return res.status(404).json({ error: 'Tenant not found' });
|
||||
}
|
||||
|
||||
// Update tenant properties
|
||||
if (name) {
|
||||
tenants[tenantIndex].name = name;
|
||||
}
|
||||
if (settings) {
|
||||
tenants[tenantIndex].settings = { ...tenants[tenantIndex].settings, ...settings };
|
||||
}
|
||||
tenants[tenantIndex].updatedAt = new Date();
|
||||
|
||||
res.status(200).json({
|
||||
message: 'Tenant updated successfully',
|
||||
tenant: {
|
||||
id: tenants[tenantIndex].id,
|
||||
name: tenants[tenantIndex].name,
|
||||
subdomain: tenants[tenantIndex].subdomain,
|
||||
settings: tenants[tenantIndex].settings,
|
||||
updatedAt: tenants[tenantIndex].updatedAt
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update tenant error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteTenant = async (req, res) => {
|
||||
try {
|
||||
const { tenantId } = req.params;
|
||||
|
||||
// Find the tenant to delete
|
||||
const tenantIndex = tenants.findIndex(t => t.id === tenantId || t.subdomain === tenantId);
|
||||
|
||||
if (tenantIndex === -1) {
|
||||
return res.status(404).json({ error: 'Tenant not found' });
|
||||
}
|
||||
|
||||
// In a real implementation, you'd want to also delete all related data
|
||||
// For now, we'll just remove the tenant from our mock storage
|
||||
tenants.splice(tenantIndex, 1);
|
||||
|
||||
res.status(200).json({
|
||||
message: 'Tenant deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Delete tenant error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getTenant,
|
||||
createTenant,
|
||||
updateTenant,
|
||||
deleteTenant
|
||||
};
|
||||
79
qwen/nodejs/docker-compose.yml
Normal file
79
qwen/nodejs/docker-compose.yml
Normal file
@@ -0,0 +1,79 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Main application
|
||||
app:
|
||||
build: .
|
||||
container_name: qwen-nodejs-app
|
||||
ports:
|
||||
- "19000:19000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=19000
|
||||
- DB_HOST=postgres
|
||||
- DB_PORT=5432
|
||||
- DB_NAME=${DB_NAME:-moh_portal}
|
||||
- DB_USER=${DB_USER:-postgres}
|
||||
- DB_PASSWORD=${DB_PASSWORD:-postgres}
|
||||
- JWT_SECRET=${JWT_SECRET:-secret_key_for_jwt_tokens}
|
||||
- SESSION_SECRET=${SESSION_SECRET:-secret_key_for_session}
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
networks:
|
||||
- moh-network
|
||||
restart: unless-stopped
|
||||
|
||||
# PostgreSQL database
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: qwen-nodejs-postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
- POSTGRES_DB=${DB_NAME:-moh_portal}
|
||||
- POSTGRES_USER=${DB_USER:-postgres}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD:-postgres}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
networks:
|
||||
- moh-network
|
||||
restart: unless-stopped
|
||||
|
||||
# Redis for session storage and caching
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: qwen-nodejs-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- moh-network
|
||||
restart: unless-stopped
|
||||
command: redis-server --appendonly yes
|
||||
|
||||
# Nginx as reverse proxy (optional, can be added later)
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: qwen-nodejs-nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./ssl:/etc/nginx/ssl
|
||||
depends_on:
|
||||
- app
|
||||
networks:
|
||||
- moh-network
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
|
||||
networks:
|
||||
moh-network:
|
||||
driver: bridge
|
||||
127
qwen/nodejs/index.js
Normal file
127
qwen/nodejs/index.js
Normal file
@@ -0,0 +1,127 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const path = require('path');
|
||||
const http = require('http');
|
||||
|
||||
// Initialize Express app
|
||||
const app = express();
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
scriptSrc: ["'self'"],
|
||||
imgSrc: ["'self'", "data:", "https:"],
|
||||
},
|
||||
},
|
||||
crossOriginEmbedderPolicy: false, // Needed for some static assets
|
||||
}));
|
||||
|
||||
app.use(cors({
|
||||
origin: process.env.NODE_ENV === 'production'
|
||||
? [process.env.FRONTEND_URL]
|
||||
: ['http://localhost:3000', 'http://localhost:19000'],
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // limit each IP to 100 requests per windowMs
|
||||
message: 'Too many requests from this IP, please try again later.'
|
||||
});
|
||||
app.use(limiter);
|
||||
|
||||
// Body parsing middleware
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Static files
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Tenant resolution and isolation middleware
|
||||
const { resolveTenant, enforceTenantIsolation } = require('./middleware/tenant');
|
||||
app.use(resolveTenant);
|
||||
app.use(enforceTenantIsolation);
|
||||
|
||||
// Import and use routes
|
||||
const authRoutes = require('./routes/auth');
|
||||
const jobSeekerRoutes = require('./routes/jobSeeker');
|
||||
const jobProviderRoutes = require('./routes/jobProvider');
|
||||
const tenantRoutes = require('./routes/tenant');
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/job-seekers', jobSeekerRoutes);
|
||||
app.use('/api/job-providers', jobProviderRoutes);
|
||||
app.use('/api/tenants', tenantRoutes);
|
||||
|
||||
// Basic route
|
||||
app.get('/', (req, res) => {
|
||||
res.json({
|
||||
message: 'Welcome to MerchantsOfHope.org - TSYS Group Recruiting Platform',
|
||||
status: 'running',
|
||||
timestamp: new Date().toISOString(),
|
||||
tenantId: req.tenantId
|
||||
});
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.status(200).json({
|
||||
status: 'OK',
|
||||
timestamp: new Date().toISOString(),
|
||||
service: 'MOH Portal API',
|
||||
tenantId: req.tenantId
|
||||
});
|
||||
});
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).json({
|
||||
error: 'Something went wrong!',
|
||||
message: process.env.NODE_ENV === 'development' ? err.message : 'Internal server error',
|
||||
tenantId: req.tenantId
|
||||
});
|
||||
});
|
||||
|
||||
// 404 handler
|
||||
app.use('*', (req, res) => {
|
||||
res.status(404).json({
|
||||
error: 'Route not found',
|
||||
tenantId: req.tenantId
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
||||
// Only start the server if this file is run directly (not imported for testing)
|
||||
if (require.main === module) {
|
||||
const server = http.createServer(app);
|
||||
const PORT = process.env.PORT || 19000;
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`MerchantsOfHope.org server running on port ${PORT}`);
|
||||
console.log(`Tenant identification enabled - using tenant: default or from request`);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('SIGTERM received, shutting down gracefully');
|
||||
server.close(() => {
|
||||
console.log('Process terminated');
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('SIGINT received, shutting down gracefully');
|
||||
server.close(() => {
|
||||
console.log('Process terminated');
|
||||
});
|
||||
});
|
||||
}
|
||||
16
qwen/nodejs/jest.config.js
Normal file
16
qwen/nodejs/jest.config.js
Normal file
@@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
collectCoverageFrom: [
|
||||
'**/*.{js,jsx,ts,tsx}',
|
||||
'!**/node_modules/**',
|
||||
'!**/coverage/**',
|
||||
'!**/dist/**',
|
||||
'!**/build/**',
|
||||
],
|
||||
testMatch: [
|
||||
'<rootDir>/tests/**/*.test.{js,jsx,ts,tsx}',
|
||||
'<rootDir>/**/?(*.)+(spec|test).{js,jsx,ts,tsx}',
|
||||
],
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
|
||||
testTimeout: 30000,
|
||||
};
|
||||
89
qwen/nodejs/middleware/tenant.js
Normal file
89
qwen/nodejs/middleware/tenant.js
Normal file
@@ -0,0 +1,89 @@
|
||||
// middleware/tenant.js
|
||||
// Middleware to handle tenant-specific operations
|
||||
|
||||
// Mock tenant storage - in a real implementation this would be a database
|
||||
const tenants = [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default Tenant',
|
||||
subdomain: 'default',
|
||||
settings: {
|
||||
allowedDomains: ['localhost', 'merchants-of-hope.org'],
|
||||
features: ['job-posting', 'resume-uploading', 'application-tracking']
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Tenant resolution middleware
|
||||
const resolveTenant = async (req, res, next) => {
|
||||
let tenantId = null;
|
||||
|
||||
// Method 1: From subdomain (e.g., tenant1.merchants-of-hope.org)
|
||||
if (req.headers.host) {
|
||||
const hostParts = req.headers.host.split('.');
|
||||
if (hostParts.length >= 3 && hostParts[0] !== 'www') {
|
||||
tenantId = hostParts[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Method 2: From header (for development)
|
||||
if (!tenantId && req.headers['x-tenant-id']) {
|
||||
tenantId = req.headers['x-tenant-id'];
|
||||
}
|
||||
|
||||
// Method 3: From URL path (e.g., /tenant/tenant1/api/...)
|
||||
if (!tenantId && req.originalUrl.startsWith('/tenant/')) {
|
||||
const pathParts = req.originalUrl.split('/');
|
||||
if (pathParts.length > 2) {
|
||||
tenantId = pathParts[2];
|
||||
// Remove tenant from URL for further routing
|
||||
req.originalUrl = req.originalUrl.replace(`/tenant/${tenantId}`, '');
|
||||
req.url = req.url.replace(`/tenant/${tenantId}`, '');
|
||||
}
|
||||
}
|
||||
|
||||
// Default to 'default' tenant if none found
|
||||
if (!tenantId) {
|
||||
tenantId = 'default';
|
||||
}
|
||||
|
||||
// Find the tenant in our mock storage
|
||||
const tenant = tenants.find(t => t.id === tenantId || t.subdomain === tenantId);
|
||||
|
||||
if (!tenant && tenantId !== 'default') {
|
||||
return res.status(404).json({
|
||||
error: 'Tenant not found',
|
||||
tenantId: tenantId
|
||||
});
|
||||
}
|
||||
|
||||
// Set tenant in request object for other middleware/routes to use
|
||||
req.tenant = tenant || {
|
||||
id: 'default',
|
||||
name: 'Default Tenant',
|
||||
subdomain: 'default',
|
||||
settings: {}
|
||||
};
|
||||
|
||||
req.tenantId = req.tenant.id;
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// Middleware to enforce tenant isolation
|
||||
const enforceTenantIsolation = async (req, res, next) => {
|
||||
// In a real implementation, this would:
|
||||
// 1. Set up a database connection or context per tenant
|
||||
// 2. Ensure queries are scoped to the current tenant
|
||||
// 3. Apply tenant-specific security policies
|
||||
|
||||
// For now, we'll just log the tenant for debugging
|
||||
console.log(`Request for tenant: ${req.tenantId}`);
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
resolveTenant,
|
||||
enforceTenantIsolation
|
||||
};
|
||||
41
qwen/nodejs/models/Tenant.js
Normal file
41
qwen/nodejs/models/Tenant.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// models/Tenant.js
|
||||
// Tenant model definition
|
||||
|
||||
class Tenant {
|
||||
constructor(id, name, subdomain, settings, createdAt, updatedAt) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.subdomain = subdomain;
|
||||
this.settings = settings || {};
|
||||
this.createdAt = createdAt || new Date();
|
||||
this.updatedAt = updatedAt || new Date();
|
||||
}
|
||||
|
||||
// Static method to create a new tenant
|
||||
static create(tenantData) {
|
||||
const id = tenantData.id || require('uuid').v4();
|
||||
return new Tenant(
|
||||
id,
|
||||
tenantData.name,
|
||||
tenantData.subdomain,
|
||||
tenantData.settings
|
||||
);
|
||||
}
|
||||
|
||||
// Method to validate a tenant
|
||||
validate() {
|
||||
if (!this.name || !this.subdomain) {
|
||||
throw new Error('Tenant name and subdomain are required');
|
||||
}
|
||||
|
||||
// Validate subdomain format (alphanumeric and hyphens only)
|
||||
const subdomainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$/;
|
||||
if (!subdomainRegex.test(this.subdomain)) {
|
||||
throw new Error('Invalid subdomain format');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Tenant;
|
||||
50
qwen/nodejs/models/User.js
Normal file
50
qwen/nodejs/models/User.js
Normal file
@@ -0,0 +1,50 @@
|
||||
// models/User.js
|
||||
// User model definition
|
||||
|
||||
class User {
|
||||
constructor(id, email, passwordHash, firstName, lastName, userType, tenantId, createdAt, updatedAt) {
|
||||
this.id = id;
|
||||
this.email = email;
|
||||
this.passwordHash = passwordHash;
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
this.userType = userType; // 'job-seeker' or 'job-provider'
|
||||
this.tenantId = tenantId;
|
||||
this.createdAt = createdAt || new Date();
|
||||
this.updatedAt = updatedAt || new Date();
|
||||
}
|
||||
|
||||
// Static method to create a new user
|
||||
static create(userData) {
|
||||
const id = userData.id || require('uuid').v4();
|
||||
return new User(
|
||||
id,
|
||||
userData.email,
|
||||
userData.passwordHash,
|
||||
userData.firstName,
|
||||
userData.lastName,
|
||||
userData.userType,
|
||||
userData.tenantId
|
||||
);
|
||||
}
|
||||
|
||||
// Method to validate a user
|
||||
validate() {
|
||||
if (!this.email || !this.passwordHash || !this.firstName || !this.lastName || !this.userType || !this.tenantId) {
|
||||
throw new Error('Missing required fields');
|
||||
}
|
||||
|
||||
if (!['job-seeker', 'job-provider'].includes(this.userType)) {
|
||||
throw new Error('User type must be either job-seeker or job-provider');
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(this.email)) {
|
||||
throw new Error('Invalid email format');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = User;
|
||||
11
qwen/nodejs/models/index.js
Normal file
11
qwen/nodejs/models/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// models/index.js
|
||||
// This would typically connect to the database and export all models
|
||||
// For now, we'll define a simple structure
|
||||
|
||||
const User = require('./User');
|
||||
const Tenant = require('./Tenant');
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
Tenant
|
||||
};
|
||||
26
qwen/nodejs/nginx.conf
Normal file
26
qwen/nodejs/nginx.conf
Normal file
@@ -0,0 +1,26 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
upstream nodejs_backend {
|
||||
server app:19000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
location / {
|
||||
proxy_pass http://nodejs_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
qwen/nodejs/package.json
Normal file
51
qwen/nodejs/package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "moh-portal",
|
||||
"version": "1.0.0",
|
||||
"description": "MerchantsOfHope.org recruiting platform for TSYS Group",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "nodemon index.js",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"keywords": [
|
||||
"recruiting",
|
||||
"job-platform",
|
||||
"multi-tenant",
|
||||
"oidc"
|
||||
],
|
||||
"author": "TSYS Group",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"cors": "^2.8.5",
|
||||
"helmet": "^7.0.0",
|
||||
"express-rate-limit": "^6.10.0",
|
||||
"joi": "^17.9.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"pg": "^8.11.2",
|
||||
"sequelize": "^6.32.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"express-session": "^1.17.3",
|
||||
"connect-session-sequelize": "^7.1.7",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"uuid": "^9.0.0",
|
||||
"axios": "^1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1",
|
||||
"jest": "^29.6.2",
|
||||
"supertest": "^6.3.3",
|
||||
"eslint": "^8.47.0",
|
||||
"@babel/core": "^7.22.10",
|
||||
"@babel/preset-env": "^7.22.10",
|
||||
"babel-jest": "^29.6.2"
|
||||
}
|
||||
}
|
||||
17
qwen/nodejs/routes/auth.js
Normal file
17
qwen/nodejs/routes/auth.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { login, register, logout, getCurrentUser } = require('../controllers/authController');
|
||||
|
||||
// Login route
|
||||
router.post('/login', login);
|
||||
|
||||
// Register route
|
||||
router.post('/register', register);
|
||||
|
||||
// Logout route
|
||||
router.post('/logout', logout);
|
||||
|
||||
// Get current user
|
||||
router.get('/me', getCurrentUser);
|
||||
|
||||
module.exports = router;
|
||||
23
qwen/nodejs/routes/jobProvider.js
Normal file
23
qwen/nodejs/routes/jobProvider.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDashboard, createJob, updateJob, deleteJob, getApplications, manageApplication } = require('../controllers/jobProviderController');
|
||||
|
||||
// Get job provider dashboard
|
||||
router.get('/dashboard', getDashboard);
|
||||
|
||||
// Create a new job
|
||||
router.post('/jobs', createJob);
|
||||
|
||||
// Update a job
|
||||
router.put('/jobs/:jobId', updateJob);
|
||||
|
||||
// Delete a job
|
||||
router.delete('/jobs/:jobId', deleteJob);
|
||||
|
||||
// Get applications for job provider's jobs
|
||||
router.get('/applications', getApplications);
|
||||
|
||||
// Manage an application
|
||||
router.put('/applications/:applicationId', manageApplication);
|
||||
|
||||
module.exports = router;
|
||||
20
qwen/nodejs/routes/jobSeeker.js
Normal file
20
qwen/nodejs/routes/jobSeeker.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getProfile, updateProfile, uploadResume, getApplications, applyForJob } = require('../controllers/jobSeekerController');
|
||||
|
||||
// Get job seeker profile
|
||||
router.get('/profile', getProfile);
|
||||
|
||||
// Update job seeker profile
|
||||
router.put('/profile', updateProfile);
|
||||
|
||||
// Upload resume
|
||||
router.post('/resume', uploadResume);
|
||||
|
||||
// Get job seeker's applications
|
||||
router.get('/applications', getApplications);
|
||||
|
||||
// Apply for a job
|
||||
router.post('/apply/:jobId', applyForJob);
|
||||
|
||||
module.exports = router;
|
||||
17
qwen/nodejs/routes/tenant.js
Normal file
17
qwen/nodejs/routes/tenant.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getTenant, createTenant, updateTenant, deleteTenant } = require('../controllers/tenantController');
|
||||
|
||||
// Get tenant by ID
|
||||
router.get('/:tenantId', getTenant);
|
||||
|
||||
// Create a new tenant
|
||||
router.post('/', createTenant);
|
||||
|
||||
// Update tenant
|
||||
router.put('/:tenantId', updateTenant);
|
||||
|
||||
// Delete tenant
|
||||
router.delete('/:tenantId', deleteTenant);
|
||||
|
||||
module.exports = router;
|
||||
106
qwen/nodejs/services/authService.js
Normal file
106
qwen/nodejs/services/authService.js
Normal file
@@ -0,0 +1,106 @@
|
||||
// services/authService.js
|
||||
const jwt = require('jsonwebtoken');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { User } = require('../models'); // Assuming we have a User model
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret';
|
||||
|
||||
// Mock database - in real implementation, this would be a real database
|
||||
const users = [];
|
||||
|
||||
const login = async (email, password) => {
|
||||
try {
|
||||
// Find user by email
|
||||
const user = users.find(u => u.email === email);
|
||||
|
||||
if (!user) {
|
||||
return { error: 'Invalid email or password' };
|
||||
}
|
||||
|
||||
// Check password
|
||||
const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
return { error: 'Invalid email or password' };
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
const token = jwt.sign(
|
||||
{ userId: user.id, email: user.email, tenantId: user.tenantId },
|
||||
JWT_SECRET,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
// Return user info and token (excluding password)
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
userType: user.userType,
|
||||
tenantId: user.tenantId
|
||||
},
|
||||
token
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Login service error:', error);
|
||||
return { error: 'Internal server error' };
|
||||
}
|
||||
};
|
||||
|
||||
const register = async (email, password, firstName, lastName, userType, tenantId) => {
|
||||
try {
|
||||
// Check if user already exists
|
||||
const existingUser = users.find(u => u.email === email);
|
||||
|
||||
if (existingUser) {
|
||||
return { error: 'User with this email already exists' };
|
||||
}
|
||||
|
||||
// Validate user type
|
||||
if (!['job-seeker', 'job-provider'].includes(userType)) {
|
||||
return { error: 'User type must be either job-seeker or job-provider' };
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const saltRounds = 12;
|
||||
const passwordHash = await bcrypt.hash(password, saltRounds);
|
||||
|
||||
// Create new user
|
||||
const newUser = {
|
||||
id: uuidv4(),
|
||||
email,
|
||||
passwordHash,
|
||||
firstName,
|
||||
lastName,
|
||||
userType,
|
||||
tenantId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
users.push(newUser);
|
||||
|
||||
// Return user info (excluding password)
|
||||
return {
|
||||
user: {
|
||||
id: newUser.id,
|
||||
email: newUser.email,
|
||||
firstName: newUser.firstName,
|
||||
lastName: newUser.lastName,
|
||||
userType: newUser.userType,
|
||||
tenantId: newUser.tenantId
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Registration service error:', error);
|
||||
return { error: 'Internal server error' };
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
login,
|
||||
register
|
||||
};
|
||||
36
qwen/nodejs/tests/app.test.js
Normal file
36
qwen/nodejs/tests/app.test.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// tests/app.test.js
|
||||
const request = require('supertest');
|
||||
const app = require('../index');
|
||||
|
||||
describe('Main Application Routes', () => {
|
||||
test('GET / should return welcome message', async () => {
|
||||
const response = await request(app)
|
||||
.get('/')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('message');
|
||||
expect(response.body.message).toBe('Welcome to MerchantsOfHope.org - TSYS Group Recruiting Platform');
|
||||
expect(response.body).toHaveProperty('status');
|
||||
expect(response.body.status).toBe('running');
|
||||
});
|
||||
|
||||
test('GET /health should return health status', async () => {
|
||||
const response = await request(app)
|
||||
.get('/health')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('status');
|
||||
expect(response.body.status).toBe('OK');
|
||||
expect(response.body).toHaveProperty('service');
|
||||
expect(response.body.service).toBe('MOH Portal API');
|
||||
});
|
||||
|
||||
test('GET /nonexistent should return 404', async () => {
|
||||
const response = await request(app)
|
||||
.get('/nonexistent')
|
||||
.expect(404);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Route not found');
|
||||
});
|
||||
});
|
||||
11
qwen/nodejs/tests/setup.js
Normal file
11
qwen/nodejs/tests/setup.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// tests/setup.js
|
||||
// Setup file for Jest tests
|
||||
|
||||
// Mock environment variables
|
||||
process.env.JWT_SECRET = 'test_secret';
|
||||
process.env.DB_HOST = 'localhost';
|
||||
process.env.DB_USER = 'test_user';
|
||||
process.env.DB_PASSWORD = 'test_password';
|
||||
process.env.DB_NAME = 'test_db';
|
||||
|
||||
console.log('Jest test environment setup complete');
|
||||
Reference in New Issue
Block a user