Initial commit - SOGOMS v1.0.0
- sogoctl: supervisor avec health checks et restart auto - sogoway: gateway HTTP, auth JWT, routing par hostname - sogoms-db: microservice MariaDB avec pool par application - Protocol IPC Unix socket JSON length-prefixed - Config YAML multi-application (prokov) - Deploy script pour container Alpine gw3 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
108
clients/prokov/api/core/Controller.php
Normal file
108
clients/prokov/api/core/Controller.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
/**
|
||||
* Contrôleur de base
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
protected Request $request;
|
||||
protected ?array $user = null;
|
||||
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requiert une authentification valide
|
||||
*/
|
||||
protected function requireAuth(): void
|
||||
{
|
||||
$sessionId = $this->request->getSessionId();
|
||||
|
||||
if (empty($sessionId)) {
|
||||
Response::unauthorized('Session ID required');
|
||||
}
|
||||
|
||||
$user = Session::validate($sessionId);
|
||||
|
||||
if ($user === null) {
|
||||
Response::unauthorized('Invalid or expired session');
|
||||
}
|
||||
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne l'ID de l'utilisateur authentifié
|
||||
*/
|
||||
protected function getUserId(): int
|
||||
{
|
||||
return $this->user['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide les champs requis dans le body
|
||||
*/
|
||||
protected function validate(array $rules): array
|
||||
{
|
||||
$body = $this->request->getBody();
|
||||
$errors = [];
|
||||
$data = [];
|
||||
|
||||
foreach ($rules as $field => $rule) {
|
||||
$value = $body[$field] ?? null;
|
||||
$ruleList = explode('|', $rule);
|
||||
|
||||
foreach ($ruleList as $r) {
|
||||
if ($r === 'required' && ($value === null || $value === '')) {
|
||||
$errors[$field] = "Le champ {$field} est requis";
|
||||
break;
|
||||
}
|
||||
|
||||
if ($r === 'email' && $value !== null && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
$errors[$field] = "Le champ {$field} doit être un email valide";
|
||||
break;
|
||||
}
|
||||
|
||||
if (str_starts_with($r, 'min:')) {
|
||||
$min = (int) substr($r, 4);
|
||||
if ($value !== null && strlen($value) < $min) {
|
||||
$errors[$field] = "Le champ {$field} doit contenir au moins {$min} caractères";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (str_starts_with($r, 'max:')) {
|
||||
$max = (int) substr($r, 4);
|
||||
if ($value !== null && strlen($value) > $max) {
|
||||
$errors[$field] = "Le champ {$field} doit contenir au maximum {$max} caractères";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($r === 'int' && $value !== null && !is_numeric($value)) {
|
||||
$errors[$field] = "Le champ {$field} doit être un nombre entier";
|
||||
break;
|
||||
}
|
||||
|
||||
if ($r === 'numeric' && $value !== null && !is_numeric($value)) {
|
||||
$errors[$field] = "Le champ {$field} doit être un nombre";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($errors[$field])) {
|
||||
$data[$field] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
Response::error('Validation failed', 422, $errors);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
112
clients/prokov/api/core/Request.php
Normal file
112
clients/prokov/api/core/Request.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
/**
|
||||
* Gestion de la requête entrante
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class Request
|
||||
{
|
||||
private string $method;
|
||||
private string $uri;
|
||||
private array $params = [];
|
||||
private array $body = [];
|
||||
private array $headers = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->method = $_SERVER['REQUEST_METHOD'];
|
||||
$this->uri = $this->parseUri();
|
||||
$this->headers = $this->parseHeaders();
|
||||
$this->body = $this->parseBody();
|
||||
}
|
||||
|
||||
private function parseUri(): string
|
||||
{
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
|
||||
// Retirer le query string
|
||||
if (($pos = strpos($uri, '?')) !== false) {
|
||||
$uri = substr($uri, 0, $pos);
|
||||
}
|
||||
|
||||
// Retirer le préfixe /api si présent
|
||||
$uri = preg_replace('#^/api#', '', $uri);
|
||||
|
||||
return '/' . trim($uri, '/');
|
||||
}
|
||||
|
||||
private function parseHeaders(): array
|
||||
{
|
||||
$headers = [];
|
||||
foreach ($_SERVER as $key => $value) {
|
||||
if (str_starts_with($key, 'HTTP_')) {
|
||||
$name = str_replace('_', '-', substr($key, 5));
|
||||
$headers[$name] = $value;
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
|
||||
private function parseBody(): array
|
||||
{
|
||||
if (in_array($this->method, ['POST', 'PUT', 'PATCH'])) {
|
||||
$input = file_get_contents('php://input');
|
||||
if (!empty($input)) {
|
||||
$decoded = json_decode($input, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
// Fallback sur $_POST ou array vide
|
||||
return $_POST ?: [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
public function getUri(): string
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
public function getHeader(string $name): ?string
|
||||
{
|
||||
$name = strtoupper(str_replace('-', '_', $name));
|
||||
return $this->headers[$name] ?? null;
|
||||
}
|
||||
|
||||
public function getSessionId(): ?string
|
||||
{
|
||||
return $this->getHeader('X-SESSION-ID');
|
||||
}
|
||||
|
||||
public function getBody(): array
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->body[$key] ?? $_GET[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function setParams(array $params): void
|
||||
{
|
||||
$this->params = $params;
|
||||
}
|
||||
|
||||
public function getParam(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->params[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function getParams(): array
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
}
|
||||
49
clients/prokov/api/core/Response.php
Normal file
49
clients/prokov/api/core/Response.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Gestion des réponses JSON
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class Response
|
||||
{
|
||||
public static function json(mixed $data, int $code = 200): void
|
||||
{
|
||||
http_response_code($code);
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function success(mixed $data = null, string $message = 'OK', int $code = 200): void
|
||||
{
|
||||
self::json([
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'data' => $data,
|
||||
], $code);
|
||||
}
|
||||
|
||||
public static function error(string $message, int $code = 400, mixed $errors = null): void
|
||||
{
|
||||
$response = [
|
||||
'success' => false,
|
||||
'message' => $message,
|
||||
];
|
||||
|
||||
if ($errors !== null) {
|
||||
$response['errors'] = $errors;
|
||||
}
|
||||
|
||||
self::json($response, $code);
|
||||
}
|
||||
|
||||
public static function notFound(string $message = 'Resource not found'): void
|
||||
{
|
||||
self::error($message, 404);
|
||||
}
|
||||
|
||||
public static function unauthorized(string $message = 'Unauthorized'): void
|
||||
{
|
||||
self::error($message, 401);
|
||||
}
|
||||
}
|
||||
145
clients/prokov/api/core/Router.php
Normal file
145
clients/prokov/api/core/Router.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
/**
|
||||
* Routeur simple pour API REST
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class Router
|
||||
{
|
||||
private array $routes = [];
|
||||
private Request $request;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->request = new Request();
|
||||
$this->registerRoutes();
|
||||
}
|
||||
|
||||
private function registerRoutes(): void
|
||||
{
|
||||
// Auth (routes publiques)
|
||||
$this->post('/auth/register', 'AuthController@register');
|
||||
$this->post('/auth/login', 'AuthController@login');
|
||||
$this->post('/auth/logout', 'AuthController@logout');
|
||||
$this->get('/auth/me', 'AuthController@me');
|
||||
|
||||
// Projects
|
||||
$this->get('/projects', 'ProjectController@index');
|
||||
$this->get('/projects/{id}', 'ProjectController@show');
|
||||
$this->post('/projects', 'ProjectController@store');
|
||||
$this->put('/projects/{id}', 'ProjectController@update');
|
||||
$this->delete('/projects/{id}', 'ProjectController@destroy');
|
||||
|
||||
// Tasks
|
||||
$this->get('/tasks', 'TaskController@index');
|
||||
$this->get('/tasks/{id}', 'TaskController@show');
|
||||
$this->post('/tasks', 'TaskController@store');
|
||||
$this->put('/tasks/{id}', 'TaskController@update');
|
||||
$this->delete('/tasks/{id}', 'TaskController@destroy');
|
||||
|
||||
// Tags
|
||||
$this->get('/tags', 'TagController@index');
|
||||
$this->get('/tags/{id}', 'TagController@show');
|
||||
$this->post('/tags', 'TagController@store');
|
||||
$this->put('/tags/{id}', 'TagController@update');
|
||||
$this->delete('/tags/{id}', 'TagController@destroy');
|
||||
|
||||
// Statuses
|
||||
$this->get('/statuses', 'StatusController@index');
|
||||
$this->get('/statuses/{id}', 'StatusController@show');
|
||||
$this->post('/statuses', 'StatusController@store');
|
||||
$this->put('/statuses/{id}', 'StatusController@update');
|
||||
$this->delete('/statuses/{id}', 'StatusController@destroy');
|
||||
}
|
||||
|
||||
private function addRoute(string $method, string $path, string $handler): void
|
||||
{
|
||||
$this->routes[] = [
|
||||
'method' => $method,
|
||||
'path' => $path,
|
||||
'handler' => $handler,
|
||||
];
|
||||
}
|
||||
|
||||
public function get(string $path, string $handler): void
|
||||
{
|
||||
$this->addRoute('GET', $path, $handler);
|
||||
}
|
||||
|
||||
public function post(string $path, string $handler): void
|
||||
{
|
||||
$this->addRoute('POST', $path, $handler);
|
||||
}
|
||||
|
||||
public function put(string $path, string $handler): void
|
||||
{
|
||||
$this->addRoute('PUT', $path, $handler);
|
||||
}
|
||||
|
||||
public function delete(string $path, string $handler): void
|
||||
{
|
||||
$this->addRoute('DELETE', $path, $handler);
|
||||
}
|
||||
|
||||
public function dispatch(): void
|
||||
{
|
||||
$method = $this->request->getMethod();
|
||||
$uri = $this->request->getUri();
|
||||
|
||||
foreach ($this->routes as $route) {
|
||||
if ($route['method'] !== $method) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$params = $this->matchRoute($route['path'], $uri);
|
||||
if ($params !== false) {
|
||||
$this->request->setParams($params);
|
||||
$this->callHandler($route['handler']);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Response::notFound('Route not found');
|
||||
}
|
||||
|
||||
private function matchRoute(string $routePath, string $uri): array|false
|
||||
{
|
||||
// Convertir /projects/{id} en regex /projects/([^/]+)
|
||||
$pattern = preg_replace('#\{(\w+)\}#', '([^/]+)', $routePath);
|
||||
$pattern = '#^' . $pattern . '$#';
|
||||
|
||||
if (preg_match($pattern, $uri, $matches)) {
|
||||
array_shift($matches); // Retirer le match complet
|
||||
|
||||
// Extraire les noms des paramètres
|
||||
preg_match_all('#\{(\w+)\}#', $routePath, $paramNames);
|
||||
$params = [];
|
||||
|
||||
foreach ($paramNames[1] as $index => $name) {
|
||||
$params[$name] = $matches[$index] ?? null;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function callHandler(string $handler): void
|
||||
{
|
||||
[$controllerName, $methodName] = explode('@', $handler);
|
||||
|
||||
if (!class_exists($controllerName)) {
|
||||
Response::error("Controller {$controllerName} not found", 500);
|
||||
}
|
||||
|
||||
$controller = new $controllerName($this->request);
|
||||
|
||||
if (!method_exists($controller, $methodName)) {
|
||||
Response::error("Method {$methodName} not found", 500);
|
||||
}
|
||||
|
||||
$controller->$methodName();
|
||||
}
|
||||
}
|
||||
162
clients/prokov/api/core/Session.php
Normal file
162
clients/prokov/api/core/Session.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
/**
|
||||
* Gestion des sessions en base de données
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class Session
|
||||
{
|
||||
private static ?array $currentSession = null;
|
||||
private static ?array $currentUser = null;
|
||||
|
||||
/**
|
||||
* Récupérer l'IP réelle du client (derrière proxy)
|
||||
*/
|
||||
public static function getClientIp(): ?string
|
||||
{
|
||||
// Headers transmis par le proxy nginx
|
||||
$headers = [
|
||||
'HTTP_X_REAL_IP',
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
'HTTP_CLIENT_IP',
|
||||
'REMOTE_ADDR',
|
||||
];
|
||||
|
||||
foreach ($headers as $header) {
|
||||
if (!empty($_SERVER[$header])) {
|
||||
// X-Forwarded-For peut contenir plusieurs IPs (client, proxy1, proxy2...)
|
||||
$ips = explode(',', $_SERVER[$header]);
|
||||
$ip = trim($ips[0]);
|
||||
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Créer une nouvelle session pour un utilisateur
|
||||
*/
|
||||
public static function create(int $userId, ?string $ipAddress = null, ?string $userAgent = null): string
|
||||
{
|
||||
$db = Database::getInstance();
|
||||
$sessionId = bin2hex(random_bytes(64)); // 128 caractères
|
||||
$expiresAt = date('Y-m-d H:i:s', time() + SESSION_LIFETIME);
|
||||
|
||||
$stmt = $db->prepare('
|
||||
INSERT INTO sessions (id, user_id, ip_address, user_agent, expires_at)
|
||||
VALUES (:id, :user_id, :ip_address, :user_agent, :expires_at)
|
||||
');
|
||||
|
||||
$stmt->execute([
|
||||
'id' => $sessionId,
|
||||
'user_id' => $userId,
|
||||
'ip_address' => $ipAddress ?? self::getClientIp(),
|
||||
'user_agent' => $userAgent ?? $_SERVER['HTTP_USER_AGENT'] ?? null,
|
||||
'expires_at' => $expiresAt,
|
||||
]);
|
||||
|
||||
return $sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valider une session et retourner l'utilisateur
|
||||
*/
|
||||
public static function validate(string $sessionId): ?array
|
||||
{
|
||||
if (self::$currentSession !== null && self::$currentSession['id'] === $sessionId) {
|
||||
return self::$currentUser;
|
||||
}
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
$stmt = $db->prepare('
|
||||
SELECT s.*, u.id as user_id, u.email, u.name
|
||||
FROM sessions s
|
||||
JOIN users u ON s.user_id = u.id
|
||||
WHERE s.id = :id AND s.expires_at > NOW()
|
||||
');
|
||||
|
||||
$stmt->execute(['id' => $sessionId]);
|
||||
$result = $stmt->fetch();
|
||||
|
||||
if (!$result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
self::$currentSession = [
|
||||
'id' => $result['id'],
|
||||
'user_id' => $result['user_id'],
|
||||
'expires_at' => $result['expires_at'],
|
||||
];
|
||||
|
||||
self::$currentUser = [
|
||||
'id' => $result['user_id'],
|
||||
'email' => $result['email'],
|
||||
'name' => $result['name'],
|
||||
];
|
||||
|
||||
return self::$currentUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Détruire une session
|
||||
*/
|
||||
public static function destroy(string $sessionId): bool
|
||||
{
|
||||
$db = Database::getInstance();
|
||||
|
||||
$stmt = $db->prepare('DELETE FROM sessions WHERE id = :id');
|
||||
$stmt->execute(['id' => $sessionId]);
|
||||
|
||||
self::$currentSession = null;
|
||||
self::$currentUser = null;
|
||||
|
||||
return $stmt->rowCount() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoyer les sessions expirées
|
||||
*/
|
||||
public static function cleanup(): int
|
||||
{
|
||||
$db = Database::getInstance();
|
||||
|
||||
$stmt = $db->prepare('DELETE FROM sessions WHERE expires_at < NOW()');
|
||||
$stmt->execute();
|
||||
|
||||
return $stmt->rowCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prolonger une session
|
||||
*/
|
||||
public static function extend(string $sessionId): bool
|
||||
{
|
||||
$db = Database::getInstance();
|
||||
$expiresAt = date('Y-m-d H:i:s', time() + SESSION_LIFETIME);
|
||||
|
||||
$stmt = $db->prepare('
|
||||
UPDATE sessions SET expires_at = :expires_at WHERE id = :id
|
||||
');
|
||||
|
||||
$stmt->execute([
|
||||
'id' => $sessionId,
|
||||
'expires_at' => $expiresAt,
|
||||
]);
|
||||
|
||||
return $stmt->rowCount() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtenir l'utilisateur courant (depuis le cache)
|
||||
*/
|
||||
public static function getCurrentUser(): ?array
|
||||
{
|
||||
return self::$currentUser;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user