Implement initial API structure with Docker support, CI/CD workflows, and authentication features
Despliegue Automático / desplegar (push) Failing after 1m29s
Despliegue Automático / desplegar (push) Failing after 1m29s
This commit is contained in:
+289
@@ -0,0 +1,289 @@
|
||||
# API de `api_notas`
|
||||
|
||||
Esta guía resume las rutas disponibles, qué datos esperan y los tipos más importantes del proyecto.
|
||||
|
||||
## Base URL
|
||||
|
||||
Todas las rutas cuelgan de:
|
||||
|
||||
```text
|
||||
/api
|
||||
```
|
||||
|
||||
Ejemplo local:
|
||||
|
||||
```text
|
||||
http://localhost:3000/api
|
||||
```
|
||||
|
||||
## Tipos principales
|
||||
|
||||
- `UUID`: identificador único en formato UUID.
|
||||
- `string`: texto plano o cifrado.
|
||||
- `boolean`: `true` o `false`.
|
||||
- `number`: valor numérico.
|
||||
- `ISO date string`: fecha en formato ISO 8601, por ejemplo `2026-05-18T10:00:00.000Z`.
|
||||
|
||||
## Rutas disponibles
|
||||
|
||||
### `GET /api/status`
|
||||
|
||||
Comprueba que la API está viva.
|
||||
|
||||
Respuesta ejemplo:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "¡API funcionando correctamente!"
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/auth/register`
|
||||
|
||||
Crea un usuario nuevo.
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "maria",
|
||||
"password": "123456",
|
||||
"encrypted_master_key": "texto_cifrado_opcional"
|
||||
}
|
||||
```
|
||||
|
||||
Campos:
|
||||
|
||||
- `username`: `string`, obligatorio.
|
||||
- `password`: `string`, obligatorio.
|
||||
- `encrypted_master_key`: `string | null`, opcional.
|
||||
|
||||
Respuesta ejemplo:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Usuario registrado con éxito",
|
||||
"accessToken": "jwt_access_token",
|
||||
"refreshToken": "token_largo_aleatorio"
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/auth/login`
|
||||
|
||||
Inicia sesión y devuelve tokens.
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "maria",
|
||||
"password": "123456",
|
||||
"deviceName": "Flutter Pixel"
|
||||
}
|
||||
```
|
||||
|
||||
Campos:
|
||||
|
||||
- `username`: `string`, obligatorio.
|
||||
- `password`: `string`, obligatorio.
|
||||
- `deviceName`: `string`, opcional.
|
||||
|
||||
Respuesta ejemplo:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Login exitoso",
|
||||
"accessToken": "jwt_access_token",
|
||||
"refreshToken": "token_largo_aleatorio",
|
||||
"encrypted_master_key": "texto_cifrado"
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/auth/refresh`
|
||||
|
||||
Genera un nuevo `accessToken` usando un `refreshToken` válido.
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{
|
||||
"refreshToken": "token_largo_aleatorio"
|
||||
}
|
||||
```
|
||||
|
||||
Campos:
|
||||
|
||||
- `refreshToken`: `string`, obligatorio.
|
||||
|
||||
Respuesta ejemplo:
|
||||
|
||||
```json
|
||||
{
|
||||
"accessToken": "nuevo_jwt_access_token"
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/auth/logout`
|
||||
|
||||
Revoca la sesión del dispositivo actual.
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{
|
||||
"refreshToken": "token_largo_aleatorio"
|
||||
}
|
||||
```
|
||||
|
||||
Campos:
|
||||
|
||||
- `refreshToken`: `string`, obligatorio.
|
||||
|
||||
Respuesta ejemplo:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Sesión cerrada en el dispositivo actual"
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/sync`
|
||||
|
||||
Endpoint único de sincronización offline-first.
|
||||
|
||||
Autenticación requerida:
|
||||
|
||||
- Enviar `Authorization: Bearer <accessToken>` (obligatorio). El servidor ya no acepta `userId` en el body.
|
||||
|
||||
#### Request
|
||||
|
||||
Body ejemplo:
|
||||
|
||||
```json
|
||||
{
|
||||
"lastSyncAt": "2026-05-18T10:00:00.000Z",
|
||||
"changes": {
|
||||
"categories": [
|
||||
{
|
||||
"id": "uuid-categoria-1",
|
||||
"encrypted_name": "texto_cifrado...",
|
||||
"serverVersion": 1,
|
||||
"isDeleted": false,
|
||||
"updatedAt": "2026-05-18T10:05:00.000Z"
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
{
|
||||
"id": "uuid-nota-1",
|
||||
"categoryId": "uuid-categoria-1",
|
||||
"encrypted_title": "titulo_cifrado...",
|
||||
"encrypted_body": "cuerpo_cifrado...",
|
||||
"serverVersion": 1,
|
||||
"position": 2000,
|
||||
"isDeleted": true,
|
||||
"updatedAt": "2026-05-18T10:10:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Campos:
|
||||
|
||||
- `lastSyncAt`: `string` ISO, opcional. Si no se envía, el servidor usa `1970-01-01T00:00:00.000Z`.
|
||||
- `changes`: `object`, obligatorio.
|
||||
- `changes.categories`: `array`, opcional.
|
||||
- `changes.notes`: `array`, opcional.
|
||||
|
||||
#### Estructura de categoría
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"encrypted_name": "texto_cifrado",
|
||||
"serverVersion": 1,
|
||||
"isDeleted": false,
|
||||
"updatedAt": "2026-05-18T10:05:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
Campos:
|
||||
|
||||
- `id`: `UUID`, obligatorio.
|
||||
- `encrypted_name`: `string`, obligatorio.
|
||||
- `serverVersion`: `number` entero >= 0, obligatorio. Es la versión base local con la que se hizo el cambio.
|
||||
- `isDeleted`: `boolean`, opcional, por defecto `false`.
|
||||
- `updatedAt`: `ISO date string`, opcional (solo informativo para UI).
|
||||
|
||||
#### Estructura de nota
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"categoryId": "uuid-categoria",
|
||||
"encrypted_title": "titulo_cifrado",
|
||||
"encrypted_body": "cuerpo_cifrado",
|
||||
"serverVersion": 1,
|
||||
"position": 2000,
|
||||
"isDeleted": false,
|
||||
"updatedAt": "2026-05-18T10:10:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
Campos:
|
||||
|
||||
- `id`: `UUID`, obligatorio.
|
||||
- `categoryId`: `UUID | null`, opcional.
|
||||
- `encrypted_title`: `string`, obligatorio.
|
||||
- `encrypted_body`: `string`, obligatorio.
|
||||
- `serverVersion`: `number` entero >= 0, obligatorio. Es la versión base local con la que se hizo el cambio.
|
||||
- `position`: `number`, opcional.
|
||||
- `isDeleted`: `boolean`, opcional, por defecto `false`.
|
||||
- `updatedAt`: `ISO date string`, opcional (solo informativo para UI).
|
||||
|
||||
#### Response
|
||||
|
||||
Respuesta ejemplo (nuevo contrato):
|
||||
|
||||
```json
|
||||
{
|
||||
"serverTimestamp": "2026-05-18T10:20:00.000Z",
|
||||
"synced": true,
|
||||
"changes": {
|
||||
"categories": [],
|
||||
"notes": [
|
||||
{
|
||||
"id": "uuid-nota-2",
|
||||
"categoryId": null,
|
||||
"encrypted_title": "titulo_cifrado...",
|
||||
"encrypted_body": "cuerpo_cifrado...",
|
||||
"serverVersion": 3,
|
||||
"position": 0,
|
||||
"isDeleted": false,
|
||||
"updatedAt": "2026-05-18T10:15:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Campos de salida:
|
||||
|
||||
- `serverTimestamp`: `string` ISO con la hora del servidor.
|
||||
- `synced`: `boolean`.
|
||||
- `changes.categories`: array con todas las categorías cambiadas en el servidor desde `lastSyncAt`, incluidas las creadas por otros dispositivos.
|
||||
- `changes.notes`: array con todas las notas cambiadas en el servidor desde `lastSyncAt`, incluidas las creadas por otros dispositivos y las copias creadas automáticamente en caso de conflicto.
|
||||
|
||||
## Reglas de sincronización
|
||||
|
||||
- El cliente debe guardar su último `lastSyncAt`.
|
||||
- Un borrado no elimina el registro localmente: se marca con `isDeleted: true`.
|
||||
- El servidor decide conflictos comparando `serverVersion` (no `updatedAt`).
|
||||
- En categorías, solo se actualiza si `incoming.serverVersion === serverVersion`. Si no coincide, gana el servidor y no se crea duplicado.
|
||||
- Si la versión entrante de una nota coincide con la versión actual del servidor, acepta el cambio y sube versión (`v -> v+1`).
|
||||
- Si la versión del servidor es mayor que la entrante en una nota, el servidor crea una nueva nota duplicada con el contenido entrante para no perder datos.
|
||||
|
||||
## Notas importantes
|
||||
|
||||
- Las notas y categorías están pensadas para contenido cifrado del lado del cliente.
|
||||
- El servidor guarda `encrypted_title`, `encrypted_body` y `encrypted_name`, pero no interpreta el contenido.
|
||||
Reference in New Issue
Block a user