Files
wordpress-theme-aksrt/src/Identity/RestController.php
T

198 lines
8.4 KiB
PHP

<?php
/**
* Public identity verification REST API.
*
* @package AKSRT
*/
namespace AKSRT\Identity;
use AKSRT\Ledger\OperationKey;
defined('ABSPATH') || exit;
final class RestController {
private const NS = 'aksrt/v1';
private ?array $request_application = null;
private string $request_uuid = '';
public function __construct(
private ?ApplicationRepository $applications = null,
private ?ApplicationService $service = null
) {
$this->applications ??= new ApplicationRepository();
$this->service ??= new ApplicationService($this->applications);
}
public function register_routes(): void {
$uuid_args = [
'uuid' => [
'type' => 'string',
'format' => 'uuid',
'required' => true,
'validate_callback' => static fn($value): bool => wp_is_uuid((string) $value),
'sanitize_callback' => static fn($value): string => strtolower(sanitize_text_field((string) $value)),
],
];
register_rest_route(self::NS, '/identity/applications', [
'methods' => \WP_REST_Server::CREATABLE,
'callback' => [$this, 'create'],
'permission_callback' => [$this, 'logged_in'],
'args' => [
'type' => ['type' => 'string', 'required' => true, 'enum' => ['personal', 'enterprise'], 'sanitize_callback' => 'sanitize_key'],
'person_name' => ['type' => 'string', 'required' => true, 'minLength' => 1, 'maxLength' => 50, 'sanitize_callback' => 'sanitize_text_field'],
'id_number' => ['type' => 'string', 'required' => true, 'minLength' => 15, 'maxLength' => 18, 'sanitize_callback' => 'sanitize_text_field'],
'company_name' => ['type' => 'string', 'required' => false, 'maxLength' => 191, 'sanitize_callback' => 'sanitize_text_field'],
'company_code' => ['type' => 'string', 'required' => false, 'maxLength' => 32, 'sanitize_callback' => 'sanitize_text_field'],
'consent' => ['type' => 'boolean', 'required' => true],
'meta_info' => ['type' => 'string', 'required' => false, 'maxLength' => 65535, 'sanitize_callback' => 'sanitize_text_field'],
],
]);
register_rest_route(self::NS, '/identity/applications/current', [
'methods' => \WP_REST_Server::READABLE,
'callback' => [$this, 'current'],
'permission_callback' => [$this, 'logged_in'],
]);
register_rest_route(self::NS, '/identity/applications/(?P<uuid>[a-f0-9-]{36})', [
'methods' => \WP_REST_Server::READABLE,
'callback' => [$this, 'get'],
'permission_callback' => [$this, 'owns_application'],
'args' => $uuid_args,
]);
register_rest_route(self::NS, '/identity/applications/(?P<uuid>[a-f0-9-]{36})/refresh', [
'methods' => \WP_REST_Server::CREATABLE,
'callback' => [$this, 'refresh'],
'permission_callback' => [$this, 'owns_application'],
'args' => $uuid_args,
]);
register_rest_route(self::NS, '/identity/applications/(?P<uuid>[a-f0-9-]{36})/manual', [
'methods' => \WP_REST_Server::CREATABLE,
'callback' => [$this, 'manual'],
'permission_callback' => [$this, 'owns_application'],
'args' => $uuid_args,
]);
}
public function logged_in(\WP_REST_Request $request) {
if (!is_user_logged_in()) {
return new \WP_Error('aksrt_identity_auth', __('请先登录', 'aksrt'), ['status' => 401]);
}
$nonce = (string) $request->get_header('X-WP-Nonce');
if ($nonce === '' || !wp_verify_nonce($nonce, 'wp_rest')) {
return new \WP_Error('aksrt_identity_nonce', __('安全校验失败,请刷新页面后重试', 'aksrt'), ['status' => 403]);
}
return true;
}
public function owns_application(\WP_REST_Request $request) {
$authenticated = $this->logged_in($request);
if (is_wp_error($authenticated)) {
return $authenticated;
}
$application = $this->application_for_request($request);
if (!is_array($application) || (int) $application['user_id'] !== get_current_user_id()) {
return new \WP_Error('aksrt_identity_forbidden', __('无权访问该认证申请', 'aksrt'), ['status' => 403]);
}
return true;
}
public function create(\WP_REST_Request $request): \WP_REST_Response {
$params = $request->get_json_params();
$params = is_array($params) ? $params : [];
$operation_key = OperationKey::from_request($request, 'identity-create', get_current_user_id());
if (is_wp_error($operation_key)) {
return $this->respond($operation_key);
}
$request_id = strtolower(trim((string) $request->get_header('Idempotency-Key')));
$result = $this->service->create(get_current_user_id(), $params, $request_id, $operation_key);
return $this->respond($result, 201);
}
public function current(): \WP_REST_Response {
$user_id = get_current_user_id();
$data = class_exists('\\AKSRT\\User\\AuthApply')
? \AKSRT\User\AuthApply::snapshot_view($user_id)
: ['user_id' => $user_id, 'current' => null, 'active' => null, 'latest' => null];
return $this->no_store(new \WP_REST_Response(['success' => true, 'data' => $data]));
}
public function get(\WP_REST_Request $request): \WP_REST_Response {
$application = $this->application_for_request($request);
if (!$application) {
return $this->no_store(new \WP_REST_Response([
'success' => false,
'code' => 'aksrt_identity_missing',
'message' => __('认证申请不存在', 'aksrt'),
], 404));
}
return $this->no_store(new \WP_REST_Response(['success' => true, 'data' => $this->service->view($application)]));
}
public function refresh(\WP_REST_Request $request): \WP_REST_Response {
$application = $this->application_for_request($request);
if (!$application) {
return $this->no_store(new \WP_REST_Response([
'success' => false,
'code' => 'aksrt_identity_missing',
'message' => __('认证申请不存在', 'aksrt'),
], 404));
}
$operation_key = OperationKey::from_request($request, 'identity-refresh', get_current_user_id());
if (is_wp_error($operation_key)) {
return $this->respond($operation_key);
}
return $this->respond($this->service->refresh($application, $operation_key));
}
public function manual(\WP_REST_Request $request): \WP_REST_Response {
$application = $this->application_for_request($request);
if (!$application) {
return $this->no_store(new \WP_REST_Response([
'success' => false,
'code' => 'aksrt_identity_missing',
'message' => __('认证申请不存在', 'aksrt'),
], 404));
}
$operation_key = OperationKey::from_request($request, 'identity-manual', get_current_user_id());
if (is_wp_error($operation_key)) {
return $this->respond($operation_key);
}
$files = $request->get_file_params();
return $this->respond($this->service->move_to_manual($application, $files['evidence'] ?? null, $operation_key));
}
/** @return array<string,mixed>|null */
private function application_for_request(\WP_REST_Request $request): ?array {
$uuid = (string) $request['uuid'];
if ($uuid !== $this->request_uuid) {
$this->request_uuid = $uuid;
$this->request_application = $this->applications->find_by_uuid($uuid);
}
return is_array($this->request_application) ? $this->request_application : null;
}
private function respond($result, int $success_status = 200): \WP_REST_Response {
if (is_wp_error($result)) {
$data = $result->get_error_data();
$status = is_array($data) ? (int) ($data['status'] ?? 0) : 0;
if ($status < 400 || $status > 599) {
$status = in_array($result->get_error_code(), ['aksrt_identity_active', 'aksrt_identity_verified', 'aksrt_identity_conflict', 'aksrt_identity_idempotency_conflict'], true) ? 409 : 400;
}
return $this->no_store(new \WP_REST_Response([
'success' => false,
'code' => $result->get_error_code(),
'message' => $result->get_error_message(),
], $status));
}
return $this->no_store(new \WP_REST_Response(['success' => true, 'data' => $result], $success_status));
}
private function no_store(\WP_REST_Response $response): \WP_REST_Response {
$response->header('Cache-Control', 'no-store, private');
$response->header('Pragma', 'no-cache');
return $response;
}
}