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", "guzzlehttp/guzzle": "^7.0",
"monolog/monolog": "^2.0", "monolog/monolog": "^2.0",
"vlucas/phpdotenv": "^5.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": { "require-dev": {
"phpunit/phpunit": "^9.0", "phpunit/phpunit": "^9.0",

View File

@@ -7,6 +7,8 @@
require_once __DIR__ . '/bootstrap.php'; require_once __DIR__ . '/bootstrap.php';
use App\Controllers\HomeController; use App\Controllers\HomeController;
use App\Middleware\TenantMiddleware;
use App\Services\TenantResolver;
use Slim\Factory\AppFactory; use Slim\Factory\AppFactory;
use Slim\Middleware\ContentLengthMiddleware; use Slim\Middleware\ContentLengthMiddleware;
use Slim\Routing\RouteCollectorProxy; use Slim\Routing\RouteCollectorProxy;
@@ -20,9 +22,18 @@ $app->addBodyParsingMiddleware();
$app->addRoutingMiddleware(); $app->addRoutingMiddleware();
$app->add(new ContentLengthMiddleware()); $app->add(new ContentLengthMiddleware());
// Add tenant middleware
$tenantResolver = new TenantResolver();
$tenantMiddleware = new TenantMiddleware($tenantResolver);
$app->add($tenantMiddleware);
// Define routes // Define routes
$app->get('/', [HomeController::class, 'index']); $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 // Group routes for API
$app->group('/api', function (RouteCollectorProxy $group) { $app->group('/api', function (RouteCollectorProxy $group) {
// Authentication routes // 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; 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\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
class AuthController class AuthController
{ {
private AuthService $authService;
public function __construct()
{
$this->authService = new AuthService();
}
public function login(Request $request, Response $response): Response 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'); return $response->withHeader('Content-Type', 'application/json');
} }
public function logout(Request $request, Response $response): Response 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'); return $response->withHeader('Content-Type', 'application/json');
} }
public function register(Request $request, Response $response): Response 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'); 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; namespace App\Controllers;
use App\Models\Job;
use App\Services\JobService;
use PDO;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
class JobController 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 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'); return $response->withHeader('Content-Type', 'application/json');
} }
public function getJob(Request $request, Response $response, array $args): Response public function getJob(Request $request, Response $response, array $args): Response
{ {
$jobId = $args['id']; $jobId = (int)$args['id'];
$response->getBody()->write(json_encode(['message' => 'Get job endpoint', 'id' => $jobId]));
// 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'); return $response->withHeader('Content-Type', 'application/json');
} }
public function myJobs(Request $request, Response $response): Response 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'); return $response->withHeader('Content-Type', 'application/json');
} }
public function createJob(Request $request, Response $response): Response public function createJob(Request $request, Response $response): Response
{ {
$response->getBody()->write(json_encode(['message' => 'Create job endpoint'])); $params = $request->getParsedBody();
return $response->withHeader('Content-Type', 'application/json');
// 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 public function updateJob(Request $request, Response $response, array $args): Response
{ {
$jobId = $args['id']; $jobId = (int)$args['id'];
$response->getBody()->write(json_encode(['message' => 'Update job endpoint', 'id' => $jobId])); $params = $request->getParsedBody();
return $response->withHeader('Content-Type', 'application/json');
// 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 public function deleteJob(Request $request, Response $response, array $args): Response
{ {
$jobId = $args['id']; $jobId = (int)$args['id'];
$response->getBody()->write(json_encode(['message' => 'Delete job endpoint', 'id' => $jobId]));
return $response->withHeader('Content-Type', 'application/json'); // 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;
}
}

View File

@@ -1,7 +1,7 @@
""" """
Applications API routes Applications API routes
""" """
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status, Request
from typing import List from typing import List
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -35,101 +35,107 @@ class ApplicationResponse(BaseModel):
from_attributes = True from_attributes = True
@router.get("/", response_model=List[ApplicationResponse]) @router.get("/", response_model=List[ApplicationResponse])
async def get_applications(skip: int = 0, limit: int = 100): async def get_applications(skip: int = 0, limit: int = 100, db: Session = Depends(SessionLocal), request: Request = None):
"""Get all applications""" """Get all applications for the current tenant"""
db = SessionLocal() tenant_id = getattr(request.state, 'tenant_id', None)
try: if not tenant_id and settings.MULTI_TENANT_ENABLED:
applications = db.query(Application).offset(skip).limit(limit).all() raise HTTPException(status_code=400, detail="Tenant ID is required")
return applications
finally: # Get applications for jobs in the current tenant or applications by users in the current tenant
db.close() applications = db.query(Application).join(JobPosting).filter(
(JobPosting.tenant_id == tenant_id) | (Application.user_id.in_(
db.query(User.id).filter(User.tenant_id == tenant_id)
))
).offset(skip).limit(limit).all()
return applications
@router.get("/{application_id}", response_model=ApplicationResponse) @router.get("/{application_id}", response_model=ApplicationResponse)
async def get_application(application_id: int): async def get_application(application_id: int, db: Session = Depends(SessionLocal), request: Request = None):
"""Get a specific application""" """Get a specific application"""
db = SessionLocal() tenant_id = getattr(request.state, 'tenant_id', None)
try: if not tenant_id and settings.MULTI_TENANT_ENABLED:
application = db.query(Application).filter(Application.id == application_id).first() raise HTTPException(status_code=400, detail="Tenant ID is required")
if not application:
raise HTTPException(status_code=404, detail="Application not found") application = db.query(Application).join(JobPosting).filter(
return application Application.id == application_id,
finally: (JobPosting.tenant_id == tenant_id) | (Application.user_id.in_(
db.close() db.query(User.id).filter(User.tenant_id == tenant_id)
))
).first()
if not application:
raise HTTPException(status_code=404, detail="Application not found")
return application
@router.post("/", response_model=ApplicationResponse) @router.post("/", response_model=ApplicationResponse)
async def create_application(application: ApplicationCreate, user_id: int = 1): # In real app, get from auth context async def create_application(application: ApplicationCreate, db: Session = Depends(SessionLocal), request: Request = None, user_id: int = 1): # In real app, get from auth context
"""Create a new job application""" """Create a new job application"""
db = SessionLocal() tenant_id = getattr(request.state, 'tenant_id', None)
try: if not tenant_id and settings.MULTI_TENANT_ENABLED:
# Verify user exists and has permission to apply raise HTTPException(status_code=400, detail="Tenant ID is required")
user = db.query(User).filter(User.id == user_id).first()
if not user or user.role != "job_seeker": # Verify user exists and has permission to apply
raise HTTPException( user = db.query(User).filter(
status_code=status.HTTP_403_FORBIDDEN, User.id == user_id,
detail="Only job seekers can apply for jobs" User.tenant_id == tenant_id # Make sure user belongs to current tenant
) ).first()
if not user or user.role != "job_seeker":
# Verify job posting exists and is active raise HTTPException(
job_posting = db.query(JobPosting).filter( status_code=status.HTTP_403_FORBIDDEN,
JobPosting.id == application.job_posting_id, detail="Only job seekers can apply for jobs"
JobPosting.is_active == True
).first()
if not job_posting:
raise HTTPException(status_code=404, detail="Job posting not found or inactive")
# Verify resume exists and belongs to user
resume = db.query(Resume).filter(
Resume.id == application.resume_id,
Resume.user_id == user_id
).first()
if not resume:
raise HTTPException(status_code=404, detail="Resume not found")
db_application = Application(
user_id=user_id,
job_posting_id=application.job_posting_id,
resume_id=application.resume_id,
cover_letter=application.cover_letter
) )
db.add(db_application)
db.commit() # Verify job posting exists and is active and belongs to the same tenant
db.refresh(db_application) job_posting = db.query(JobPosting).filter(
return db_application JobPosting.id == application.job_posting_id,
finally: JobPosting.is_active == True,
db.close() JobPosting.tenant_id == tenant_id # Ensure job belongs to current tenant
).first()
if not job_posting:
raise HTTPException(status_code=404, detail="Job posting not found or inactive or not in your tenant")
# Verify resume exists and belongs to user
resume = db.query(Resume).filter(
Resume.id == application.resume_id,
Resume.user_id == user_id
).first()
if not resume:
raise HTTPException(status_code=404, detail="Resume not found")
db_application = Application(
user_id=user_id,
job_posting_id=application.job_posting_id,
resume_id=application.resume_id,
cover_letter=application.cover_letter
)
db.add(db_application)
db.commit()
db.refresh(db_application)
return db_application
@router.put("/{application_id}", response_model=ApplicationResponse) @router.put("/{application_id}", response_model=ApplicationResponse)
async def update_application(application_id: int, app_update: ApplicationUpdate): async def update_application(application_id: int, app_update: ApplicationUpdate, db: Session = Depends(SessionLocal)):
"""Update an application""" """Update an application"""
db = SessionLocal() db_application = db.query(Application).filter(Application.id == application_id).first()
try: if not db_application:
db_application = db.query(Application).filter(Application.id == application_id).first() raise HTTPException(status_code=404, detail="Application not found")
if not db_application:
raise HTTPException(status_code=404, detail="Application not found") # Update fields if provided
if app_update.status is not None:
# Update fields if provided db_application.status = app_update.status.value
if app_update.status is not None: if app_update.cover_letter is not None:
db_application.status = app_update.status.value db_application.cover_letter = app_update.cover_letter
if app_update.cover_letter is not None:
db_application.cover_letter = app_update.cover_letter db.commit()
db.refresh(db_application)
db.commit() return db_application
db.refresh(db_application)
return db_application
finally:
db.close()
@router.delete("/{application_id}") @router.delete("/{application_id}")
async def delete_application(application_id: int): async def delete_application(application_id: int, db: Session = Depends(SessionLocal)):
"""Delete an application""" """Delete an application"""
db = SessionLocal() db_application = db.query(Application).filter(Application.id == application_id).first()
try: if not db_application:
db_application = db.query(Application).filter(Application.id == application_id).first() raise HTTPException(status_code=404, detail="Application not found")
if not db_application:
raise HTTPException(status_code=404, detail="Application not found") db.delete(db_application)
db.commit()
db.delete(db_application) return {"message": "Application deleted successfully"}
db.commit()
return {"message": "Application deleted successfully"}
finally:
db.close()

View File

@@ -1,7 +1,7 @@
""" """
Jobs API routes Jobs API routes
""" """
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status, Request
from typing import List from typing import List
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -49,18 +49,29 @@ class JobResponse(BaseModel):
from_attributes = True from_attributes = True
@router.get("/", response_model=List[JobResponse]) @router.get("/", response_model=List[JobResponse])
async def get_jobs(skip: int = 0, limit: int = 100, is_active: bool = True, db: Session = Depends(SessionLocal)): async def get_jobs(skip: int = 0, limit: int = 100, is_active: bool = True, db: Session = Depends(SessionLocal), request: Request = None):
"""Get all jobs""" """Get all jobs for the current tenant"""
query = db.query(JobPosting) tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
query = db.query(JobPosting).filter(JobPosting.tenant_id == tenant_id)
if is_active is not None: if is_active is not None:
query = query.filter(JobPosting.is_active == is_active) query = query.filter(JobPosting.is_active == is_active)
jobs = query.offset(skip).limit(limit).all() jobs = query.offset(skip).limit(limit).all()
return jobs return jobs
@router.get("/{job_id}", response_model=JobResponse) @router.get("/{job_id}", response_model=JobResponse)
async def get_job(job_id: int, db: Session = Depends(SessionLocal)): async def get_job(job_id: int, db: Session = Depends(SessionLocal), request: Request = None):
"""Get a specific job""" """Get a specific job"""
job = db.query(JobPosting).filter(JobPosting.id == job_id).first() tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
job = db.query(JobPosting).filter(
JobPosting.id == job_id,
JobPosting.tenant_id == tenant_id # Ensure job belongs to current tenant
).first()
if not job: if not job:
raise HTTPException(status_code=404, detail="Job not found") raise HTTPException(status_code=404, detail="Job not found")
if not job.is_active: if not job.is_active:
@@ -68,10 +79,17 @@ async def get_job(job_id: int, db: Session = Depends(SessionLocal)):
return job return job
@router.post("/", response_model=JobResponse) @router.post("/", response_model=JobResponse)
async def create_job(job: JobCreate, db: Session = Depends(SessionLocal), user_id: int = 1): # In real app, get from auth context async def create_job(job: JobCreate, db: Session = Depends(SessionLocal), request: Request = None, user_id: int = 1): # In real app, get from auth context
"""Create a new job posting""" """Create a new job posting"""
tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
# Verify user exists and has permission to create job postings # Verify user exists and has permission to create job postings
user = db.query(User).filter(User.id == user_id).first() user = db.query(User).filter(
User.id == user_id,
User.tenant_id == tenant_id # Ensure user belongs to current tenant
).first()
if not user or user.role not in ["job_provider", "admin"]: if not user or user.role not in ["job_provider", "admin"]:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
@@ -86,7 +104,7 @@ async def create_job(job: JobCreate, db: Session = Depends(SessionLocal), user_i
salary_min=job.salary_min, salary_min=job.salary_min,
salary_max=job.salary_max, salary_max=job.salary_max,
is_remote=job.is_remote, is_remote=job.is_remote,
tenant_id=user.tenant_id, # Use user's tenant tenant_id=tenant_id, # Use current tenant
created_by_user_id=user_id created_by_user_id=user_id
) )
db.add(db_job) db.add(db_job)
@@ -95,9 +113,16 @@ async def create_job(job: JobCreate, db: Session = Depends(SessionLocal), user_i
return db_job return db_job
@router.put("/{job_id}", response_model=JobResponse) @router.put("/{job_id}", response_model=JobResponse)
async def update_job(job_id: int, job_update: JobUpdate, db: Session = Depends(SessionLocal)): async def update_job(job_id: int, job_update: JobUpdate, db: Session = Depends(SessionLocal), request: Request = None):
"""Update a job posting""" """Update a job posting"""
db_job = db.query(JobPosting).filter(JobPosting.id == job_id).first() tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
db_job = db.query(JobPosting).filter(
JobPosting.id == job_id,
JobPosting.tenant_id == tenant_id # Ensure job belongs to current tenant
).first()
if not db_job: if not db_job:
raise HTTPException(status_code=404, detail="Job not found") raise HTTPException(status_code=404, detail="Job not found")
@@ -110,9 +135,16 @@ async def update_job(job_id: int, job_update: JobUpdate, db: Session = Depends(S
return db_job return db_job
@router.delete("/{job_id}") @router.delete("/{job_id}")
async def delete_job(job_id: int, db: Session = Depends(SessionLocal)): async def delete_job(job_id: int, db: Session = Depends(SessionLocal), request: Request = None):
"""Delete a job posting (soft delete by setting is_active to False)""" """Delete a job posting (soft delete by setting is_active to False)"""
db_job = db.query(JobPosting).filter(JobPosting.id == job_id).first() tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
db_job = db.query(JobPosting).filter(
JobPosting.id == job_id,
JobPosting.tenant_id == tenant_id # Ensure job belongs to current tenant
).first()
if not db_job: if not db_job:
raise HTTPException(status_code=404, detail="Job not found") raise HTTPException(status_code=404, detail="Job not found")

View File

@@ -1,14 +1,14 @@
""" """
Users API routes Users API routes
""" """
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status, Request
from typing import List from typing import List
from pydantic import BaseModel from pydantic import BaseModel
import hashlib import hashlib
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ..database import SessionLocal from ..database import SessionLocal
from ..models import User, UserRole from ..models import User, UserRole, Tenant
from ..config.settings import settings from ..config.settings import settings
router = APIRouter() router = APIRouter()
@@ -42,22 +42,42 @@ def hash_password(password: str) -> str:
return hashlib.sha256(password.encode()).hexdigest() return hashlib.sha256(password.encode()).hexdigest()
@router.get("/", response_model=List[UserResponse]) @router.get("/", response_model=List[UserResponse])
async def get_users(skip: int = 0, limit: int = 100, db: Session = Depends(SessionLocal)): async def get_users(skip: int = 0, limit: int = 100, db: Session = Depends(SessionLocal), request: Request = None):
"""Get all users""" """Get all users for the current tenant"""
users = db.query(User).offset(skip).limit(limit).all() tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
query = db.query(User)
if settings.MULTI_TENANT_ENABLED:
query = query.filter(User.tenant_id == tenant_id)
users = query.offset(skip).limit(limit).all()
return users return users
@router.get("/{user_id}", response_model=UserResponse) @router.get("/{user_id}", response_model=UserResponse)
async def get_user(user_id: int, db: Session = Depends(SessionLocal)): async def get_user(user_id: int, db: Session = Depends(SessionLocal), request: Request = None):
"""Get a specific user""" """Get a specific user"""
user = db.query(User).filter(User.id == user_id).first() tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
query = db.query(User).filter(User.id == user_id)
if settings.MULTI_TENANT_ENABLED:
query = query.filter(User.tenant_id == tenant_id)
user = query.first()
if not user: if not user:
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
return user return user
@router.post("/", response_model=UserResponse) @router.post("/", response_model=UserResponse)
async def create_user(user: UserCreate, db: Session = Depends(SessionLocal)): async def create_user(user: UserCreate, db: Session = Depends(SessionLocal), request: Request = None):
"""Create a new user""" """Create a new user"""
tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
# Check if user already exists # Check if user already exists
existing_user = db.query(User).filter( existing_user = db.query(User).filter(
(User.email == user.email) | (User.username == user.username) (User.email == user.email) | (User.username == user.username)
@@ -73,7 +93,7 @@ async def create_user(user: UserCreate, db: Session = Depends(SessionLocal)):
username=user.username, username=user.username,
hashed_password=hashed_pwd, hashed_password=hashed_pwd,
role=user.role.value, role=user.role.value,
tenant_id=1 # Default tenant, in real app would come from context tenant_id=tenant_id # Use the current tenant
) )
db.add(db_user) db.add(db_user)
db.commit() db.commit()
@@ -81,9 +101,16 @@ async def create_user(user: UserCreate, db: Session = Depends(SessionLocal)):
return db_user return db_user
@router.put("/{user_id}", response_model=UserResponse) @router.put("/{user_id}", response_model=UserResponse)
async def update_user(user_id: int, user_update: UserUpdate, db: Session = Depends(SessionLocal)): async def update_user(user_id: int, user_update: UserUpdate, db: Session = Depends(SessionLocal), request: Request = None):
"""Update a user""" """Update a user"""
db_user = db.query(User).filter(User.id == user_id).first() tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
db_user = db.query(User).filter(
User.id == user_id,
User.tenant_id == tenant_id # Ensure user belongs to current tenant
).first()
if not db_user: if not db_user:
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
@@ -100,9 +127,16 @@ async def update_user(user_id: int, user_update: UserUpdate, db: Session = Depen
return db_user return db_user
@router.delete("/{user_id}") @router.delete("/{user_id}")
async def delete_user(user_id: int, db: Session = Depends(SessionLocal)): async def delete_user(user_id: int, db: Session = Depends(SessionLocal), request: Request = None):
"""Delete a user""" """Delete a user"""
db_user = db.query(User).filter(User.id == user_id).first() tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
db_user = db.query(User).filter(
User.id == user_id,
User.tenant_id == tenant_id # Ensure user belongs to current tenant
).first()
if not db_user: if not db_user:
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")

View File

@@ -1,31 +1,44 @@
""" """
Tenant middleware for handling multi-tenant requests Tenant middleware for handling multi-tenant requests
""" """
import uuid
from typing import Optional from typing import Optional
from fastapi import Request, HTTPException, status from fastapi import Request, HTTPException, status
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from sqlalchemy.orm import Session
from .config.settings import settings from .config.settings import settings
from .models import Tenant from .models import Tenant
from .database import SessionLocal
class TenantMiddleware(BaseHTTPMiddleware): class TenantMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next): async def dispatch(self, request: Request, call_next):
# Get tenant ID from header or subdomain # Get tenant ID from header
tenant_id = request.headers.get(settings.TENANT_ID_HEADER) tenant_id = request.headers.get(settings.TENANT_ID_HEADER)
# If not in header, try to extract from subdomain
if not tenant_id: if not tenant_id:
# Try to extract tenant from subdomain
host = request.headers.get("host", "") host = request.headers.get("host", "")
tenant_id = self.extract_tenant_from_host(host) tenant_id = self.extract_tenant_from_host(host)
if not tenant_id and settings.MULTI_TENANT_ENABLED: # Look up tenant in database
tenant = None
if tenant_id:
db: Session = SessionLocal()
try:
tenant = db.query(Tenant).filter(Tenant.subdomain == tenant_id).first()
finally:
db.close()
if settings.MULTI_TENANT_ENABLED and not tenant:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant ID is required" detail="Valid tenant ID is required"
) )
# Attach tenant info to request # Attach tenant info to request
request.state.tenant_id = tenant_id request.state.tenant = tenant
request.state.tenant_id = tenant.id if tenant else None
response = await call_next(request) response = await call_next(request)
return response return response
@@ -34,7 +47,9 @@ class TenantMiddleware(BaseHTTPMiddleware):
""" """
Extract tenant from host (subdomain.tenant.com) Extract tenant from host (subdomain.tenant.com)
""" """
# For now, return a default tenant or None import re
# In a real implementation, you would parse the subdomain # Match subdomain from host (e.g., "tenant1.example.com" -> "tenant1")
# and look up the corresponding tenant in the database match = re.match(r'^([^.]+)\.', host)
return "default" if match:
return match.group(1)
return None