This commit is contained in:
2025-10-24 14:54:44 -05:00
parent cb06217ef7
commit 6a58e19b10
16 changed files with 1172 additions and 138 deletions

View File

@@ -18,7 +18,10 @@
"guzzlehttp/guzzle": "^7.0",
"monolog/monolog": "^2.0",
"vlucas/phpdotenv": "^5.0",
"php-di/php-di": "^6.0"
"php-di/php-di": "^6.0",
"league/oauth2-client": "^2.0",
"league/oauth2-google": "^4.0",
"league/oauth2-github": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0",

View File

@@ -7,6 +7,8 @@
require_once __DIR__ . '/bootstrap.php';
use App\Controllers\HomeController;
use App\Middleware\TenantMiddleware;
use App\Services\TenantResolver;
use Slim\Factory\AppFactory;
use Slim\Middleware\ContentLengthMiddleware;
use Slim\Routing\RouteCollectorProxy;
@@ -20,9 +22,18 @@ $app->addBodyParsingMiddleware();
$app->addRoutingMiddleware();
$app->add(new ContentLengthMiddleware());
// Add tenant middleware
$tenantResolver = new TenantResolver();
$tenantMiddleware = new TenantMiddleware($tenantResolver);
$app->add($tenantMiddleware);
// Define routes
$app->get('/', [HomeController::class, 'index']);
// Social login routes (not under API group to avoid tenant isolation)
$app->get('/auth/google/callback', [App\Controllers\AuthController::class, 'googleCallback']);
$app->get('/auth/github/callback', [App\Controllers\AuthController::class, 'githubCallback']);
// Group routes for API
$app->group('/api', function (RouteCollectorProxy $group) {
// Authentication routes

View File

@@ -0,0 +1,15 @@
<?hh // strict
namespace App\Controllers;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class ApplicationController
{
public function apply(Request $request, Response $response): Response
{
$response->getBody()->write(json_encode(['message' => 'Apply for job endpoint']));
return $response->withHeader('Content-Type', 'application/json');
}
}

View File

@@ -2,26 +2,166 @@
namespace App\Controllers;
use App\Services\AuthService;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class AuthController
{
private AuthService $authService;
public function __construct()
{
$this->authService = new AuthService();
}
public function login(Request $request, Response $response): Response
{
$response->getBody()->write(json_encode(['message' => 'Login endpoint']));
// For traditional login, you would validate credentials here
// For OIDC/social login, redirect to appropriate provider
$params = $request->getParsedBody();
$provider = $params['provider'] ?? null;
switch ($provider) {
case 'google':
$authUrl = $this->authService->getGoogleAuthorizationUrl();
$response->getBody()->write(json_encode(['redirect_url' => $authUrl]));
break;
case 'github':
$authUrl = $this->authService->getGithubAuthorizationUrl();
$response->getBody()->write(json_encode(['redirect_url' => $authUrl]));
break;
default:
// Traditional login
$email = $params['email'] ?? '';
$password = $params['password'] ?? '';
// Validate credentials (simplified)
if ($this->validateCredentials($email, $password)) {
$token = $this->generateToken($email);
$response->getBody()->write(json_encode(['token' => $token, 'message' => 'Login successful']));
} else {
$response = $response->withStatus(401);
$response->getBody()->write(json_encode(['error' => 'Invalid credentials']));
}
break;
}
return $response->withHeader('Content-Type', 'application/json');
}
public function logout(Request $request, Response $response): Response
{
$response->getBody()->write(json_encode(['message' => 'Logout endpoint']));
// Clear any tokens, sessions, etc.
$response->getBody()->write(json_encode(['message' => 'Logout successful']));
return $response->withHeader('Content-Type', 'application/json');
}
public function register(Request $request, Response $response): Response
{
$response->getBody()->write(json_encode(['message' => 'Register endpoint']));
$params = $request->getParsedBody();
$email = $params['email'] ?? '';
$name = $params['name'] ?? '';
$password = $params['password'] ?? '';
// Validate input
if (empty($email) || empty($name) || empty($password)) {
$response = $response->withStatus(400);
$response->getBody()->write(json_encode(['error' => 'All fields are required']));
return $response->withHeader('Content-Type', 'application/json');
}
// Register user (simplified)
if ($this->registerUser($email, $name, $password)) {
$token = $this->generateToken($email);
$response->getBody()->write(json_encode(['token' => $token, 'message' => 'Registration successful']));
} else {
$response = $response->withStatus(500);
$response->getBody()->write(json_encode(['error' => 'Registration failed']));
}
return $response->withHeader('Content-Type', 'application/json');
}
public function googleCallback(Request $request, Response $response): Response
{
try {
$userData = $this->authService->handleGoogleCallback($request);
// Check if user exists, create if not
$user = $this->findOrCreateUser($userData);
// Generate JWT token
$token = $this->generateToken($user['email']);
// Redirect to frontend with token
$redirectUrl = $_ENV['FRONTEND_URL'] ?? 'http://localhost:3000' . "/auth/callback?token={$token}";
return $response->withHeader('Location', $redirectUrl)->withStatus(302);
} catch (\Exception $e) {
$response = $response->withStatus(500);
$response->getBody()->write(json_encode(['error' => $e->getMessage()]));
return $response->withHeader('Content-Type', 'application/json');
}
}
public function githubCallback(Request $request, Response $response): Response
{
try {
$userData = $this->authService->handleGithubCallback($request);
// Check if user exists, create if not
$user = $this->findOrCreateUser($userData);
// Generate JWT token
$token = $this->generateToken($user['email']);
// Redirect to frontend with token
$redirectUrl = $_ENV['FRONTEND_URL'] ?? 'http://localhost:3000' . "/auth/callback?token={$token}";
return $response->withHeader('Location', $redirectUrl)->withStatus(302);
} catch (\Exception $e) {
$response = $response->withStatus(500);
$response->getBody()->write(json_encode(['error' => $e->getMessage()]));
return $response->withHeader('Content-Type', 'application/json');
}
}
private function validateCredentials(string $email, string $password): bool
{
// This would normally check against a database
// For now, just return true for demo purposes
return !empty($email) && !empty($password);
}
private function registerUser(string $email, string $name, string $password): bool
{
// This would normally store the user in a database
// For now, just return true for demo purposes
return true;
}
private function findOrCreateUser(array $userData): array
{
// This would normally query the database to find or create a user
// For now, return mock user data
return [
'id' => 1,
'email' => $userData['email'],
'name' => $userData['name'],
];
}
private function generateToken(string $email): string
{
$payload = [
'iss' => 'MerchantsOfHope', // Issuer
'sub' => $email, // Subject
'iat' => time(), // Issued at
'exp' => time() + 3600 // Expiration time (1 hour)
];
return JWT::encode($payload, $_ENV['JWT_SECRET'], 'HS256');
}
}

View File

@@ -2,47 +2,220 @@
namespace App\Controllers;
use App\Models\Job;
use App\Services\JobService;
use PDO;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class JobController
{
private JobService $jobService;
public function __construct()
{
// In a real application, this would be injected via DI container
$this->jobService = new JobService($this->getDbConnection());
}
public function listJobs(Request $request, Response $response): Response
{
$response->getBody()->write(json_encode(['message' => 'List jobs endpoint']));
// Extract filters from query parameters
$params = $request->getQueryParams();
$filters = [];
if (isset($params['location'])) {
$filters['location'] = $params['location'];
}
if (isset($params['keywords'])) {
$filters['keywords'] = $params['keywords'];
}
// Get tenant from request attribute (set by middleware)
$tenant = $request->getAttribute('tenant');
$jobs = $this->jobService->getAllJobs($tenant, $filters);
$jobsArray = [];
foreach ($jobs as $job) {
$jobsArray[] = [
'id' => $job->getId(),
'title' => $job->getTitle(),
'description' => $job->getDescription(),
'location' => $job->getLocation(),
'employment_type' => $job->getEmploymentType(),
'created_at' => $job->getCreatedAt()->format('Y-m-d H:i:s')
];
}
$response->getBody()->write(json_encode($jobsArray));
return $response->withHeader('Content-Type', 'application/json');
}
public function getJob(Request $request, Response $response, array $args): Response
{
$jobId = $args['id'];
$response->getBody()->write(json_encode(['message' => 'Get job endpoint', 'id' => $jobId]));
$jobId = (int)$args['id'];
// Get tenant from request attribute (set by middleware)
$tenant = $request->getAttribute('tenant');
$job = $this->jobService->getJobById($jobId, $tenant);
if (!$job) {
$response = $response->withStatus(404);
$response->getBody()->write(json_encode(['error' => 'Job not found']));
return $response->withHeader('Content-Type', 'application/json');
}
$jobData = [
'id' => $job->getId(),
'title' => $job->getTitle(),
'description' => $job->getDescription(),
'location' => $job->getLocation(),
'employment_type' => $job->getEmploymentType(),
'created_at' => $job->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $job->getUpdatedAt()->format('Y-m-d H:i:s')
];
$response->getBody()->write(json_encode($jobData));
return $response->withHeader('Content-Type', 'application/json');
}
public function myJobs(Request $request, Response $response): Response
{
$response->getBody()->write(json_encode(['message' => 'My jobs endpoint']));
// Get tenant from request attribute (set by middleware)
$tenant = $request->getAttribute('tenant');
// For job providers, get jobs they've posted
// This would typically involve checking user permissions
// For now, just return all jobs for the tenant
$jobs = $this->jobService->getAllJobs($tenant);
$jobsArray = [];
foreach ($jobs as $job) {
$jobsArray[] = [
'id' => $job->getId(),
'title' => $job->getTitle(),
'description' => $job->getDescription(),
'location' => $job->getLocation(),
'employment_type' => $job->getEmploymentType(),
'created_at' => $job->getCreatedAt()->format('Y-m-d H:i:s')
];
}
$response->getBody()->write(json_encode($jobsArray));
return $response->withHeader('Content-Type', 'application/json');
}
public function createJob(Request $request, Response $response): Response
{
$response->getBody()->write(json_encode(['message' => 'Create job endpoint']));
return $response->withHeader('Content-Type', 'application/json');
$params = $request->getParsedBody();
// Validate required fields
$required = ['title', 'description', 'location', 'employment_type'];
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 job
$job = new Job(
id: 0, // Will be set by database
title: $params['title'],
description: $params['description'],
location: $params['location'],
employmentType: $params['employment_type'],
tenantId: $tenantId
);
$result = $this->jobService->createJob($job);
if ($result) {
$response->getBody()->write(json_encode(['message' => 'Job created successfully']));
return $response->withHeader('Content-Type', 'application/json')->withStatus(201);
} else {
$response = $response->withStatus(500);
$response->getBody()->write(json_encode(['error' => 'Failed to create job']));
return $response->withHeader('Content-Type', 'application/json');
}
}
public function updateJob(Request $request, Response $response, array $args): Response
{
$jobId = $args['id'];
$response->getBody()->write(json_encode(['message' => 'Update job endpoint', 'id' => $jobId]));
return $response->withHeader('Content-Type', 'application/json');
$jobId = (int)$args['id'];
$params = $request->getParsedBody();
// Get tenant from request attribute (set by middleware)
$tenant = $request->getAttribute('tenant');
// First get the existing job to ensure it belongs to this tenant
$existingJob = $this->jobService->getJobById($jobId, $tenant);
if (!$existingJob) {
$response = $response->withStatus(404);
$response->getBody()->write(json_encode(['error' => 'Job not found or does not belong to your tenant']));
return $response->withHeader('Content-Type', 'application/json');
}
// Update the job with new data
$existingJob->setTitle($params['title'] ?? $existingJob->getTitle());
$existingJob->setDescription($params['description'] ?? $existingJob->getDescription());
$existingJob->setLocation($params['location'] ?? $existingJob->getLocation());
$existingJob->setEmploymentType($params['employment_type'] ?? $existingJob->getEmploymentType());
$result = $this->jobService->updateJob($existingJob);
if ($result) {
$response->getBody()->write(json_encode(['message' => 'Job updated successfully']));
return $response->withHeader('Content-Type', 'application/json');
} else {
$response = $response->withStatus(500);
$response->getBody()->write(json_encode(['error' => 'Failed to update job']));
return $response->withHeader('Content-Type', 'application/json');
}
}
public function deleteJob(Request $request, Response $response, array $args): Response
{
$jobId = $args['id'];
$response->getBody()->write(json_encode(['message' => 'Delete job endpoint', 'id' => $jobId]));
return $response->withHeader('Content-Type', 'application/json');
$jobId = (int)$args['id'];
// Get tenant from request attribute (set by middleware)
$tenant = $request->getAttribute('tenant');
$result = $this->jobService->deleteJob($jobId, $tenant);
if ($result) {
$response->getBody()->write(json_encode(['message' => 'Job deleted successfully']));
return $response->withHeader('Content-Type', 'application/json');
} else {
$response = $response->withStatus(404);
$response->getBody()->write(json_encode(['error' => 'Job not found or does not belong to your tenant']));
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,
]);
}
}

View File

@@ -0,0 +1,52 @@
<?hh // strict
namespace App\Middleware;
use App\Services\TenantResolver;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Psr7\Response;
class TenantMiddleware implements MiddlewareInterface
{
private TenantResolver $tenantResolver;
public function __construct(TenantResolver $tenantResolver)
{
$this->tenantResolver = $tenantResolver;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// Resolve the current tenant
$tenant = $this->tenantResolver->resolveTenant($request);
// Attach the tenant to the request attributes
$request = $request->withAttribute('tenant', $tenant);
// If we have a tenant, verify it's active
if ($tenant !== null) {
if (!$tenant->getIsActive()) {
$response = new Response();
$response->getBody()->write(json_encode([
'error' => 'Tenant is inactive',
'message' => 'This tenant account is currently inactive.'
]));
return $response
->withHeader('Content-Type', 'application/json')
->withStatus(403);
}
// Set tenant-specific headers
$response = $handler->handle($request);
return $response->withAddedHeader('X-Tenant-ID', $tenant->getId());
}
// If no tenant found, we might want to return an error
// Or provide a default experience
$response = $handler->handle($request);
return $response->withAddedHeader('X-Tenant-ID', 'default');
}
}

View File

@@ -0,0 +1,101 @@
<?hh // strict
namespace App\Models;
use DateTime;
class Job
{
private int $id;
private string $title;
private string $description;
private string $location;
private string $employmentType;
private string $tenantId;
private DateTime $createdAt;
private DateTime $updatedAt;
public function __construct(
int $id,
string $title,
string $description,
string $location,
string $employmentType,
string $tenantId
) {
$this->id = $id;
$this->title = $title;
$this->description = $description;
$this->location = $location;
$this->employmentType = $employmentType;
$this->tenantId = $tenantId;
$this->createdAt = new DateTime();
$this->updatedAt = new DateTime();
}
// Getters
public function getId(): int
{
return $this->id;
}
public function getTitle(): string
{
return $this->title;
}
public function getDescription(): string
{
return $this->description;
}
public function getLocation(): string
{
return $this->location;
}
public function getEmploymentType(): string
{
return $this->employmentType;
}
public function getTenantId(): string
{
return $this->tenantId;
}
public function getCreatedAt(): DateTime
{
return $this->createdAt;
}
public function getUpdatedAt(): DateTime
{
return $this->updatedAt;
}
// Setters
public function setTitle(string $title): void
{
$this->title = $title;
$this->updatedAt = new DateTime();
}
public function setDescription(string $description): void
{
$this->description = $description;
$this->updatedAt = new DateTime();
}
public function setLocation(string $location): void
{
$this->location = $location;
$this->updatedAt = new DateTime();
}
public function setEmploymentType(string $employmentType): void
{
$this->employmentType = $employmentType;
$this->updatedAt = new DateTime();
}
}

View File

@@ -0,0 +1,79 @@
<?hh // strict
namespace App\Models;
use DateTime;
class Tenant
{
private string $id;
private string $name;
private string $subdomain;
private bool $isActive;
private DateTime $createdAt;
private DateTime $updatedAt;
public function __construct(
string $id,
string $name,
string $subdomain,
bool $isActive = true
) {
$this->id = $id;
$this->name = $name;
$this->subdomain = $subdomain;
$this->isActive = $isActive;
$this->createdAt = new DateTime();
$this->updatedAt = new DateTime();
}
// Getters
public function getId(): string
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function getSubdomain(): string
{
return $this->subdomain;
}
public function getIsActive(): bool
{
return $this->isActive;
}
public function getCreatedAt(): DateTime
{
return $this->createdAt;
}
public function getUpdatedAt(): DateTime
{
return $this->updatedAt;
}
// Setters
public function setName(string $name): void
{
$this->name = $name;
$this->updatedAt = new DateTime();
}
public function setSubdomain(string $subdomain): void
{
$this->subdomain = $subdomain;
$this->updatedAt = new DateTime();
}
public function setIsActive(bool $isActive): void
{
$this->isActive = $isActive;
$this->updatedAt = new DateTime();
}
}

View File

@@ -0,0 +1,87 @@
<?hh // strict
namespace App\Models;
use DateTime;
class User
{
private int $id;
private string $email;
private string $name;
private string $role; // 'job_seeker' or 'job_provider'
private string $tenantId;
private DateTime $createdAt;
private DateTime $updatedAt;
public function __construct(
int $id,
string $email,
string $name,
string $role,
string $tenantId
) {
$this->id = $id;
$this->email = $email;
$this->name = $name;
$this->role = $role;
$this->tenantId = $tenantId;
$this->createdAt = new DateTime();
$this->updatedAt = new DateTime();
}
// Getters
public function getId(): int
{
return $this->id;
}
public function getEmail(): string
{
return $this->email;
}
public function getName(): string
{
return $this->name;
}
public function getRole(): string
{
return $this->role;
}
public function getTenantId(): string
{
return $this->tenantId;
}
public function getCreatedAt(): DateTime
{
return $this->createdAt;
}
public function getUpdatedAt(): DateTime
{
return $this->updatedAt;
}
// Setters
public function setEmail(string $email): void
{
$this->email = $email;
$this->updatedAt = new DateTime();
}
public function setName(string $name): void
{
$this->name = $name;
$this->updatedAt = new DateTime();
}
public function setRole(string $role): void
{
$this->role = $role;
$this->updatedAt = new DateTime();
}
}

View File

@@ -0,0 +1,88 @@
<?hh // strict
namespace App\Services;
use League\OAuth2\Client\Provider\Google;
use League\OAuth2\Client\Provider\Github;
use Psr\Http\Message\ServerRequestInterface;
class AuthService
{
private Google $googleProvider;
private Github $githubProvider;
public function __construct()
{
$this->googleProvider = new Google([
'clientId' => $_ENV['GOOGLE_CLIENT_ID'] ?? '',
'clientSecret' => $_ENV['GOOGLE_CLIENT_SECRET'] ?? '',
'redirectUri' => $_ENV['APP_URL'] . '/auth/google/callback' ?? 'http://localhost:18000/auth/google/callback',
]);
$this->githubProvider = new Github([
'clientId' => $_ENV['GITHUB_CLIENT_ID'] ?? '',
'clientSecret' => $_ENV['GITHUB_CLIENT_SECRET'] ?? '',
'redirectUri' => $_ENV['APP_URL'] . '/auth/github/callback' ?? 'http://localhost:18000/auth/github/callback',
]);
}
public function getGoogleAuthorizationUrl(): string
{
return $this->googleProvider->getAuthorizationUrl([
'scope' => ['email', 'profile']
]);
}
public function getGithubAuthorizationUrl(): string
{
return $this->githubProvider->getAuthorizationUrl([
'scope' => ['user:email']
]);
}
public function handleGoogleCallback(ServerRequestInterface $request): array
{
$code = $request->getQueryParams()['code'] ?? null;
if (!$code) {
throw new \Exception('No authorization code received from Google');
}
$token = $this->googleProvider->getAccessToken('authorization_code', [
'code' => $code
]);
$user = $this->googleProvider->getResourceOwner($token);
return [
'provider' => 'google',
'id' => $user->getId(),
'email' => $user->getEmail(),
'name' => $user->getName(),
'avatar' => $user->getAvatar(),
];
}
public function handleGithubCallback(ServerRequestInterface $request): array
{
$code = $request->getQueryParams()['code'] ?? null;
if (!$code) {
throw new \Exception('No authorization code received from GitHub');
}
$token = $this->githubProvider->getAccessToken('authorization_code', [
'code' => $code
]);
$user = $this->githubProvider->getResourceOwner($token);
return [
'provider' => 'github',
'id' => $user->getId(),
'email' => $user->getEmail(),
'name' => $user->getName(),
'avatar' => $user->getAvatarUrl(),
];
}
}

View File

@@ -0,0 +1,143 @@
<?hh // strict
namespace App\Services;
use App\Models\Job;
use App\Models\Tenant;
use PDO;
class JobService
{
private PDO $db;
public function __construct(PDO $db)
{
$this->db = $db;
}
/**
* Get all jobs for the current tenant
*/
public function getAllJobs(?Tenant $tenant, ?array $filters = null): array
{
$tenantId = $tenant ? $tenant->getId() : 'default';
$sql = "SELECT * FROM jobs WHERE tenant_id = :tenant_id AND status = 'active'";
// Add filters if provided
$params = [':tenant_id' => $tenantId];
if ($filters) {
if (isset($filters['location'])) {
$sql .= " AND location LIKE :location";
$params[':location'] = '%' . $filters['location'] . '%';
}
if (isset($filters['keywords'])) {
$sql .= " AND (title LIKE :keywords OR description LIKE :keywords)";
$params[':keywords'] = '%' . $filters['keywords'] . '%';
}
}
$sql .= " ORDER BY created_at DESC";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$jobs = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$jobs[] = new Job(
id: (int)$row['id'],
title: $row['title'],
description: $row['description'],
location: $row['location'],
employmentType: $row['employment_type'],
tenantId: $row['tenant_id']
);
}
return $jobs;
}
/**
* Get a specific job by ID for the current tenant
*/
public function getJobById(int $jobId, ?Tenant $tenant): ?Job
{
$tenantId = $tenant ? $tenant->getId() : 'default';
$sql = "SELECT * FROM jobs WHERE id = :id AND tenant_id = :tenant_id";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':id' => $jobId,
':tenant_id' => $tenantId
]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
return null;
}
return new Job(
id: (int)$row['id'],
title: $row['title'],
description: $row['description'],
location: $row['location'],
employmentType: $row['employment_type'],
tenantId: $row['tenant_id']
);
}
/**
* Create a new job posting for the tenant
*/
public function createJob(Job $job): bool
{
$sql = "INSERT INTO jobs (title, description, location, employment_type, tenant_id) VALUES (:title, :description, :location, :employment_type, :tenant_id)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
':title' => $job->getTitle(),
':description' => $job->getDescription(),
':location' => $job->getLocation(),
':employment_type' => $job->getEmploymentType(),
':tenant_id' => $job->getTenantId()
]);
}
/**
* Update an existing job for the tenant
*/
public function updateJob(Job $job): bool
{
$sql = "UPDATE jobs SET title = :title, description = :description, location = :location, employment_type = :employment_type, updated_at = NOW() WHERE id = :id AND tenant_id = :tenant_id";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
':id' => $job->getId(),
':title' => $job->getTitle(),
':description' => $job->getDescription(),
':location' => $job->getLocation(),
':employment_type' => $job->getEmploymentType(),
':tenant_id' => $job->getTenantId()
]);
}
/**
* Delete a job for the tenant
*/
public function deleteJob(int $jobId, ?Tenant $tenant): bool
{
$tenantId = $tenant ? $tenant->getId() : 'default';
$sql = "DELETE FROM jobs WHERE id = :id AND tenant_id = :tenant_id";
$stmt = $this->db->prepare($sql);
$result = $stmt->execute([
':id' => $jobId,
':tenant_id' => $tenantId
]);
return $result && $stmt->rowCount() > 0;
}
}

View File

@@ -0,0 +1,55 @@
<?hh // strict
namespace App\Services;
use App\Models\Tenant;
use Psr\Http\Message\ServerRequestInterface;
class TenantResolver
{
/**
* Resolve the tenant based on the request
*/
public function resolveTenant(ServerRequestInterface $request): ?Tenant
{
$host = $request->getUri()->getHost();
$path = $request->getUri()->getPath();
// Try to extract tenant from subdomain
// Format: tenant.merchantsofhope.org
$hostParts = explode('.', $host);
if (count($hostParts) >= 3) {
$subdomain = $hostParts[0];
return $this->findTenantBySubdomain($subdomain);
}
// Alternatively, look for tenant in path
// Format: merchantsofhope.org/tenant-name
$pathParts = explode('/', trim($path, '/'));
if (!empty($pathParts[0])) {
return $this->findTenantBySubdomain($pathParts[0]);
}
// Default to a global tenant if none found
return null;
}
/**
* Find tenant by subdomain in the database
*/
private function findTenantBySubdomain(string $subdomain): ?Tenant
{
// This would normally query the database
// For now, we'll return a mock tenant if subdomain exists
if (!empty($subdomain)) {
return new Tenant(
id: 'tenant-' . $subdomain,
name: ucfirst($subdomain) . ' Tenant',
subdomain: $subdomain,
isActive: true
);
}
return null;
}
}