mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-29 10:47:53 -04:00
Implement comprehensive REST API for OSPOS with the following: Database: - Migration for ospos_api_keys table - Seeder for module permissions Models: - ApiKey model with key generation, validation, revocation - SHA-256 hashing for secure key storage - Support for key expiration Filters: - ApiAuth filter for X-API-Key header authentication - CSRF exemption for API routes Controllers: - Api/BaseController with response helpers and field transformation - Api/Customers (CRUD + batch delete, suggestions) - Api/Suppliers (CRUD + batch delete, suggestions) - Api/Items (CRUD + batch delete, quantities endpoint) - Api/Inventory (adjustments with set/adjust modes, bulk support) - ApiKeys (UI controller for key management) Routes: - /api/v1/* endpoints with apiauth filter - /office/api-keys/* endpoints for key management UI Tests: - ApiKeyTest for model functionality - ApiAuthTest for authentication filter Features: - camelCase JSON field names (API standard) - Offset/limit pagination - Soft delete support - Permission-based authorization - Key prefix for UI identification - Last used timestamp tracking Refs: #2463, #615, #3789, #3809, #1680, #876, #1959, #157
145 lines
3.8 KiB
PHP
145 lines
3.8 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use CodeIgniter\Model;
|
|
|
|
class ApiKey extends Model
|
|
{
|
|
protected $table = 'api_keys';
|
|
protected $primaryKey = 'api_key_id';
|
|
protected $useAutoIncrement = true;
|
|
protected $useSoftDeletes = false;
|
|
protected $allowedFields = [
|
|
'employee_id',
|
|
'key_hash',
|
|
'key_prefix',
|
|
'name',
|
|
'last_used',
|
|
'expires_at',
|
|
'disabled'
|
|
];
|
|
|
|
protected $useTimestamps = false;
|
|
protected $createdField = 'created';
|
|
|
|
private const KEY_PREFIX = 'ospos_';
|
|
private const KEY_BYTES = 32;
|
|
|
|
public function generateKey(int $employeeId, ?string $name = null, ?string $expiresAt = null): string|false
|
|
{
|
|
$rawKey = bin2hex(random_bytes(self::KEY_BYTES));
|
|
$apiKey = self::KEY_PREFIX . $rawKey;
|
|
|
|
$keyHash = hash('sha256', $apiKey);
|
|
$keyPrefix = substr($apiKey, 0, 12);
|
|
|
|
$data = [
|
|
'employee_id' => $employeeId,
|
|
'key_hash' => $keyHash,
|
|
'key_prefix' => $keyPrefix,
|
|
'name' => $name,
|
|
'expires_at' => $expiresAt
|
|
];
|
|
|
|
if ($this->insert($data)) {
|
|
return $apiKey;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function validateKey(string $apiKey): int|false
|
|
{
|
|
if (!str_starts_with($apiKey, self::KEY_PREFIX)) {
|
|
return false;
|
|
}
|
|
|
|
if (strlen($apiKey) !== strlen(self::KEY_PREFIX) + (self::KEY_BYTES * 2)) {
|
|
return false;
|
|
}
|
|
|
|
$keyHash = hash('sha256', $apiKey);
|
|
|
|
$builder = $this->builder();
|
|
$builder->where('key_hash', $keyHash);
|
|
$builder->where('disabled', 0);
|
|
$builder->groupStart();
|
|
$builder->where('expires_at IS NULL');
|
|
$builder->orWhere('expires_at >', date('Y-m-d H:i:s'));
|
|
$builder->groupEnd();
|
|
|
|
$result = $builder->get()->getRow();
|
|
|
|
if ($result) {
|
|
$this->update($result->api_key_id, ['last_used' => date('Y-m-d H:i:s')]);
|
|
return (int) $result->employee_id;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function getKeysForEmployee(int $employeeId): array
|
|
{
|
|
$builder = $this->builder();
|
|
$builder->where('employee_id', $employeeId);
|
|
$builder->orderBy('created', 'DESC');
|
|
|
|
return $builder->get()->getResultArray();
|
|
}
|
|
|
|
public function revokeKey(int $apiKeyId, int $employeeId): bool
|
|
{
|
|
$builder = $this->builder();
|
|
$builder->where('api_key_id', $apiKeyId);
|
|
$builder->where('employee_id', $employeeId);
|
|
|
|
return $builder->update(['disabled' => 1]) !== false;
|
|
}
|
|
|
|
public function regenerateKey(int $apiKeyId, int $employeeId): string|false
|
|
{
|
|
$existingKey = $this->builder()
|
|
->getWhere([
|
|
'api_key_id' => $apiKeyId,
|
|
'employee_id' => $employeeId
|
|
])
|
|
->getRow();
|
|
|
|
if (!$existingKey) {
|
|
return false;
|
|
}
|
|
|
|
$newKey = $this->generateKey(
|
|
$employeeId,
|
|
$existingKey->name,
|
|
$existingKey->expires_at
|
|
);
|
|
|
|
if ($newKey) {
|
|
$this->delete($apiKeyId);
|
|
return $newKey;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function cleanupExpired(): int
|
|
{
|
|
$builder = $this->builder();
|
|
$builder->where('disabled', 0);
|
|
$builder->where('expires_at <', date('Y-m-d H:i:s'));
|
|
$builder->where('expires_at IS NOT NULL');
|
|
|
|
$expiredKeys = $builder->get()->getResultArray();
|
|
$count = 0;
|
|
|
|
foreach ($expiredKeys as $key) {
|
|
if ($this->update($key['api_key_id'], ['disabled' => 1])) {
|
|
$count++;
|
|
}
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
} |