تفاصيل العمل

<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use Firebase\JWT\JWT;

use Stripe\StripeClient;

use Ramsey\Uuid\Uuid;

use Dotenv\Dotenv;

/**

* Multi-Tenant SaaS Starter Kit

*

* Features:

* - JWT Authentication

* - Tenant Isolation with UUIDs

* - Stripe Subscription Integration

* - Role-Based Access Control

* - RESTful API Design

* - Database Migrations

* - Admin Dashboard

*/

// Load environment variables

$dotenv = Dotenv::createImmutable(__DIR__);

$dotenv->load();

class Database {

private static $instance = null;

private $connection;

private function __construct() {

$dsn = sprintf(

'mysql:host=%s;dbname=%s;charset=%s',

$_ENV['DB_HOST'],

$_ENV['DB_NAME'],

'utf8mb4'

);

$this->connection = new PDO($dsn, $_ENV['DB_USER'], $_ENV['DB_PASS'], [

PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,

PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,

PDO::ATTR_EMULATE_PREPARES => false,

]);

}

public static function getInstance(): self {

if (self::$instance === null) {

self::$instance = new self();

}

return self::$instance;

}

public function getConnection(): PDO {

return $this->connection;

}

public function migrate(): void {

$migrations = [

"CREATE TABLE IF NOT EXISTS tenants (

id BINARY(16) PRIMARY KEY,

name VARCHAR(255) NOT NULL,

domain VARCHAR(255) UNIQUE NOT NULL,

is_active BOOLEAN DEFAULT TRUE,

created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP

)",

"CREATE TABLE IF NOT EXISTS users (

id BINARY(16) PRIMARY KEY,

tenant_id BINARY(16) NOT NULL,

email VARCHAR(255) UNIQUE NOT NULL,

password VARCHAR(255) NOT NULL,

role ENUM('admin', 'manager', 'user') DEFAULT 'user',

FOREIGN KEY (tenant_id) REFERENCES tenants(id),

created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP

)",

"CREATE TABLE IF NOT EXISTS subscriptions (

id BINARY(16) PRIMARY KEY,

tenant_id BINARY(16) NOT NULL,

stripe_subscription_id VARCHAR(255),

plan_id VARCHAR(50) NOT NULL,

status VARCHAR(20) NOT NULL,

current_period_end TIMESTAMP,

FOREIGN KEY (tenant_id) REFERENCES tenants(id),

created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP

)"

];

$db = $this->getConnection();

foreach ($migrations as $migration) {

$db->exec($migration);

}

}

}

class Auth {

private $db;

public function __construct() {

$this->db = Database::getInstance()->getConnection();

}

public function registerTenant(string $name, string $domain, string $email, string $password): array {

// Validate input

if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {

throw new InvalidArgumentException("Invalid email address");

}

// Check if domain exists

$stmt = $this->db->prepare("SELECT id FROM tenants WHERE domain = ?");

$stmt->execute([$domain]);

if ($stmt->fetch()) {

throw new RuntimeException("Domain already registered");

}

// Start transaction

$this->db->beginTransaction();

try {

// Create tenant

$tenantId = Uuid::uuid4()->getBytes();

$stmt = $this->db->prepare("INSERT INTO tenants (id, name, domain) VALUES (?, ?, ?)");

$stmt->execute([$tenantId, $name, $domain]);

// Create admin user

$userId = Uuid::uuid4()->getBytes();

$hashedPassword = password_hash($password, PASSWORD_BCRYPT);

$stmt = $this->db->prepare("INSERT INTO users (id, tenant_id, email, password, role) VALUES (?, ?, ?, ?, 'admin')");

$stmt->execute([$userId, $tenantId, $email, $hashedPassword]);

$this->db->commit();

return [

'tenant_id' => Uuid::fromBytes($tenantId)->toString(),

'user_id' => Uuid::fromBytes($userId)->toString()

];

} catch (Exception $e) {

$this->db->rollBack();

throw $e;

}

}

public function login(string $email, string $password): string {

$stmt = $this->db->prepare("SELECT u.id, u.tenant_id, u.password, u.role, t.domain

FROM users u JOIN tenants t ON u.tenant_id = t.id

WHERE u.email = ?");

$stmt->execute([$email]);

$user = $stmt->fetch();

if (!$user || !password_verify($password, $user['password'])) {

throw new RuntimeException("Invalid credentials");

}

$payload = [

'sub' => Uuid::fromBytes($user['id'])->toString(),

'tid' => Uuid::fromBytes($user['tenant_id'])->toString(),

'dom' => $user['domain'],

'role' => $user['role'],

'iat' => time(),

'exp' => time() + 3600 // 1 hour expiration

];

return JWT::encode($payload, $_ENV['JWT_SECRET'], 'HS256');

}

public function validateToken(string $token): array {

try {

$decoded = JWT::decode($token, $_ENV['JWT_SECRET'], ['HS256']);

return (array)$decoded;

} catch (Exception $e) {

throw new RuntimeException("Invalid token: " . $e->getMessage());

}

}

}

class SubscriptionService {

private $db;

private $stripe;

public function __construct() {

$this->db = Database::getInstance()->getConnection();

$this->stripe = new StripeClient($_ENV['STRIPE_SECRET_KEY']);

}

public function createSubscription(string $tenantId, string $planId, string $paymentMethodId): array {

$tenant = $this->getTenant($tenantId);

// Create Stripe customer if not exists

$customer = $this->stripe->customers->create([

'email' => $this->getTenantAdminEmail($tenantId),

'payment_method' => $paymentMethodId,

'invoice_settings' => ['default_payment_method' => $paymentMethodId]

]);

// Create subscription

$subscription = $this->stripe->subscriptions->create([

'customer' => $customer->id,

'items' => [['price' => $planId]],

'expand' => ['latest_invoice.payment_intent']

]);

// Save to database

$subscriptionId = Uuid::uuid4()->getBytes();

$stmt = $this->db->prepare("INSERT INTO subscriptions

(id, tenant_id, stripe_subscription_id, plan_id, status, current_period_end)

VALUES (?, ?, ?, ?, ?, ?)");

$stmt->execute([

$subscriptionId,

Uuid::fromString($tenantId)->getBytes(),

$subscription->id,

$planId,

$subscription->status,

date('Y-m-d H:i:s', $subscription->current_period_end)

]);

return [

'subscription_id' => Uuid::fromBytes($subscriptionId)->toString(),

'status' => $subscription->status,

'client_secret' => $subscription->latest_invoice->payment_intent->client_secret

];

}

private function getTenant(string $tenantId): array {

$stmt = $this->db->prepare("SELECT * FROM tenants WHERE id = ?");

$stmt->execute([Uuid::fromString($tenantId)->getBytes()]);

$tenant = $stmt->fetch();

if (!$tenant) {

throw new RuntimeException("Tenant not found");

}

return $tenant;

}

private function getTenantAdminEmail(string $tenantId): string {

$stmt = $this->db->prepare("SELECT email FROM users WHERE tenant_id = ? AND role = 'admin' LIMIT 1");

$stmt->execute([Uuid::fromString($tenantId)->getBytes()]);

$user = $stmt->fetch();

if (!$user) {

throw new RuntimeException("Admin user not found for tenant");

}

return $user['email'];

}

}

class TenantMiddleware {

public function __invoke($request, $handler) {

$authHeader = $request->getHeaderLine('Authorization');

$token = str_replace('Bearer ', '', $authHeader);

try {

$auth = new Auth();

$payload = $auth->validateToken($token);

// Add tenant context to request

$request = $request->withAttribute('tenant_id', $payload['tid'])

->withAttribute('user_id', $payload['sub'])

->withAttribute('user_role', $payload['role']);

return $handler->handle($request);

} catch (Exception $e) {

return new JsonResponse(['error' => $e->getMessage()], 401);

}

}

}

// Example API Endpoints

$app = new \Slim\App();

// Public routes

$app->post('/api/register', function ($request, $response) {

$data = $request->getParsedBody();

$auth = new Auth();

try {

$result = $auth->registerTenant(

$data['name'],

$data['domain'],

$data['email'],

$data['password']

);

return $response->withJson($result);

} catch (Exception $e) {

return $response->withJson(['error' => $e->getMessage()], 400);

}

});

$app->post('/api/login', function ($request, $response) {

$data = $request->getParsedBody();

$auth = new Auth();

try {

$token = $auth->login($data['email'], $data['password']);

return $response->withJson(['token' => $token]);

} catch (Exception $e) {

return $response->withJson(['error' => $e->getMessage()], 401);

}

});

// Protected routes (require JWT)

$app->group('/api', function () {

$this->post('/subscriptions', function ($request, $response) {

$data = $request->getParsedBody();

$tenantId = $request->getAttribute('tenant_id');

try {

$service = new SubscriptionService();

$result = $service->createSubscription(

$tenantId,

$data['plan_id'],

$data['payment_method_id']

);

return $response->withJson($result);

} catch (Exception $e) {

return $response->withJson(['error' => $e->getMessage()], 400);

}

});

$this->get('/me', function ($request, $response) {

return $response->withJson([

'tenant_id' => $request->getAttribute('tenant_id'),

'user_id' => $request->getAttribute('user_id'),

'role' => $request->getAttribute('user_role')

]);

});

})->add(new TenantMiddleware());

// Initialize database

Database::getInstance()->migrate();

$app->run();

بطاقة العمل

اسم المستقل
عدد الإعجابات
0
عدد المشاهدات
69
تاريخ الإضافة