From d7a412b3c6b78ec4788d58e78957281bb87b08f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 10:13:46 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Ajoute=20reCAPTCHA=20v3=20sur=20les=20formu?= =?UTF-8?q?laires=20publics,=20r=C3=A9glable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'inscription bénévole et le formulaire de contact n'étaient protégés que par un champ piège : suffisant contre un robot naïf, inutile contre un envoi automatisé un peu sérieux. Configuration propose maintenant reCAPTCHA v3, désactivé par défaut. Deux clés à coller depuis la console Google, et une exigence à choisir : souple, équilibrée ou stricte. La clé de site part dans la page — c'est sa nature — la clé secrète est chiffrée en base et n'est jamais renvoyée au navigateur. Elle est confirmée auprès de Google à l'enregistrement : une clé refusée bloque l'activation en le disant, plutôt que d'installer une protection qui ne protège rien. Le jeton est vérifié côté serveur, pour l'action attendue — un jeton obtenu sur un formulaire ne vaut pas pour l'autre — et comparé à l'exigence retenue. Deux refus de vérifier ne ferment pas le formulaire, par choix : si Google est injoignable, ou si la clé secrète est refusée, l'envoi passe et l'incident est journalisé. Sur le site d'une association, empêcher un parent de s'inscrire parce qu'un service tiers est en panne coûte plus cher que de laisser passer un message indésirable. Un jeton absent ou mal noté, en revanche, arrête l'envoi. Rien n'est chargé chez Google tant que la protection n'est pas activée : le script n'est même pas demandé. La politique de confidentialité gagne une section sur reCAPTCHA, affichée seulement quand il est actif. Vérifié sur une instance réelle : clé refusée à l'activation, clé acceptée, puis les cinq issues d'un envoi — sans jeton, bien noté, mal noté, refusé, Google injoignable — et l'absence totale d'appel à Google une fois la protection désactivée. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014SfQYBU4xXTeSEHKhHQXdD --- drizzle/0015_recaptcha_settings.sql | 5 + drizzle/meta/0015_snapshot.json | 2973 ++++++++++++++++++ drizzle/meta/_journal.json | 7 + src/app/api/join/route.ts | 17 +- src/app/api/signup/route.ts | 17 +- src/app/confidentialite/page.tsx | 44 +- src/app/dashboard/settings/page.tsx | 5 + src/app/inscription/[token]/page.tsx | 9 +- src/app/rejoindre/page.tsx | 7 +- src/components/association-settings-form.tsx | 126 +- src/components/join-form.tsx | 11 +- src/components/volunteer-signup-form.tsx | 5 + src/lib/db/schema.ts | 14 + src/lib/services/association-settings.ts | 81 + src/lib/services/recaptcha.ts | 135 + src/lib/use-recaptcha.ts | 62 + src/lib/validation.ts | 10 + 17 files changed, 3520 insertions(+), 8 deletions(-) create mode 100644 drizzle/0015_recaptcha_settings.sql create mode 100644 drizzle/meta/0015_snapshot.json create mode 100644 src/lib/services/recaptcha.ts create mode 100644 src/lib/use-recaptcha.ts diff --git a/drizzle/0015_recaptcha_settings.sql b/drizzle/0015_recaptcha_settings.sql new file mode 100644 index 0000000..2127f28 --- /dev/null +++ b/drizzle/0015_recaptcha_settings.sql @@ -0,0 +1,5 @@ +ALTER TABLE "association_settings" ADD COLUMN "recaptcha_enabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "association_settings" ADD COLUMN "recaptcha_site_key" text;--> statement-breakpoint +ALTER TABLE "association_settings" ADD COLUMN "encrypted_recaptcha_secret" text;--> statement-breakpoint +ALTER TABLE "association_settings" ADD COLUMN "recaptcha_min_score" integer DEFAULT 50 NOT NULL;--> statement-breakpoint +ALTER TABLE "association_settings" ADD CONSTRAINT "association_settings_recaptcha_min_score_check" CHECK ("association_settings"."recaptcha_min_score" >= 0 and "association_settings"."recaptcha_min_score" <= 100); \ No newline at end of file diff --git a/drizzle/meta/0015_snapshot.json b/drizzle/meta/0015_snapshot.json new file mode 100644 index 0000000..a97c68f --- /dev/null +++ b/drizzle/meta/0015_snapshot.json @@ -0,0 +1,2973 @@ +{ + "id": "226673d8-0632-46b4-99a4-8534fe0c123b", + "prevId": "5667c180-701d-4a21-8a65-c9c0cf945103", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounting_categories": { + "name": "accounting_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "accounting_category_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounting_categories_type_active_idx": { + "name": "accounting_categories_type_active_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.accounting_entries": { + "name": "accounting_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "accounting_entry_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "accounting_entry_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "category_id": { + "name": "category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "counterparty": { + "name": "counterparty", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_method": { + "name": "payment_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attachment_url": { + "name": "attachment_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounting_entries_occurred_at_idx": { + "name": "accounting_entries_occurred_at_idx", + "columns": [ + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounting_entries_status_occurred_at_idx": { + "name": "accounting_entries_status_occurred_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounting_entries_account_idx": { + "name": "accounting_entries_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounting_entries_category_idx": { + "name": "accounting_entries_category_idx", + "columns": [ + { + "expression": "category_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounting_entries_event_idx": { + "name": "accounting_entries_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounting_entries_account_id_financial_accounts_id_fk": { + "name": "accounting_entries_account_id_financial_accounts_id_fk", + "tableFrom": "accounting_entries", + "tableTo": "financial_accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "accounting_entries_category_id_accounting_categories_id_fk": { + "name": "accounting_entries_category_id_accounting_categories_id_fk", + "tableFrom": "accounting_entries", + "tableTo": "accounting_categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "accounting_entries_event_id_events_id_fk": { + "name": "accounting_entries_event_id_events_id_fk", + "tableFrom": "accounting_entries", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "accounting_entries_created_by_users_id_fk": { + "name": "accounting_entries_created_by_users_id_fk", + "tableFrom": "accounting_entries", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "accounting_entries_amount_cents_check": { + "name": "accounting_entries_amount_cents_check", + "value": "\"accounting_entries\".\"amount_cents\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.association_documents": { + "name": "association_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "association_document_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "association_document_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_date": { + "name": "document_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "member_id": { + "name": "member_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "association_documents_type_status_date_idx": { + "name": "association_documents_type_status_date_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "association_documents_member_idx": { + "name": "association_documents_member_idx", + "columns": [ + { + "expression": "member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "association_documents_member_id_association_members_id_fk": { + "name": "association_documents_member_id_association_members_id_fk", + "tableFrom": "association_documents", + "tableTo": "association_members", + "columnsFrom": [ + "member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "association_documents_created_by_users_id_fk": { + "name": "association_documents_created_by_users_id_fk", + "tableFrom": "association_documents", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.association_members": { + "name": "association_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line_1": { + "name": "address_line_1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line_2": { + "name": "address_line_2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code": { + "name": "postal_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'France'" + }, + "status": { + "name": "status", + "type": "association_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "school_year": { + "name": "school_year", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "membership_fee_cents": { + "name": "membership_fee_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fee_paid_at": { + "name": "fee_paid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "association_members_user_idx": { + "name": "association_members_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"association_members\".\"user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "association_members_status_school_year_idx": { + "name": "association_members_status_school_year_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "school_year", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "association_members_name_idx": { + "name": "association_members_name_idx", + "columns": [ + { + "expression": "last_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "first_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "association_members_user_id_users_id_fk": { + "name": "association_members_user_id_users_id_fk", + "tableFrom": "association_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "association_members_membership_fee_cents_check": { + "name": "association_members_membership_fee_cents_check", + "value": "\"association_members\".\"membership_fee_cents\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.association_settings": { + "name": "association_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "'default'" + }, + "association_name": { + "name": "association_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'APEL Manager'" + }, + "school_name": { + "name": "school_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Votre établissement'" + }, + "contact_email": { + "name": "contact_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rna": { + "name": "rna", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_reminder_window_days": { + "name": "task_reminder_window_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "volunteer_reminder_window_days": { + "name": "volunteer_reminder_window_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "telegram_enabled": { + "name": "telegram_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "encrypted_telegram_bot_token": { + "name": "encrypted_telegram_bot_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegram_token_last_four": { + "name": "telegram_token_last_four", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegram_bot_username": { + "name": "telegram_bot_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegram_token_verified_at": { + "name": "telegram_token_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "recaptcha_enabled": { + "name": "recaptcha_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "recaptcha_site_key": { + "name": "recaptcha_site_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_recaptcha_secret": { + "name": "encrypted_recaptcha_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recaptcha_min_score": { + "name": "recaptcha_min_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "association_settings_updated_by_users_id_fk": { + "name": "association_settings_updated_by_users_id_fk", + "tableFrom": "association_settings", + "tableTo": "users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "association_settings_singleton_check": { + "name": "association_settings_singleton_check", + "value": "\"association_settings\".\"id\" = 'default'" + }, + "association_settings_task_reminder_window_check": { + "name": "association_settings_task_reminder_window_check", + "value": "\"association_settings\".\"task_reminder_window_days\" >= 0 and \"association_settings\".\"task_reminder_window_days\" <= 30" + }, + "association_settings_recaptcha_min_score_check": { + "name": "association_settings_recaptcha_min_score_check", + "value": "\"association_settings\".\"recaptcha_min_score\" >= 0 and \"association_settings\".\"recaptcha_min_score\" <= 100" + }, + "association_settings_volunteer_reminder_window_check": { + "name": "association_settings_volunteer_reminder_window_check", + "value": "\"association_settings\".\"volunteer_reminder_window_days\" >= 0 and \"association_settings\".\"volunteer_reminder_window_days\" <= 30" + } + }, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'web'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_logs_entity_idx": { + "name": "audit_logs_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_actor_created_idx": { + "name": "audit_logs_actor_created_idx", + "columns": [ + { + "expression": "actor_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_client_created_idx": { + "name": "audit_logs_client_created_idx", + "columns": [ + { + "expression": "oauth_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_actor_user_id_users_id_fk": { + "name": "audit_logs_actor_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_oauth_client_id_oauth_clients_id_fk": { + "name": "audit_logs_oauth_client_id_oauth_clients_id_fk", + "tableFrom": "audit_logs", + "tableTo": "oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.checklist_templates": { + "name": "checklist_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tasks": { + "name": "tasks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.event_attachments": { + "name": "event_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "event_attachments_event_idx": { + "name": "event_attachments_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "event_attachments_event_id_events_id_fk": { + "name": "event_attachments_event_id_events_id_fk", + "tableFrom": "event_attachments", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "event_attachments_uploaded_by_users_id_fk": { + "name": "event_attachments_uploaded_by_users_id_fk", + "tableFrom": "event_attachments", + "tableTo": "users", + "columnsFrom": [ + "uploaded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_description": { + "name": "public_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_at": { + "name": "start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_at": { + "name": "end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "event_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "share_token": { + "name": "share_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "events_status_start_idx": { + "name": "events_status_start_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "events_created_by_users_id_fk": { + "name": "events_created_by_users_id_fk", + "tableFrom": "events", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "events_share_token_unique": { + "name": "events_share_token_unique", + "nullsNotDistinct": false, + "columns": [ + "share_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.financial_accounts": { + "name": "financial_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "financial_account_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "financial_accounts_type_active_idx": { + "name": "financial_accounts_type_active_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications_log": { + "name": "notifications_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_log_task_user_kind_idx": { + "name": "notifications_log_task_user_kind_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_log_task_id_tasks_id_fk": { + "name": "notifications_log_task_id_tasks_id_fk", + "tableFrom": "notifications_log", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_log_user_id_users_id_fk": { + "name": "notifications_log_user_id_users_id_fk", + "tableFrom": "notifications_log", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_authorization_codes": { + "name": "oauth_authorization_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'S256'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oauth_authorization_codes_code_hash_idx": { + "name": "oauth_authorization_codes_code_hash_idx", + "columns": [ + { + "expression": "code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_authorization_codes_client_expires_idx": { + "name": "oauth_authorization_codes_client_expires_idx", + "columns": [ + { + "expression": "oauth_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_authorization_codes_user_idx": { + "name": "oauth_authorization_codes_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_authorization_codes_oauth_client_id_oauth_clients_id_fk": { + "name": "oauth_authorization_codes_oauth_client_id_oauth_clients_id_fk", + "tableFrom": "oauth_authorization_codes", + "tableTo": "oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_authorization_codes_user_id_users_id_fk": { + "name": "oauth_authorization_codes_user_id_users_id_fk", + "tableFrom": "oauth_authorization_codes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_clients": { + "name": "oauth_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "grant_types": { + "name": "grant_types", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"authorization_code\",\"refresh_token\"]'::jsonb" + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oauth_clients_client_id_idx": { + "name": "oauth_clients_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_clients_enabled_idx": { + "name": "oauth_clients_enabled_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_clients_created_by_users_id_fk": { + "name": "oauth_clients_created_by_users_id_fk", + "tableFrom": "oauth_clients", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_tokens": { + "name": "oauth_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "oauth_token_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "authorization_code_id": { + "name": "authorization_code_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oauth_tokens_token_hash_idx": { + "name": "oauth_tokens_token_hash_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_tokens_client_type_expires_idx": { + "name": "oauth_tokens_client_type_expires_idx", + "columns": [ + { + "expression": "oauth_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_tokens_user_idx": { + "name": "oauth_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_tokens_authorization_code_idx": { + "name": "oauth_tokens_authorization_code_idx", + "columns": [ + { + "expression": "authorization_code_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_tokens_oauth_client_id_oauth_clients_id_fk": { + "name": "oauth_tokens_oauth_client_id_oauth_clients_id_fk", + "tableFrom": "oauth_tokens", + "tableTo": "oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_tokens_user_id_users_id_fk": { + "name": "oauth_tokens_user_id_users_id_fk", + "tableFrom": "oauth_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_tokens_authorization_code_id_oauth_authorization_codes_id_fk": { + "name": "oauth_tokens_authorization_code_id_oauth_authorization_codes_id_fk", + "tableFrom": "oauth_tokens", + "tableTo": "oauth_authorization_codes", + "columnsFrom": [ + "authorization_code_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbound_mail_settings": { + "name": "outbound_mail_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "'default'" + }, + "provider": { + "name": "provider", + "type": "outbound_mail_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'resend'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reply_to": { + "name": "reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_last_four": { + "name": "key_last_four", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "smtp_host": { + "name": "smtp_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "smtp_port": { + "name": "smtp_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "smtp_secure": { + "name": "smtp_secure", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "smtp_username": { + "name": "smtp_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_smtp_password": { + "name": "encrypted_smtp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_tested_at": { + "name": "last_tested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_test_status": { + "name": "last_test_status", + "type": "outbound_mail_test_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'untested'" + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "outbound_mail_settings_updated_by_users_id_fk": { + "name": "outbound_mail_settings_updated_by_users_id_fk", + "tableFrom": "outbound_mail_settings", + "tableTo": "users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "outbound_mail_settings_singleton_check": { + "name": "outbound_mail_settings_singleton_check", + "value": "\"outbound_mail_settings\".\"id\" = 'default'" + }, + "outbound_mail_settings_smtp_port_check": { + "name": "outbound_mail_settings_smtp_port_check", + "value": "\"outbound_mail_settings\".\"smtp_port\" is null or (\"outbound_mail_settings\".\"smtp_port\" >= 1 and \"outbound_mail_settings\".\"smtp_port\" <= 65535)" + } + }, + "isRLSEnabled": false + }, + "public.password_reset_tokens": { + "name": "password_reset_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_assignees": { + "name": "task_assignees", + "schema": "", + "columns": { + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "task_assignees_user_idx": { + "name": "task_assignees_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_assignees_task_id_tasks_id_fk": { + "name": "task_assignees_task_id_tasks_id_fk", + "tableFrom": "task_assignees", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_assignees_user_id_users_id_fk": { + "name": "task_assignees_user_id_users_id_fk", + "tableFrom": "task_assignees", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "task_assignees_task_id_user_id_pk": { + "name": "task_assignees_task_id_user_id_pk", + "columns": [ + "task_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lead_time_days": { + "name": "lead_time_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 7 + }, + "lead_time_value": { + "name": "lead_time_value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 7 + }, + "lead_time_unit": { + "name": "lead_time_unit", + "type": "task_lead_time_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'days'" + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'todo'" + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tasks_event_idx": { + "name": "tasks_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_status_due_idx": { + "name": "tasks_status_due_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tasks_event_id_events_id_fk": { + "name": "tasks_event_id_events_id_fk", + "tableFrom": "tasks", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "tasks_lead_time_value_check": { + "name": "tasks_lead_time_value_check", + "value": "\"tasks\".\"lead_time_value\" >= 0" + }, + "tasks_lead_time_consistency_check": { + "name": "tasks_lead_time_consistency_check", + "value": "\"tasks\".\"lead_time_days\" = \"tasks\".\"lead_time_value\" * case \"tasks\".\"lead_time_unit\" when 'days' then 1 when 'weeks' then 7 when 'months' then 30 end" + } + }, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "telegram_chat_id": { + "name": "telegram_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_epoch": { + "name": "session_epoch", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "onboarding_seen_at": { + "name": "onboarding_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.volunteer_signups": { + "name": "volunteer_signups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slot_id": { + "name": "slot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancel_token": { + "name": "cancel_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reminded_at": { + "name": "reminded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "volunteer_signups_slot_idx": { + "name": "volunteer_signups_slot_idx", + "columns": [ + { + "expression": "slot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "volunteer_signups_user_idx": { + "name": "volunteer_signups_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "volunteer_signups_cancel_token_idx": { + "name": "volunteer_signups_cancel_token_idx", + "columns": [ + { + "expression": "cancel_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "volunteer_signups_slot_email_idx": { + "name": "volunteer_signups_slot_email_idx", + "columns": [ + { + "expression": "slot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"volunteer_signups\".\"email\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "volunteer_signups_slot_id_volunteer_slots_id_fk": { + "name": "volunteer_signups_slot_id_volunteer_slots_id_fk", + "tableFrom": "volunteer_signups", + "tableTo": "volunteer_slots", + "columnsFrom": [ + "slot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volunteer_signups_user_id_users_id_fk": { + "name": "volunteer_signups_user_id_users_id_fk", + "tableFrom": "volunteer_signups", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.volunteer_slots": { + "name": "volunteer_slots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_at": { + "name": "start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "end_at": { + "name": "end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "volunteer_slots_event_idx": { + "name": "volunteer_slots_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "volunteer_slots_event_id_events_id_fk": { + "name": "volunteer_slots_event_id_events_id_fk", + "tableFrom": "volunteer_slots", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.accounting_category_type": { + "name": "accounting_category_type", + "schema": "public", + "values": [ + "income", + "expense" + ] + }, + "public.accounting_entry_status": { + "name": "accounting_entry_status", + "schema": "public", + "values": [ + "draft", + "posted" + ] + }, + "public.accounting_entry_type": { + "name": "accounting_entry_type", + "schema": "public", + "values": [ + "income", + "expense" + ] + }, + "public.association_document_status": { + "name": "association_document_status", + "schema": "public", + "values": [ + "draft", + "final", + "archived" + ] + }, + "public.association_document_type": { + "name": "association_document_type", + "schema": "public", + "values": [ + "ag_minutes", + "attestation", + "statutes", + "internal_rules", + "insurance", + "agreement", + "other" + ] + }, + "public.association_member_status": { + "name": "association_member_status", + "schema": "public", + "values": [ + "active", + "pending", + "inactive" + ] + }, + "public.event_status": { + "name": "event_status", + "schema": "public", + "values": [ + "draft", + "published", + "archived" + ] + }, + "public.financial_account_type": { + "name": "financial_account_type", + "schema": "public", + "values": [ + "bank", + "cash" + ] + }, + "public.oauth_token_type": { + "name": "oauth_token_type", + "schema": "public", + "values": [ + "access", + "refresh" + ] + }, + "public.outbound_mail_provider": { + "name": "outbound_mail_provider", + "schema": "public", + "values": [ + "resend", + "smtp" + ] + }, + "public.outbound_mail_test_status": { + "name": "outbound_mail_test_status", + "schema": "public", + "values": [ + "untested", + "success", + "failed" + ] + }, + "public.role": { + "name": "role", + "schema": "public", + "values": [ + "admin", + "manager", + "member" + ] + }, + "public.task_lead_time_unit": { + "name": "task_lead_time_unit", + "schema": "public", + "values": [ + "days", + "weeks", + "months" + ] + }, + "public.task_status": { + "name": "task_status", + "schema": "public", + "values": [ + "todo", + "in_progress", + "done" + ] + } + }, + "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 2caef9d..e761f32 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -106,6 +106,13 @@ "when": 1786180289391, "tag": "0014_onboarding_seen", "breakpoints": true + }, + { + "idx": 15, + "version": "7", + "when": 1786183503290, + "tag": "0015_recaptcha_settings", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/api/join/route.ts b/src/app/api/join/route.ts index 839eac0..a5f4d8c 100644 --- a/src/app/api/join/route.ts +++ b/src/app/api/join/route.ts @@ -3,7 +3,11 @@ import { NextResponse } from "next/server"; import { handleApiError, HttpError } from "@/lib/auth/guards"; import { sendEmail } from "@/lib/notifications/email"; import { joinRequestEmail } from "@/lib/notifications/emails"; -import { getAssociationSettings } from "@/lib/services/association-settings"; +import { + getAssociationSettings, + getRecaptchaRuntimeConfig, +} from "@/lib/services/association-settings"; +import { verifyRecaptcha } from "@/lib/services/recaptcha"; import { joinRequestSchema } from "@/lib/validation"; export const dynamic = "force-dynamic"; @@ -23,6 +27,17 @@ export async function POST(req: Request) { return NextResponse.json({ ok: true }); } + const recaptcha = await getRecaptchaRuntimeConfig(); + if (recaptcha) { + await verifyRecaptcha({ + secret: recaptcha.secret, + token: data.recaptchaToken, + action: "contact", + minScore: recaptcha.minScore, + ip: req.headers.get("x-forwarded-for")?.split(",")[0]?.trim(), + }); + } + const settings = await getAssociationSettings(); const destination = settings.contactEmail?.trim(); if (!destination) { diff --git a/src/app/api/signup/route.ts b/src/app/api/signup/route.ts index 3bee675..1b68623 100644 --- a/src/app/api/signup/route.ts +++ b/src/app/api/signup/route.ts @@ -10,7 +10,11 @@ import { db } from "@/lib/db"; import { events, volunteerSignups } from "@/lib/db/schema"; import { sendEmail } from "@/lib/notifications/email"; import { volunteerConfirmationEmail } from "@/lib/notifications/emails"; -import { getAssociationSettings } from "@/lib/services/association-settings"; +import { + getAssociationSettings, + getRecaptchaRuntimeConfig, +} from "@/lib/services/association-settings"; +import { verifyRecaptcha } from "@/lib/services/recaptcha"; import { generateToken } from "@/lib/tokens"; import { emptyToNull } from "@/lib/utils"; import { signupSchema } from "@/lib/validation"; @@ -35,6 +39,17 @@ export async function POST(req: Request) { return NextResponse.json({ ok: true }); } + const recaptcha = await getRecaptchaRuntimeConfig(); + if (recaptcha) { + await verifyRecaptcha({ + secret: recaptcha.secret, + token: data.recaptchaToken, + action: "inscription", + minScore: recaptcha.minScore, + ip: req.headers.get("x-forwarded-for")?.split(",")[0]?.trim(), + }); + } + const event = await db.query.events.findFirst({ where: eq(events.shareToken, data.token), with: { volunteerSlots: true }, diff --git a/src/app/confidentialite/page.tsx b/src/app/confidentialite/page.tsx index 59f0e62..d7634a8 100644 --- a/src/app/confidentialite/page.tsx +++ b/src/app/confidentialite/page.tsx @@ -1,4 +1,5 @@ import { + Bot, Clock3, Database, ShieldCheck, @@ -14,11 +15,18 @@ import { getAssociationSettings } from "@/lib/services/association-settings"; export const dynamic = "force-dynamic"; export const metadata = { title: "Politique de confidentialité" }; -const sectionIcons = [ShieldCheck, Database, Target, Clock3, UserRoundCheck]; +const sectionIcons = [ + ShieldCheck, + Database, + Target, + Clock3, + UserRoundCheck, + Bot, +]; export default async function PrivacyPage() { const settings = await getAssociationSettings(); - const sections = [ + const sections: { title: string; content: React.ReactNode }[] = [ { title: "Responsable", content: ( @@ -76,6 +84,38 @@ export default async function PrivacyPage() { }, ]; + if (settings.recaptchaReady) { + sections.push({ + title: "Protection anti-robot", + content: ( + <> + Les formulaires publics sont protégés par reCAPTCHA v3, un service de + Google qui analyse votre navigation sur la page pour distinguer un + visiteur d'un robot. Google peut à cette occasion déposer des + cookies et collecter des données techniques, soumis à sa{" "} + + politique de confidentialité + {" "} + et à ses{" "} + + conditions d'utilisation + + . Ce contrôle sert uniquement à écarter les envois automatisés. + + ), + }); + } + return (
diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx index eaad624..6775527 100644 --- a/src/app/dashboard/settings/page.tsx +++ b/src/app/dashboard/settings/page.tsx @@ -39,6 +39,11 @@ export default async function SettingsPage() { telegramBotUsername: associationStatus.telegramBotUsername, telegramTokenVerifiedAt: associationStatus.telegramTokenVerifiedAt, telegramReady: associationStatus.telegramReady, + recaptchaEnabled: associationStatus.recaptchaEnabled, + recaptchaSiteKey: associationStatus.recaptchaSiteKey, + recaptchaSecretConfigured: associationStatus.recaptchaSecretConfigured, + recaptchaMinScore: associationStatus.recaptchaMinScore, + recaptchaReady: associationStatus.recaptchaReady, legacyEnvironment: associationStatus.legacyEnvironment, }; const settings: MailSettingsView = { diff --git a/src/app/inscription/[token]/page.tsx b/src/app/inscription/[token]/page.tsx index 3663fb2..8316433 100644 --- a/src/app/inscription/[token]/page.tsx +++ b/src/app/inscription/[token]/page.tsx @@ -12,6 +12,7 @@ import Link from "next/link"; import { FormattedText } from "@/components/formatted-text"; import { SiteFooter } from "@/components/site-footer"; import { SiteHeader } from "@/components/site-header"; +import { getAssociationSettings } from "@/lib/services/association-settings"; import { VolunteerSignupForm, type SignupSlotOption, @@ -76,7 +77,10 @@ export default async function InscriptionPage({ ); } - const currentUser = await getCurrentUser(); + const [currentUser, association] = await Promise.all([ + getCurrentUser(), + getAssociationSettings(), + ]); const slotOptions: SignupSlotOption[] = event.volunteerSlots.map((slot) => { const remaining = Math.max(0, slot.capacity - slot.signups.length); @@ -193,6 +197,9 @@ export default async function InscriptionPage({ diff --git a/src/app/rejoindre/page.tsx b/src/app/rejoindre/page.tsx index 3e9e5d7..9c5237c 100644 --- a/src/app/rejoindre/page.tsx +++ b/src/app/rejoindre/page.tsx @@ -181,7 +181,12 @@ export default async function RejoindrePage() { )}

- +
diff --git a/src/components/association-settings-form.tsx b/src/components/association-settings-form.tsx index 185189c..4a33b8c 100644 --- a/src/components/association-settings-form.tsx +++ b/src/components/association-settings-form.tsx @@ -14,7 +14,7 @@ import { useState } from "react"; import { LogoUploadField } from "@/components/logo-upload-field"; import { useToast } from "@/components/toast"; -import { Button, Card, Field, Input } from "@/components/ui"; +import { Button, Card, Field, Input, Select } from "@/components/ui"; import { api } from "@/lib/client"; export interface AssociationSettingsView { @@ -31,6 +31,11 @@ export interface AssociationSettingsView { telegramBotUsername: string | null; telegramTokenVerifiedAt: Date | null; telegramReady: boolean; + recaptchaEnabled: boolean; + recaptchaSiteKey: string | null; + recaptchaSecretConfigured: boolean; + recaptchaMinScore: number; + recaptchaReady: boolean; legacyEnvironment: boolean; } @@ -50,6 +55,7 @@ export function AssociationSettingsForm({ const telegramBotToken = String( form.get("telegramBotToken") ?? "", ).trim(); + const recaptchaSecret = String(form.get("recaptchaSecret") ?? "").trim(); try { await api("/api/settings/association", { @@ -71,6 +77,12 @@ export function AssociationSettingsForm({ !telegramBotToken && form.get("clearTelegramBotToken") === "on", ...(telegramBotToken ? { telegramBotToken } : {}), + recaptchaEnabled: form.get("recaptchaEnabled") === "on", + recaptchaSiteKey: form.get("recaptchaSiteKey") || null, + recaptchaMinScore: Number(form.get("recaptchaMinScore")), + clearRecaptchaSecret: + !recaptchaSecret && form.get("clearRecaptchaSecret") === "on", + ...(recaptchaSecret ? { recaptchaSecret } : {}), }, }); toast("Réglages de l’association enregistrés."); @@ -325,6 +337,118 @@ export function AssociationSettingsForm({ +
+
+ + +
+

+ Protection anti-robot +

+

+ reCAPTCHA v3 sur l’inscription des bénévoles et le formulaire + de contact. Les deux clés se créent sur{" "} + + google.com/recaptcha/admin + {" "} + en choisissant « v3 ». +

+
+
+
+ +
+ + + + +
+
+ {settings.recaptchaSecretConfigured && ( + + )} +
+ + + +
+
+
+