299 lines
6.8 KiB
Markdown
299 lines
6.8 KiB
Markdown
# 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,
|
|
"isPermanentlyDeleted": false,
|
|
"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",
|
|
"name": "texto_cifrado",
|
|
"serverVersion": 1,
|
|
"isDeleted": false,
|
|
"isDirty": true,
|
|
"colorValue": 4281558681,
|
|
"iconCodePoint": 58896,
|
|
"updatedAt": "2026-05-18T10:05:00.000Z"
|
|
}
|
|
```
|
|
|
|
Campos:
|
|
|
|
- `id`: `UUID`, obligatorio.
|
|
- `name`: `string` (cifrado), obligatorio. Equivale a `encrypted_name`; ambos contienen el nombre encriptado.
|
|
- `serverVersion`: `number` entero >= 0, obligatorio. Es la versión base local con la que se hizo el cambio.
|
|
- `isDeleted`: `boolean`, opcional, por defecto `false`.
|
|
- `isDirty`: `boolean`, opcional. El servidor lo ignora en la escritura y devuelve `false` en la respuesta de sync.
|
|
- `colorValue`: `number`, opcional.
|
|
- `iconCodePoint`: `number`, opcional.
|
|
- `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`.
|
|
- `isPermanentlyDeleted`: `boolean`, opcional, por defecto `false`. Si llega en `true`, el servidor guarda la nota con `encrypted_title` y `encrypted_body` vacíos, `position` en `0` y `isDeleted` en `true`.
|
|
- `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,
|
|
"isPermanentlyDeleted": 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.
|