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

View File

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

View File

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

View File

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