docs(ccsp): formalize ccsp v1 json schema error catalog and versioning policy
This commit is contained in:
+41
-11
@@ -357,22 +357,52 @@ Quando qualquer operação falha, o servidor local CCSP/1 responde com um objeto
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Mapeamento de Status HTTP
|
### Lista Completa de Códigos de Erro Estruturados
|
||||||
|
|
||||||
| Código HTTP | Erro Típico | Descrição |
|
Todos os códigos de erro são estáveis, estruturados e interoperáveis entre o backend Java e o SDK JavaScript:
|
||||||
|
|
||||||
|
| Código de Erro (`error_code`) | Descrição e Causa Raiz | Mapeamento no SDK |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `400 Bad Request` | `USER_CANCELLED`, `PIN_INCORRECT`, `SIGNATURE_FAILED` | Requisição malformada, cancelamento pelo usuário ou falha de PIN |
|
| `CERTIFICATE_NOT_FOUND` | Certificado não encontrado pelo ID ou hint especificado | `CertificateNotFoundError` |
|
||||||
| `401 Unauthorized` | `APPLICATION_NOT_AUTHORIZED` | Aplicação não autorizada ou sem cabeçalho `X-CyberCore-AppId` |
|
| `CERTIFICATE_EXPIRED` | Certificado ultrapassou a data limite de validade (`notAfter`) | `CertificateExpiredError` |
|
||||||
| `403 Forbidden` | `INVALID_ORIGIN` | Cabeçalho `Origin` não confere com o domínio cadastrado |
|
| `CERTIFICATE_INVALID` | Certificado corrompido, revogado ou com estrutura X.509 inválida | `CertificateInvalidError` |
|
||||||
| `404 Not Found` | `CERTIFICATE_NOT_FOUND`, `TOKEN_NOT_FOUND` | Certificado ou dispositivo físico ausente |
|
| `PRIVATE_KEY_NOT_AVAILABLE` | Chave privada inacessível, protegida por hardware desconectado ou ausente | `PrivateKeyNotAvailableError` |
|
||||||
| `409 Conflict` | `REPLAY_DETECTED` | Nonce já utilizado anteriormente na janela de validação |
|
| `TOKEN_NOT_FOUND` | Nenhum token USB, smart card ou leitora física detectada | `TokenNotFoundError` |
|
||||||
| `422 Unprocessable`| `REQUEST_EXPIRED`, `CERTIFICATE_EXPIRED` | Parâmetros sintaticamente válidos mas expirados temporalmente |
|
| `TOKEN_REMOVED` | O dispositivo físico foi desconectado durante a operação | `TokenNotFoundError` |
|
||||||
| `500 Server Error` | `PKCS11_ERROR`, `PROVIDER_NOT_AVAILABLE` | Falha interna no middleware do fabricante ou driver criptográfico |
|
| `PIN_REQUIRED` | O hardware criptográfico A3 exige digitação de PIN para desbloqueio | `PinRequiredError` |
|
||||||
| `503 Service Unavail`| `TOKEN_REMOVED`, `COMMUNICATION_ERROR` | Hardware desconectado ou serviço em recuperação |
|
| `PIN_INCORRECT` | O PIN digitado pelo usuário foi rejeitado pelo token (risco de bloqueio de PUK) | `PinIncorrectError` |
|
||||||
|
| `PKCS11_ERROR` | Falha interna no driver, middleware PKCS#11 ou DLL do fabricante | `CyberCoreError` |
|
||||||
|
| `SIGNATURE_FAILED` | Erro matemático na cifra assimétrica ou payload malformado | `SignatureFailedError` |
|
||||||
|
| `APPLICATION_NOT_AUTHORIZED` | A aplicação web solicitante não possui autorização concedida pelo usuário | `ApplicationNotAuthorizedError` |
|
||||||
|
| `REQUEST_EXPIRED` | O timestamp `expires_at` da requisição foi ultrapassado (timeout) | `RequestExpiredError` |
|
||||||
|
| `REPLAY_DETECTED` | O identificador `nonce` já foi processado anteriormente (ataque de replay) | `ReplayDetectedError` |
|
||||||
|
| `INVALID_ORIGIN` | O cabeçalho `Origin` não confere com os domínios homologados para o AppId | `InvalidOriginError` |
|
||||||
|
| `INVALID_HOST` | O cabeçalho `Host` foi adulterado (tentativa de DNS Rebinding) | `CyberCoreError` |
|
||||||
|
| `UNSUPPORTED_ALGORITHM` | Algoritmo criptográfico solicitado não é suportado pelo provider | `UnsupportedAlgorithmError` |
|
||||||
|
| `USER_CANCELLED` | O usuário clicou explicitamente em 'Cancelar' no diálogo nativo do sistema | `UserCancelledError` |
|
||||||
|
| `PROVIDER_NOT_AVAILABLE` | Provedor criptográfico nativo indisponível na plataforma atual | `ProviderNotAvailableError` |
|
||||||
|
| `COMMUNICATION_ERROR` | Falha na camada de transporte HTTP local do loopback | `CyberCoreError` |
|
||||||
|
| `AGENT_NOT_RUNNING` | O agente desktop não está rodando em 127.0.0.1:21325 (lado SDK) | `AgentNotRunningError` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Diagrama de Sequência Completo
|
## 5. Especificação JSON Schema e Versionamento
|
||||||
|
|
||||||
|
O contrato canônico e exaustivo de todos os endpoints e payloads do protocolo CCSP/1 está formalizado no arquivo:
|
||||||
|
- [docs/ccsp-v1.schema.json](file:///var/www/cybercore/cybercore-signer/docs/ccsp-v1.schema.json)
|
||||||
|
|
||||||
|
### Política de Versionamento e Retrocompatibilidade
|
||||||
|
|
||||||
|
1. **Identificador de Versão Maior (`version: "1"`):**
|
||||||
|
- O campo `version` em todos os requests e responses identifica a versão maior do protocolo. O agente desktop garante compatibilidade retroativa com todas as revisões do CCSP/1.
|
||||||
|
2. **Evolução Aditiva (Não Quebrante):**
|
||||||
|
- Novos campos opcionais de requisição e novos metadados informativos de resposta podem ser adicionados dentro da versão CCSP/1 sem quebrar clientes existentes. Clientes e servidores devem ignorar propriedades adicionais desconhecidas (`additionalProperties` tolerantes).
|
||||||
|
3. **Mudança Quebrante (Nova Versão Maior):**
|
||||||
|
- Qualquer alteração na semântica de autenticação, remoção de campos obrigatórios ou mudança estrutural de URLs exigirá a introdução de uma nova versão de protocolo (ex: CCSP/2 com prefixo `/ccsp/v2/`). O agente manterá os endpoints `/ccsp/v1/` operacionais em paralelo para garantir transição suave.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Diagrama de Sequência Completo
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
|
|||||||
@@ -0,0 +1,356 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://cybercore.com.br/schemas/ccsp-v1.json",
|
||||||
|
"title": "CyberCore Signing Protocol v1 (CCSP/1) Specification",
|
||||||
|
"description": "Especificação formal do protocolo JSON para comunicação local entre aplicações web e o agente desktop CyberCore Signer.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["1"],
|
||||||
|
"description": "Versão do protocolo CCSP (sempre '1' para CCSP/1)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"ErrorCode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"CERTIFICATE_NOT_FOUND",
|
||||||
|
"CERTIFICATE_EXPIRED",
|
||||||
|
"CERTIFICATE_INVALID",
|
||||||
|
"PRIVATE_KEY_NOT_AVAILABLE",
|
||||||
|
"TOKEN_NOT_FOUND",
|
||||||
|
"TOKEN_REMOVED",
|
||||||
|
"PIN_REQUIRED",
|
||||||
|
"PIN_INCORRECT",
|
||||||
|
"PKCS11_ERROR",
|
||||||
|
"SIGNATURE_FAILED",
|
||||||
|
"APPLICATION_NOT_AUTHORIZED",
|
||||||
|
"REQUEST_EXPIRED",
|
||||||
|
"REPLAY_DETECTED",
|
||||||
|
"INVALID_ORIGIN",
|
||||||
|
"INVALID_HOST",
|
||||||
|
"UNSUPPORTED_ALGORITHM",
|
||||||
|
"USER_CANCELLED",
|
||||||
|
"PROVIDER_NOT_AVAILABLE",
|
||||||
|
"COMMUNICATION_ERROR",
|
||||||
|
"BAD_REQUEST",
|
||||||
|
"METHOD_NOT_ALLOWED"
|
||||||
|
],
|
||||||
|
"description": "Códigos formais de erro estruturados do protocolo CCSP/1."
|
||||||
|
},
|
||||||
|
|
||||||
|
"ErrorResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["version", "status", "error_code", "message"],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"status": { "type": "string", "enum": ["error"] },
|
||||||
|
"error_code": { "$ref": "#/definitions/ErrorCode" },
|
||||||
|
"message": { "type": "string" },
|
||||||
|
"request_id": { "type": ["string", "null"] }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"StatusResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["version", "status", "agent", "agent_version", "os", "timestamp"],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"status": { "type": "string", "enum": ["ready", "busy", "error"] },
|
||||||
|
"agent": { "type": "string" },
|
||||||
|
"agent_version": { "type": "string" },
|
||||||
|
"os": { "type": "string" },
|
||||||
|
"timestamp": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"AuthorizeRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["version", "application_id"],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"application_id": { "type": "string", "pattern": "^[0-9a-fA-F-]{36}$" },
|
||||||
|
"name": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"AuthorizeResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["version", "status", "application_id"],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"status": { "type": "string", "enum": ["authorized"] },
|
||||||
|
"application_id": { "type": "string" },
|
||||||
|
"message": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"CertificateItem": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "alias", "type", "can_sign", "status"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"alias": { "type": "string" },
|
||||||
|
"common_name": { "type": ["string", "null"] },
|
||||||
|
"subject_dn": { "type": ["string", "null"] },
|
||||||
|
"issuer_dn": { "type": ["string", "null"] },
|
||||||
|
"type": { "type": "string", "enum": ["A1", "A3"] },
|
||||||
|
"provider_id": { "type": ["string", "null"] },
|
||||||
|
"device_name": { "type": ["string", "null"] },
|
||||||
|
"valid_from": { "type": ["string", "null"] },
|
||||||
|
"valid_to": { "type": ["string", "null"] },
|
||||||
|
"serial_number": { "type": ["string", "null"] },
|
||||||
|
"cpf": { "type": ["string", "null"] },
|
||||||
|
"cnpj": { "type": ["string", "null"] },
|
||||||
|
"can_sign": { "type": "boolean" },
|
||||||
|
"status": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"CertificatesResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["version", "status", "certificates"],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"status": { "type": "string", "enum": ["success"] },
|
||||||
|
"certificates": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/definitions/CertificateItem" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"SealThemeItem": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "name"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"description": { "type": ["string", "null"] },
|
||||||
|
"width": { "type": "number" },
|
||||||
|
"height": { "type": "number" },
|
||||||
|
"primary_color": { "type": ["string", "null"] },
|
||||||
|
"secondary_color": { "type": ["string", "null"] },
|
||||||
|
"background_color": { "type": ["string", "null"] },
|
||||||
|
"border_color": { "type": ["string", "null"] },
|
||||||
|
"text_color": { "type": ["string", "null"] },
|
||||||
|
"icon_style": { "type": ["string", "null"] },
|
||||||
|
"custom": { "type": "boolean" }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"SealsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["version", "status", "themes"],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"status": { "type": "string", "enum": ["success"] },
|
||||||
|
"default_seal_id": { "type": ["string", "null"] },
|
||||||
|
"themes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/definitions/SealThemeItem" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"SignRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"version",
|
||||||
|
"request_id",
|
||||||
|
"application_id",
|
||||||
|
"operation",
|
||||||
|
"algorithm",
|
||||||
|
"digest",
|
||||||
|
"nonce",
|
||||||
|
"expires_at"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"request_id": { "type": "string" },
|
||||||
|
"application_id": { "type": "string" },
|
||||||
|
"operation": { "type": "string", "enum": ["sign", "authenticate"] },
|
||||||
|
"algorithm": { "type": "string" },
|
||||||
|
"digest": { "type": "string", "description": "Digest SHA-256 em Base64" },
|
||||||
|
"certificate_hint": { "type": ["string", "null"] },
|
||||||
|
"document_name": { "type": ["string", "null"] },
|
||||||
|
"document_type": { "type": ["string", "null"] },
|
||||||
|
"purpose": { "type": ["string", "null"] },
|
||||||
|
"challenge": { "type": ["string", "null"] },
|
||||||
|
"nonce": { "type": "string" },
|
||||||
|
"expires_at": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"SignResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"version",
|
||||||
|
"request_id",
|
||||||
|
"status",
|
||||||
|
"signature",
|
||||||
|
"certificate",
|
||||||
|
"algorithm"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"request_id": { "type": "string" },
|
||||||
|
"status": { "type": "string", "enum": ["signed", "authenticated"] },
|
||||||
|
"signature": { "type": "string", "description": "Assinatura digital em Base64" },
|
||||||
|
"certificate": { "type": "string", "description": "Certificado X.509 do signatário em Base64" },
|
||||||
|
"certificate_chain": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"algorithm": { "type": "string" },
|
||||||
|
"sha256": { "type": ["string", "null"] },
|
||||||
|
"subject_name": { "type": ["string", "null"] },
|
||||||
|
"subject_cpf_cnpj": { "type": ["string", "null"] },
|
||||||
|
"document_name": { "type": ["string", "null"] },
|
||||||
|
"document_type": { "type": ["string", "null"] },
|
||||||
|
"challenge": { "type": ["string", "null"] }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"SignPdfRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"version",
|
||||||
|
"request_id",
|
||||||
|
"application_id",
|
||||||
|
"operation",
|
||||||
|
"pdf",
|
||||||
|
"nonce",
|
||||||
|
"expires_at"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"request_id": { "type": "string" },
|
||||||
|
"application_id": { "type": "string" },
|
||||||
|
"operation": { "type": "string", "enum": ["sign_pdf"] },
|
||||||
|
"pdf": { "type": "string", "description": "Documento PDF completo codificado em Base64" },
|
||||||
|
"certificate_hint": { "type": ["string", "null"] },
|
||||||
|
"document_name": { "type": ["string", "null"] },
|
||||||
|
"page": { "type": ["integer", "null"] },
|
||||||
|
"page_choice": { "type": ["string", "null"] },
|
||||||
|
"position": { "type": ["string", "null"] },
|
||||||
|
"x": { "type": ["number", "null"] },
|
||||||
|
"y": { "type": ["number", "null"] },
|
||||||
|
"scale": { "type": ["number", "null"] },
|
||||||
|
"visible_seal": { "type": ["boolean", "null"] },
|
||||||
|
"seal_theme_id": { "type": ["string", "null"] },
|
||||||
|
"seal_custom": { "type": ["object", "null"] },
|
||||||
|
"reason": { "type": ["string", "null"] },
|
||||||
|
"location": { "type": ["string", "null"] },
|
||||||
|
"cpf_display_mode": { "type": ["string", "null"] },
|
||||||
|
"include_date_time": { "type": ["boolean", "null"] },
|
||||||
|
"custom_extra_info": { "type": ["string", "null"] },
|
||||||
|
"nonce": { "type": "string" },
|
||||||
|
"expires_at": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"SignPdfResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"version",
|
||||||
|
"request_id",
|
||||||
|
"status",
|
||||||
|
"signed_pdf",
|
||||||
|
"signature",
|
||||||
|
"certificate",
|
||||||
|
"algorithm"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"request_id": { "type": "string" },
|
||||||
|
"status": { "type": "string", "enum": ["signed"] },
|
||||||
|
"signed_pdf": { "type": "string", "description": "PDF assinado completo com envelope PAdES em Base64" },
|
||||||
|
"sha256": { "type": ["string", "null"] },
|
||||||
|
"signature": { "type": "string" },
|
||||||
|
"certificate": { "type": "string" },
|
||||||
|
"certificate_chain": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"algorithm": { "type": "string" },
|
||||||
|
"subject_name": { "type": ["string", "null"] },
|
||||||
|
"subject_cpf_cnpj": { "type": ["string", "null"] },
|
||||||
|
"document_name": { "type": ["string", "null"] }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"SignXmlRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"version",
|
||||||
|
"request_id",
|
||||||
|
"application_id",
|
||||||
|
"operation",
|
||||||
|
"nonce",
|
||||||
|
"expires_at"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"request_id": { "type": "string" },
|
||||||
|
"application_id": { "type": "string" },
|
||||||
|
"operation": { "type": "string", "enum": ["sign_xml"] },
|
||||||
|
"xml": { "type": ["string", "null"], "description": "XML em texto puro UTF-8" },
|
||||||
|
"xml_base64": { "type": ["string", "null"], "description": "XML codificado em Base64" },
|
||||||
|
"certificate_hint": { "type": ["string", "null"] },
|
||||||
|
"document_name": { "type": ["string", "null"] },
|
||||||
|
"profile": { "type": ["string", "null"], "enum": ["XADES_BES", "XMLDSIG", null] },
|
||||||
|
"target_element_tag": { "type": ["string", "null"] },
|
||||||
|
"target_element_id": { "type": ["string", "null"] },
|
||||||
|
"algorithm": { "type": ["string", "null"] },
|
||||||
|
"nonce": { "type": "string" },
|
||||||
|
"expires_at": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"SignXmlResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"version",
|
||||||
|
"request_id",
|
||||||
|
"status",
|
||||||
|
"signed_xml",
|
||||||
|
"signature",
|
||||||
|
"certificate",
|
||||||
|
"algorithm"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"request_id": { "type": "string" },
|
||||||
|
"status": { "type": "string", "enum": ["signed"] },
|
||||||
|
"signed_xml": { "type": "string", "description": "Documento XML assinado completo com assinatura enveloped" },
|
||||||
|
"sha256": { "type": ["string", "null"] },
|
||||||
|
"signature": { "type": "string" },
|
||||||
|
"certificate": { "type": "string" },
|
||||||
|
"certificate_chain": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"algorithm": { "type": "string" },
|
||||||
|
"subject_name": { "type": ["string", "null"] },
|
||||||
|
"subject_cpf_cnpj": { "type": ["string", "null"] },
|
||||||
|
"document_name": { "type": ["string", "null"] }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user