.
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user