.
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
15
qwen/hack/src/Controllers/ApplicationController.php
Normal file
15
qwen/hack/src/Controllers/ApplicationController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
52
qwen/hack/src/Middleware/TenantMiddleware.php
Normal file
52
qwen/hack/src/Middleware/TenantMiddleware.php
Normal 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');
|
||||
}
|
||||
}
|
||||
101
qwen/hack/src/Models/Job.php
Normal file
101
qwen/hack/src/Models/Job.php
Normal 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();
|
||||
}
|
||||
}
|
||||
79
qwen/hack/src/Models/Tenant.php
Normal file
79
qwen/hack/src/Models/Tenant.php
Normal 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();
|
||||
}
|
||||
}
|
||||
87
qwen/hack/src/Models/User.php
Normal file
87
qwen/hack/src/Models/User.php
Normal 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();
|
||||
}
|
||||
}
|
||||
88
qwen/hack/src/Services/AuthService.php
Normal file
88
qwen/hack/src/Services/AuthService.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
143
qwen/hack/src/Services/JobService.php
Normal file
143
qwen/hack/src/Services/JobService.php
Normal 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;
|
||||
}
|
||||
}
|
||||
55
qwen/hack/src/Services/TenantResolver.php
Normal file
55
qwen/hack/src/Services/TenantResolver.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Applications API routes
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -35,101 +35,107 @@ class ApplicationResponse(BaseModel):
|
||||
from_attributes = True
|
||||
|
||||
@router.get("/", response_model=List[ApplicationResponse])
|
||||
async def get_applications(skip: int = 0, limit: int = 100):
|
||||
"""Get all applications"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
applications = db.query(Application).offset(skip).limit(limit).all()
|
||||
return applications
|
||||
finally:
|
||||
db.close()
|
||||
async def get_applications(skip: int = 0, limit: int = 100, db: Session = Depends(SessionLocal), request: Request = None):
|
||||
"""Get all applications for the current tenant"""
|
||||
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")
|
||||
|
||||
# Get applications for jobs in the current tenant or applications by users in the current tenant
|
||||
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)
|
||||
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"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
application = db.query(Application).filter(Application.id == application_id).first()
|
||||
if not application:
|
||||
raise HTTPException(status_code=404, detail="Application not found")
|
||||
return application
|
||||
finally:
|
||||
db.close()
|
||||
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")
|
||||
|
||||
application = db.query(Application).join(JobPosting).filter(
|
||||
Application.id == application_id,
|
||||
(JobPosting.tenant_id == tenant_id) | (Application.user_id.in_(
|
||||
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)
|
||||
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"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Verify user exists and has permission to apply
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user or user.role != "job_seeker":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only job seekers can apply for jobs"
|
||||
)
|
||||
|
||||
# Verify job posting exists and is active
|
||||
job_posting = db.query(JobPosting).filter(
|
||||
JobPosting.id == application.job_posting_id,
|
||||
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
|
||||
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 apply
|
||||
user = db.query(User).filter(
|
||||
User.id == user_id,
|
||||
User.tenant_id == tenant_id # Make sure user belongs to current tenant
|
||||
).first()
|
||||
if not user or user.role != "job_seeker":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only job seekers can apply for jobs"
|
||||
)
|
||||
db.add(db_application)
|
||||
db.commit()
|
||||
db.refresh(db_application)
|
||||
return db_application
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# Verify job posting exists and is active and belongs to the same tenant
|
||||
job_posting = db.query(JobPosting).filter(
|
||||
JobPosting.id == application.job_posting_id,
|
||||
JobPosting.is_active == True,
|
||||
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)
|
||||
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"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
db_application = db.query(Application).filter(Application.id == application_id).first()
|
||||
if not db_application:
|
||||
raise HTTPException(status_code=404, detail="Application not found")
|
||||
|
||||
# Update fields if provided
|
||||
if app_update.status is not None:
|
||||
db_application.status = app_update.status.value
|
||||
if app_update.cover_letter is not None:
|
||||
db_application.cover_letter = app_update.cover_letter
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_application)
|
||||
return db_application
|
||||
finally:
|
||||
db.close()
|
||||
db_application = db.query(Application).filter(Application.id == application_id).first()
|
||||
if not db_application:
|
||||
raise HTTPException(status_code=404, detail="Application not found")
|
||||
|
||||
# Update fields if provided
|
||||
if app_update.status is not None:
|
||||
db_application.status = app_update.status.value
|
||||
if app_update.cover_letter is not None:
|
||||
db_application.cover_letter = app_update.cover_letter
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_application)
|
||||
return db_application
|
||||
|
||||
@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"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
db_application = db.query(Application).filter(Application.id == application_id).first()
|
||||
if not db_application:
|
||||
raise HTTPException(status_code=404, detail="Application not found")
|
||||
|
||||
db.delete(db_application)
|
||||
db.commit()
|
||||
return {"message": "Application deleted successfully"}
|
||||
finally:
|
||||
db.close()
|
||||
db_application = db.query(Application).filter(Application.id == application_id).first()
|
||||
if not db_application:
|
||||
raise HTTPException(status_code=404, detail="Application not found")
|
||||
|
||||
db.delete(db_application)
|
||||
db.commit()
|
||||
return {"message": "Application deleted successfully"}
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Jobs API routes
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -49,18 +49,29 @@ class JobResponse(BaseModel):
|
||||
from_attributes = True
|
||||
|
||||
@router.get("/", response_model=List[JobResponse])
|
||||
async def get_jobs(skip: int = 0, limit: int = 100, is_active: bool = True, db: Session = Depends(SessionLocal)):
|
||||
"""Get all jobs"""
|
||||
query = db.query(JobPosting)
|
||||
async def get_jobs(skip: int = 0, limit: int = 100, is_active: bool = True, db: Session = Depends(SessionLocal), request: Request = None):
|
||||
"""Get all jobs for the current tenant"""
|
||||
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:
|
||||
query = query.filter(JobPosting.is_active == is_active)
|
||||
jobs = query.offset(skip).limit(limit).all()
|
||||
return jobs
|
||||
|
||||
@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"""
|
||||
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:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
if not job.is_active:
|
||||
@@ -68,10 +79,17 @@ async def get_job(job_id: int, db: Session = Depends(SessionLocal)):
|
||||
return job
|
||||
|
||||
@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"""
|
||||
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
|
||||
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"]:
|
||||
raise HTTPException(
|
||||
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_max=job.salary_max,
|
||||
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
|
||||
)
|
||||
db.add(db_job)
|
||||
@@ -95,9 +113,16 @@ async def create_job(job: JobCreate, db: Session = Depends(SessionLocal), user_i
|
||||
return db_job
|
||||
|
||||
@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"""
|
||||
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:
|
||||
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
|
||||
|
||||
@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)"""
|
||||
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:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"""
|
||||
Users API routes
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
import hashlib
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import SessionLocal
|
||||
from ..models import User, UserRole
|
||||
from ..models import User, UserRole, Tenant
|
||||
from ..config.settings import settings
|
||||
|
||||
router = APIRouter()
|
||||
@@ -42,22 +42,42 @@ def hash_password(password: str) -> str:
|
||||
return hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
@router.get("/", response_model=List[UserResponse])
|
||||
async def get_users(skip: int = 0, limit: int = 100, db: Session = Depends(SessionLocal)):
|
||||
"""Get all users"""
|
||||
users = db.query(User).offset(skip).limit(limit).all()
|
||||
async def get_users(skip: int = 0, limit: int = 100, db: Session = Depends(SessionLocal), request: Request = None):
|
||||
"""Get all users for the current tenant"""
|
||||
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
|
||||
|
||||
@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"""
|
||||
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:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return user
|
||||
|
||||
@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"""
|
||||
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
|
||||
existing_user = db.query(User).filter(
|
||||
(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,
|
||||
hashed_password=hashed_pwd,
|
||||
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.commit()
|
||||
@@ -81,9 +101,16 @@ async def create_user(user: UserCreate, db: Session = Depends(SessionLocal)):
|
||||
return db_user
|
||||
|
||||
@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"""
|
||||
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:
|
||||
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
|
||||
|
||||
@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"""
|
||||
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:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
|
||||
@@ -1,31 +1,44 @@
|
||||
"""
|
||||
Tenant middleware for handling multi-tenant requests
|
||||
"""
|
||||
import uuid
|
||||
from typing import Optional
|
||||
from fastapi import Request, HTTPException, status
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .config.settings import settings
|
||||
from .models import Tenant
|
||||
from .database import SessionLocal
|
||||
|
||||
|
||||
class TenantMiddleware(BaseHTTPMiddleware):
|
||||
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)
|
||||
|
||||
# If not in header, try to extract from subdomain
|
||||
if not tenant_id:
|
||||
# Try to extract tenant from subdomain
|
||||
host = request.headers.get("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(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Tenant ID is required"
|
||||
detail="Valid tenant ID is required"
|
||||
)
|
||||
|
||||
# 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)
|
||||
return response
|
||||
@@ -34,7 +47,9 @@ class TenantMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Extract tenant from host (subdomain.tenant.com)
|
||||
"""
|
||||
# For now, return a default tenant or None
|
||||
# In a real implementation, you would parse the subdomain
|
||||
# and look up the corresponding tenant in the database
|
||||
return "default"
|
||||
import re
|
||||
# Match subdomain from host (e.g., "tenant1.example.com" -> "tenant1")
|
||||
match = re.match(r'^([^.]+)\.', host)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
Reference in New Issue
Block a user