diff --git a/.env.example b/.env.example index 43b7b0c..5066a66 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,33 @@ STRIPE_SECRET_KEY= # stripe listen --forward-to localhost:5173/api/stripe/webhook STRIPE_WEBHOOK_SECRET= +# --- Entrar con Google / Facebook -------------------------------------------- +# Sin estas claves, los botones no se pintan y solo queda el código por correo: +# un botón de «Continuar con Google» sin configurar solo lleva a un error. +# +# La dirección de retorno tiene que estar dada de alta EXACTA en cada consola, +# y ser la misma que ORIGIN de más abajo: +# +# https://TU-DOMINIO/acceso/google/retorno +# https://TU-DOMINIO/acceso/facebook/retorno +# +# Dos avisos que cuestan una tarde si se descubren tarde: +# +# - Google NO admite direcciones IP como retorno, solo nombres de dominio (y +# http://localhost para desarrollo). Con la web en 192.168.18.171 a secas, +# esto no se puede probar: hace falta el dominio. +# - Facebook exige HTTPS y, para que lo pueda usar cualquiera y no solo los +# administradores de la app, pasar su verificación de empresa y la revisión +# del permiso `email`. Eso son días de trámite, no minutos. +# +# Google: https://console.cloud.google.com/apis/credentials +# Tipo «ID de cliente de OAuth» → «Aplicación web». +# Facebook: https://developers.facebook.com/apps → producto «Inicio de sesión». +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +FACEBOOK_CLIENT_ID= +FACEBOOK_CLIENT_SECRET= + # --- Correo (Resend) --------------------------------------------------------- # Manda los códigos de acceso y los mensajes del formulario de contacto. # Sin RESEND_API_KEY, en desarrollo el código se imprime por consola. diff --git a/drizzle/0001_identidades.sql b/drizzle/0001_identidades.sql new file mode 100644 index 0000000..ff11de5 --- /dev/null +++ b/drizzle/0001_identidades.sql @@ -0,0 +1,13 @@ +CREATE TABLE "identidad" ( + "id" text PRIMARY KEY NOT NULL, + "proveedor" text NOT NULL, + "sujeto" text NOT NULL, + "usuario_id" text NOT NULL, + "email" text, + "creada_en" timestamp with time zone NOT NULL, + "ultimo_acceso_en" timestamp with time zone +); +--> statement-breakpoint +ALTER TABLE "identidad" ADD CONSTRAINT "identidad_usuario_id_usuario_id_fk" FOREIGN KEY ("usuario_id") REFERENCES "public"."usuario"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "identidad_proveedor_sujeto" ON "identidad" USING btree ("proveedor","sujeto");--> statement-breakpoint +CREATE INDEX "identidad_usuario" ON "identidad" USING btree ("usuario_id"); \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..aacf456 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,864 @@ +{ + "id": "73fba167-b287-4e75-945d-78ce984184ff", + "prevId": "de6718aa-a69c-4f5b-8cfa-5a4a7b3a470b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.carrito": { + "name": "carrito", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "usuario_id": { + "name": "usuario_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creado_en": { + "name": "creado_en", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "actualizado_en": { + "name": "actualizado_en", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "carrito_usuario": { + "name": "carrito_usuario", + "columns": [ + { + "expression": "usuario_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "carrito_usuario_id_usuario_id_fk": { + "name": "carrito_usuario_id_usuario_id_fk", + "tableFrom": "carrito", + "tableTo": "usuario", + "columnsFrom": [ + "usuario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.carrito_item": { + "name": "carrito_item", + "schema": "", + "columns": { + "carrito_id": { + "name": "carrito_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cancion_slug": { + "name": "cancion_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anadido_en": { + "name": "anadido_en", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "carrito_item_carrito_id_carrito_id_fk": { + "name": "carrito_item_carrito_id_carrito_id_fk", + "tableFrom": "carrito_item", + "tableTo": "carrito", + "columnsFrom": [ + "carrito_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "carrito_item_carrito_id_cancion_slug_pk": { + "name": "carrito_item_carrito_id_cancion_slug_pk", + "columns": [ + "carrito_id", + "cancion_slug" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.codigo_acceso": { + "name": "codigo_acceso", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expira_en": { + "name": "expira_en", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "intentos": { + "name": "intentos", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "usado_en": { + "name": "usado_en", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "creado_en": { + "name": "creado_en", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "codigo_acceso_email": { + "name": "codigo_acceso_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.compra": { + "name": "compra", + "schema": "", + "columns": { + "usuario_id": { + "name": "usuario_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cancion_slug": { + "name": "cancion_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pedido_id": { + "name": "pedido_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "comprado_en": { + "name": "comprado_en", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "compra_usuario_id_usuario_id_fk": { + "name": "compra_usuario_id_usuario_id_fk", + "tableFrom": "compra", + "tableTo": "usuario", + "columnsFrom": [ + "usuario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "compra_pedido_id_pedido_id_fk": { + "name": "compra_pedido_id_pedido_id_fk", + "tableFrom": "compra", + "tableTo": "pedido", + "columnsFrom": [ + "pedido_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "compra_usuario_id_cancion_slug_pk": { + "name": "compra_usuario_id_cancion_slug_pk", + "columns": [ + "usuario_id", + "cancion_slug" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.favorito": { + "name": "favorito", + "schema": "", + "columns": { + "usuario_id": { + "name": "usuario_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cancion_slug": { + "name": "cancion_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "marcado_en": { + "name": "marcado_en", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_favorito_usuario": { + "name": "idx_favorito_usuario", + "columns": [ + { + "expression": "usuario_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "favorito_usuario_id_usuario_id_fk": { + "name": "favorito_usuario_id_usuario_id_fk", + "tableFrom": "favorito", + "tableTo": "usuario", + "columnsFrom": [ + "usuario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "favorito_usuario_id_cancion_slug_pk": { + "name": "favorito_usuario_id_cancion_slug_pk", + "columns": [ + "usuario_id", + "cancion_slug" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.identidad": { + "name": "identidad", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "proveedor": { + "name": "proveedor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sujeto": { + "name": "sujeto", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usuario_id": { + "name": "usuario_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creada_en": { + "name": "creada_en", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ultimo_acceso_en": { + "name": "ultimo_acceso_en", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "identidad_proveedor_sujeto": { + "name": "identidad_proveedor_sujeto", + "columns": [ + { + "expression": "proveedor", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sujeto", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "identidad_usuario": { + "name": "identidad_usuario", + "columns": [ + { + "expression": "usuario_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "identidad_usuario_id_usuario_id_fk": { + "name": "identidad_usuario_id_usuario_id_fk", + "tableFrom": "identidad", + "tableTo": "usuario", + "columnsFrom": [ + "usuario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.limite": { + "name": "limite", + "schema": "", + "columns": { + "clave": { + "name": "clave", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "intentos": { + "name": "intentos", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expira_en": { + "name": "expira_en", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "limite_expira": { + "name": "limite_expira", + "columns": [ + { + "expression": "expira_en", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pedido": { + "name": "pedido", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "usuario_id": { + "name": "usuario_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sesion_pago_id": { + "name": "sesion_pago_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "estado": { + "name": "estado", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pendiente'" + }, + "packs": { + "name": "packs", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "total_centimos": { + "name": "total_centimos", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "moneda": { + "name": "moneda", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'eur'" + }, + "creado_en": { + "name": "creado_en", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "pagado_en": { + "name": "pagado_en", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "pedido_usuario": { + "name": "pedido_usuario", + "columns": [ + { + "expression": "usuario_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pedido_usuario_id_usuario_id_fk": { + "name": "pedido_usuario_id_usuario_id_fk", + "tableFrom": "pedido", + "tableTo": "usuario", + "columnsFrom": [ + "usuario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pedido_sesion_pago_id_unique": { + "name": "pedido_sesion_pago_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sesion_pago_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pedido_item": { + "name": "pedido_item", + "schema": "", + "columns": { + "pedido_id": { + "name": "pedido_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cancion_slug": { + "name": "cancion_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "titulo": { + "name": "titulo", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "pedido_item_pedido_id_pedido_id_fk": { + "name": "pedido_item_pedido_id_pedido_id_fk", + "tableFrom": "pedido_item", + "tableTo": "pedido", + "columnsFrom": [ + "pedido_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "pedido_item_pedido_id_cancion_slug_pk": { + "name": "pedido_item_pedido_id_cancion_slug_pk", + "columns": [ + "pedido_id", + "cancion_slug" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sesion": { + "name": "sesion", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "usuario_id": { + "name": "usuario_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expira_en": { + "name": "expira_en", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "creada_en": { + "name": "creada_en", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "sesion_usuario": { + "name": "sesion_usuario", + "columns": [ + { + "expression": "usuario_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sesion_usuario_id_usuario_id_fk": { + "name": "sesion_usuario_id_usuario_id_fk", + "tableFrom": "sesion", + "tableTo": "usuario", + "columnsFrom": [ + "usuario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usuario": { + "name": "usuario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nombre": { + "name": "nombre", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creado_en": { + "name": "creado_en", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ultimo_acceso_en": { + "name": "ultimo_acceso_en", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "usuario_email_unique": { + "name": "usuario_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.valoracion": { + "name": "valoracion", + "schema": "", + "columns": { + "usuario_id": { + "name": "usuario_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cancion_slug": { + "name": "cancion_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "puntuacion": { + "name": "puntuacion", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "creada_en": { + "name": "creada_en", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "actualizada_en": { + "name": "actualizada_en", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_valoracion_cancion": { + "name": "idx_valoracion_cancion", + "columns": [ + { + "expression": "cancion_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "valoracion_usuario_id_usuario_id_fk": { + "name": "valoracion_usuario_id_usuario_id_fk", + "tableFrom": "valoracion", + "tableTo": "usuario", + "columnsFrom": [ + "usuario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "valoracion_usuario_id_cancion_slug_pk": { + "name": "valoracion_usuario_id_cancion_slug_pk", + "columns": [ + "usuario_id", + "cancion_slug" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 5f33e3c..984bab7 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1788780351154, "tag": "0000_aromatic_bug", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1788820606303, + "tag": "0001_identidades", + "breakpoints": true } ] } \ No newline at end of file diff --git a/e2e/tienda.e2e.ts b/e2e/tienda.e2e.ts index d7452db..8db213d 100644 --- a/e2e/tienda.e2e.ts +++ b/e2e/tienda.e2e.ts @@ -196,6 +196,34 @@ test.describe('Modal de acceso', () => { await aparte.close(); }); + test('un proveedor sin configurar no se ofrece ni deja empezar', async ({ page, request }) => { + /* + * Un botón de «Continuar con Google» sin credenciales solo sirve para + * llevar a un error, así que no se pinta. Y la ruta tampoco arranca el + * baile: contesta que aquí eso no está montado. + */ + await page.goto('/entrar'); + await expect(page.locator('.proveedor')).toHaveCount(0); + + const respuesta = await request.get('/acceso/google', { maxRedirects: 0 }); + expect(respuesta.status()).toBe(503); + }); + + test('una vuelta de proveedor inventada no abre sesión', async ({ request }) => { + /* + * Es la defensa del `state`: sin la cookie que se guardó al empezar, una + * vuelta fabricada por cualquiera no vale. Si valiera, bastaría con hacerte + * visitar una URL para meterte en la cuenta de otro. + */ + const respuesta = await request.get('/acceso/google/retorno?state=inventado&code=x', { + maxRedirects: 0 + }); + + expect(respuesta.status()).toBe(303); + expect(respuesta.headers()['location']).toContain('/entrar?fallo='); + expect(respuesta.headers()['set-cookie'] ?? '').not.toContain('sesion='); + }); + test('sin JavaScript, «Entrar» sigue llevando a la página', async ({ browser }) => { const contexto = await browser.newContext({ javaScriptEnabled: false }); const pagina = await contexto.newPage(); diff --git a/src/content/legal/cookies.md b/src/content/legal/cookies.md index fd5f44a..2313ff6 100644 --- a/src/content/legal/cookies.md +++ b/src/content/legal/cookies.md @@ -1,24 +1,31 @@ --- titulo: 'Política de cookies' -resumen: 'Dos cookies, las dos necesarias, ninguna de seguimiento.' +resumen: 'Todas necesarias, ninguna de seguimiento y ninguna de terceros.' actualizada: 2026-09-07 orden: 4 --- -Esta web usa **dos cookies, y las dos son necesarias para que funcione**. No -hay cookies de analítica, ni de publicidad, ni de terceros. +Todas las cookies de esta web son **necesarias para que funcione**. No hay +cookies de analítica, ni de publicidad, ni de terceros: las pone este dominio y +no salen de aquí. Por eso no verás un aviso pidiéndote permiso: la normativa solo exige consentimiento para las que no son imprescindibles, y aquí no hay ninguna. ## Las que hay -| Nombre | Para qué | Cuánto dura | -| --------- | ----------------------------------------------------------------------------------- | --------------------------------- | -| `sesion` | Mantener la sesión abierta cuando entras con tu cuenta | Hasta que cierras sesión o caduca | -| `carrito` | Recordar el carrito antes de que entres, para que no se pierda al cambiar de página | Mientras dure el carrito | +| Nombre | Para qué | Cuánto dura | +| ------------------- | ----------------------------------------------------------------------------------- | --------------------------------- | +| `sesion` | Mantener la sesión abierta cuando entras con tu cuenta | Hasta que cierras sesión o caduca | +| `carrito` | Recordar el carrito antes de que entres, para que no se pierda al cambiar de página | Mientras dure el carrito | +| `oauth_estado` | Comprobar que la vuelta de Google o Facebook es la que pediste tú | Diez minutos | +| `oauth_verificador` | La otra mitad de esa comprobación, que nunca sale de este servidor | Diez minutos | +| `oauth_vuelta` | Recordar en qué página estabas al entrar, para devolverte ahí | Diez minutos | -Las dos van marcadas como `HttpOnly` y `SameSite`, lo que significa que ningún +Las tres últimas solo aparecen si pulsas «Continuar con Google» o «Continuar +con Facebook», y se borran en cuanto vuelves, salga bien o mal. + +Todas van marcadas como `HttpOnly` y `SameSite`, lo que significa que ningún script puede leerlas y que no viajan a otros sitios. ## Lo que se guarda en tu navegador, que no son cookies @@ -34,5 +41,5 @@ Se borran vaciando los datos del sitio en tu navegador. ## Cómo quitarlas Puedes borrar y bloquear las cookies desde tu navegador. Ten en cuenta que si -bloqueas las dos de arriba **no podrás entrar en tu cuenta ni comprar**, porque -son justamente las que sostienen esas dos cosas. +las bloqueas **no podrás entrar en tu cuenta ni comprar**, porque son +justamente las que sostienen esas dos cosas. diff --git a/src/content/legal/privacidad.md b/src/content/legal/privacidad.md index f5326f7..61f24d3 100644 --- a/src/content/legal/privacidad.md +++ b/src/content/legal/privacidad.md @@ -38,19 +38,50 @@ guarda un historial: solo un contador que caduca. ## Con quién se comparten -Solo con dos empresas, y solo lo imprescindible: +Solo lo imprescindible, y solo con estas: - **Stripe Payments Europe, Ltd.** (Irlanda) procesa los pagos. Los datos de la tarjeta **no pasan por esta web en ningún momento**: se escriben directamente en la pasarela de Stripe. - **Resend** envía los correos con los códigos de acceso, y para eso recibe la dirección de destino. - -**Nada más sale de aquí.** Las tipografías se sirven desde este mismo dominio y -no desde Google, precisamente para que tu dirección IP no llegue a un tercero -solo por abrir una página. Los vídeos de YouTube no cargan nada hasta que -pulsas el botón de reproducir: hasta ese momento lo que se ve es una imagen -servida desde aquí. +- **Google Ireland Ltd.** y **Meta Platforms Ireland Ltd.**, y solo si eliges + entrar con su botón. Ver más abajo. + +**Si no pulsas nada, nada más sale de aquí.** Las tipografías se sirven desde +este mismo dominio y no desde Google, precisamente para que tu dirección IP no +llegue a un tercero solo por abrir una página. Los logotipos de los botones de +acceso van dibujados en la propia página, no traídos de fuera. Y los vídeos de +YouTube no cargan nada hasta que pulsas el botón de reproducir: hasta ese +momento lo que se ve es una imagen servida desde aquí. + +## Entrar con Google o con Facebook + +Es opcional: existe también el código por correo, que no involucra a nadie más. + +Si eliges esa vía, al pulsar el botón se te lleva al proveedor. **Es entonces +—y solo entonces— cuando ese proveedor sabe que estás entrando en este sitio.** +Nosotros no le mandamos nada antes: hasta que pulsas, no hay ninguna conexión +con ellos. + +De vuelta, el proveedor nos entrega tres cosas y ninguna más: **un +identificador suyo, tu correo y tu nombre**. No se piden permisos para nada +más —ni contactos, ni publicaciones, ni la lista de amigos— y esos permisos +figuran en la pantalla que te enseña el proveedor antes de que aceptes. + +Lo que se guarda aquí es ese identificador, para reconocerte la próxima vez. +**No se conserva ningún token del proveedor**: se usa una vez, en el momento de +entrar, y se descarta. Con él no podríamos hacer nada más aunque quisiéramos. + +Si tu correo del proveedor coincide con el de una cuenta que ya existe aquí, +las dos se unen: es la misma persona. Para eso se exige que el proveedor +confirme que esa dirección está verificada; si no lo confirma, no se enlaza +nada y se te pide entrar con el código por correo. + +Puedes revocar el acceso cuando quieras desde tu cuenta de Google +(_Seguridad → Tus conexiones a aplicaciones de terceros_) o de Facebook +(_Configuración → Apps y sitios web_). Tu cuenta aquí y tus compras siguen +siendo tuyas: entrarías con el código por correo. ## Cuánto se guardan diff --git a/src/lib/components/BotonesDeProveedor.svelte b/src/lib/components/BotonesDeProveedor.svelte new file mode 100644 index 0000000..d9dc959 --- /dev/null +++ b/src/lib/components/BotonesDeProveedor.svelte @@ -0,0 +1,120 @@ + + +{#if proveedores.length > 0} +
Escribe tu correo y te enviamos un código de seis dígitos. No hace falta contraseña: si es la
primera vez, la cuenta se crea sola.
diff --git a/src/lib/components/ModalAcceso.svelte b/src/lib/components/ModalAcceso.svelte
index 7db93c3..1b93f39 100644
--- a/src/lib/components/ModalAcceso.svelte
+++ b/src/lib/components/ModalAcceso.svelte
@@ -18,6 +18,7 @@
* forma habitual de acabar con un modal del que no se puede salir con el
* teclado.
*/
+ import { page } from '$app/state';
import { acceso } from '$lib/acceso.svelte';
import { heroe } from '$lib/site';
import FormularioAcceso from './FormularioAcceso.svelte';
@@ -100,7 +101,12 @@
primer paso cada vez que se abre, en vez de reaparecer con el
código de la vez anterior a medio escribir.
-->
-