the middle of the idiots
This commit is contained in:
191
qwen/hack/ARCHITECTURE.md
Normal file
191
qwen/hack/ARCHITECTURE.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# MerchantsOfHope.org - Architecture Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
MerchantsOfHope.org is a multi-tenant recruiting platform built with Hack/PHP, designed to serve multiple lines of business within TSYS Group while maintaining complete isolation between tenants.
|
||||
|
||||
## System Architecture
|
||||
|
||||
### High-Level Architecture
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ Load Balancer │────│ Nginx │────│ Application │
|
||||
└─────────────────┘ │ (Reverse Proxy)│ │ (PHP/Hack) │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
│
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌──────▼──────────┐
|
||||
│ Frontend │ │ Caching │ │ PostgreSQL │
|
||||
│ (React/Vue) │ │ (Redis) │ │ Database │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│
|
||||
┌─────────────▼──────────┐
|
||||
│ Mail Service │
|
||||
│ (SMTP/MailHog) │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
### Service Architecture
|
||||
- **Application Service**: Main PHP/Hack application serving business logic
|
||||
- **Database Service**: PostgreSQL for primary data storage
|
||||
- **Cache Service**: Redis for session storage and caching
|
||||
- **Mail Service**: SMTP service for email communications
|
||||
- **OAuth Services**: External providers for authentication (Google, GitHub)
|
||||
|
||||
## Multi-Tenancy Implementation
|
||||
|
||||
### Tenant Isolation Strategy
|
||||
|
||||
1. **Data Isolation**: Each tenant's data is isolated using a `tenant_id` column in all relevant tables
|
||||
2. **Request Context**: Tenant context is determined via subdomain (tenant.merchantsofhope.org) or path (merchantsofhope.org/tenant)
|
||||
3. **Database Queries**: All data access queries include tenant_id filters to prevent cross-tenant data access
|
||||
|
||||
### Tenant Resolution Flow
|
||||
1. Request comes in with hostname or path
|
||||
2. TenantMiddleware extracts tenant identifier
|
||||
3. TenantResolverService looks up tenant information
|
||||
4. Tenant information attached to request context
|
||||
5. All subsequent operations validate tenant access
|
||||
|
||||
## Security & Compliance
|
||||
|
||||
### Authentication & Authorization
|
||||
- **OIDC Integration**: Support for OpenID Connect providers
|
||||
- **Social Login**: Google and GitHub OAuth integration
|
||||
- **JWT Tokens**: Stateful authentication using JWT tokens
|
||||
- **Role-Based Access Control**: Different permissions for job seekers vs job providers
|
||||
|
||||
### Compliance Frameworks Implemented
|
||||
|
||||
#### 1. USA Employment Law Compliance
|
||||
- **Anti-Discrimination**: Validation to prevent discriminatory language in job postings
|
||||
- **Data Retention**: Automatic anonymization of personal data after required periods
|
||||
- **Audit Logging**: Complete audit trail for all data access
|
||||
|
||||
#### 2. Accessibility (Section 508/WCAG 2.1 AA)
|
||||
- **Semantic HTML**: Proper heading hierarchy and structure
|
||||
- **Alt Text**: Required for all images
|
||||
- **Form Labels**: Associated with input elements
|
||||
- **Color Contrast**: Sufficient contrast ratios
|
||||
|
||||
#### 3. PCI DSS Compliance
|
||||
- **No Sensitive Data Storage**: PAN, CVV, and track data are never stored
|
||||
- **Proper Masking**: When PAN is displayed, it's properly masked (first 6, last 4)
|
||||
- **Audit Logs**: Logging of all access to payment-related data
|
||||
|
||||
#### 4. GDPR Compliance
|
||||
- **Data Subject Rights**: APIs to handle access, rectification, erasure, portability, and restriction requests
|
||||
- **Consent Management**: Explicit consent for data processing
|
||||
- **Data Minimization**: Only necessary data is collected
|
||||
|
||||
#### 5. SOC 2 Compliance
|
||||
- **Access Controls**: Proper authentication and authorization for all operations
|
||||
- **Audit Logging**: Complete audit trail of all system access
|
||||
- **Change Management**: Proper procedures for system changes
|
||||
|
||||
#### 6. FedRAMP Compliance
|
||||
- **Security Controls**: Implementation of required security controls based on data classification
|
||||
- **Continuous Monitoring**: Ongoing assessment of security posture
|
||||
- **Incident Response**: Defined procedures for security incidents
|
||||
|
||||
## Application Components
|
||||
|
||||
### Models
|
||||
- `User`: Represents users (job seekers or job providers)
|
||||
- `Job`: Represents job postings
|
||||
- `Application`: Represents job applications
|
||||
- `Tenant`: Represents individual tenants
|
||||
|
||||
### Services
|
||||
- `TenantResolverService`: Resolves tenant from request context
|
||||
- `JobService`: Handles job-related operations with tenant isolation
|
||||
- `ApplicationService`: Manages job applications
|
||||
- `AuthService`: Handles authentication and OAuth flows
|
||||
- `ComplianceService`: Ensures compliance with various regulations
|
||||
- `AccessibilityService`: Validates and generates accessible content
|
||||
- `SecurityComplianceService`: Handles security compliance requirements
|
||||
|
||||
### Controllers
|
||||
- `HomeController`: Basic home page
|
||||
- `AuthController`: Authentication endpoints
|
||||
- `JobController`: Job-related endpoints
|
||||
- `ApplicationController`: Application-related endpoints
|
||||
|
||||
### Middleware
|
||||
- `TenantMiddleware`: Ensures tenant isolation for each request
|
||||
|
||||
## API Design
|
||||
|
||||
### RESTful Endpoints
|
||||
- Consistent naming conventions
|
||||
- Proper HTTP methods (GET, POST, PUT, DELETE)
|
||||
- Standard response formats
|
||||
- Proper status codes
|
||||
|
||||
### Error Handling
|
||||
- Consistent error response format
|
||||
- Appropriate HTTP status codes
|
||||
- Detailed error messages (only in development)
|
||||
|
||||
## Database Design
|
||||
|
||||
### Key Tables
|
||||
- `tenants`: Tenant isolation information
|
||||
- `users`: User accounts with tenant_id
|
||||
- `jobs`: Job postings with tenant_id
|
||||
- `applications`: Job applications with tenant_id
|
||||
- `audit_logs`: Compliance audit logs
|
||||
|
||||
### Indexing Strategy
|
||||
- Indexes on tenant_id for all tenant-isolated tables
|
||||
- Indexes on frequently queried fields
|
||||
- Proper foreign key constraints
|
||||
|
||||
## Deployment Architecture
|
||||
|
||||
### Kubernetes Configuration
|
||||
The application is designed for Kubernetes deployment with:
|
||||
- **Deployments**: For application and service scaling
|
||||
- **Services**: For internal and external networking
|
||||
- **ConfigMaps**: For configuration management
|
||||
- **Secrets**: For sensitive data like API keys
|
||||
- **PersistentVolumes**: For data persistence
|
||||
- **Ingress**: For external access and load balancing
|
||||
|
||||
### Environment Configuration
|
||||
- Separate configurations for development, staging, and production
|
||||
- Environment-specific variable management
|
||||
- Database migration strategy
|
||||
- Backup and recovery procedures
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Input Validation
|
||||
- Server-side validation for all inputs
|
||||
- Sanitization of user-generated content
|
||||
- Prevention of SQL injection and XSS attacks
|
||||
|
||||
### Data Protection
|
||||
- Encryption at rest for sensitive data
|
||||
- Encryption in transit using HTTPS
|
||||
- Proper session management
|
||||
- Secure password handling
|
||||
|
||||
### Monitoring & Logging
|
||||
- Comprehensive audit logging
|
||||
- Performance monitoring
|
||||
- Security event monitoring
|
||||
- Log aggregation and analysis
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Caching Strategy
|
||||
- Redis for session storage
|
||||
- Redis for application-level caching
|
||||
- Database query optimization
|
||||
- Response caching where appropriate
|
||||
|
||||
### Scalability
|
||||
- Stateless application design
|
||||
- Horizontal pod scaling in Kubernetes
|
||||
- Database read replicas for read-heavy operations
|
||||
- CDN for static assets
|
||||
256
qwen/hack/KUBERNETES.md
Normal file
256
qwen/hack/KUBERNETES.md
Normal file
@@ -0,0 +1,256 @@
|
||||
# MerchantsOfHope.org - Kubernetes Configuration
|
||||
|
||||
## Namespace
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: merchantsofhope
|
||||
```
|
||||
|
||||
## ConfigMap for Application Configuration
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: moh-config
|
||||
namespace: merchantsofhope
|
||||
data:
|
||||
APP_NAME: "MerchantsOfHope"
|
||||
APP_VERSION: "0.1.0"
|
||||
APP_ENV: "production"
|
||||
DEBUG: "false"
|
||||
TIMEZONE: "UTC"
|
||||
DB_HOST: "moh-postgres"
|
||||
DB_NAME: "moh"
|
||||
DB_PORT: "5432"
|
||||
JWT_SECRET: "changeme-in-production"
|
||||
TENANT_ISOLATION_ENABLED: "true"
|
||||
ACCESSIBILITY_ENABLED: "true"
|
||||
GDPR_COMPLIANCE_ENABLED: "true"
|
||||
PCI_DSS_COMPLIANCE_ENABLED: "true"
|
||||
```
|
||||
|
||||
## Secrets for Sensitive Configuration
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: moh-secrets
|
||||
namespace: merchantsofhope
|
||||
type: Opaque
|
||||
data:
|
||||
DB_USER: bW9oX3VzZXI= # base64 encoded "moh_user"
|
||||
DB_PASS: bW9oX3Bhc3N3b3Jk # base64 encoded "moh_password"
|
||||
GOOGLE_CLIENT_ID: <base64-encoded-google-client-id>
|
||||
GOOGLE_CLIENT_SECRET: <base64-encoded-google-client-secret>
|
||||
GITHUB_CLIENT_ID: <base64-encoded-github-client-id>
|
||||
GITHUB_CLIENT_SECRET: <base64-encoded-github-client-secret>
|
||||
MAIL_USERNAME: <base64-encoded-mail-username>
|
||||
MAIL_PASSWORD: <base64-encoded-mail-password>
|
||||
```
|
||||
|
||||
## Deployment for Application
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: moh-app
|
||||
namespace: merchantsofhope
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: moh-app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: moh-app
|
||||
spec:
|
||||
containers:
|
||||
- name: app
|
||||
image: qwen-hack-moh:latest
|
||||
ports:
|
||||
- containerPort: 18000
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: moh-config
|
||||
- secretRef:
|
||||
name: moh-secrets
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 18000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 18000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
volumeMounts:
|
||||
- name: app-logs
|
||||
mountPath: /var/log/app
|
||||
volumes:
|
||||
- name: app-logs
|
||||
emptyDir: {}
|
||||
```
|
||||
|
||||
## Service for Application
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: moh-app-service
|
||||
namespace: merchantsofhope
|
||||
spec:
|
||||
selector:
|
||||
app: moh-app
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 18000
|
||||
type: ClusterIP
|
||||
```
|
||||
|
||||
## Ingress for External Access
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: moh-ingress
|
||||
namespace: merchantsofhope
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- merchantsofhope.org
|
||||
secretName: merchantsofhope-tls
|
||||
rules:
|
||||
- host: merchantsofhope.org
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: moh-app-service
|
||||
port:
|
||||
number: 80
|
||||
```
|
||||
|
||||
## PostgreSQL StatefulSet (Example)
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: moh-postgres
|
||||
namespace: merchantsofhope
|
||||
spec:
|
||||
serviceName: moh-postgres
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: moh-postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: moh-postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:13
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
env:
|
||||
- name: POSTGRES_DB
|
||||
value: moh
|
||||
- name: POSTGRES_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: moh-secrets
|
||||
key: DB_USER
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: moh-secrets
|
||||
key: DB_PASS
|
||||
volumeMounts:
|
||||
- name: postgres-storage
|
||||
mountPath: /var/lib/postgresql/data
|
||||
volumes:
|
||||
- name: postgres-storage
|
||||
persistentVolumeClaim:
|
||||
claimName: postgres-pvc
|
||||
```
|
||||
|
||||
## PostgreSQL Service
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: moh-postgres
|
||||
namespace: merchantsofhope
|
||||
spec:
|
||||
selector:
|
||||
app: moh-postgres
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5432
|
||||
targetPort: 5432
|
||||
clusterIP: None # Headless service for StatefulSet
|
||||
```
|
||||
|
||||
## PersistentVolumeClaim for PostgreSQL
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: postgres-pvc
|
||||
namespace: merchantsofhope
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
```
|
||||
|
||||
## Horizontal Pod Autoscaler for Application
|
||||
```yaml
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: moh-app-hpa
|
||||
namespace: merchantsofhope
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: moh-app
|
||||
minReplicas: 3
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 70
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 80
|
||||
```
|
||||
70
qwen/hack/Makefile
Normal file
70
qwen/hack/Makefile
Normal file
@@ -0,0 +1,70 @@
|
||||
# Makefile for MerchantsOfHope.org deployment
|
||||
|
||||
.PHONY: help build docker-build docker-push deploy undeploy helm-install helm-uninstall k8s-apply k8s-delete test
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@echo "Usage: make [target]"
|
||||
@echo ""
|
||||
@echo "Targets:"
|
||||
@echo " help Show this help message"
|
||||
@echo " build Build the application"
|
||||
@echo " docker-build Build the Docker image"
|
||||
@echo " docker-push Push the Docker image to registry"
|
||||
@echo " deploy Deploy to Kubernetes using manifests"
|
||||
@echo " undeploy Remove deployment from Kubernetes"
|
||||
@echo " helm-install Install using Helm"
|
||||
@echo " helm-uninstall Uninstall using Helm"
|
||||
@echo " k8s-apply Apply Kubernetes manifests"
|
||||
@echo " k8s-delete Delete Kubernetes resources"
|
||||
@echo " test Run tests"
|
||||
|
||||
# Build the application
|
||||
build:
|
||||
composer install --no-dev --optimize-autoloader
|
||||
|
||||
# Build Docker image
|
||||
docker-build:
|
||||
docker build -t qwen-hack-moh:latest .
|
||||
|
||||
# Push Docker image to registry
|
||||
docker-push:
|
||||
docker tag qwen-hack-moh:latest qwen-hack-moh:$(shell git rev-parse --short HEAD)
|
||||
docker push qwen-hack-moh:$(shell git rev-parse --short HEAD)
|
||||
docker push qwen-hack-moh:latest
|
||||
|
||||
# Deploy using kubectl
|
||||
deploy: k8s-apply
|
||||
|
||||
# Remove deployment
|
||||
undeploy: k8s-delete
|
||||
|
||||
# Install using Helm
|
||||
helm-install:
|
||||
helm install moh-app ./helm/moh-app --namespace merchantsofhope --create-namespace
|
||||
|
||||
# Uninstall using Helm
|
||||
helm-uninstall:
|
||||
helm uninstall moh-app --namespace merchantsofhope
|
||||
|
||||
# Apply Kubernetes manifests
|
||||
k8s-apply:
|
||||
kubectl apply -f k8s/namespace.yaml
|
||||
kubectl apply -f k8s/configmap.yaml
|
||||
kubectl apply -f k8s/secrets.yaml
|
||||
kubectl apply -f k8s/service.yaml
|
||||
kubectl apply -f k8s/deployment.yaml
|
||||
kubectl apply -f k8s/ingress.yaml
|
||||
|
||||
# Delete Kubernetes resources
|
||||
k8s-delete:
|
||||
kubectl delete -f k8s/ingress.yaml
|
||||
kubectl delete -f k8s/deployment.yaml
|
||||
kubectl delete -f k8s/service.yaml
|
||||
kubectl delete -f k8s/secrets.yaml
|
||||
kubectl delete -f k8s/configmap.yaml
|
||||
kubectl delete -f k8s/namespace.yaml
|
||||
|
||||
# Run tests
|
||||
test:
|
||||
vendor/bin/phpunit
|
||||
@@ -49,4 +49,82 @@ This project implements:
|
||||
- GDPR compliance
|
||||
- SOC compliance
|
||||
- FedRAMP compliance
|
||||
- USA law compliance
|
||||
- USA law compliance
|
||||
|
||||
## API Documentation
|
||||
|
||||
### Authentication Endpoints
|
||||
- `POST /api/auth/login` - Authenticate user
|
||||
- `POST /api/auth/logout` - Logout user
|
||||
- `POST /api/auth/register` - Register new user
|
||||
- `GET /auth/google/callback` - Google OAuth callback
|
||||
- `GET /auth/github/callback` - GitHub OAuth callback
|
||||
|
||||
### Job Endpoints
|
||||
- `GET /api/jobs` - List all jobs with optional filters
|
||||
- `GET /api/jobs/{id}` - Get specific job
|
||||
- `POST /api/jobs` - Create new job (for job providers)
|
||||
- `PUT /api/jobs/{id}` - Update job (for job providers)
|
||||
- `DELETE /api/jobs/{id}` - Delete job (for job providers)
|
||||
- `GET /api/my-jobs` - Get jobs for current tenant (for job providers)
|
||||
|
||||
### Application Endpoints
|
||||
- `POST /api/applications` - Apply for a job
|
||||
- `GET /api/my-applications` - Get applications for current user
|
||||
- `GET /api/jobs/{id}/applications` - Get applications for a specific job (for job providers)
|
||||
|
||||
## Database Schema
|
||||
|
||||
The application uses PostgreSQL with the following main tables:
|
||||
- `tenants` - Stores tenant information
|
||||
- `users` - Stores user accounts
|
||||
- `jobs` - Stores job postings
|
||||
- `applications` - Stores job applications
|
||||
- `audit_logs` - Stores compliance audit logs
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The application expects the following environment variables (defined in `.env`):
|
||||
- `APP_NAME` - Application name
|
||||
- `APP_VERSION` - Application version
|
||||
- `APP_ENV` - Environment (development, production)
|
||||
- `DEBUG` - Enable debug mode
|
||||
- `TIMEZONE` - Application timezone
|
||||
- `DB_HOST` - Database host
|
||||
- `DB_NAME` - Database name
|
||||
- `DB_USER` - Database user
|
||||
- `DB_PASS` - Database password
|
||||
- `DB_PORT` - Database port
|
||||
- `JWT_SECRET` - Secret for JWT tokens
|
||||
- `SESSION_LIFETIME` - Session lifetime in seconds
|
||||
- `TENANT_ISOLATION_ENABLED` - Enable tenant isolation
|
||||
- `ACCESSIBILITY_ENABLED` - Enable accessibility features
|
||||
- `GDPR_COMPLIANCE_ENABLED` - Enable GDPR compliance
|
||||
- `PCI_DSS_COMPLIANCE_ENABLED` - Enable PCI DSS compliance
|
||||
- `GOOGLE_CLIENT_ID` - Google OAuth client ID
|
||||
- `GOOGLE_CLIENT_SECRET` - Google OAuth client secret
|
||||
- `GITHUB_CLIENT_ID` - GitHub OAuth client ID
|
||||
- `GITHUB_CLIENT_SECRET` - GitHub OAuth client secret
|
||||
- `MAIL_HOST` - Mail server host
|
||||
- `MAIL_PORT` - Mail server port
|
||||
- `MAIL_USERNAME` - Mail server username
|
||||
- `MAIL_PASSWORD` - Mail server password
|
||||
- `MAIL_ENCRYPTION` - Mail server encryption method
|
||||
|
||||
## Docker Configuration
|
||||
|
||||
The application is configured to run with Docker and Docker Compose, including:
|
||||
- Application service
|
||||
- PostgreSQL database
|
||||
- Redis for caching/session storage
|
||||
- MailHog for development email testing
|
||||
- Nginx as a reverse proxy
|
||||
|
||||
## Kubernetes Deployment
|
||||
|
||||
The application is designed for Kubernetes deployment with:
|
||||
- Proper resource requests and limits
|
||||
- Health checks
|
||||
- Configuration via ConfigMaps and Secrets
|
||||
- Service definitions for internal and external access
|
||||
- Ingress configuration for routing
|
||||
213
qwen/hack/deploy.sh
Executable file
213
qwen/hack/deploy.sh
Executable file
@@ -0,0 +1,213 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Deployment script for MerchantsOfHope.org
|
||||
# This script handles the deployment process to Kubernetes
|
||||
|
||||
set -e # Exit immediately if a command exits with a non-zero status
|
||||
|
||||
# Configuration
|
||||
NAMESPACE="merchantsofhope"
|
||||
IMAGE_NAME="qwen-hack-moh"
|
||||
IMAGE_TAG="latest"
|
||||
HELM_RELEASE_NAME="moh-app"
|
||||
HELM_CHART_PATH="./helm/moh-app"
|
||||
|
||||
# Function to print messages
|
||||
print_msg() {
|
||||
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
# Function to check prerequisites
|
||||
check_prerequisites() {
|
||||
print_msg "Checking prerequisites..."
|
||||
|
||||
# Check if kubectl is installed
|
||||
if ! command -v kubectl &> /dev/null; then
|
||||
echo "kubectl is not installed. Please install kubectl and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if helm is installed
|
||||
if ! command -v helm &> /dev/null; then
|
||||
echo "helm is not installed. Please install helm and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if docker is installed and running
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "docker is not installed. Please install docker and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker info &> /dev/null; then
|
||||
echo "docker is not running. Please start docker and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_msg "All prerequisites are satisfied."
|
||||
}
|
||||
|
||||
# Function to build the Docker image
|
||||
build_image() {
|
||||
print_msg "Building Docker image: ${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
|
||||
docker build -t ${IMAGE_NAME}:${IMAGE_TAG} .
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
print_msg "Failed to build Docker image"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_msg "Docker image built successfully"
|
||||
}
|
||||
|
||||
# Function to push the Docker image
|
||||
push_image() {
|
||||
print_msg "Pushing Docker image: ${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
|
||||
# Tag with git commit hash if available
|
||||
GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "dev")
|
||||
docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:${GIT_COMMIT}
|
||||
|
||||
# Push both tags
|
||||
docker push ${IMAGE_NAME}:${IMAGE_TAG}
|
||||
docker push ${IMAGE_NAME}:${GIT_COMMIT}
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
print_msg "Failed to push Docker image"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_msg "Docker image pushed successfully"
|
||||
}
|
||||
|
||||
# Function to create namespace
|
||||
create_namespace() {
|
||||
print_msg "Creating namespace: ${NAMESPACE}"
|
||||
|
||||
kubectl get namespace ${NAMESPACE} &> /dev/null || kubectl create namespace ${NAMESPACE}
|
||||
|
||||
print_msg "Namespace created or already exists"
|
||||
}
|
||||
|
||||
# Function to deploy using Helm
|
||||
deploy_helm() {
|
||||
print_msg "Deploying using Helm..."
|
||||
|
||||
# Check if the release already exists
|
||||
if helm status ${HELM_RELEASE_NAME} -n ${NAMESPACE} &> /dev/null; then
|
||||
print_msg "Helm release exists, upgrading..."
|
||||
helm upgrade ${HELM_RELEASE_NAME} ${HELM_CHART_PATH} --namespace ${NAMESPACE} --wait
|
||||
else
|
||||
print_msg "Installing Helm release..."
|
||||
helm install ${HELM_RELEASE_NAME} ${HELM_CHART_PATH} --namespace ${NAMESPACE} --create-namespace --wait
|
||||
fi
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
print_msg "Failed to deploy with Helm"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_msg "Helm deployment completed successfully"
|
||||
}
|
||||
|
||||
# Function to verify the deployment
|
||||
verify_deployment() {
|
||||
print_msg "Verifying deployment..."
|
||||
|
||||
# Wait for pods to be ready
|
||||
kubectl wait --for=condition=ready pod -l app=moh-app -n ${NAMESPACE} --timeout=300s
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
print_msg "Deployment verification failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_msg "Deployment verification completed successfully"
|
||||
}
|
||||
|
||||
# Function to show deployment status
|
||||
show_status() {
|
||||
print_msg "Deployment status:"
|
||||
kubectl get pods -n ${NAMESPACE}
|
||||
kubectl get services -n ${NAMESPACE}
|
||||
kubectl get ingress -n ${NAMESPACE}
|
||||
}
|
||||
|
||||
# Function to run tests
|
||||
run_tests() {
|
||||
print_msg "Running tests..."
|
||||
|
||||
# Run unit tests
|
||||
vendor/bin/phpunit --configuration phpunit.xml --coverage-text
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
print_msg "Tests failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_msg "All tests passed"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
print_msg "Starting deployment process for MerchantsOfHope.org"
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--build-only)
|
||||
build_image
|
||||
exit 0
|
||||
;;
|
||||
--push-only)
|
||||
push_image
|
||||
exit 0
|
||||
;;
|
||||
--deploy-only)
|
||||
check_prerequisites
|
||||
create_namespace
|
||||
deploy_helm
|
||||
verify_deployment
|
||||
show_status
|
||||
exit 0
|
||||
;;
|
||||
--test-only)
|
||||
run_tests
|
||||
exit 0
|
||||
;;
|
||||
--help)
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo "Options:"
|
||||
echo " --build-only Only build the Docker image"
|
||||
echo " --push-only Only push the Docker image"
|
||||
echo " --deploy-only Only deploy to Kubernetes"
|
||||
echo " --test-only Only run tests"
|
||||
echo " --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Use --help for usage information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Execute full deployment process
|
||||
check_prerequisites
|
||||
run_tests
|
||||
build_image
|
||||
push_image
|
||||
create_namespace
|
||||
deploy_helm
|
||||
verify_deployment
|
||||
show_status
|
||||
|
||||
print_msg "Deployment completed successfully!"
|
||||
print_msg "Access the application at: https://merchantsofhope.org"
|
||||
}
|
||||
|
||||
# Execute main function with all arguments
|
||||
main "$@"
|
||||
6
qwen/hack/helm/moh-app/Chart.yaml
Normal file
6
qwen/hack/helm/moh-app/Chart.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: moh-app
|
||||
description: A Helm chart for the MerchantsOfHope.org recruiting platform
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "0.1.0"
|
||||
62
qwen/hack/helm/moh-app/templates/_helpers.tpl
Normal file
62
qwen/hack/helm/moh-app/templates/_helpers.tpl
Normal file
@@ -0,0 +1,62 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "moh-app.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "moh-app.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "moh-app.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "moh-app.labels" -}}
|
||||
helm.sh/chart: {{ include "moh-app.chart" . }}
|
||||
{{ include "moh-app.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "moh-app.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "moh-app.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "moh-app.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "moh-app.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
8
qwen/hack/helm/moh-app/templates/configmap.yaml
Normal file
8
qwen/hack/helm/moh-app/templates/configmap.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "moh-app.fullname" . }}-config
|
||||
data:
|
||||
{{- range $key, $value := .Values.config }}
|
||||
{{ $key }}: {{ $value | quote }}
|
||||
{{- end }}
|
||||
80
qwen/hack/helm/moh-app/templates/deployment.yaml
Normal file
80
qwen/hack/helm/moh-app/templates/deployment.yaml
Normal file
@@ -0,0 +1,80 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "moh-app.fullname" . }}
|
||||
labels:
|
||||
{{- include "moh-app.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "moh-app.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "moh-app.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "moh-app.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 18000
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "moh-app.fullname" . }}-config
|
||||
- secretRef:
|
||||
name: {{ include "moh-app.fullname" . }}-secrets
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: app-logs
|
||||
mountPath: /var/log/app
|
||||
volumes:
|
||||
- name: app-logs
|
||||
emptyDir: {}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
61
qwen/hack/helm/moh-app/templates/ingress.yaml
Normal file
61
qwen/hack/helm/moh-app/templates/ingress.yaml
Normal file
@@ -0,0 +1,61 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "moh-app.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{- include "moh-app.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||
pathType: {{ .pathType }}
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- else }}
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: {{ $svcPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
9
qwen/hack/helm/moh-app/templates/secrets.yaml
Normal file
9
qwen/hack/helm/moh-app/templates/secrets.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "moh-app.fullname" . }}-secrets
|
||||
type: Opaque
|
||||
data:
|
||||
{{- range $key, $value := .Values.secrets }}
|
||||
{{ $key }}: {{ $value | quote }}
|
||||
{{- end }}
|
||||
15
qwen/hack/helm/moh-app/templates/service.yaml
Normal file
15
qwen/hack/helm/moh-app/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "moh-app.fullname" . }}
|
||||
labels:
|
||||
{{- include "moh-app.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: 18000
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "moh-app.selectorLabels" . | nindent 4 }}
|
||||
118
qwen/hack/helm/moh-app/values.yaml
Normal file
118
qwen/hack/helm/moh-app/values.yaml
Normal file
@@ -0,0 +1,118 @@
|
||||
# Default values for moh-app.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
replicaCount: 3
|
||||
|
||||
image:
|
||||
repository: qwen-hack-moh
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ""
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
create: true
|
||||
# Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: ""
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
nginx.ingress.kubernetes.io/rate-limit: "100"
|
||||
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
|
||||
hosts:
|
||||
- host: merchantsofhope.org
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
- host: api.merchantsofhope.org
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
# Application-specific configuration
|
||||
config:
|
||||
APP_NAME: "MerchantsOfHope"
|
||||
APP_VERSION: "0.1.0"
|
||||
APP_ENV: "production"
|
||||
DEBUG: "false"
|
||||
TIMEZONE: "UTC"
|
||||
DB_HOST: "moh-postgres.merchantsofhope.svc.cluster.local"
|
||||
DB_NAME: "moh"
|
||||
DB_PORT: "5432"
|
||||
JWT_SECRET: "changeme-in-production"
|
||||
TENANT_ISOLATION_ENABLED: "true"
|
||||
ACCESSIBILITY_ENABLED: "true"
|
||||
GDPR_COMPLIANCE_ENABLED: "true"
|
||||
PCI_DSS_COMPLIANCE_ENABLED: "true"
|
||||
FRONTEND_URL: "https://merchantsofhope.org"
|
||||
APP_URL: "https://api.merchantsofhope.org"
|
||||
|
||||
secrets:
|
||||
# These should be properly base64 encoded in production
|
||||
DB_USER: "bW9oX3VzZXI="
|
||||
DB_PASS: "bW9oX3Bhc3N3b3Jk"
|
||||
GOOGLE_CLIENT_ID: ""
|
||||
GOOGLE_CLIENT_SECRET: ""
|
||||
GITHUB_CLIENT_ID: ""
|
||||
GITHUB_CLIENT_SECRET: ""
|
||||
MAIL_USERNAME: ""
|
||||
MAIL_PASSWORD: ""
|
||||
JWT_SECRET: ""
|
||||
21
qwen/hack/k8s/configmap.yaml
Normal file
21
qwen/hack/k8s/configmap.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: moh-config
|
||||
namespace: merchantsofhope
|
||||
data:
|
||||
APP_NAME: "MerchantsOfHope"
|
||||
APP_VERSION: "0.1.0"
|
||||
APP_ENV: "production"
|
||||
DEBUG: "false"
|
||||
TIMEZONE: "UTC"
|
||||
DB_HOST: "moh-postgres.merchantsofhope.svc.cluster.local"
|
||||
DB_NAME: "moh"
|
||||
DB_PORT: "5432"
|
||||
JWT_SECRET: "changeme-in-production"
|
||||
TENANT_ISOLATION_ENABLED: "true"
|
||||
ACCESSIBILITY_ENABLED: "true"
|
||||
GDPR_COMPLIANCE_ENABLED: "true"
|
||||
PCI_DSS_COMPLIANCE_ENABLED: "true"
|
||||
FRONTEND_URL: "https://merchantsofhope.org"
|
||||
APP_URL: "https://api.merchantsofhope.org"
|
||||
63
qwen/hack/k8s/deployment.yaml
Normal file
63
qwen/hack/k8s/deployment.yaml
Normal file
@@ -0,0 +1,63 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: moh-app
|
||||
namespace: merchantsofhope
|
||||
spec:
|
||||
replicas: 3
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 1
|
||||
maxSurge: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: moh-app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: moh-app
|
||||
spec:
|
||||
containers:
|
||||
- name: app
|
||||
image: qwen-hack-moh:latest
|
||||
ports:
|
||||
- containerPort: 18000
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: moh-config
|
||||
- secretRef:
|
||||
name: moh-secrets
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 18000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 18000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
volumeMounts:
|
||||
- name: app-logs
|
||||
mountPath: /var/log/app
|
||||
volumes:
|
||||
- name: app-logs
|
||||
emptyDir: {}
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
fsGroup: 2000
|
||||
37
qwen/hack/k8s/ingress.yaml
Normal file
37
qwen/hack/k8s/ingress.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: moh-ingress
|
||||
namespace: merchantsofhope
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
nginx.ingress.kubernetes.io/rate-limit: "100"
|
||||
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
|
||||
spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- merchantsofhope.org
|
||||
- api.merchantsofhope.org
|
||||
secretName: merchantsofhope-tls
|
||||
rules:
|
||||
- host: merchantsofhope.org
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: moh-app-service
|
||||
port:
|
||||
number: 80
|
||||
- host: api.merchantsofhope.org
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: moh-app-service
|
||||
port:
|
||||
number: 80
|
||||
4
qwen/hack/k8s/namespace.yaml
Normal file
4
qwen/hack/k8s/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: merchantsofhope
|
||||
17
qwen/hack/k8s/secrets.yaml
Normal file
17
qwen/hack/k8s/secrets.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: moh-secrets
|
||||
namespace: merchantsofhope
|
||||
type: Opaque
|
||||
data:
|
||||
# These values should be replaced with actual base64 encoded values in production
|
||||
DB_USER: bW9oX3VzZXI= # base64 encoded "moh_user"
|
||||
DB_PASS: bW9oX3Bhc3N3b3Jk # base64 encoded "moh_password"
|
||||
GOOGLE_CLIENT_ID: "" # base64 encoded Google client ID
|
||||
GOOGLE_CLIENT_SECRET: "" # base64 encoded Google client secret
|
||||
GITHUB_CLIENT_ID: "" # base64 encoded GitHub client ID
|
||||
GITHUB_CLIENT_SECRET: "" # base64 encoded GitHub client secret
|
||||
MAIL_USERNAME: "" # base64 encoded mail username
|
||||
MAIL_PASSWORD: "" # base64 encoded mail password
|
||||
JWT_SECRET: "" # base64 encoded JWT secret
|
||||
13
qwen/hack/k8s/service.yaml
Normal file
13
qwen/hack/k8s/service.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: moh-app-service
|
||||
namespace: merchantsofhope
|
||||
spec:
|
||||
selector:
|
||||
app: moh-app
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 18000
|
||||
type: ClusterIP
|
||||
36
qwen/hack/phpunit.xml
Normal file
36
qwen/hack/phpunit.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false">
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">src</directory>
|
||||
</include>
|
||||
<exclude>
|
||||
<directory suffix=".php">src/Config</directory>
|
||||
<directory suffix=".php">src/bootstrap.php</directory>
|
||||
</exclude>
|
||||
</coverage>
|
||||
<testsuites>
|
||||
<testsuite name="Models">
|
||||
<directory>tests/Models</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Services">
|
||||
<directory>tests/Services</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Controllers">
|
||||
<directory>tests/Controllers</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<logging>
|
||||
<log type="coverage-html" target="build/coverage"/>
|
||||
<log type="coverage-text" target="php://stdout"/>
|
||||
<log type="junit" target="build/logs/junit.xml"/>
|
||||
</logging>
|
||||
</phpunit>
|
||||
@@ -45,12 +45,14 @@ $app->group('/api', function (RouteCollectorProxy $group) {
|
||||
$group->get('/jobs', [App\Controllers\JobController::class, 'listJobs']);
|
||||
$group->get('/jobs/{id}', [App\Controllers\JobController::class, 'getJob']);
|
||||
$group->post('/applications', [App\Controllers\ApplicationController::class, 'apply']);
|
||||
$group->get('/my-applications', [App\Controllers\ApplicationController::class, 'getApplicationsByUser']);
|
||||
|
||||
// Job provider routes
|
||||
$group->get('/my-jobs', [App\Controllers\JobController::class, 'myJobs']);
|
||||
$group->post('/jobs', [App\Controllers\JobController::class, 'createJob']);
|
||||
$group->put('/jobs/{id}', [App\Controllers\JobController::class, 'updateJob']);
|
||||
$group->delete('/jobs/{id}', [App\Controllers\JobController::class, 'deleteJob']);
|
||||
$group->get('/jobs/{id}/applications', [App\Controllers\ApplicationController::class, 'getApplicationsByJob']);
|
||||
});
|
||||
|
||||
// Add error middleware in development
|
||||
|
||||
@@ -2,14 +2,130 @@
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Services\ApplicationService;
|
||||
use PDO;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
|
||||
class ApplicationController
|
||||
{
|
||||
private ApplicationService $applicationService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// In a real application, this would be injected via DI container
|
||||
$this->applicationService = new ApplicationService($this->getDbConnection());
|
||||
}
|
||||
|
||||
public function apply(Request $request, Response $response): Response
|
||||
{
|
||||
$response->getBody()->write(json_encode(['message' => 'Apply for job endpoint']));
|
||||
$params = $request->getParsedBody();
|
||||
|
||||
// Validate required fields
|
||||
$required = ['job_id', 'user_id', 'resume_url'];
|
||||
foreach ($required as $field) {
|
||||
if (empty($params[$field])) {
|
||||
$response = $response->withStatus(400);
|
||||
$response->getBody()->write(json_encode(['error' => "Missing required field: {$field}"]));
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
}
|
||||
}
|
||||
|
||||
// Get tenant from request attribute (set by middleware)
|
||||
$tenant = $request->getAttribute('tenant');
|
||||
$tenantId = $tenant ? $tenant->getId() : 'default';
|
||||
|
||||
// Create application
|
||||
$application = new Application(
|
||||
id: 0, // Will be set by database
|
||||
jobId: (int)$params['job_id'],
|
||||
userId: (int)$params['user_id'],
|
||||
resumeUrl: $params['resume_url'],
|
||||
coverLetter: $params['cover_letter'] ?? '',
|
||||
status: 'pending', // Default status
|
||||
tenantId: $tenantId
|
||||
);
|
||||
|
||||
$result = $this->applicationService->submitApplication($application);
|
||||
|
||||
if ($result) {
|
||||
$response->getBody()->write(json_encode(['message' => 'Application submitted successfully']));
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(201);
|
||||
} else {
|
||||
$response = $response->withStatus(500);
|
||||
$response->getBody()->write(json_encode(['error' => 'Failed to submit application']));
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
}
|
||||
}
|
||||
|
||||
public function getApplicationsByUser(Request $request, Response $response): Response
|
||||
{
|
||||
// Get tenant from request attribute (set by middleware)
|
||||
$tenant = $request->getAttribute('tenant');
|
||||
|
||||
// In a real application, you would get the authenticated user ID from the JWT token
|
||||
// For now, we'll use a placeholder user ID
|
||||
$userId = $request->getAttribute('user_id') ?? 1;
|
||||
|
||||
$applications = $this->applicationService->getApplicationsByUser($userId, $tenant);
|
||||
|
||||
$applicationsArray = [];
|
||||
foreach ($applications as $application) {
|
||||
$applicationsArray[] = [
|
||||
'id' => $application->getId(),
|
||||
'job_id' => $application->getJobId(),
|
||||
'user_id' => $application->getUserId(),
|
||||
'resume_url' => $application->getResumeUrl(),
|
||||
'cover_letter' => $application->getCoverLetter(),
|
||||
'status' => $application->getStatus(),
|
||||
'created_at' => $application->getCreatedAt()->format('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
|
||||
$response->getBody()->write(json_encode($applicationsArray));
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
public function getApplicationsByJob(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
$jobId = (int)$args['job_id'];
|
||||
|
||||
// Get tenant from request attribute (set by middleware)
|
||||
$tenant = $request->getAttribute('tenant');
|
||||
|
||||
$applications = $this->applicationService->getApplicationsByJob($jobId, $tenant);
|
||||
|
||||
$applicationsArray = [];
|
||||
foreach ($applications as $application) {
|
||||
$applicationsArray[] = [
|
||||
'id' => $application->getId(),
|
||||
'job_id' => $application->getJobId(),
|
||||
'user_id' => $application->getUserId(),
|
||||
'resume_url' => $application->getResumeUrl(),
|
||||
'cover_letter' => $application->getCoverLetter(),
|
||||
'status' => $application->getStatus(),
|
||||
'created_at' => $application->getCreatedAt()->format('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
|
||||
$response->getBody()->write(json_encode($applicationsArray));
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
private function getDbConnection(): PDO
|
||||
{
|
||||
// In a real application, this would be configured properly
|
||||
$host = $_ENV['DB_HOST'] ?? 'localhost';
|
||||
$dbname = $_ENV['DB_NAME'] ?? 'moh';
|
||||
$username = $_ENV['DB_USER'] ?? 'moh_user';
|
||||
$password = $_ENV['DB_PASS'] ?? 'moh_password';
|
||||
$port = $_ENV['DB_PORT'] ?? '5432';
|
||||
|
||||
$dsn = "pgsql:host={$host};port={$port};dbname={$dbname};";
|
||||
return new PDO($dsn, $username, $password, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
}
|
||||
}
|
||||
103
qwen/hack/src/Models/Application.php
Normal file
103
qwen/hack/src/Models/Application.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?hh // strict
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use DateTime;
|
||||
|
||||
class Application
|
||||
{
|
||||
private int $id;
|
||||
private int $jobId;
|
||||
private int $userId;
|
||||
private string $resumeUrl;
|
||||
private string $coverLetter;
|
||||
private string $status; // 'pending', 'reviewed', 'accepted', 'rejected'
|
||||
private string $tenantId;
|
||||
private DateTime $createdAt;
|
||||
private DateTime $updatedAt;
|
||||
|
||||
public function __construct(
|
||||
int $id,
|
||||
int $jobId,
|
||||
int $userId,
|
||||
string $resumeUrl,
|
||||
string $coverLetter,
|
||||
string $status,
|
||||
string $tenantId
|
||||
) {
|
||||
$this->id = $id;
|
||||
$this->jobId = $jobId;
|
||||
$this->userId = $userId;
|
||||
$this->resumeUrl = $resumeUrl;
|
||||
$this->coverLetter = $coverLetter;
|
||||
$this->status = $status;
|
||||
$this->tenantId = $tenantId;
|
||||
$this->createdAt = new DateTime();
|
||||
$this->updatedAt = new DateTime();
|
||||
}
|
||||
|
||||
// Getters
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getJobId(): int
|
||||
{
|
||||
return $this->jobId;
|
||||
}
|
||||
|
||||
public function getUserId(): int
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function getResumeUrl(): string
|
||||
{
|
||||
return $this->resumeUrl;
|
||||
}
|
||||
|
||||
public function getCoverLetter(): string
|
||||
{
|
||||
return $this->coverLetter;
|
||||
}
|
||||
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function getTenantId(): string
|
||||
{
|
||||
return $this->tenantId;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTime
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTime
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
// Setters
|
||||
public function setResumeUrl(string $resumeUrl): void
|
||||
{
|
||||
$this->resumeUrl = $resumeUrl;
|
||||
$this->updatedAt = new DateTime();
|
||||
}
|
||||
|
||||
public function setCoverLetter(string $coverLetter): void
|
||||
{
|
||||
$this->coverLetter = $coverLetter;
|
||||
$this->updatedAt = new DateTime();
|
||||
}
|
||||
|
||||
public function setStatus(string $status): void
|
||||
{
|
||||
$this->status = $status;
|
||||
$this->updatedAt = new DateTime();
|
||||
}
|
||||
}
|
||||
121
qwen/hack/src/Services/AccessibilityService.php
Normal file
121
qwen/hack/src/Services/AccessibilityService.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?hh // strict
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class AccessibilityService
|
||||
{
|
||||
/**
|
||||
* Check if HTML content meets Section 508/WCAG 2.1 AA compliance
|
||||
*/
|
||||
public function validateHtmlAccessibility(string $html): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// Check for proper heading hierarchy
|
||||
$headings = [];
|
||||
preg_match_all('/<h([1-6])[^>]*>.*?<\/h[1-6]>/i', $html, $matches);
|
||||
if (!empty($matches[0])) {
|
||||
foreach ($matches[0] as $index => $heading) {
|
||||
$level = (int)$matches[1][$index];
|
||||
$headings[] = $level;
|
||||
}
|
||||
|
||||
// Ensure heading levels don't skip (e.g., h1 to h3 without h2)
|
||||
for ($i = 1; $i < count($headings); $i++) {
|
||||
if ($headings[$i] > $headings[$i-1] + 1) {
|
||||
$errors[] = "Heading level skipped from h{$headings[$i-1]} to h{$headings[$i]}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for alt attributes on images
|
||||
preg_match_all('/<img[^>]*>/i', $html, $imgMatches);
|
||||
foreach ($imgMatches[0] as $img) {
|
||||
if (strpos($img, 'alt=') === false) {
|
||||
$errors[] = "Image without alt attribute: {$img}";
|
||||
} else {
|
||||
// Check if alt is empty
|
||||
if (preg_match('/alt=["\']["\']/', $img)) {
|
||||
$errors[] = "Image with empty alt attribute: {$img}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for form labels
|
||||
preg_match_all('/<input[^>]*name=["\']([^"\']*)["\'][^>]*>/i', $html, $inputMatches);
|
||||
foreach ($inputMatches[1] as $inputName) {
|
||||
if (strpos($html, 'label') !== false) {
|
||||
if (!preg_match('/<label[^>]*for=["\']' . preg_quote($inputName, '/') . '["\'][^>]*>.*?<\/label>/i', $html) &&
|
||||
!preg_match('/<label[^>]*>.*?<input[^>]*name=["\']' . preg_quote($inputName, '/') . '["\'][^>]*>.*?<\/label>/i', $html)) {
|
||||
$errors[] = "Input field with name '{$inputName}' missing associated label";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for sufficient color contrast (simplified check)
|
||||
preg_match_all('/style=["\'].*?color:\s*([^;]*);.*?background-color:\s*([^;]*);/i', $html, $styleMatches);
|
||||
foreach ($styleMatches[0] as $index => $style) {
|
||||
$textColor = $styleMatches[1][$index];
|
||||
$bgColor = $styleMatches[2][$index];
|
||||
if (!$this->hasSufficientContrast($textColor, $bgColor)) {
|
||||
$errors[] = "Insufficient color contrast between text '{$textColor}' and background '{$bgColor}'";
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate accessible HTML from basic content
|
||||
*/
|
||||
public function generateAccessibleHtml(array $content): string
|
||||
{
|
||||
$html = '<div class="accessible-content">';
|
||||
|
||||
if (isset($content['title'])) {
|
||||
$html .= '<h1>' . htmlspecialchars($content['title']) . '</h1>';
|
||||
}
|
||||
|
||||
if (isset($content['description'])) {
|
||||
$html .= '<p>' . htmlspecialchars($content['description']) . '</p>';
|
||||
}
|
||||
|
||||
if (isset($content['items']) && is_array($content['items'])) {
|
||||
$html .= '<ul aria-label="Content items">';
|
||||
foreach ($content['items'] as $item) {
|
||||
$html .= '<li>' . htmlspecialchars($item) . '</li>';
|
||||
}
|
||||
$html .= '</ul>';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two colors have sufficient contrast for accessibility
|
||||
*/
|
||||
private function hasSufficientContrast(string $color1, string $color2): bool
|
||||
{
|
||||
// Simplified contrast check - in a real app, you'd implement the full WCAG algorithm
|
||||
return true; // Placeholder implementation
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate accessibility statement
|
||||
*/
|
||||
public function generateAccessibilityStatement(): string
|
||||
{
|
||||
return "
|
||||
<h2>Accessibility Statement</h2>
|
||||
<p>MerchantsOfHope.org is committed to ensuring digital accessibility for people with disabilities.</p>
|
||||
<p>We are continually improving the user experience for everyone and applying the relevant accessibility standards.</p>
|
||||
<h3>Conformance</h3>
|
||||
<p>The Web Content Accessibility Guidelines (WCAG) defines requirements for designers and developers to improve accessibility for people with disabilities.</p>
|
||||
<p>We are conforming to level AA of WCAG 2.1.</p>
|
||||
<h3>Feedback</h3>
|
||||
<p>We welcome your feedback on the accessibility of this website. Please contact us if you encounter accessibility barriers.</p>
|
||||
";
|
||||
}
|
||||
}
|
||||
114
qwen/hack/src/Services/ApplicationService.php
Normal file
114
qwen/hack/src/Services/ApplicationService.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?hh // strict
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Tenant;
|
||||
use PDO;
|
||||
|
||||
class ApplicationService
|
||||
{
|
||||
private PDO $db;
|
||||
|
||||
public function __construct(PDO $db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a new application for a job
|
||||
*/
|
||||
public function submitApplication(Application $application): bool
|
||||
{
|
||||
$sql = "INSERT INTO applications (job_id, user_id, resume_url, cover_letter, status, tenant_id) VALUES (:job_id, :user_id, :resume_url, :cover_letter, :status, :tenant_id)";
|
||||
$stmt = $this->db->prepare($sql);
|
||||
|
||||
return $stmt->execute([
|
||||
':job_id' => $application->getJobId(),
|
||||
':user_id' => $application->getUserId(),
|
||||
':resume_url' => $application->getResumeUrl(),
|
||||
':cover_letter' => $application->getCoverLetter(),
|
||||
':status' => $application->getStatus(),
|
||||
':tenant_id' => $application->getTenantId()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get applications for a specific user (job seeker)
|
||||
*/
|
||||
public function getApplicationsByUser(int $userId, ?Tenant $tenant): array
|
||||
{
|
||||
$tenantId = $tenant ? $tenant->getId() : 'default';
|
||||
|
||||
$sql = "SELECT * FROM applications WHERE user_id = :user_id AND tenant_id = :tenant_id ORDER BY created_at DESC";
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute([
|
||||
':user_id' => $userId,
|
||||
':tenant_id' => $tenantId
|
||||
]);
|
||||
|
||||
$applications = [];
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$applications[] = new Application(
|
||||
id: (int)$row['id'],
|
||||
jobId: (int)$row['job_id'],
|
||||
userId: (int)$row['user_id'],
|
||||
resumeUrl: $row['resume_url'],
|
||||
coverLetter: $row['cover_letter'],
|
||||
status: $row['status'],
|
||||
tenantId: $row['tenant_id']
|
||||
);
|
||||
}
|
||||
|
||||
return $applications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get applications for a specific job (for job provider)
|
||||
*/
|
||||
public function getApplicationsByJob(int $jobId, ?Tenant $tenant): array
|
||||
{
|
||||
$tenantId = $tenant ? $tenant->getId() : 'default';
|
||||
|
||||
$sql = "SELECT * FROM applications WHERE job_id = :job_id AND tenant_id = :tenant_id ORDER BY created_at DESC";
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute([
|
||||
':job_id' => $jobId,
|
||||
':tenant_id' => $tenantId
|
||||
]);
|
||||
|
||||
$applications = [];
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$applications[] = new Application(
|
||||
id: (int)$row['id'],
|
||||
jobId: (int)$row['job_id'],
|
||||
userId: (int)$row['user_id'],
|
||||
resumeUrl: $row['resume_url'],
|
||||
coverLetter: $row['cover_letter'],
|
||||
status: $row['status'],
|
||||
tenantId: $row['tenant_id']
|
||||
);
|
||||
}
|
||||
|
||||
return $applications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update application status
|
||||
*/
|
||||
public function updateApplicationStatus(int $applicationId, string $newStatus, ?Tenant $tenant): bool
|
||||
{
|
||||
$tenantId = $tenant ? $tenant->getId() : 'default';
|
||||
|
||||
$sql = "UPDATE applications SET status = :status, updated_at = NOW() WHERE id = :id AND tenant_id = :tenant_id";
|
||||
$stmt = $this->db->prepare($sql);
|
||||
|
||||
$result = $stmt->execute([
|
||||
':id' => $applicationId,
|
||||
':status' => $newStatus,
|
||||
':tenant_id' => $tenantId
|
||||
]);
|
||||
|
||||
return $result && $stmt->rowCount() > 0;
|
||||
}
|
||||
}
|
||||
121
qwen/hack/src/Services/ComplianceService.php
Normal file
121
qwen/hack/src/Services/ComplianceService.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?hh // strict
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use DateTime;
|
||||
|
||||
class ComplianceService
|
||||
{
|
||||
/**
|
||||
* Check if user data is compliant with USA employment laws
|
||||
*/
|
||||
public function isUserDataCompliant(User $user): bool
|
||||
{
|
||||
// Check if required fields for USA compliance are present
|
||||
$requiredFields = [
|
||||
'name' => $user->getName(),
|
||||
'email' => $user->getEmail(),
|
||||
];
|
||||
|
||||
foreach ($requiredFields as $field => $value) {
|
||||
if (empty($value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure job posting is compliant with USA employment laws
|
||||
*/
|
||||
public function isJobPostingCompliant(array $jobData): bool
|
||||
{
|
||||
// Ensure no discriminatory language in job title or description
|
||||
$discriminatoryTerms = [
|
||||
'male only', 'female only', 'under 30', 'over 40',
|
||||
'no disabled', 'young only', 'recent graduate only',
|
||||
'no seniors', 'must be citizen', // unless citizenship is required by law
|
||||
];
|
||||
|
||||
$textToCheck = strtolower($jobData['title'] . ' ' . $jobData['description']);
|
||||
|
||||
foreach ($discriminatoryTerms as $term) {
|
||||
if (strpos($textToCheck, $term) !== false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure required information is present
|
||||
$requiredFields = ['title', 'description', 'location'];
|
||||
foreach ($requiredFields as $field) {
|
||||
if (empty($jobData[$field])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log data access for compliance auditing
|
||||
*/
|
||||
public function logDataAccess(string $userId, string $action, string $resource, ?string $tenantId = null): void
|
||||
{
|
||||
$logEntry = [
|
||||
'user_id' => $userId,
|
||||
'action' => $action,
|
||||
'resource' => $resource,
|
||||
'tenant_id' => $tenantId,
|
||||
'timestamp' => (new DateTime())->format('Y-m-d H:i:s'),
|
||||
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
|
||||
];
|
||||
|
||||
// In a real application, this would be stored in a dedicated audit log table
|
||||
error_log(json_encode($logEntry));
|
||||
}
|
||||
|
||||
/**
|
||||
* Anonymize user data in accordance with compliance requirements
|
||||
*/
|
||||
public function anonymizeUserData(array &$userData): void
|
||||
{
|
||||
// Remove or anonymize PII based on retention policies
|
||||
if (isset($userData['email'])) {
|
||||
$userData['email'] = '[ANONYMIZED]';
|
||||
}
|
||||
|
||||
if (isset($userData['name'])) {
|
||||
$userData['name'] = '[ANONYMIZED]';
|
||||
}
|
||||
|
||||
if (isset($userData['phone'])) {
|
||||
$userData['phone'] = '[ANONYMIZED]';
|
||||
}
|
||||
|
||||
// Add an anonymization timestamp
|
||||
$userData['anonymized_at'] = (new DateTime())->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that data retention follows USA law requirements
|
||||
*/
|
||||
public function validateDataRetention(string $dataCategory, DateTime $creationDate): bool
|
||||
{
|
||||
// Different data types have different retention requirements
|
||||
$retentionPeriods = [
|
||||
'application' => 4 * 365, // 4 years for employment applications
|
||||
'interview' => 4 * 365, // 4 years for interview records
|
||||
'offer' => 4 * 365, // 4 years for offer letters
|
||||
'employee' => 4 * 365, // 4 years for employee records
|
||||
'general' => 7 * 365, // 7 years for general business records
|
||||
];
|
||||
|
||||
$daysStored = (new DateTime())->diff($creationDate)->days;
|
||||
$maxRetentionDays = $retentionPeriods[$dataCategory] ?? $retentionPeriods['general'];
|
||||
|
||||
return $daysStored <= $maxRetentionDays;
|
||||
}
|
||||
}
|
||||
239
qwen/hack/src/Services/SecurityComplianceService.php
Normal file
239
qwen/hack/src/Services/SecurityComplianceService.php
Normal file
@@ -0,0 +1,239 @@
|
||||
<?hh // strict
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use DateTime;
|
||||
|
||||
class SecurityComplianceService
|
||||
{
|
||||
/**
|
||||
* Check if data handling meets PCI DSS requirements
|
||||
*/
|
||||
public function isPciCompliant(array $data): bool
|
||||
{
|
||||
// Check if any sensitive authentication data is being stored
|
||||
$sensitiveData = [
|
||||
'full_track_data',
|
||||
'full_primary_account_number',
|
||||
'cvv',
|
||||
'cvc',
|
||||
'pin'
|
||||
];
|
||||
|
||||
foreach ($sensitiveData as $field) {
|
||||
if (isset($data[$field]) && !empty($data[$field])) {
|
||||
// This data should not be stored
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that if PAN (Primary Account Number) is present, it's properly masked
|
||||
if (isset($data['pan'])) {
|
||||
$pan = $data['pan'];
|
||||
// PAN should be masked: first 6 and last 4 digits visible, rest masked
|
||||
if (strlen($pan) > 10) {
|
||||
$maskedPan = substr($pan, 0, 6) . str_repeat('*', strlen($pan) - 10) . substr($pan, -4);
|
||||
if ($maskedPan !== $data['pan']) {
|
||||
return false; // PAN is not properly masked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GDPR data subject rights requests
|
||||
*/
|
||||
public function handleGdprRequest(string $requestType, string $userId, ?string $tenantId = null): array
|
||||
{
|
||||
switch ($requestType) {
|
||||
case 'access':
|
||||
return $this->handleDataAccessRequest($userId, $tenantId);
|
||||
case 'rectification':
|
||||
return ['status' => 'success', 'message' => 'Data rectification request received'];
|
||||
case 'erasure':
|
||||
return $this->handleErasureRequest($userId, $tenantId);
|
||||
case 'portability':
|
||||
return $this->handleDataPortabilityRequest($userId, $tenantId);
|
||||
case 'restriction':
|
||||
return $this->handleProcessingRestrictionRequest($userId, $tenantId);
|
||||
default:
|
||||
return ['status' => 'error', 'message' => 'Invalid request type'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify SOC 2 compliance for data access
|
||||
*/
|
||||
public function isSoc2Compliant(string $action, string $userId, ?string $tenantId = null): bool
|
||||
{
|
||||
// Log the access for audit trail (SOC 2 requirement)
|
||||
$this->logAccess($action, $userId, $tenantId);
|
||||
|
||||
// Check if the action is authorized for this user
|
||||
$authorized = $this->isActionAuthorized($action, $userId, $tenantId);
|
||||
|
||||
// SOC 2 requires proper authorization controls
|
||||
return $authorized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure FedRAMP compliance for data handling
|
||||
*/
|
||||
public function isFedRampCompliant(string $dataClassification, array $securityControls): bool
|
||||
{
|
||||
// FedRAMP requires specific security controls based on data classification
|
||||
$requiredControls = [];
|
||||
|
||||
switch ($dataClassification) {
|
||||
case 'low':
|
||||
$requiredControls = ['access_control', 'audit_logging'];
|
||||
break;
|
||||
case 'moderate':
|
||||
$requiredControls = [
|
||||
'access_control', 'audit_logging', 'data_encryption',
|
||||
'incident_response', 'personnel_security'
|
||||
];
|
||||
break;
|
||||
case 'high':
|
||||
$requiredControls = [
|
||||
'access_control', 'audit_logging', 'data_encryption',
|
||||
'incident_response', 'personnel_security', 'continuous_monitoring',
|
||||
'penetration_testing'
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if all required controls are implemented
|
||||
foreach ($requiredControls as $control) {
|
||||
if (!isset($securityControls[$control]) || !$securityControls[$control]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt sensitive data for compliance
|
||||
*/
|
||||
public function encryptSensitiveData(string $data, string $key): string
|
||||
{
|
||||
// In a real application, use proper encryption (AES-256-GCM)
|
||||
// For this implementation, we'll use a simplified approach with base64 encoding
|
||||
// (NOT secure for production, only for demonstration)
|
||||
$iv = random_bytes(16); // In real app, use proper IV generation
|
||||
return base64_encode($iv . $data); // Simplified - real app would use openssl_encrypt
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt sensitive data
|
||||
*/
|
||||
public function decryptSensitiveData(string $encryptedData, string $key): string
|
||||
{
|
||||
$data = base64_decode($encryptedData);
|
||||
$iv = substr($data, 0, 16);
|
||||
$encrypted = substr($data, 16);
|
||||
return $encrypted; // Simplified - real app would use openssl_decrypt
|
||||
}
|
||||
|
||||
/**
|
||||
* Create data processing records for compliance
|
||||
*/
|
||||
public function createDataProcessingRecord(string $action, string $userId, string $resource, ?string $tenantId = null): void
|
||||
{
|
||||
$record = [
|
||||
'action' => $action,
|
||||
'user_id' => $userId,
|
||||
'resource' => $resource,
|
||||
'tenant_id' => $tenantId,
|
||||
'timestamp' => (new DateTime())->format('Y-m-d H:i:s'),
|
||||
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
|
||||
'session_id' => session_id() ?? 'unknown'
|
||||
];
|
||||
|
||||
// In a real application, this would be stored in a secure audit log
|
||||
error_log('DATA_PROCESSING_RECORD: ' . json_encode($record));
|
||||
}
|
||||
|
||||
private function handleDataAccessRequest(string $userId, ?string $tenantId): array
|
||||
{
|
||||
// In a real app, this would retrieve all personal data for the user
|
||||
return [
|
||||
'status' => 'success',
|
||||
'message' => 'Data access request fulfilled',
|
||||
'data' => [
|
||||
'user_id' => $userId,
|
||||
'request_date' => (new DateTime())->format('Y-m-d H:i:s'),
|
||||
'data_categories' => ['identity', 'preferences', 'activity']
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
private function handleErasureRequest(string $userId, ?string $tenantId): array
|
||||
{
|
||||
// In a real app, this would anonymize or delete the user's personal data
|
||||
// while maintaining data for legal/auditing purposes in compliance with retention requirements
|
||||
$this->anonymizeUserData($userId, $tenantId);
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
'message' => 'User data has been anonymized in compliance with retention policies'
|
||||
];
|
||||
}
|
||||
|
||||
private function handleDataPortabilityRequest(string $userId, ?string $tenantId): array
|
||||
{
|
||||
// In a real app, this would provide user data in a commonly used format
|
||||
return [
|
||||
'status' => 'success',
|
||||
'message' => 'Data portability request fulfilled',
|
||||
'data_format' => 'json',
|
||||
'data' => [
|
||||
'user_id' => $userId,
|
||||
'export_date' => (new DateTime())->format('Y-m-d H:i:s')
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
private function handleProcessingRestrictionRequest(string $userId, ?string $tenantId): array
|
||||
{
|
||||
// In a real app, this would temporarily restrict processing of user data
|
||||
// until the issue is resolved
|
||||
return [
|
||||
'status' => 'success',
|
||||
'message' => 'Processing restriction applied until issue is resolved'
|
||||
];
|
||||
}
|
||||
|
||||
private function anonymizeUserData(string $userId, ?string $tenantId): void
|
||||
{
|
||||
// In a real app, this would anonymize user data while preserving it for legal requirements
|
||||
error_log("ANONYMIZING user {$userId} in tenant {$tenantId}");
|
||||
}
|
||||
|
||||
private function logAccess(string $action, string $userId, ?string $tenantId = null): void
|
||||
{
|
||||
// Log access for audit trail
|
||||
$logEntry = [
|
||||
'timestamp' => (new DateTime())->format('Y-m-d H:i:s'),
|
||||
'user_id' => $userId,
|
||||
'action' => $action,
|
||||
'tenant_id' => $tenantId,
|
||||
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
'session_id' => session_id() ?? 'unknown'
|
||||
];
|
||||
|
||||
error_log('ACCESS_LOG: ' . json_encode($logEntry));
|
||||
}
|
||||
|
||||
private function isActionAuthorized(string $action, string $userId, ?string $tenantId = null): bool
|
||||
{
|
||||
// In a real application, this would check permissions against a permissions system
|
||||
// For now, just return true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
60
qwen/hack/tests/Models/JobTest.php
Normal file
60
qwen/hack/tests/Models/JobTest.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?hh // strict
|
||||
|
||||
namespace Tests\Models;
|
||||
|
||||
use App\Models\Job;
|
||||
use DateTime;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class JobTest extends TestCase
|
||||
{
|
||||
public function testJobCreation(): void
|
||||
{
|
||||
$job = new Job(
|
||||
id: 1,
|
||||
title: 'Software Engineer',
|
||||
description: 'We are looking for a skilled software engineer...',
|
||||
location: 'New York, NY',
|
||||
employmentType: 'Full-time',
|
||||
tenantId: 'tenant-123'
|
||||
);
|
||||
|
||||
$this->assertEquals(1, $job->getId());
|
||||
$this->assertEquals('Software Engineer', $job->getTitle());
|
||||
$this->assertEquals('We are looking for a skilled software engineer...', $job->getDescription());
|
||||
$this->assertEquals('New York, NY', $job->getLocation());
|
||||
$this->assertEquals('Full-time', $job->getEmploymentType());
|
||||
$this->assertEquals('tenant-123', $job->getTenantId());
|
||||
$this->assertInstanceOf(DateTime::class, $job->getCreatedAt());
|
||||
$this->assertInstanceOf(DateTime::class, $job->getUpdatedAt());
|
||||
}
|
||||
|
||||
public function testJobSetters(): void
|
||||
{
|
||||
$job = new Job(
|
||||
id: 1,
|
||||
title: 'Software Engineer',
|
||||
description: 'We are looking for a skilled software engineer...',
|
||||
location: 'New York, NY',
|
||||
employmentType: 'Full-time',
|
||||
tenantId: 'tenant-123'
|
||||
);
|
||||
|
||||
$originalUpdatedAt = $job->getUpdatedAt();
|
||||
|
||||
// Update the job
|
||||
$job->setTitle('Senior Software Engineer');
|
||||
$job->setDescription('We are looking for a senior software engineer...');
|
||||
$job->setLocation('Remote');
|
||||
$job->setEmploymentType('Contract');
|
||||
|
||||
// Verify updates
|
||||
$this->assertEquals('Senior Software Engineer', $job->getTitle());
|
||||
$this->assertEquals('We are looking for a senior software engineer...', $job->getDescription());
|
||||
$this->assertEquals('Remote', $job->getLocation());
|
||||
$this->assertEquals('Contract', $job->getEmploymentType());
|
||||
|
||||
// Verify that updated_at was updated
|
||||
$this->assertNotEquals($originalUpdatedAt, $job->getUpdatedAt());
|
||||
}
|
||||
}
|
||||
52
qwen/hack/tests/Models/TenantTest.php
Normal file
52
qwen/hack/tests/Models/TenantTest.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?hh // strict
|
||||
|
||||
namespace Tests\Models;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use DateTime;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class TenantTest extends TestCase
|
||||
{
|
||||
public function testTenantCreation(): void
|
||||
{
|
||||
$tenant = new Tenant(
|
||||
id: 'tenant-123',
|
||||
name: 'Test Tenant',
|
||||
subdomain: 'test',
|
||||
isActive: true
|
||||
);
|
||||
|
||||
$this->assertEquals('tenant-123', $tenant->getId());
|
||||
$this->assertEquals('Test Tenant', $tenant->getName());
|
||||
$this->assertEquals('test', $tenant->getSubdomain());
|
||||
$this->assertTrue($tenant->getIsActive());
|
||||
$this->assertInstanceOf(DateTime::class, $tenant->getCreatedAt());
|
||||
$this->assertInstanceOf(DateTime::class, $tenant->getUpdatedAt());
|
||||
}
|
||||
|
||||
public function testTenantSetters(): void
|
||||
{
|
||||
$tenant = new Tenant(
|
||||
id: 'tenant-123',
|
||||
name: 'Test Tenant',
|
||||
subdomain: 'test',
|
||||
isActive: true
|
||||
);
|
||||
|
||||
$originalUpdatedAt = $tenant->getUpdatedAt();
|
||||
|
||||
// Update the tenant
|
||||
$tenant->setName('Updated Test Tenant');
|
||||
$tenant->setSubdomain('updated-test');
|
||||
$tenant->setIsActive(false);
|
||||
|
||||
// Verify updates
|
||||
$this->assertEquals('Updated Test Tenant', $tenant->getName());
|
||||
$this->assertEquals('updated-test', $tenant->getSubdomain());
|
||||
$this->assertFalse($tenant->getIsActive());
|
||||
|
||||
// Verify that updated_at was updated
|
||||
$this->assertNotEquals($originalUpdatedAt, $tenant->getUpdatedAt());
|
||||
}
|
||||
}
|
||||
89
qwen/hack/tests/Services/ComplianceServiceTest.php
Normal file
89
qwen/hack/tests/Services/ComplianceServiceTest.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?hh // strict
|
||||
|
||||
namespace Tests\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\ComplianceService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ComplianceServiceTest extends TestCase
|
||||
{
|
||||
private ComplianceService $complianceService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->complianceService = new ComplianceService();
|
||||
}
|
||||
|
||||
public function testUserDataCompliance(): void
|
||||
{
|
||||
$user = new User(
|
||||
id: 1,
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
role: 'job_seeker',
|
||||
tenantId: 'tenant-123'
|
||||
);
|
||||
|
||||
$this->assertTrue($this->complianceService->isUserDataCompliant($user));
|
||||
}
|
||||
|
||||
public function testUserDataComplianceWithMissingName(): void
|
||||
{
|
||||
$user = new User(
|
||||
id: 1,
|
||||
email: 'test@example.com',
|
||||
name: '',
|
||||
role: 'job_seeker',
|
||||
tenantId: 'tenant-123'
|
||||
);
|
||||
|
||||
$this->assertFalse($this->complianceService->isUserDataCompliant($user));
|
||||
}
|
||||
|
||||
public function testUserDataComplianceWithMissingEmail(): void
|
||||
{
|
||||
$user = new User(
|
||||
id: 1,
|
||||
email: '',
|
||||
name: 'Test User',
|
||||
role: 'job_seeker',
|
||||
tenantId: 'tenant-123'
|
||||
);
|
||||
|
||||
$this->assertFalse($this->complianceService->isUserDataCompliant($user));
|
||||
}
|
||||
|
||||
public function testJobPostingCompliance(): void
|
||||
{
|
||||
$jobData = [
|
||||
'title' => 'Software Engineer',
|
||||
'description' => 'Looking for a skilled developer',
|
||||
'location' => 'New York'
|
||||
];
|
||||
|
||||
$this->assertTrue($this->complianceService->isJobPostingCompliant($jobData));
|
||||
}
|
||||
|
||||
public function testJobPostingComplianceWithDiscriminatoryLanguage(): void
|
||||
{
|
||||
$jobData = [
|
||||
'title' => 'Software Engineer',
|
||||
'description' => 'Looking for a young male developer under 30',
|
||||
'location' => 'New York'
|
||||
];
|
||||
|
||||
$this->assertFalse($this->complianceService->isJobPostingCompliant($jobData));
|
||||
}
|
||||
|
||||
public function testJobPostingComplianceWithMissingField(): void
|
||||
{
|
||||
$jobData = [
|
||||
'title' => 'Software Engineer',
|
||||
'description' => 'Looking for a skilled developer'
|
||||
// Missing location
|
||||
];
|
||||
|
||||
$this->assertFalse($this->complianceService->isJobPostingCompliant($jobData));
|
||||
}
|
||||
}
|
||||
61
qwen/hack/tests/Services/TenantResolverTest.php
Normal file
61
qwen/hack/tests/Services/TenantResolverTest.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?hh // strict
|
||||
|
||||
namespace Tests\Services;
|
||||
|
||||
use App\Services\TenantResolver;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Slim\Psr7\Factory\ServerRequestFactory;
|
||||
|
||||
class TenantResolverTest extends TestCase
|
||||
{
|
||||
private TenantResolver $tenantResolver;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tenantResolver = new TenantResolver();
|
||||
}
|
||||
|
||||
public function testResolveTenantFromSubdomain(): void
|
||||
{
|
||||
$requestFactory = new ServerRequestFactory();
|
||||
$request = $requestFactory->createServerRequest('GET', 'https://abc.merchantsofhope.org');
|
||||
|
||||
$tenant = $this->tenantResolver->resolveTenant($request);
|
||||
|
||||
$this->assertNotNull($tenant);
|
||||
$this->assertEquals('abc', $tenant->getSubdomain());
|
||||
$this->assertEquals('Abc Tenant', $tenant->getName());
|
||||
}
|
||||
|
||||
public function testResolveTenantFromPath(): void
|
||||
{
|
||||
$requestFactory = new ServerRequestFactory();
|
||||
$request = $requestFactory->createServerRequest('GET', 'https://merchantsofhope.org/xyz');
|
||||
|
||||
$tenant = $this->tenantResolver->resolveTenant($request);
|
||||
|
||||
$this->assertNotNull($tenant);
|
||||
$this->assertEquals('xyz', $tenant->getSubdomain());
|
||||
$this->assertEquals('Xyz Tenant', $tenant->getName());
|
||||
}
|
||||
|
||||
public function testResolveTenantWithNoTenant(): void
|
||||
{
|
||||
$requestFactory = new ServerRequestFactory();
|
||||
$request = $requestFactory->createServerRequest('GET', 'https://merchantsofhope.org');
|
||||
|
||||
$tenant = $this->tenantResolver->resolveTenant($request);
|
||||
|
||||
$this->assertNull($tenant);
|
||||
}
|
||||
|
||||
public function testResolveTenantFromInvalidSubdomain(): void
|
||||
{
|
||||
$requestFactory = new ServerRequestFactory();
|
||||
$request = $requestFactory->createServerRequest('GET', 'https://merchantsofhope.org');
|
||||
|
||||
$tenant = $this->tenantResolver->resolveTenant($request);
|
||||
|
||||
$this->assertNull($tenant);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user