diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 925ada5..92e31b2 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,6 +1,6 @@ name: Docker image -# Builds ghcr.io/codextde/dispatch for linux/amd64 and linux/arm64 on native +# Builds ghcr.io/lsalami/dispatch for linux/amd64 and linux/arm64 on native # runners (no QEMU) with Docker's reusable github-builder workflow, which adds # SLSA provenance, an SBOM and signed attestations. # @@ -41,7 +41,7 @@ jobs: output: image push: ${{ github.event_name != 'pull_request' }} platforms: linux/amd64,linux/arm64 - meta-images: ghcr.io/codextde/dispatch + meta-images: ghcr.io/lsalami/dispatch meta-tags: | type=ref,event=branch type=semver,pattern={{version}} diff --git a/Dockerfile b/Dockerfile index 8fc9e5d..40ada74 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # syntax=docker/dockerfile:1 # # Dispatch — one image, two roles: -# docker run ghcr.io/codextde/dispatch web app (Next.js standalone server on :3000) -# docker run ghcr.io/codextde/dispatch worker background worker (IMAP sync, sending, rules, webhooks) +# docker run ghcr.io/lsalami/dispatch web app (Next.js standalone server on :3000) +# docker run ghcr.io/lsalami/dispatch worker background worker (IMAP sync, sending, rules, webhooks) # See docker-compose.yml for the full stack and docs/self-hosting.md for details. # --------------------------------------------------------------------------- @@ -41,11 +41,11 @@ FROM node:24-alpine AS runner LABEL org.opencontainers.image.title="Dispatch" \ org.opencontainers.image.description="The open-source collaborative inbox for teams: shared Gmail, Outlook and IMAP inboxes with comments, assignments and automation." \ - org.opencontainers.image.source="https://github.com/codextde/dispatch" \ - org.opencontainers.image.url="https://github.com/codextde/dispatch" \ - org.opencontainers.image.documentation="https://github.com/codextde/dispatch/tree/main/docs" \ + org.opencontainers.image.source="https://github.com/LSalami/dispatch" \ + org.opencontainers.image.url="https://github.com/LSalami/dispatch" \ + org.opencontainers.image.documentation="https://github.com/LSalami/dispatch/tree/main/docs" \ org.opencontainers.image.licenses="AGPL-3.0-only" \ - org.opencontainers.image.vendor="Codext GmbH" + org.opencontainers.image.vendor="LSalami" # tini: proper PID 1 (signal forwarding, zombie reaping) RUN apk add --no-cache tini \ diff --git a/README.md b/README.md index 40f5aff..44cb4d5 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,9 @@ Self-host for free or use Dispatch Cloud for $50/month.

[![License: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-blue.svg)](LICENSE) -[![GitHub stars](https://img.shields.io/github/stars/codextde/dispatch?style=flat&logo=github)](https://github.com/codextde/dispatch/stargazers) -[![Docker image](https://img.shields.io/badge/ghcr.io-codextde%2Fdispatch-2496ED?logo=docker&logoColor=white)](https://github.com/codextde/dispatch/pkgs/container/dispatch) -[![CI](https://github.com/codextde/dispatch/actions/workflows/ci.yml/badge.svg)](https://github.com/codextde/dispatch/actions/workflows/ci.yml) +[![GitHub stars](https://img.shields.io/github/stars/LSalami/dispatch?style=flat&logo=github)](https://github.com/LSalami/dispatch/stargazers) +[![Docker image](https://img.shields.io/badge/ghcr.io-lsalami%2Fdispatch-2496ED?logo=docker&logoColor=white)](https://github.com/LSalami/dispatch/pkgs/container/dispatch) +[![CI](https://github.com/LSalami/dispatch/actions/workflows/ci.yml/badge.svg)](https://github.com/LSalami/dispatch/actions/workflows/ci.yml) [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) [![Deploy on Coolify](https://img.shields.io/badge/deploy%20on-Coolify-8C52FF.svg)](docs/coolify.md) @@ -92,7 +92,7 @@ Self-host for free or use Dispatch Cloud for $50/month.

On any server with Docker and a domain pointing to it: ```bash -curl -fsSLO https://raw.githubusercontent.com/codextde/dispatch/main/docker-compose.yml +curl -fsSLO https://raw.githubusercontent.com/LSalami/dispatch/main/docker-compose.yml echo "DOMAIN=mail.example.com" > .env docker compose up -d ``` @@ -100,7 +100,7 @@ docker compose up -d This starts the app, worker and PostgreSQL. The compose file publishes no ports, so put your reverse proxy in front of `app:3000`. Or download the [override with a bundled Caddy](docker-compose.override.example.yml) for automatic HTTPS: ```bash -curl -fsSL https://raw.githubusercontent.com/codextde/dispatch/main/docker-compose.override.example.yml -o docker-compose.override.yml +curl -fsSL https://raw.githubusercontent.com/LSalami/dispatch/main/docker-compose.override.example.yml -o docker-compose.override.yml docker compose up -d ``` @@ -169,7 +169,7 @@ The **app** serves the UI, API and realtime stream. The **worker** syncs mailbox - [ ] Website live chat widget - [ ] Importers for Missive and Front -Have an idea? [Start a discussion](https://github.com/codextde/dispatch/discussions) or upvote existing [feature requests](https://github.com/codextde/dispatch/issues?q=is%3Aissue+label%3Aenhancement). +Have an idea? [Start a discussion](https://github.com/LSalami/dispatch/discussions) or upvote existing [feature requests](https://github.com/LSalami/dispatch/issues?q=is%3Aissue+label%3Aenhancement). ## Contributing @@ -177,7 +177,7 @@ Contributions are welcome: bug reports, docs, translations and code. Read [CONTR ## Security -Please report vulnerabilities privately via [GitHub security advisories](https://github.com/codextde/dispatch/security/advisories/new), not in public issues. See [SECURITY.md](SECURITY.md). +Please report vulnerabilities privately via [GitHub security advisories](https://github.com/LSalami/dispatch/security/advisories/new), not in public issues. See [SECURITY.md](SECURITY.md). ## License @@ -187,7 +187,7 @@ Dispatch is licensed under the [GNU Affero General Public License v3.0](LICENSE) - **You can modify it.** If you let people use a modified version over a network, for example as a hosted service, you must offer them the source code of your modifications under the AGPL-3.0. - **Running it as a service is allowed.** The same condition applies: changes you make must be published to your users under the AGPL. -Dispatch is built and maintained by [Codext GmbH](https://github.com/codextde), Germany. Dispatch Cloud is the hosted version run by the maintainers, and it funds development. +Dispatch is originally built by [Codext GmbH](https://github.com/codextde), Germany. This is an independent fork maintained in this repository (AGPL-3.0-only, see [LICENSE](LICENSE)). ## Acknowledgements diff --git a/docker-compose.yml b/docker-compose.yml index fd0ee9c..95d8694 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,7 @@ services: app: - image: ghcr.io/codextde/dispatch:${DISPATCH_VERSION:-latest} + image: ghcr.io/lsalami/dispatch:${DISPATCH_VERSION:-latest} restart: unless-stopped environment: # Public hostname, e.g. mail.example.com. On Coolify it falls back to the @@ -39,7 +39,7 @@ services: start_period: 60s worker: - image: ghcr.io/codextde/dispatch:${DISPATCH_VERSION:-latest} + image: ghcr.io/lsalami/dispatch:${DISPATCH_VERSION:-latest} command: worker restart: unless-stopped environment: diff --git a/docs/coolify.md b/docs/coolify.md index b620c36..df57807 100644 --- a/docs/coolify.md +++ b/docs/coolify.md @@ -33,11 +33,11 @@ Pick **one** of the two ways. Both use the same compose file. The compose file stays in sync with the repository, and Coolify can redeploy when it changes. 1. In your project, click **+ New** and choose **Public Repository**. -2. Repository URL: `https://github.com/codextde/dispatch`, branch `main`. +2. Repository URL: `https://github.com/LSalami/dispatch`, branch `main`. 3. Build pack: **Docker Compose**. Base directory `/`, Docker Compose location `/docker-compose.yml`. 4. Click **Continue**. Coolify reads the compose file and shows the services `app`, `worker` and `db`. -Nothing is built on your server: the compose file uses the prebuilt image `ghcr.io/codextde/dispatch`, so Coolify only pulls it. +Nothing is built on your server: the compose file uses the prebuilt image `ghcr.io/lsalami/dispatch`, so Coolify only pulls it. ### Option B: paste the compose file @@ -92,7 +92,7 @@ Deleting the resource with "delete volumes" deletes all data. Back up `dispatch- Dispatch applies database migrations automatically when the new version starts. -- **Pinned version (recommended):** set `DISPATCH_VERSION` to the new release (see [releases](https://github.com/codextde/dispatch/releases)) and click **Redeploy**. A new tag always makes Coolify pull the new image. +- **Pinned version (recommended):** set `DISPATCH_VERSION` to the new release (see [releases](https://github.com/LSalami/dispatch/releases)) and click **Redeploy**. A new tag always makes Coolify pull the new image. - **Tracking `latest`:** redeploy and make sure Coolify pulls the image again instead of reusing the cached `latest` (depending on your Coolify version, via the *pull latest images* option of the redeploy/restart action). Read the [upgrade notes](upgrading.md) before jumping major versions, and take a backup first. diff --git a/docs/development.md b/docs/development.md index 6714d6f..b9a86e6 100644 --- a/docs/development.md +++ b/docs/development.md @@ -11,7 +11,7 @@ This guide gets Dispatch running locally and explains how the code is organized. ## Setup ```bash -git clone https://github.com/codextde/dispatch.git +git clone https://github.com/LSalami/dispatch.git cd dispatch corepack enable pnpm install @@ -109,7 +109,7 @@ The full list lives in [AGENTS.md](../AGENTS.md). The essentials: ## Building the Docker image locally ```bash -docker build -t ghcr.io/codextde/dispatch:dev . +docker build -t ghcr.io/lsalami/dispatch:dev . echo "DISPATCH_VERSION=dev" >> .env # next to docker-compose.yml docker compose up -d ``` diff --git a/docs/imap-completion-plan.md b/docs/imap-completion-plan.md new file mode 100644 index 0000000..dcd35c0 --- /dev/null +++ b/docs/imap-completion-plan.md @@ -0,0 +1,8 @@ +# Dispatch e Aruba + +L'obiettivo è usare Dispatch come webmail per le caselle collegate, con lo storico disponibile via IMAP, le bozze sincronizzate e una copia locale leggera per le funzioni collaborative. + +1. **Navigazione e identità dei messaggi.** Le cartelle e lo storico sono accessibili nella vista Cartelle sul server. Corpo e allegati vengono richiesti all'apertura. Il worker riconcilia gli spostamenti univoci dei messaggi già importati e sincronizza lo stato di lettura per le conversazioni con un solo messaggio nelle caselle personali abilitate. +2. **Identità delle bozze.** I tentativi ripetuti di copiare la stessa revisione usano un identificativo stabile. L'importazione della stessa copia remota riapre la bozza locale esistente, se ancora presente. Il collegamento attivato dal composer pubblica automaticamente i salvataggi successivi di Dispatch. Una revisione precedente viene rimossa solo quando posizione e contenuto sono verificati e il server supporta la cancellazione per UID. Dopo un invio confermato o uno scarto, il worker rimuove la copia remota solo se riconosce ancora la revisione esatta. Con il composer aperto, Dispatch recupera una modifica remota quando Aruba conserva il Message-ID e la nuova revisione è univoca. La apre come bozza separata per proteggere anche le modifiche locali non ancora salvate. Se Aruba cambia l’identificativo, la revisione rimane accessibile da Cartelle sul server. Criteri di accettazione: una modifica da un lato è visibile dall'altro, le modifiche concorrenti conservano entrambe le versioni, e invio o scarto non eliminano modifiche concorrenti. +3. **Recupero dello spazio.** La copia HTML dei messaggi ricevuti può essere rimossa per singolo messaggio dopo una verifica IMAP. Dispatch conserva il testo e recupera la formattazione all’apertura e quando cita il messaggio. Anche gli allegati si possono liberare per singolo messaggio dopo aver verificato i byte su IMAP. Download e inoltro usano il riferimento remoto. La pulizia in blocco è disponibile dalla vista Cartelle sul server per chi gestisce la casella. Si può fermare e riprendere nella stessa sessione. Verificare per ogni copia che il contenuto remoto corrisponda e proteggere gli allegati ancora referenziati. Conservare le copie dei messaggi senza un riscontro remoto certo. Misurare lo spazio prima e dopo su dati di prova. +4. **Verifica e rilascio.** Provare il flusso completo con una casella di test, includendo la riconnessione, la perdita della risposta dopo un salvataggio e lo spostamento da un altro client. Verificare poi i comportamenti specifici di Aruba prima di applicare la pulizia alle caselle reali. diff --git a/docs/self-hosting.md b/docs/self-hosting.md index 37fc211..d43b40c 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -35,8 +35,8 @@ A small cloud VM (e.g. 2 vCPU / 4 GB) comfortably serves a team of dozens with s mkdir -p /opt/dispatch && cd /opt/dispatch # 1. Get the compose file and the Caddy override (automatic HTTPS) -curl -fsSLO https://raw.githubusercontent.com/codextde/dispatch/main/docker-compose.yml -curl -fsSL https://raw.githubusercontent.com/codextde/dispatch/main/docker-compose.override.example.yml -o docker-compose.override.yml +curl -fsSLO https://raw.githubusercontent.com/LSalami/dispatch/main/docker-compose.yml +curl -fsSL https://raw.githubusercontent.com/LSalami/dispatch/main/docker-compose.override.example.yml -o docker-compose.override.yml # 2. Set your domain (the only setting) echo "DOMAIN=mail.example.com" > .env @@ -229,4 +229,4 @@ docker compose exec app sh # shell in the app | Realtime updates only after refresh | A proxy is buffering Server-Sent Events. Disable buffering (`proxy_buffering off` in nginx). | | `password authentication failed for user "dispatch"` | The secrets volume was recreated while the database was kept. Restart `db` first, which re-applies the password, then `app` and `worker`. | -Still stuck? Open a [discussion](https://github.com/codextde/dispatch/discussions) or a [bug report](https://github.com/codextde/dispatch/issues/new/choose) with the output of `docker compose ps` and the relevant logs (remove personal data). +Still stuck? Open a [discussion](https://github.com/LSalami/dispatch/discussions) or a [bug report](https://github.com/LSalami/dispatch/issues/new/choose) with the output of `docker compose ps` and the relevant logs (remove personal data). diff --git a/docs/upgrading.md b/docs/upgrading.md index 90d32ac..f39d4a4 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -1,6 +1,6 @@ # Upgrading -Dispatch follows [semantic versioning](https://semver.org). Minor and patch releases can be applied directly. Major releases may need manual steps, which are listed below and in the [release notes](https://github.com/codextde/dispatch/releases). +Dispatch follows [semantic versioning](https://semver.org). Minor and patch releases can be applied directly. Major releases may need manual steps, which are listed below and in the [release notes](https://github.com/LSalami/dispatch/releases). Database migrations run automatically when the new version starts. They're idempotent and guarded by a Postgres advisory lock, so the app and worker never run them twice. @@ -39,7 +39,7 @@ Image tags: Also refresh `docker-compose.yml` from the repository when a release note says so: ```bash -curl -fsSLO https://raw.githubusercontent.com/codextde/dispatch/main/docker-compose.yml +curl -fsSLO https://raw.githubusercontent.com/LSalami/dispatch/main/docker-compose.yml ``` ## Coolify diff --git a/drizzle/0003_password_auth.sql b/drizzle/0003_password_auth.sql new file mode 100644 index 0000000..8f6e006 --- /dev/null +++ b/drizzle/0003_password_auth.sql @@ -0,0 +1 @@ +ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "password_hash" text; diff --git a/drizzle/0004_calendar.sql b/drizzle/0004_calendar.sql new file mode 100644 index 0000000..aa05416 --- /dev/null +++ b/drizzle/0004_calendar.sql @@ -0,0 +1,41 @@ +CREATE TABLE "calendar_events" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "org_id" uuid NOT NULL, + "calendar_id" uuid NOT NULL, + "creator_id" uuid, + "conversation_id" uuid, + "ics" text NOT NULL, + "remote_path" text, + "etag" text, + "version" uuid DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "calendars" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "org_id" uuid NOT NULL, + "owner_id" uuid, + "account_id" uuid, + "name" text NOT NULL, + "remote_path" text, + "credential_enc" text, + "synced_at" timestamp with time zone, + "sync_error" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "calendar_events" ADD CONSTRAINT "calendar_events_org_id_organizations_id_fk" FOREIGN KEY ("org_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "calendar_events" ADD CONSTRAINT "calendar_events_calendar_id_calendars_id_fk" FOREIGN KEY ("calendar_id") REFERENCES "public"."calendars"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "calendar_events" ADD CONSTRAINT "calendar_events_creator_id_users_id_fk" FOREIGN KEY ("creator_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "calendar_events" ADD CONSTRAINT "calendar_events_conversation_id_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."conversations"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "calendars" ADD CONSTRAINT "calendars_org_id_organizations_id_fk" FOREIGN KEY ("org_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "calendars" ADD CONSTRAINT "calendars_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "calendars" ADD CONSTRAINT "calendars_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "calendar_events_org_calendar_idx" ON "calendar_events" USING btree ("org_id","calendar_id");--> statement-breakpoint +CREATE UNIQUE INDEX "calendar_events_remote_uidx" ON "calendar_events" USING btree ("calendar_id","remote_path");--> statement-breakpoint +CREATE INDEX "calendars_org_idx" ON "calendars" USING btree ("org_id");--> statement-breakpoint +CREATE UNIQUE INDEX "calendars_remote_uidx" ON "calendars" USING btree ("org_id","account_id","remote_path"); +--> statement-breakpoint +UPDATE "roles" SET "permissions" = "permissions" || ARRAY['calendar.use'] WHERE "key" IN ('owner', 'admin', 'member') AND NOT ('calendar.use' = ANY("permissions")); +--> statement-breakpoint +UPDATE "roles" SET "permissions" = "permissions" || ARRAY['calendar.manage'] WHERE "key" IN ('owner', 'admin') AND NOT ('calendar.manage' = ANY("permissions")); diff --git a/drizzle/0005_calendar_invitations.sql b/drizzle/0005_calendar_invitations.sql new file mode 100644 index 0000000..57691a1 --- /dev/null +++ b/drizzle/0005_calendar_invitations.sql @@ -0,0 +1,5 @@ +ALTER TABLE "calendar_events" ADD COLUMN "invitation_key" text;--> statement-breakpoint +ALTER TABLE "calendar_events" ADD COLUMN "invitation_versions" jsonb;--> statement-breakpoint +ALTER TABLE "calendar_events" ADD COLUMN "organizer_account_id" uuid;--> statement-breakpoint +ALTER TABLE "calendar_events" ADD CONSTRAINT "calendar_events_organizer_account_id_accounts_id_fk" FOREIGN KEY ("organizer_account_id") REFERENCES "public"."accounts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "calendar_events_invitation_idx" ON "calendar_events" USING btree ("org_id","invitation_key"); \ No newline at end of file diff --git a/drizzle/meta/0004_snapshot.json b/drizzle/meta/0004_snapshot.json new file mode 100644 index 0000000..681ec22 --- /dev/null +++ b/drizzle/meta/0004_snapshot.json @@ -0,0 +1,5572 @@ +{ + "id": "6167a035-2d6b-40a5-9e4d-0e9a0f3665cc", + "prevId": "b65f4a57-dfae-4e60-86a4-1456f232cf6d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account_access": { + "name": "account_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "access_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'reply'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_access_account_idx": { + "name": "account_access_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_access_account_id_accounts_id_fk": { + "name": "account_access_account_id_accounts_id_fk", + "tableFrom": "account_access", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "account_access_user_id_users_id_fk": { + "name": "account_access_user_id_users_id_fk", + "tableFrom": "account_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "account_access_team_id_teams_id_fk": { + "name": "account_access_team_id_teams_id_fk", + "tableFrom": "account_access", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "account_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'imap'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aliases": { + "name": "aliases", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#22c55e'" + }, + "credentials_enc": { + "name": "credentials_enc", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "account_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_from_date": { + "name": "sync_from_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "signature_id": { + "name": "signature_id", + "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": { + "accounts_org_idx": { + "name": "accounts_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounts_owner_idx": { + "name": "accounts_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_org_id_organizations_id_fk": { + "name": "accounts_org_id_organizations_id_fk", + "tableFrom": "accounts", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "accounts_owner_user_id_users_id_fk": { + "name": "accounts_owner_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "accounts_team_id_teams_id_fk": { + "name": "accounts_team_id_teams_id_fk", + "tableFrom": "accounts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_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 + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "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": { + "api_keys_hash_idx": { + "name": "api_keys_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_keys_org_idx": { + "name": "api_keys_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_org_id_organizations_id_fk": { + "name": "api_keys_org_id_organizations_id_fk", + "tableFrom": "api_keys", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.attachments": { + "name": "attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'application/octet-stream'" + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_id": { + "name": "content_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_inline": { + "name": "is_inline", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "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": { + "attachments_message_idx": { + "name": "attachments_message_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "attachments_comment_idx": { + "name": "attachments_comment_idx", + "columns": [ + { + "expression": "comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "attachments_org_id_organizations_id_fk": { + "name": "attachments_org_id_organizations_id_fk", + "tableFrom": "attachments", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "attachments_message_id_messages_id_fk": { + "name": "attachments_message_id_messages_id_fk", + "tableFrom": "attachments", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "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_org_idx": { + "name": "audit_logs_org_idx", + "columns": [ + { + "expression": "org_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_idx": { + "name": "audit_logs_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_org_id_organizations_id_fk": { + "name": "audit_logs_org_id_organizations_id_fk", + "tableFrom": "audit_logs", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.calendar_events": { + "name": "calendar_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "calendar_id": { + "name": "calendar_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "creator_id": { + "name": "creator_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ics": { + "name": "ics", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remote_path": { + "name": "remote_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "calendar_events_org_calendar_idx": { + "name": "calendar_events_org_calendar_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "calendar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "calendar_events_remote_uidx": { + "name": "calendar_events_remote_uidx", + "columns": [ + { + "expression": "calendar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "calendar_events_org_id_organizations_id_fk": { + "name": "calendar_events_org_id_organizations_id_fk", + "tableFrom": "calendar_events", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "calendar_events_calendar_id_calendars_id_fk": { + "name": "calendar_events_calendar_id_calendars_id_fk", + "tableFrom": "calendar_events", + "tableTo": "calendars", + "columnsFrom": [ + "calendar_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "calendar_events_creator_id_users_id_fk": { + "name": "calendar_events_creator_id_users_id_fk", + "tableFrom": "calendar_events", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "calendar_events_conversation_id_conversations_id_fk": { + "name": "calendar_events_conversation_id_conversations_id_fk", + "tableFrom": "calendar_events", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.calendars": { + "name": "calendars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remote_path": { + "name": "remote_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_enc": { + "name": "credential_enc", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "calendars_org_idx": { + "name": "calendars_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "calendars_remote_uidx": { + "name": "calendars_remote_uidx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "calendars_org_id_organizations_id_fk": { + "name": "calendars_org_id_organizations_id_fk", + "tableFrom": "calendars", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "calendars_owner_id_users_id_fk": { + "name": "calendars_owner_id_users_id_fk", + "tableFrom": "calendars", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "calendars_account_id_accounts_id_fk": { + "name": "calendars_account_id_accounts_id_fk", + "tableFrom": "calendars", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.canned_responses": { + "name": "canned_responses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shortcut": { + "name": "shortcut", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "canned_responses_org_idx": { + "name": "canned_responses_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "canned_responses_org_id_organizations_id_fk": { + "name": "canned_responses_org_id_organizations_id_fk", + "tableFrom": "canned_responses", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "canned_responses_owner_user_id_users_id_fk": { + "name": "canned_responses_owner_user_id_users_id_fk", + "tableFrom": "canned_responses", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "canned_responses_team_id_teams_id_fk": { + "name": "canned_responses_team_id_teams_id_fk", + "tableFrom": "canned_responses", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mentions": { + "name": "mentions", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::uuid[]" + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "edited_at": { + "name": "edited_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_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": { + "comments_conversation_idx": { + "name": "comments_conversation_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "comments_org_id_organizations_id_fk": { + "name": "comments_org_id_organizations_id_fk", + "tableFrom": "comments", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comments_conversation_id_conversations_id_fk": { + "name": "comments_conversation_id_conversations_id_fk", + "tableFrom": "comments", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comments_author_id_users_id_fk": { + "name": "comments_author_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "comments_parent_id_comments_id_fk": { + "name": "comments_parent_id_comments_id_fk", + "tableFrom": "comments", + "tableTo": "comments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contacts": { + "name": "contacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company": { + "name": "company", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "custom_fields": { + "name": "custom_fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "alternate_emails": { + "name": "alternate_emails", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "last_contacted_at": { + "name": "last_contacted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "message_count": { + "name": "message_count", + "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": { + "contacts_org_idx": { + "name": "contacts_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contacts_org_email_idx": { + "name": "contacts_org_email_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"contacts\".\"owner_user_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "contacts_owner_email_idx": { + "name": "contacts_owner_email_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"contacts\".\"owner_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "contacts_alternate_emails_idx": { + "name": "contacts_alternate_emails_idx", + "columns": [ + { + "expression": "alternate_emails", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "contacts_org_id_organizations_id_fk": { + "name": "contacts_org_id_organizations_id_fk", + "tableFrom": "contacts", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contacts_owner_user_id_users_id_fk": { + "name": "contacts_owner_user_id_users_id_fk", + "tableFrom": "contacts", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversation_assignees": { + "name": "conversation_assignees", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "conv_assignees_user_idx": { + "name": "conv_assignees_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversation_assignees_conversation_id_conversations_id_fk": { + "name": "conversation_assignees_conversation_id_conversations_id_fk", + "tableFrom": "conversation_assignees", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversation_assignees_user_id_users_id_fk": { + "name": "conversation_assignees_user_id_users_id_fk", + "tableFrom": "conversation_assignees", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "conversation_assignees_conversation_id_user_id_pk": { + "name": "conversation_assignees_conversation_id_user_id_pk", + "columns": [ + "conversation_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversation_events": { + "name": "conversation_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "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": { + "conversation_events_conv_idx": { + "name": "conversation_events_conv_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversation_events_org_id_organizations_id_fk": { + "name": "conversation_events_org_id_organizations_id_fk", + "tableFrom": "conversation_events", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversation_events_conversation_id_conversations_id_fk": { + "name": "conversation_events_conversation_id_conversations_id_fk", + "tableFrom": "conversation_events", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversation_labels": { + "name": "conversation_labels", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "added_by": { + "name": "added_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "conv_labels_label_idx": { + "name": "conv_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversation_labels_conversation_id_conversations_id_fk": { + "name": "conversation_labels_conversation_id_conversations_id_fk", + "tableFrom": "conversation_labels", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversation_labels_label_id_labels_id_fk": { + "name": "conversation_labels_label_id_labels_id_fk", + "tableFrom": "conversation_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "conversation_labels_conversation_id_label_id_pk": { + "name": "conversation_labels_conversation_id_label_id_pk", + "columns": [ + "conversation_id", + "label_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversation_user_state": { + "name": "conversation_user_state", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "unread": { + "name": "unread", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "starred": { + "name": "starred", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "following": { + "name": "following", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "muted": { + "name": "muted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "conv_user_state_user_idx": { + "name": "conv_user_state_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversation_user_state_conversation_id_conversations_id_fk": { + "name": "conversation_user_state_conversation_id_conversations_id_fk", + "tableFrom": "conversation_user_state", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversation_user_state_user_id_users_id_fk": { + "name": "conversation_user_state_user_id_users_id_fk", + "tableFrom": "conversation_user_state", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "conversation_user_state_conversation_id_user_id_pk": { + "name": "conversation_user_state_conversation_id_user_id_pk", + "columns": [ + "conversation_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversations": { + "name": "conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "conversation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'email'" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "custom_subject": { + "name": "custom_subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "snippet": { + "name": "snippet", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "conversation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "is_spam": { + "name": "is_spam", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_trash": { + "name": "is_trash", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "snoozed_until": { + "name": "snoozed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "snoozed_by": { + "name": "snoozed_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "participants": { + "name": "participants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "chat_member_ids": { + "name": "chat_member_ids", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::uuid[]" + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "comment_count": { + "name": "comment_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_inbound_at": { + "name": "last_inbound_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_outbound_at": { + "name": "last_outbound_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "first_response_at": { + "name": "first_response_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_by": { + "name": "closed_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_thread_id": { + "name": "provider_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "merged_into_id": { + "name": "merged_into_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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": { + "conversations_org_number_idx": { + "name": "conversations_org_number_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_org_activity_idx": { + "name": "conversations_org_activity_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_activity_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_account_idx": { + "name": "conversations_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_team_idx": { + "name": "conversations_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_snooze_idx": { + "name": "conversations_snooze_idx", + "columns": [ + { + "expression": "snoozed_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_thread_idx": { + "name": "conversations_thread_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_search_idx": { + "name": "conversations_search_idx", + "columns": [ + { + "expression": "to_tsvector('simple', coalesce(\"subject\", '') || ' ' || coalesce(\"snippet\", ''))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "conversations_org_id_organizations_id_fk": { + "name": "conversations_org_id_organizations_id_fk", + "tableFrom": "conversations", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_account_id_accounts_id_fk": { + "name": "conversations_account_id_accounts_id_fk", + "tableFrom": "conversations", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "conversations_team_id_teams_id_fk": { + "name": "conversations_team_id_teams_id_fk", + "tableFrom": "conversations", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_settings": { + "name": "instance_settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitations": { + "name": "invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_ids": { + "name": "team_ids", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::uuid[]" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "invitations_token_idx": { + "name": "invitations_token_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitations_org_idx": { + "name": "invitations_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitations_org_id_organizations_id_fk": { + "name": "invitations_org_id_organizations_id_fk", + "tableFrom": "invitations", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitations_role_id_roles_id_fk": { + "name": "invitations_role_id_roles_id_fk", + "tableFrom": "invitations", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitations_invited_by_users_id_fk": { + "name": "invitations_invited_by_users_id_fk", + "tableFrom": "invitations", + "tableTo": "users", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jobs": { + "name": "jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "run_at": { + "name": "run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "jobs_pending_idx": { + "name": "jobs_pending_idx", + "columns": [ + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.labels": { + "name": "labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#64748b'" + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "label_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'shared'" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_in_sidebar": { + "name": "show_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "labels_org_idx": { + "name": "labels_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "labels_org_id_organizations_id_fk": { + "name": "labels_org_id_organizations_id_fk", + "tableFrom": "labels", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "labels_parent_id_labels_id_fk": { + "name": "labels_parent_id_labels_id_fk", + "tableFrom": "labels", + "tableTo": "labels", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "labels_owner_user_id_users_id_fk": { + "name": "labels_owner_user_id_users_id_fk", + "tableFrom": "labels", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "labels_team_id_teams_id_fk": { + "name": "labels_team_id_teams_id_fk", + "tableFrom": "labels", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.login_tokens": { + "name": "login_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_to": { + "name": "redirect_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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 + } + }, + "indexes": { + "login_tokens_token_idx": { + "name": "login_tokens_token_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "login_tokens_email_idx": { + "name": "login_tokens_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mailbox_sync_state": { + "name": "mailbox_sync_state", + "schema": "", + "columns": { + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "mailbox": { + "name": "mailbox", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uid_validity": { + "name": "uid_validity", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "last_uid": { + "name": "last_uid", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "highest_modseq": { + "name": "highest_modseq", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mailbox_sync_state_account_id_accounts_id_fk": { + "name": "mailbox_sync_state_account_id_accounts_id_fk", + "tableFrom": "mailbox_sync_state", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mailbox_sync_state_account_id_mailbox_pk": { + "name": "mailbox_sync_state_account_id_mailbox_pk", + "columns": [ + "account_id", + "mailbox" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memberships": { + "name": "memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "membership_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "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": { + "memberships_org_user_idx": { + "name": "memberships_org_user_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memberships_user_idx": { + "name": "memberships_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memberships_org_id_organizations_id_fk": { + "name": "memberships_org_id_organizations_id_fk", + "tableFrom": "memberships", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_user_id_users_id_fk": { + "name": "memberships_user_id_users_id_fk", + "tableFrom": "memberships", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_role_id_roles_id_fk": { + "name": "memberships_role_id_roles_id_fk", + "tableFrom": "memberships", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "direction": { + "name": "direction", + "type": "message_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "message_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "references": { + "name": "references", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "to": { + "name": "to", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cc": { + "name": "cc", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "bcc": { + "name": "bcc", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "reply_to": { + "name": "reply_to", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "text_body": { + "name": "text_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "html_body": { + "name": "html_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "snippet": { + "name": "snippet", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_shared_draft": { + "name": "is_shared_draft", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "draft_version": { + "name": "draft_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_edited_by": { + "name": "last_edited_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reply_to_message_id": { + "name": "reply_to_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "send_at": { + "name": "send_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "send_error": { + "name": "send_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "send_attempts": { + "name": "send_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "imap_mailbox": { + "name": "imap_mailbox", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "imap_uid": { + "name": "imap_uid", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "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": { + "messages_conversation_idx": { + "name": "messages_conversation_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_org_msgid_idx": { + "name": "messages_org_msgid_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_account_msgid_idx": { + "name": "messages_account_msgid_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_account_reply_idx": { + "name": "messages_account_reply_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "in_reply_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_status_send_idx": { + "name": "messages_status_send_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "send_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_author_idx": { + "name": "messages_author_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_search_idx": { + "name": "messages_search_idx", + "columns": [ + { + "expression": "to_tsvector('simple', coalesce(\"subject\", '') || ' ' || coalesce(\"text_body\", ''))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "messages_org_id_organizations_id_fk": { + "name": "messages_org_id_organizations_id_fk", + "tableFrom": "messages", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_conversation_id_conversations_id_fk": { + "name": "messages_conversation_id_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_account_id_accounts_id_fk": { + "name": "messages_account_id_accounts_id_fk", + "tableFrom": "messages", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "messages_author_id_users_id_fk": { + "name": "messages_author_id_users_id_fk", + "tableFrom": "messages", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "read_at": { + "name": "read_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": { + "notifications_user_idx": { + "name": "notifications_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_org_id_organizations_id_fk": { + "name": "notifications_org_id_organizations_id_fk", + "tableFrom": "notifications", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_conversation_id_conversations_id_fk": { + "name": "notifications_conversation_id_conversations_id_fk", + "tableFrom": "notifications", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "org_plan", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'self_hosted'" + }, + "subscription_status": { + "name": "subscription_status", + "type": "subscription_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trial_ends_at": { + "name": "trial_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_ends_at": { + "name": "current_period_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "conversation_seq": { + "name": "conversation_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "onboarding_completed_at": { + "name": "onboarding_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suspended_reason": { + "name": "suspended_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "organizations_slug_idx": { + "name": "organizations_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limits": { + "name": "rate_limits", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reset_at": { + "name": "reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reactions": { + "name": "reactions", + "schema": "", + "columns": { + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reactions_comment_id_comments_id_fk": { + "name": "reactions_comment_id_comments_id_fk", + "tableFrom": "reactions", + "tableTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reactions_user_id_users_id_fk": { + "name": "reactions_user_id_users_id_fk", + "tableFrom": "reactions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "reactions_comment_id_user_id_emoji_pk": { + "name": "reactions_comment_id_user_id_emoji_pk", + "columns": [ + "comment_id", + "user_id", + "emoji" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": { + "roles_org_idx": { + "name": "roles_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "roles_org_key_idx": { + "name": "roles_org_key_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "roles_org_id_organizations_id_fk": { + "name": "roles_org_id_organizations_id_fk", + "tableFrom": "roles", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rules": { + "name": "rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'incoming'" + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"match\":\"all\",\"conditions\":[]}'::jsonb" + }, + "actions": { + "name": "actions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "account_ids": { + "name": "account_ids", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::uuid[]" + }, + "stop_processing": { + "name": "stop_processing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "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": { + "rules_org_idx": { + "name": "rules_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rules_org_id_organizations_id_fk": { + "name": "rules_org_id_organizations_id_fk", + "tableFrom": "rules", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rules_owner_user_id_users_id_fk": { + "name": "rules_owner_user_id_users_id_fk", + "tableFrom": "rules", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "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 + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "device_label": { + "name": "device_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonator_id": { + "name": "impersonator_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "sessions_token_idx": { + "name": "sessions_token_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signatures": { + "name": "signatures", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": { + "signatures_org_idx": { + "name": "signatures_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "signatures_org_id_organizations_id_fk": { + "name": "signatures_org_id_organizations_id_fk", + "tableFrom": "signatures", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "signatures_owner_user_id_users_id_fk": { + "name": "signatures_owner_user_id_users_id_fk", + "tableFrom": "signatures", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "signatures_account_id_accounts_id_fk": { + "name": "signatures_account_id_accounts_id_fk", + "tableFrom": "signatures", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'todo'" + }, + "assignee_id": { + "name": "assignee_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "tasks_org_idx": { + "name": "tasks_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_assignee_idx": { + "name": "tasks_assignee_idx", + "columns": [ + { + "expression": "assignee_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tasks_org_id_organizations_id_fk": { + "name": "tasks_org_id_organizations_id_fk", + "tableFrom": "tasks", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_conversation_id_conversations_id_fk": { + "name": "tasks_conversation_id_conversations_id_fk", + "tableFrom": "tasks", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_assignee_id_users_id_fk": { + "name": "tasks_assignee_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "columnsFrom": [ + "assignee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_team_id_teams_id_fk": { + "name": "tasks_team_id_teams_id_fk", + "tableFrom": "tasks", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "is_lead": { + "name": "is_lead", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_members_user_idx": { + "name": "team_members_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_members_team_id_user_id_pk": { + "name": "team_members_team_id_user_id_pk", + "columns": [ + "team_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#22c55e'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignment_strategy": { + "name": "assignment_strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "last_assigned_user_id": { + "name": "last_assigned_user_id", + "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": { + "teams_org_idx": { + "name": "teams_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_org_id_organizations_id_fk": { + "name": "teams_org_id_organizations_id_fk", + "tableFrom": "teams", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_identities": { + "name": "user_identities", + "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 + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_identities_subject_idx": { + "name": "user_identities_subject_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_identities_user_idx": { + "name": "user_identities_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_identities_user_id_users_id_fk": { + "name": "user_identities_user_id_users_id_fk", + "tableFrom": "user_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_super_admin": { + "name": "is_super_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "user_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preferences": { + "name": "preferences", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "away_until": { + "name": "away_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "away_message": { + "name": "away_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_deliveries": { + "name": "webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "webhook_id": { + "name": "webhook_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "webhook_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "webhook_deliveries_pending_idx": { + "name": "webhook_deliveries_pending_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_deliveries_webhook_id_webhooks_id_fk": { + "name": "webhook_deliveries_webhook_id_webhooks_id_fk", + "tableFrom": "webhook_deliveries", + "tableTo": "webhooks", + "columnsFrom": [ + "webhook_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhooks": { + "name": "webhooks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_enc": { + "name": "secret_enc", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "events": { + "name": "events", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_status": { + "name": "last_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_delivered_at": { + "name": "last_delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_count": { + "name": "failure_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "webhooks_org_idx": { + "name": "webhooks_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhooks_org_id_organizations_id_fk": { + "name": "webhooks_org_id_organizations_id_fk", + "tableFrom": "webhooks", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.access_level": { + "name": "access_level", + "schema": "public", + "values": [ + "read", + "reply", + "manage" + ] + }, + "public.account_provider": { + "name": "account_provider", + "schema": "public", + "values": [ + "imap", + "gmail", + "outlook", + "demo" + ] + }, + "public.account_status": { + "name": "account_status", + "schema": "public", + "values": [ + "pending", + "syncing", + "active", + "error", + "paused" + ] + }, + "public.conversation_kind": { + "name": "conversation_kind", + "schema": "public", + "values": [ + "email", + "chat" + ] + }, + "public.conversation_status": { + "name": "conversation_status", + "schema": "public", + "values": [ + "open", + "closed" + ] + }, + "public.label_visibility": { + "name": "label_visibility", + "schema": "public", + "values": [ + "shared", + "private" + ] + }, + "public.membership_status": { + "name": "membership_status", + "schema": "public", + "values": [ + "active", + "suspended" + ] + }, + "public.message_direction": { + "name": "message_direction", + "schema": "public", + "values": [ + "inbound", + "outbound" + ] + }, + "public.message_status": { + "name": "message_status", + "schema": "public", + "values": [ + "draft", + "scheduled", + "queued", + "sending", + "sent", + "failed", + "received" + ] + }, + "public.org_plan": { + "name": "org_plan", + "schema": "public", + "values": [ + "self_hosted", + "free", + "cloud", + "comped" + ] + }, + "public.subscription_status": { + "name": "subscription_status", + "schema": "public", + "values": [ + "none", + "trialing", + "active", + "past_due", + "canceled", + "unpaid", + "incomplete" + ] + }, + "public.task_status": { + "name": "task_status", + "schema": "public", + "values": [ + "todo", + "in_progress", + "done" + ] + }, + "public.user_status": { + "name": "user_status", + "schema": "public", + "values": [ + "active", + "disabled" + ] + }, + "public.webhook_delivery_status": { + "name": "webhook_delivery_status", + "schema": "public", + "values": [ + "pending", + "success", + "failed" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0005_snapshot.json b/drizzle/meta/0005_snapshot.json new file mode 100644 index 0000000..87c9475 --- /dev/null +++ b/drizzle/meta/0005_snapshot.json @@ -0,0 +1,5624 @@ +{ + "id": "3f8a7df9-32e6-4675-a8b2-8aa378e59afd", + "prevId": "6167a035-2d6b-40a5-9e4d-0e9a0f3665cc", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account_access": { + "name": "account_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "access_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'reply'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_access_account_idx": { + "name": "account_access_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_access_account_id_accounts_id_fk": { + "name": "account_access_account_id_accounts_id_fk", + "tableFrom": "account_access", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "account_access_user_id_users_id_fk": { + "name": "account_access_user_id_users_id_fk", + "tableFrom": "account_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "account_access_team_id_teams_id_fk": { + "name": "account_access_team_id_teams_id_fk", + "tableFrom": "account_access", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "account_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'imap'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aliases": { + "name": "aliases", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#22c55e'" + }, + "credentials_enc": { + "name": "credentials_enc", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "account_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_from_date": { + "name": "sync_from_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "signature_id": { + "name": "signature_id", + "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": { + "accounts_org_idx": { + "name": "accounts_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounts_owner_idx": { + "name": "accounts_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_org_id_organizations_id_fk": { + "name": "accounts_org_id_organizations_id_fk", + "tableFrom": "accounts", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "accounts_owner_user_id_users_id_fk": { + "name": "accounts_owner_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "accounts_team_id_teams_id_fk": { + "name": "accounts_team_id_teams_id_fk", + "tableFrom": "accounts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_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 + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "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": { + "api_keys_hash_idx": { + "name": "api_keys_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_keys_org_idx": { + "name": "api_keys_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_org_id_organizations_id_fk": { + "name": "api_keys_org_id_organizations_id_fk", + "tableFrom": "api_keys", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.attachments": { + "name": "attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'application/octet-stream'" + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_id": { + "name": "content_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_inline": { + "name": "is_inline", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "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": { + "attachments_message_idx": { + "name": "attachments_message_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "attachments_comment_idx": { + "name": "attachments_comment_idx", + "columns": [ + { + "expression": "comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "attachments_org_id_organizations_id_fk": { + "name": "attachments_org_id_organizations_id_fk", + "tableFrom": "attachments", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "attachments_message_id_messages_id_fk": { + "name": "attachments_message_id_messages_id_fk", + "tableFrom": "attachments", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "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_org_idx": { + "name": "audit_logs_org_idx", + "columns": [ + { + "expression": "org_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_idx": { + "name": "audit_logs_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_org_id_organizations_id_fk": { + "name": "audit_logs_org_id_organizations_id_fk", + "tableFrom": "audit_logs", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.calendar_events": { + "name": "calendar_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "calendar_id": { + "name": "calendar_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "creator_id": { + "name": "creator_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ics": { + "name": "ics", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invitation_key": { + "name": "invitation_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "invitation_versions": { + "name": "invitation_versions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "organizer_account_id": { + "name": "organizer_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "remote_path": { + "name": "remote_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "calendar_events_invitation_idx": { + "name": "calendar_events_invitation_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invitation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "calendar_events_org_calendar_idx": { + "name": "calendar_events_org_calendar_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "calendar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "calendar_events_remote_uidx": { + "name": "calendar_events_remote_uidx", + "columns": [ + { + "expression": "calendar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "calendar_events_org_id_organizations_id_fk": { + "name": "calendar_events_org_id_organizations_id_fk", + "tableFrom": "calendar_events", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "calendar_events_calendar_id_calendars_id_fk": { + "name": "calendar_events_calendar_id_calendars_id_fk", + "tableFrom": "calendar_events", + "tableTo": "calendars", + "columnsFrom": [ + "calendar_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "calendar_events_creator_id_users_id_fk": { + "name": "calendar_events_creator_id_users_id_fk", + "tableFrom": "calendar_events", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "calendar_events_conversation_id_conversations_id_fk": { + "name": "calendar_events_conversation_id_conversations_id_fk", + "tableFrom": "calendar_events", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "calendar_events_organizer_account_id_accounts_id_fk": { + "name": "calendar_events_organizer_account_id_accounts_id_fk", + "tableFrom": "calendar_events", + "tableTo": "accounts", + "columnsFrom": [ + "organizer_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.calendars": { + "name": "calendars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remote_path": { + "name": "remote_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_enc": { + "name": "credential_enc", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "calendars_org_idx": { + "name": "calendars_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "calendars_remote_uidx": { + "name": "calendars_remote_uidx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "calendars_org_id_organizations_id_fk": { + "name": "calendars_org_id_organizations_id_fk", + "tableFrom": "calendars", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "calendars_owner_id_users_id_fk": { + "name": "calendars_owner_id_users_id_fk", + "tableFrom": "calendars", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "calendars_account_id_accounts_id_fk": { + "name": "calendars_account_id_accounts_id_fk", + "tableFrom": "calendars", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.canned_responses": { + "name": "canned_responses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shortcut": { + "name": "shortcut", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "canned_responses_org_idx": { + "name": "canned_responses_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "canned_responses_org_id_organizations_id_fk": { + "name": "canned_responses_org_id_organizations_id_fk", + "tableFrom": "canned_responses", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "canned_responses_owner_user_id_users_id_fk": { + "name": "canned_responses_owner_user_id_users_id_fk", + "tableFrom": "canned_responses", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "canned_responses_team_id_teams_id_fk": { + "name": "canned_responses_team_id_teams_id_fk", + "tableFrom": "canned_responses", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mentions": { + "name": "mentions", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::uuid[]" + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "edited_at": { + "name": "edited_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_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": { + "comments_conversation_idx": { + "name": "comments_conversation_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "comments_org_id_organizations_id_fk": { + "name": "comments_org_id_organizations_id_fk", + "tableFrom": "comments", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comments_conversation_id_conversations_id_fk": { + "name": "comments_conversation_id_conversations_id_fk", + "tableFrom": "comments", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comments_author_id_users_id_fk": { + "name": "comments_author_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "comments_parent_id_comments_id_fk": { + "name": "comments_parent_id_comments_id_fk", + "tableFrom": "comments", + "tableTo": "comments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contacts": { + "name": "contacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company": { + "name": "company", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "custom_fields": { + "name": "custom_fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "alternate_emails": { + "name": "alternate_emails", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "last_contacted_at": { + "name": "last_contacted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "message_count": { + "name": "message_count", + "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": { + "contacts_org_idx": { + "name": "contacts_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contacts_org_email_idx": { + "name": "contacts_org_email_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"contacts\".\"owner_user_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "contacts_owner_email_idx": { + "name": "contacts_owner_email_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"contacts\".\"owner_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "contacts_alternate_emails_idx": { + "name": "contacts_alternate_emails_idx", + "columns": [ + { + "expression": "alternate_emails", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "contacts_org_id_organizations_id_fk": { + "name": "contacts_org_id_organizations_id_fk", + "tableFrom": "contacts", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contacts_owner_user_id_users_id_fk": { + "name": "contacts_owner_user_id_users_id_fk", + "tableFrom": "contacts", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversation_assignees": { + "name": "conversation_assignees", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "conv_assignees_user_idx": { + "name": "conv_assignees_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversation_assignees_conversation_id_conversations_id_fk": { + "name": "conversation_assignees_conversation_id_conversations_id_fk", + "tableFrom": "conversation_assignees", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversation_assignees_user_id_users_id_fk": { + "name": "conversation_assignees_user_id_users_id_fk", + "tableFrom": "conversation_assignees", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "conversation_assignees_conversation_id_user_id_pk": { + "name": "conversation_assignees_conversation_id_user_id_pk", + "columns": [ + "conversation_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversation_events": { + "name": "conversation_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "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": { + "conversation_events_conv_idx": { + "name": "conversation_events_conv_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversation_events_org_id_organizations_id_fk": { + "name": "conversation_events_org_id_organizations_id_fk", + "tableFrom": "conversation_events", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversation_events_conversation_id_conversations_id_fk": { + "name": "conversation_events_conversation_id_conversations_id_fk", + "tableFrom": "conversation_events", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversation_labels": { + "name": "conversation_labels", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "added_by": { + "name": "added_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "conv_labels_label_idx": { + "name": "conv_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversation_labels_conversation_id_conversations_id_fk": { + "name": "conversation_labels_conversation_id_conversations_id_fk", + "tableFrom": "conversation_labels", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversation_labels_label_id_labels_id_fk": { + "name": "conversation_labels_label_id_labels_id_fk", + "tableFrom": "conversation_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "conversation_labels_conversation_id_label_id_pk": { + "name": "conversation_labels_conversation_id_label_id_pk", + "columns": [ + "conversation_id", + "label_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversation_user_state": { + "name": "conversation_user_state", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "unread": { + "name": "unread", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "starred": { + "name": "starred", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "following": { + "name": "following", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "muted": { + "name": "muted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "conv_user_state_user_idx": { + "name": "conv_user_state_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversation_user_state_conversation_id_conversations_id_fk": { + "name": "conversation_user_state_conversation_id_conversations_id_fk", + "tableFrom": "conversation_user_state", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversation_user_state_user_id_users_id_fk": { + "name": "conversation_user_state_user_id_users_id_fk", + "tableFrom": "conversation_user_state", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "conversation_user_state_conversation_id_user_id_pk": { + "name": "conversation_user_state_conversation_id_user_id_pk", + "columns": [ + "conversation_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversations": { + "name": "conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "conversation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'email'" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "custom_subject": { + "name": "custom_subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "snippet": { + "name": "snippet", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "conversation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "is_spam": { + "name": "is_spam", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_trash": { + "name": "is_trash", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "snoozed_until": { + "name": "snoozed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "snoozed_by": { + "name": "snoozed_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "participants": { + "name": "participants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "chat_member_ids": { + "name": "chat_member_ids", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::uuid[]" + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "comment_count": { + "name": "comment_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_inbound_at": { + "name": "last_inbound_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_outbound_at": { + "name": "last_outbound_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "first_response_at": { + "name": "first_response_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_by": { + "name": "closed_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_thread_id": { + "name": "provider_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "merged_into_id": { + "name": "merged_into_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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": { + "conversations_org_number_idx": { + "name": "conversations_org_number_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_org_activity_idx": { + "name": "conversations_org_activity_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_activity_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_account_idx": { + "name": "conversations_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_team_idx": { + "name": "conversations_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_snooze_idx": { + "name": "conversations_snooze_idx", + "columns": [ + { + "expression": "snoozed_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_thread_idx": { + "name": "conversations_thread_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_search_idx": { + "name": "conversations_search_idx", + "columns": [ + { + "expression": "to_tsvector('simple', coalesce(\"subject\", '') || ' ' || coalesce(\"snippet\", ''))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "conversations_org_id_organizations_id_fk": { + "name": "conversations_org_id_organizations_id_fk", + "tableFrom": "conversations", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_account_id_accounts_id_fk": { + "name": "conversations_account_id_accounts_id_fk", + "tableFrom": "conversations", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "conversations_team_id_teams_id_fk": { + "name": "conversations_team_id_teams_id_fk", + "tableFrom": "conversations", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_settings": { + "name": "instance_settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitations": { + "name": "invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_ids": { + "name": "team_ids", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::uuid[]" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "invitations_token_idx": { + "name": "invitations_token_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitations_org_idx": { + "name": "invitations_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitations_org_id_organizations_id_fk": { + "name": "invitations_org_id_organizations_id_fk", + "tableFrom": "invitations", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitations_role_id_roles_id_fk": { + "name": "invitations_role_id_roles_id_fk", + "tableFrom": "invitations", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitations_invited_by_users_id_fk": { + "name": "invitations_invited_by_users_id_fk", + "tableFrom": "invitations", + "tableTo": "users", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jobs": { + "name": "jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "run_at": { + "name": "run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "jobs_pending_idx": { + "name": "jobs_pending_idx", + "columns": [ + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.labels": { + "name": "labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#64748b'" + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "label_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'shared'" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_in_sidebar": { + "name": "show_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "labels_org_idx": { + "name": "labels_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "labels_org_id_organizations_id_fk": { + "name": "labels_org_id_organizations_id_fk", + "tableFrom": "labels", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "labels_parent_id_labels_id_fk": { + "name": "labels_parent_id_labels_id_fk", + "tableFrom": "labels", + "tableTo": "labels", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "labels_owner_user_id_users_id_fk": { + "name": "labels_owner_user_id_users_id_fk", + "tableFrom": "labels", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "labels_team_id_teams_id_fk": { + "name": "labels_team_id_teams_id_fk", + "tableFrom": "labels", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.login_tokens": { + "name": "login_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_to": { + "name": "redirect_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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 + } + }, + "indexes": { + "login_tokens_token_idx": { + "name": "login_tokens_token_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "login_tokens_email_idx": { + "name": "login_tokens_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mailbox_sync_state": { + "name": "mailbox_sync_state", + "schema": "", + "columns": { + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "mailbox": { + "name": "mailbox", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uid_validity": { + "name": "uid_validity", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "last_uid": { + "name": "last_uid", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "highest_modseq": { + "name": "highest_modseq", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mailbox_sync_state_account_id_accounts_id_fk": { + "name": "mailbox_sync_state_account_id_accounts_id_fk", + "tableFrom": "mailbox_sync_state", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mailbox_sync_state_account_id_mailbox_pk": { + "name": "mailbox_sync_state_account_id_mailbox_pk", + "columns": [ + "account_id", + "mailbox" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memberships": { + "name": "memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "membership_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "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": { + "memberships_org_user_idx": { + "name": "memberships_org_user_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memberships_user_idx": { + "name": "memberships_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memberships_org_id_organizations_id_fk": { + "name": "memberships_org_id_organizations_id_fk", + "tableFrom": "memberships", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_user_id_users_id_fk": { + "name": "memberships_user_id_users_id_fk", + "tableFrom": "memberships", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_role_id_roles_id_fk": { + "name": "memberships_role_id_roles_id_fk", + "tableFrom": "memberships", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "direction": { + "name": "direction", + "type": "message_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "message_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "references": { + "name": "references", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "to": { + "name": "to", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cc": { + "name": "cc", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "bcc": { + "name": "bcc", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "reply_to": { + "name": "reply_to", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "text_body": { + "name": "text_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "html_body": { + "name": "html_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "snippet": { + "name": "snippet", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_shared_draft": { + "name": "is_shared_draft", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "draft_version": { + "name": "draft_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_edited_by": { + "name": "last_edited_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reply_to_message_id": { + "name": "reply_to_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "send_at": { + "name": "send_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "send_error": { + "name": "send_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "send_attempts": { + "name": "send_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "imap_mailbox": { + "name": "imap_mailbox", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "imap_uid": { + "name": "imap_uid", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "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": { + "messages_conversation_idx": { + "name": "messages_conversation_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_org_msgid_idx": { + "name": "messages_org_msgid_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_account_msgid_idx": { + "name": "messages_account_msgid_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_account_reply_idx": { + "name": "messages_account_reply_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "in_reply_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_status_send_idx": { + "name": "messages_status_send_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "send_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_author_idx": { + "name": "messages_author_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_search_idx": { + "name": "messages_search_idx", + "columns": [ + { + "expression": "to_tsvector('simple', coalesce(\"subject\", '') || ' ' || coalesce(\"text_body\", ''))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "messages_org_id_organizations_id_fk": { + "name": "messages_org_id_organizations_id_fk", + "tableFrom": "messages", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_conversation_id_conversations_id_fk": { + "name": "messages_conversation_id_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_account_id_accounts_id_fk": { + "name": "messages_account_id_accounts_id_fk", + "tableFrom": "messages", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "messages_author_id_users_id_fk": { + "name": "messages_author_id_users_id_fk", + "tableFrom": "messages", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "read_at": { + "name": "read_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": { + "notifications_user_idx": { + "name": "notifications_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_org_id_organizations_id_fk": { + "name": "notifications_org_id_organizations_id_fk", + "tableFrom": "notifications", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_conversation_id_conversations_id_fk": { + "name": "notifications_conversation_id_conversations_id_fk", + "tableFrom": "notifications", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "org_plan", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'self_hosted'" + }, + "subscription_status": { + "name": "subscription_status", + "type": "subscription_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trial_ends_at": { + "name": "trial_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_ends_at": { + "name": "current_period_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "conversation_seq": { + "name": "conversation_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "onboarding_completed_at": { + "name": "onboarding_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suspended_reason": { + "name": "suspended_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "organizations_slug_idx": { + "name": "organizations_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limits": { + "name": "rate_limits", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reset_at": { + "name": "reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reactions": { + "name": "reactions", + "schema": "", + "columns": { + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reactions_comment_id_comments_id_fk": { + "name": "reactions_comment_id_comments_id_fk", + "tableFrom": "reactions", + "tableTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reactions_user_id_users_id_fk": { + "name": "reactions_user_id_users_id_fk", + "tableFrom": "reactions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "reactions_comment_id_user_id_emoji_pk": { + "name": "reactions_comment_id_user_id_emoji_pk", + "columns": [ + "comment_id", + "user_id", + "emoji" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": { + "roles_org_idx": { + "name": "roles_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "roles_org_key_idx": { + "name": "roles_org_key_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "roles_org_id_organizations_id_fk": { + "name": "roles_org_id_organizations_id_fk", + "tableFrom": "roles", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rules": { + "name": "rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'incoming'" + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"match\":\"all\",\"conditions\":[]}'::jsonb" + }, + "actions": { + "name": "actions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "account_ids": { + "name": "account_ids", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::uuid[]" + }, + "stop_processing": { + "name": "stop_processing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "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": { + "rules_org_idx": { + "name": "rules_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rules_org_id_organizations_id_fk": { + "name": "rules_org_id_organizations_id_fk", + "tableFrom": "rules", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rules_owner_user_id_users_id_fk": { + "name": "rules_owner_user_id_users_id_fk", + "tableFrom": "rules", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "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 + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "device_label": { + "name": "device_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonator_id": { + "name": "impersonator_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "sessions_token_idx": { + "name": "sessions_token_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signatures": { + "name": "signatures", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": { + "signatures_org_idx": { + "name": "signatures_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "signatures_org_id_organizations_id_fk": { + "name": "signatures_org_id_organizations_id_fk", + "tableFrom": "signatures", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "signatures_owner_user_id_users_id_fk": { + "name": "signatures_owner_user_id_users_id_fk", + "tableFrom": "signatures", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "signatures_account_id_accounts_id_fk": { + "name": "signatures_account_id_accounts_id_fk", + "tableFrom": "signatures", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'todo'" + }, + "assignee_id": { + "name": "assignee_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "tasks_org_idx": { + "name": "tasks_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_assignee_idx": { + "name": "tasks_assignee_idx", + "columns": [ + { + "expression": "assignee_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tasks_org_id_organizations_id_fk": { + "name": "tasks_org_id_organizations_id_fk", + "tableFrom": "tasks", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_conversation_id_conversations_id_fk": { + "name": "tasks_conversation_id_conversations_id_fk", + "tableFrom": "tasks", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_assignee_id_users_id_fk": { + "name": "tasks_assignee_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "columnsFrom": [ + "assignee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_team_id_teams_id_fk": { + "name": "tasks_team_id_teams_id_fk", + "tableFrom": "tasks", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "is_lead": { + "name": "is_lead", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_members_user_idx": { + "name": "team_members_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_members_team_id_user_id_pk": { + "name": "team_members_team_id_user_id_pk", + "columns": [ + "team_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#22c55e'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignment_strategy": { + "name": "assignment_strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "last_assigned_user_id": { + "name": "last_assigned_user_id", + "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": { + "teams_org_idx": { + "name": "teams_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_org_id_organizations_id_fk": { + "name": "teams_org_id_organizations_id_fk", + "tableFrom": "teams", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_identities": { + "name": "user_identities", + "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 + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_identities_subject_idx": { + "name": "user_identities_subject_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_identities_user_idx": { + "name": "user_identities_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_identities_user_id_users_id_fk": { + "name": "user_identities_user_id_users_id_fk", + "tableFrom": "user_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_super_admin": { + "name": "is_super_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "user_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preferences": { + "name": "preferences", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "away_until": { + "name": "away_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "away_message": { + "name": "away_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_deliveries": { + "name": "webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "webhook_id": { + "name": "webhook_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "webhook_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "webhook_deliveries_pending_idx": { + "name": "webhook_deliveries_pending_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_deliveries_webhook_id_webhooks_id_fk": { + "name": "webhook_deliveries_webhook_id_webhooks_id_fk", + "tableFrom": "webhook_deliveries", + "tableTo": "webhooks", + "columnsFrom": [ + "webhook_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhooks": { + "name": "webhooks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_enc": { + "name": "secret_enc", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "events": { + "name": "events", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_status": { + "name": "last_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_delivered_at": { + "name": "last_delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_count": { + "name": "failure_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "webhooks_org_idx": { + "name": "webhooks_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhooks_org_id_organizations_id_fk": { + "name": "webhooks_org_id_organizations_id_fk", + "tableFrom": "webhooks", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.access_level": { + "name": "access_level", + "schema": "public", + "values": [ + "read", + "reply", + "manage" + ] + }, + "public.account_provider": { + "name": "account_provider", + "schema": "public", + "values": [ + "imap", + "gmail", + "outlook", + "demo" + ] + }, + "public.account_status": { + "name": "account_status", + "schema": "public", + "values": [ + "pending", + "syncing", + "active", + "error", + "paused" + ] + }, + "public.conversation_kind": { + "name": "conversation_kind", + "schema": "public", + "values": [ + "email", + "chat" + ] + }, + "public.conversation_status": { + "name": "conversation_status", + "schema": "public", + "values": [ + "open", + "closed" + ] + }, + "public.label_visibility": { + "name": "label_visibility", + "schema": "public", + "values": [ + "shared", + "private" + ] + }, + "public.membership_status": { + "name": "membership_status", + "schema": "public", + "values": [ + "active", + "suspended" + ] + }, + "public.message_direction": { + "name": "message_direction", + "schema": "public", + "values": [ + "inbound", + "outbound" + ] + }, + "public.message_status": { + "name": "message_status", + "schema": "public", + "values": [ + "draft", + "scheduled", + "queued", + "sending", + "sent", + "failed", + "received" + ] + }, + "public.org_plan": { + "name": "org_plan", + "schema": "public", + "values": [ + "self_hosted", + "free", + "cloud", + "comped" + ] + }, + "public.subscription_status": { + "name": "subscription_status", + "schema": "public", + "values": [ + "none", + "trialing", + "active", + "past_due", + "canceled", + "unpaid", + "incomplete" + ] + }, + "public.task_status": { + "name": "task_status", + "schema": "public", + "values": [ + "todo", + "in_progress", + "done" + ] + }, + "public.user_status": { + "name": "user_status", + "schema": "public", + "values": [ + "active", + "disabled" + ] + }, + "public.webhook_delivery_status": { + "name": "webhook_delivery_status", + "schema": "public", + "values": [ + "pending", + "success", + "failed" + ] + } + }, + "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 03bec33..321790d 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -22,6 +22,27 @@ "when": 1790540929283, "tag": "0002_role_read_permissions", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1790540929284, + "tag": "0003_password_auth", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1793957398311, + "tag": "0004_calendar", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1793958068383, + "tag": "0005_calendar_invitations", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index 5a9baeb..db8b3bc 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,10 @@ "version": "1.0.0", "description": "The open-source collaborative inbox for teams — shared Gmail & IMAP inboxes, internal comments, assignments and automation.", "license": "AGPL-3.0-only", - "homepage": "https://github.com/codextde/dispatch", + "homepage": "https://github.com/LSalami/dispatch", "repository": { "type": "git", - "url": "https://github.com/codextde/dispatch.git" + "url": "https://github.com/LSalami/dispatch.git" }, "private": true, "scripts": { @@ -30,6 +30,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@js-temporal/polyfill": "^0.5.1", "@tanstack/react-query": "^5.104.0", "@tiptap/core": "3.31.3", "@tiptap/extension-image": "^3.31.3", @@ -47,9 +48,11 @@ "cmdk": "^1.1.1", "date-fns": "^4.4.0", "drizzle-orm": "^0.45.3", + "fast-xml-parser": "^5.12.0", "frimousse": "^0.4.0", "geist": "^1.7.2", "html-to-text": "^10.0.1", + "ical.js": "^2.2.1", "imapflow": "^2.0.8", "input-otp": "^1.5.0", "isomorphic-dompurify": "^4.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e26759e..fde208e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@19.3.0) + '@js-temporal/polyfill': + specifier: ^0.5.1 + version: 0.5.1 '@tanstack/react-query': specifier: ^5.104.0 version: 5.104.0(react@19.3.0) @@ -74,6 +77,9 @@ importers: drizzle-orm: specifier: ^0.45.3 version: 0.45.3(postgres@3.4.9) + fast-xml-parser: + specifier: ^5.12.0 + version: 5.12.0 frimousse: specifier: ^0.4.0 version: 0.4.0(react@19.3.0)(typescript@5.9.3) @@ -83,6 +89,9 @@ importers: html-to-text: specifier: ^10.0.1 version: 10.0.1 + ical.js: + specifier: ^2.2.1 + version: 2.2.1 imapflow: specifier: ^2.0.8 version: 2.0.8 @@ -1237,6 +1246,10 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@js-temporal/polyfill@0.5.1': + resolution: {integrity: sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==} + engines: {node: '>=12'} + '@modelcontextprotocol/sdk@1.30.1': resolution: {integrity: sha512-H2HxLvC3HDNybePJaLdSrU1hhUK5iQw+WvV1b01myFyI7sdVGe1u/IPTE5D9fGCiJDVtgMV/lmFkQXLmQyIFYA==} engines: {node: '>=18'} @@ -1308,6 +1321,9 @@ packages: cpu: [x64] os: [win32] + '@nodable/entities@3.1.0': + resolution: {integrity: sha512-LsS/DjHr+uDM647Gru/cA8+J3a3HfhttwCKLyuoyN7yXTFCxKBtSgMQBvrW9yNPa2/zDRUNuGPaH5QUFoa4arQ==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2768,6 +2784,9 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + anynum@1.0.1: + resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -3641,6 +3660,13 @@ packages: fast-uri@3.1.8: resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} + fast-xml-builder@1.3.1: + resolution: {integrity: sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==} + + fast-xml-parser@5.12.0: + resolution: {integrity: sha512-/pXxRBtwxY9w8z7YbgsvIPPcPvFDsTbIkWwTQJhzNJPokj+TbN6ftmcv4j/pEQDvOCNnKGDzuk5Bo2oogIOiFg==} + hasBin: true + fastq@1.20.3: resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} @@ -3881,6 +3907,9 @@ packages: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} + ical.js@2.2.1: + resolution: {integrity: sha512-yK/UlPbEs316igb/tjRgbFA8ZV75rCsBJp/hWOatpyaPNlgw0dGDmU+FoicOcwX4xXkeXOkYiOmCqNPFpNPkQg==} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -4106,6 +4135,9 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} + is-unsafe@2.0.3: + resolution: {integrity: sha512-ZBllczCCO267vgdFb6Tq3pv+LhHaCiGMdr+ZsuL+OopruAg7S+3ZDs1vD0RPC0uSrkjzbraxaGjtX1YBxjPeFQ==} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -4158,6 +4190,9 @@ packages: resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true + jsbi@4.3.2: + resolution: {integrity: sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==} + jsdom@30.1.1: resolution: {integrity: sha512-FahmoPK5vbPc+jxV1iErMHmAZypCZ942NHF4+qqaWAuvaKKTBZxawnmAtrbGWLU7MtlxfqIP0qw6aSI+aWGtLg==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} @@ -4728,6 +4763,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-expression-matcher@1.6.2: + resolution: {integrity: sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==} + engines: {node: '>=14.0.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -5350,6 +5389,9 @@ packages: '@types/node': optional: true + strnum@2.5.1: + resolution: {integrity: sha512-UM4nTLWzQiDBseVxUGxN0wJzIk0UU89Qi7pKXz9qISo+lhviJikTuikBsK/oFRlpvYHvSdz0TVoLyQ+sEpYwxA==} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -5746,6 +5788,10 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} + xml-naming@0.3.0: + resolution: {integrity: sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==} + engines: {node: '>=16.0.0'} + xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -6707,6 +6753,10 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.6.0 + '@js-temporal/polyfill@0.5.1': + dependencies: + jsbi: 4.3.2 + '@modelcontextprotocol/sdk@1.30.1(zod@3.25.76)': dependencies: '@hono/node-server': 2.1.1(hono@4.13.9) @@ -6769,6 +6819,8 @@ snapshots: '@next/swc-win32-x64-msvc@16.3.6': optional: true + '@nodable/entities@3.1.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -8226,6 +8278,8 @@ snapshots: dependencies: color-convert: 2.0.1 + anynum@1.0.1: {} + argparse@2.0.1: {} aria-hidden@1.2.6: @@ -9252,6 +9306,20 @@ snapshots: fast-uri@3.1.8: {} + fast-xml-builder@1.3.1: + dependencies: + path-expression-matcher: 1.6.2 + xml-naming: 0.3.0 + + fast-xml-parser@5.12.0: + dependencies: + '@nodable/entities': 3.1.0 + fast-xml-builder: 1.3.1 + is-unsafe: 2.0.3 + path-expression-matcher: 1.6.2 + strnum: 2.5.1 + xml-naming: 0.3.0 + fastq@1.20.3: dependencies: reusify: 1.1.0 @@ -9491,6 +9559,8 @@ snapshots: human-signals@8.0.1: {} + ical.js@2.2.1: {} + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -9687,6 +9757,8 @@ snapshots: is-unicode-supported@2.1.0: {} + is-unsafe@2.0.3: {} + is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -9739,6 +9811,8 @@ snapshots: dependencies: argparse: 2.0.1 + jsbi@4.3.2: {} + jsdom@30.1.1: dependencies: '@asamuzakjp/css-color': 7.1.2 @@ -10272,6 +10346,8 @@ snapshots: path-exists@4.0.0: {} + path-expression-matcher@1.6.2: {} + path-key@3.1.1: {} path-key@4.0.0: {} @@ -11047,6 +11123,10 @@ snapshots: optionalDependencies: '@types/node': 24.19.0 + strnum@2.5.1: + dependencies: + anynum: 1.0.1 + styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.3.0): dependencies: client-only: 0.0.1 @@ -11435,6 +11515,8 @@ snapshots: xml-name-validator@5.0.0: {} + xml-naming@0.3.0: {} + xmlchars@2.2.0: {} yallist@3.1.1: {} diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..7e4faac --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Generic deploy for this fork. Builds the image on the target host from the +# pushed tip of a branch, then restarts app + worker via docker compose. +# +# DISPATCH_DEPLOY_HOST=server1 ./scripts/deploy.sh +# +# Optional overrides (defaults in brackets): +# DISPATCH_BRANCH branch to deploy [current branch] +# DISPATCH_SOURCE_DIR clean checkout dir on the host [/opt/dispatch-deploy/dispatch] +# DISPATCH_APP_DIR compose dir on the host [/opt/dispatch] +# DISPATCH_IMAGE_REPO image name [ghcr.io/lsalami/dispatch] +# DISPATCH_IMAGE_TAG_PREFIX tag prefix [per-host "deploy"] +# DISPATCH_HEALTH_URL health endpoint checked after deploy [none] + +REPO_ROOT="$(git rev-parse --show-toplevel)" +BRANCH="${DISPATCH_BRANCH:-$(git -C "$REPO_ROOT" branch --show-current)}" +REPO_URL="$(git -C "$REPO_ROOT" remote get-url origin)" +COMMIT="$(git -C "$REPO_ROOT" rev-parse HEAD)" +readonly DEPLOY_HOST="${DISPATCH_DEPLOY_HOST:?Set DISPATCH_DEPLOY_HOST to the SSH host.}" +readonly SOURCE_DIR="${DISPATCH_SOURCE_DIR:-/opt/dispatch-deploy/dispatch}" +readonly APP_DIR="${DISPATCH_APP_DIR:-/opt/dispatch}" +readonly IMAGE_REPO="${DISPATCH_IMAGE_REPO:-ghcr.io/lsalami/dispatch}" +readonly TAG_PREFIX="${DISPATCH_IMAGE_TAG_PREFIX:-deploy}" +readonly HEALTH_URL="${DISPATCH_HEALTH_URL:-}" + +if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then + printf 'Refusing deploy: commit or stash the working tree before deploying.\n' >&2 + exit 1 +fi + +REMOTE_COMMIT="$(git -C "$REPO_ROOT" ls-remote "$REPO_URL" "refs/heads/$BRANCH" | awk 'NR == 1 { print $1 }')" +if [[ -z "$REMOTE_COMMIT" || "$REMOTE_COMMIT" != "$COMMIT" ]]; then + printf 'Refusing deploy: local commit %s is not the published tip of %s.\n' "$COMMIT" "$BRANCH" >&2 + exit 1 +fi + +printf 'Deploying %s at %s to %s…\n' "$BRANCH" "${COMMIT:0:12}" "$DEPLOY_HOST" + +ssh "$DEPLOY_HOST" bash -s -- \ + "$REPO_URL" "$BRANCH" "$COMMIT" "$SOURCE_DIR" "$APP_DIR" "$IMAGE_REPO" "$TAG_PREFIX" "$HEALTH_URL" <<'REMOTE_SCRIPT' +set -Eeuo pipefail + +repo_url="$1" +branch="$2" +commit="$3" +source_dir="$4" +app_dir="$5" +image_repo="$6" +tag_prefix="$7" +health_url="$8" + +mkdir -p "$(dirname "$source_dir")" +if [[ ! -d "$source_dir/.git" ]]; then + git clone --no-checkout "$repo_url" "$source_dir" +fi + +configured_url="$(git -C "$source_dir" remote get-url origin)" +if [[ "$configured_url" != "$repo_url" ]]; then + printf 'Refusing deploy: %s points to %s, expected %s.\n' "$source_dir" "$configured_url" "$repo_url" >&2 + exit 1 +fi + +if [[ -n "$(git -C "$source_dir" status --porcelain)" ]]; then + printf 'Refusing deploy: dedicated source checkout %s has local changes.\n' "$source_dir" >&2 + exit 1 +fi + +git -C "$source_dir" fetch --prune origin "$branch" +fetched_commit="$(git -C "$source_dir" rev-parse FETCH_HEAD)" +if [[ "$fetched_commit" != "$commit" ]]; then + printf 'Refusing deploy: remote branch moved during deploy (%s != %s).\n' "$fetched_commit" "$commit" >&2 + exit 1 +fi +git -C "$source_dir" checkout --detach "$commit" + +if [[ -n "$(git -C "$source_dir" status --porcelain)" ]]; then + printf 'Refusing deploy: source checkout %s is not clean after checkout.\n' "$source_dir" >&2 + exit 1 +fi + +tag="${tag_prefix}-${commit:0:12}" +previous_image="$(docker inspect --format '{{.Config.Image}}' dispatch-app-1 2>/dev/null || true)" +docker build -t "$image_repo:$tag" "$source_dir" + +if ! (cd "$app_dir" && DISPATCH_VERSION="$tag" docker compose up -d --force-recreate app worker); then + printf 'Deploy failed while starting the new containers.\n' >&2 + if [[ -n "$previous_image" ]]; then + previous_tag="${previous_image##*:}" + (cd "$app_dir" && DISPATCH_VERSION="$previous_tag" docker compose up -d --force-recreate app worker) || true + fi + exit 1 +fi + +if [[ -n "$health_url" ]]; then + if ! curl --fail --silent --show-error --retry 24 --retry-delay 5 --retry-connrefused --retry-all-errors "$health_url"; then + printf '\nHealth check failed; restoring the previous image.\n' >&2 + if [[ -n "$previous_image" ]]; then + previous_tag="${previous_image##*:}" + (cd "$app_dir" && DISPATCH_VERSION="$previous_tag" docker compose up -d --force-recreate app worker) + curl --fail --silent --show-error --retry 12 --retry-delay 5 --retry-connrefused --retry-all-errors "$health_url" || true + fi + exit 1 + fi +fi + +printf '\nDeployed image: %s:%s\n' "$image_repo" "$tag" +docker compose -f "$app_dir/docker-compose.yml" ps --format '{{.Service}} {{.Image}} {{.State}}' +REMOTE_SCRIPT diff --git a/src/app/(legal)/legal/[slug]/page.tsx b/src/app/(legal)/legal/[slug]/page.tsx index 77229d5..218db29 100644 --- a/src/app/(legal)/legal/[slug]/page.tsx +++ b/src/app/(legal)/legal/[slug]/page.tsx @@ -6,11 +6,12 @@ import sanitizeHtml from "sanitize-html" import { FileText } from "lucide-react" import { MonoLabel, Section } from "@/components/marketing/primitives" import { getSettings } from "@/server/settings" +import { pageTitle } from "@/server/locale" const pages = { - imprint: { title: "Imprint", field: "imprint" }, - privacy: { title: "Privacy policy", field: "privacy" }, - terms: { title: "Terms of service", field: "terms" }, + imprint: { title: "Imprint", it: "Note legali", field: "imprint" }, + privacy: { title: "Privacy policy", it: "Informativa sulla privacy", field: "privacy" }, + terms: { title: "Terms of service", it: "Termini di servizio", field: "terms" }, } as const type LegalSlug = keyof typeof pages @@ -29,7 +30,7 @@ export async function generateMetadata({ params }: PageProps<"/legal/[slug]">): const legal = await getSettings("legal") const published = Boolean(legal[pages[slug].field].trim()) return { - title: pages[slug].title, + title: await pageTitle(pages[slug].title, pages[slug].it), alternates: { canonical: `/legal/${slug}` }, // Placeholder pages shouldn't end up in search results. ...(published ? {} : { robots: { index: false } }), diff --git a/src/app/admin/audit/page.tsx b/src/app/admin/audit/page.tsx index a3f3282..d2d3f3c 100644 --- a/src/app/admin/audit/page.tsx +++ b/src/app/admin/audit/page.tsx @@ -1,3 +1,4 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import { ScrollText } from "lucide-react" import { requireSuperAdminPage } from "@/server/authz" @@ -7,7 +8,9 @@ import { Pager } from "@/components/admin/client" import { AuditFilters } from "@/components/admin/audit/audit-filters" import { AuditTable, type AuditRowView } from "@/components/admin/audit/audit-table" -export const metadata: Metadata = { title: "Audit log" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("Audit log", "Registro attività") } +} const PAGE_SIZE = 50 @@ -38,7 +41,8 @@ function one(v: string | string[] | undefined) { } export default async function AdminAuditPage({ searchParams }: PageProps<"/admin/audit">) { - await requireSuperAdminPage() + const { user } = await requireSuperAdminPage() + const it = user.locale === "it" const sp = await searchParams const filters = { orgId: one(sp.org) ?? null, @@ -87,10 +91,10 @@ export default async function AdminAuditPage({ searchParams }: PageProps<"/admin return ( <> @@ -99,19 +103,19 @@ export default async function AdminAuditPage({ searchParams }: PageProps<"/admin
) : ( <>

- {formatNumber(events.total)} {events.total === 1 ? "event" : "events"} - {filtered ? " match" : " recorded"} + {formatNumber(events.total)} {it ? (events.total === 1 ? "evento" : "eventi") : (events.total === 1 ? "event" : "events")} + {filtered ? (it ? " corrispondenti" : " match") : (it ? " registrati" : " recorded")}

diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 645d56b..ac89d74 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,12 +1,17 @@ import type { Metadata } from "next" +import { requestLocale } from "@/server/locale" import { requireSuperAdminPage } from "@/server/authz" import { getSettings } from "@/server/settings" import { resolveHomePath } from "@/server/orgs" import { AdminShell } from "@/components/admin/admin-shell" -export const metadata: Metadata = { - title: { default: "Instance admin", template: "%s · Instance admin" }, - robots: { index: false, follow: false }, +export async function generateMetadata(): Promise { + const it = (await requestLocale()) === "it" + const name = it ? "Amministrazione istanza" : "Instance admin" + return { + title: { default: name, template: `%s · ${name}` }, + robots: { index: false, follow: false }, + } } /** Super admin panel shell. Non super admins get a 404 (the panel's existence isn't revealed). */ diff --git a/src/app/admin/loading.tsx b/src/app/admin/loading.tsx index 21cf516..fc9fb87 100644 --- a/src/app/admin/loading.tsx +++ b/src/app/admin/loading.tsx @@ -1,9 +1,13 @@ +"use client" + import { Skeleton } from "@/components/ui/skeleton" +import { useI18n } from "@/lib/i18n" /** Generic admin skeleton shown while a page's data loads. */ export default function AdminLoading() { + const { locale } = useI18n() return ( -
+
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index f4191f2..5337a46 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,5 +1,6 @@ import Link from "next/link" import type { Metadata } from "next" +import { pageTitle } from "@/server/locale" import { Activity, ArrowRight, Building2, CircleDollarSign, Inbox, MessagesSquare, Send, UserCheck, Users } from "lucide-react" import { requireSuperAdminPage } from "@/server/authz" import { getSettings } from "@/server/settings" @@ -18,11 +19,11 @@ import { SetupChecklist } from "@/components/admin/overview/setup-checklist" import { RecentAuditList, RecentWorkspacesList } from "@/components/admin/overview/recent-lists" // Same segment as the admin layout, so its title template doesn't apply here -export const metadata: Metadata = { - title: { absolute: "Overview · Instance admin" }, +export async function generateMetadata(): Promise { + return { title: { absolute: await pageTitle("Overview · Instance admin", "Panoramica · Amministrazione istanza") } } } -function greeting(timeZone: string) { +function greeting(timeZone: string, locale: string) { let hour = new Date().getUTCHours() try { hour = Number( @@ -35,6 +36,7 @@ function greeting(timeZone: string) { } catch { /* invalid time zone → UTC */ } + if (locale === "it") return hour >= 5 && hour < 12 ? "Buongiorno" : hour < 18 && hour >= 12 ? "Buon pomeriggio" : "Buonasera" if (hour < 5) return "Good evening" if (hour < 12) return "Good morning" if (hour < 18) return "Good afternoon" @@ -43,6 +45,7 @@ function greeting(timeZone: string) { export default async function AdminOverviewPage() { const { user } = await requireSuperAdminPage() + const it = user.locale === "it" const [general, stats, growth, checklist, recentWorkspaces, recentAudit] = await Promise.all([ getSettings("general"), getOverviewStats(), @@ -59,79 +62,79 @@ export default async function AdminOverviewPage() { <> } /> -
+
- + 0 ? "brand" : "default"} icon={CircleDollarSign} @@ -140,21 +143,21 @@ export default async function AdminOverviewPage() {
- + - +
- View all + {it ? "Vedi tutti" : "View all"} } @@ -162,11 +165,11 @@ export default async function AdminOverviewPage() { - Audit log + {it ? "Registro attività" : "Audit log"} } diff --git a/src/app/admin/settings/[section]/page.tsx b/src/app/admin/settings/[section]/page.tsx index ec59a00..644fa64 100644 --- a/src/app/admin/settings/[section]/page.tsx +++ b/src/app/admin/settings/[section]/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next" +import { requestLocale } from "@/server/locale" import path from "node:path" import { notFound } from "next/navigation" import { requireSuperAdminPage } from "@/server/authz" @@ -28,7 +29,13 @@ function isSection(value: string): value is SettingsSectionSlug { export async function generateMetadata({ params }: PageProps<"/admin/settings/[section]">): Promise { const { section } = await params const meta = SETTINGS_SECTIONS.find((s) => s.slug === section) - return { title: meta ? `${meta.label} · Settings` : "Settings" } + const it = (await requestLocale()) === "it" + const labels: Partial> = { + general: "Generale", authentication: "Accesso", email: "Email", oauth: "App OAuth", billing: "Fatturazione", + storage: "Archiviazione", ai: "IA", branding: "Marchio", security: "Sicurezza", legal: "Note legali", + } + const label = meta ? (it ? labels[meta.slug] ?? meta.label : meta.label) : it ? "Impostazioni" : "Settings" + return { title: `${label} · ${it ? "Impostazioni" : "Settings"}` } } export default async function AdminSettingsSectionPage({ params }: PageProps<"/admin/settings/[section]">) { @@ -36,10 +43,23 @@ export default async function AdminSettingsSectionPage({ params }: PageProps<"/a const { section } = await params if (!isSection(section)) notFound() + const it = (await requestLocale()) === "it" const header = SECTION_HEADERS[section] + const italianHeaders: Record = { + general: { title: "Generale", quiet: "impostazioni", description: "Dai un nome all’istanza, scegli a chi è rivolta e comunica con tutti." }, + authentication: { title: "Accesso", quiet: "e sessioni", description: "Chi può creare un account, per quanto restano connessi i dispositivi e quali accessi social sono disponibili." }, + email: { title: "Invio", quiet: "email", description: "Link di accesso, inviti e notifiche vengono inviati tramite il provider configurato qui." }, + oauth: { title: "App", quiet: "OAuth", description: "Registra Dispatch con Google e Microsoft per collegare le caselle e accedere con un clic." }, + billing: { title: "Fatturazione", quiet: "e abbonamenti", description: "Addebita i workspace ospitati tramite Stripe. Le istanze private self-hosted non ne hanno bisogno." }, + storage: { title: "Archiviazione", quiet: "allegati", description: "Conserva i file sul server o in un bucket compatibile con S3." }, + ai: { title: "Funzioni", quiet: "IA", description: "Collega un provider di modelli per offrire assistenza IA nei workspace." }, + branding: { title: "Marchio", quiet: "e identità", description: "Personalizza il prodotto con nome, logo e colore principale." }, + security: { title: "Sicurezza", quiet: "e privacy", description: "Restrizioni di rete, limiti antiabuso e impostazioni predefinite per la privacy." }, + legal: { title: "Pagine", quiet: "legali", description: "Note legali, informativa sulla privacy e termini pubblicati sul sito." }, + } return ( <> - + ) diff --git a/src/app/admin/settings/billing/page.tsx b/src/app/admin/settings/billing/page.tsx index c2c4e12..eca84bb 100644 --- a/src/app/admin/settings/billing/page.tsx +++ b/src/app/admin/settings/billing/page.tsx @@ -1,3 +1,4 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import { requireSuperAdminPage } from "@/server/authz" import { getSettings } from "@/server/settings" @@ -7,7 +8,9 @@ import { AdminPageHeader } from "@/components/admin/ui" import { BillingSettingsForm } from "@/components/admin/settings/billing-form" import { SECTION_HEADERS } from "@/app/admin/settings/sections" -export const metadata: Metadata = { title: "Billing · Settings" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("Billing · Settings", "Fatturazione · Impostazioni") } +} /** Billing lives in its own route (not ./[section]) so only this page loads the Stripe integration. */ export default async function AdminBillingSettingsPage() { diff --git a/src/app/admin/system/page.tsx b/src/app/admin/system/page.tsx index e57fa08..47c7b92 100644 --- a/src/app/admin/system/page.tsx +++ b/src/app/admin/system/page.tsx @@ -1,3 +1,4 @@ +import { pageTitle } from "@/server/locale" import Link from "next/link" import type { Metadata } from "next" import { CheckCircle2, CircleCheck, Cpu, Inbox, ListChecks, XCircle } from "lucide-react" @@ -27,7 +28,9 @@ import { LocalTime } from "@/components/app/local-time" import { FailedJobsList, RecheckButton, RetryAllButton, type FailedJobView } from "@/components/admin/system/system-client" import { cn } from "@/lib/utils" -export const metadata: Metadata = { title: "System" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("System", "Sistema") } +} function formatDuration(totalSeconds: number) { const s = Math.max(0, Math.floor(totalSeconds)) @@ -66,7 +69,8 @@ function JobCounter({ label, value, hint, tone }: { label: string; value: number } export default async function AdminSystemPage() { - await requireSuperAdminPage() + const { user } = await requireSuperAdminPage() + const it = user.locale === "it" const [health, worker, jobs, failedJobs, inboxErrors] = await Promise.all([ getHealthChecks(), getWorkerStatus(), @@ -90,23 +94,23 @@ export default async function AdminSystemPage() { return ( <> - } /> + } />
{healthy ? "All systems operational" : "Attention needed"} + {healthy ? (it ? "Tutti i sistemi operativi" : "All systems operational") : (it ? "È richiesta attenzione" : "Attention needed")} } >
    {health.checks.map((c) => (
  • {c.ok ? ( - + ) : ( - + )}
    {c.label}
    @@ -120,17 +124,17 @@ export default async function AdminSystemPage() { Online ) : worker.reported ? ( - Offline + {it ? "Non connesso" : "Offline"} ) : ( - Not reported + {it ? "Nessun segnale" : "Not reported"} ) } > @@ -139,7 +143,7 @@ export default async function AdminSystemPage() { ) : ( @@ -147,37 +151,37 @@ export default async function AdminSystemPage() { ), }, { - label: "Uptime", + label: it ? "Tempo di attività" : "Uptime", value: worker.online && worker.startedAt && worker.uptimeSeconds !== null ? ( <> - {formatDuration(worker.uptimeSeconds)} · since + {formatDuration(worker.uptimeSeconds)} · {it ? "dall’avvio" : "since"} ) : worker.startedAt ? ( <> - Last started + {it ? "Ultimo avvio" : "Last started"} ) : ( "—" ), }, { - label: "Version", + label: it ? "Versione" : "Version", value: worker.version ?? "—", mono: true, }, { - label: "Inboxes", + label: it ? "Caselle" : "Inboxes", value: worker.activeAccounts === null && worker.errorAccounts === null ? "—" - : `${formatNumber(worker.activeAccounts ?? 0)} syncing · ${formatNumber(worker.errorAccounts ?? 0)} with errors`, + : `${formatNumber(worker.activeAccounts ?? 0)} ${it ? "in sincronizzazione" : "syncing"} · ${formatNumber(worker.errorAccounts ?? 0)} ${it ? "con errori" : "with errors"}`, }, ]} /> {worker.lastError && (
    - Last error + {it ? "Ultimo errore" : "Last error"}
                         {worker.lastError}
                       
    @@ -185,7 +189,7 @@ export default async function AdminSystemPage() { )} {!worker.online && (

    - No heartbeat for more than 90 seconds. Check that the worker container is running:{" "} + {it ? "Nessun segnale da oltre 90 secondi. Verifica che il container worker sia in esecuzione:" : "No heartbeat for more than 90 seconds. Check that the worker container is running:"}{" "} docker compose ps worker @@ -195,15 +199,15 @@ export default async function AdminSystemPage() { ) : ( - Inbox sync and scheduled sending need the worker process. Start it with{" "} + {it ? "La sincronizzazione delle caselle e l’invio programmato richiedono il processo worker. Avvialo con" : "Inbox sync and scheduled sending need the worker process. Start it with"}{" "} docker compose up -d worker {" "} (or pnpm dev:worker{" "} - in development). + {it ? "in sviluppo)." : "in development)."} } className="py-6" @@ -214,48 +218,48 @@ export default async function AdminSystemPage() { 0 ? : undefined} >

    - oldest + {it ? "più vecchia" : "oldest"} ) - : "Queue is clear" + : (it ? "Coda vuota" : "Queue is clear") } /> ${JOB_STUCK_MS / 60_000} min` : "Locked by a worker"} + hint={jobs.stuck ? `${formatNumber(jobs.stuck)} ${it ? "bloccate da oltre" : "locked >"} ${JOB_STUCK_MS / 60_000} min` : (it ? "Bloccate da un worker" : "Locked by a worker")} tone={jobs.stuck ? "warn" : undefined} /> - - + +
    -

    Failed jobs

    +

    {it ? "Attività non riuscite" : "Failed jobs"}

    {jobs.failed > failedJobs.length && ( - Showing the latest {failedJobs.length} of {formatNumber(jobs.failed)} + {it ? `Mostrate le ultime ${failedJobs.length} su ${formatNumber(jobs.failed)}` : `Showing the latest ${failedJobs.length} of ${formatNumber(jobs.failed)}`} )}
    @@ -263,8 +267,7 @@ export default async function AdminSystemPage() { ) : (

    - No failed jobs. Jobs that exhaust their - retry attempts show up here. + {it ? "Nessuna attività non riuscita. Qui compaiono le attività che esauriscono tutti i tentativi." : "No failed jobs. Jobs that exhaust their retry attempts show up here."}

    )}
    @@ -273,21 +276,21 @@ export default async function AdminSystemPage() {

    - Inboxes with sync errors{" "} + {it ? "Caselle con errori di sincronizzazione" : "Inboxes with sync errors"}{" "} {inboxErrors.total > 0 && ( · {formatNumber(inboxErrors.total)} )}

    {inboxErrors.total > inboxErrors.rows.length && ( - Showing the latest {inboxErrors.rows.length} + {it ? `Mostrate le ultime ${inboxErrors.rows.length}` : `Showing the latest ${inboxErrors.rows.length}`} )}
    {inboxErrors.rows.length === 0 ? (
    @@ -305,13 +308,13 @@ export default async function AdminSystemPage() { {PROVIDER_LABEL[a.provider] ?? a.provider}
    -

    {a.lastError ?? "Unknown error"}

    +

    {a.lastError ?? (it ? "Errore sconosciuto" : "Unknown error")}

    {a.orgName} - Last synced + {it ? "Ultima sincronizzazione" : "Last synced"}
  • @@ -320,11 +323,11 @@ export default async function AdminSystemPage() { - Inbox - Workspace - Provider - Last error - Last synced + {it ? "Casella" : "Inbox"} + {it ? "Workspace" : "Workspace"} + {it ? "Fornitore" : "Provider"} + {it ? "Ultimo errore" : "Last error"} + {it ? "Ultima sincronizzazione" : "Last synced"} @@ -347,14 +350,14 @@ export default async function AdminSystemPage() {

    - {a.lastError ?? "Unknown error"} + {a.lastError ?? (it ? "Errore sconosciuto" : "Unknown error")}

    {a.lastSyncedAt ? ( ) : ( - "Never" + it ? "Mai" : "Never" )} @@ -367,8 +370,8 @@ export default async function AdminSystemPage() { } >
    @@ -377,22 +380,22 @@ export default async function AdminSystemPage() { { label: "Dispatch", value: `v${version.version}`, mono: true }, { label: "Node.js", value: version.node, mono: true }, { label: "Next.js", value: version.next, mono: true }, - { label: "Environment", value: version.environment, mono: true }, - { label: "Platform", value: version.platform, mono: true }, + { label: it ? "Ambiente" : "Environment", value: version.environment, mono: true }, + { label: it ? "Piattaforma" : "Platform", value: version.platform, mono: true }, ]} /> diff --git a/src/app/admin/users/[id]/page.tsx b/src/app/admin/users/[id]/page.tsx index 49747f4..2cf3fa5 100644 --- a/src/app/admin/users/[id]/page.tsx +++ b/src/app/admin/users/[id]/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next" +import { pageTitle } from "@/server/locale" import Link from "next/link" import { notFound } from "next/navigation" import { UAParser } from "ua-parser-js" @@ -16,12 +17,15 @@ import { SuperAdminControl, type SessionItem, } from "@/components/admin/users/user-controls" +import { CredentialsControl } from "@/components/admin/users/credentials-control" +import { localizeRoleName } from "@/lib/i18n" export async function generateMetadata({ params }: PageProps<"/admin/users/[id]">): Promise { await requireSuperAdminPage() const { id } = await params const detail = await getUserDetail(id) - return { title: detail ? `${detail.user.name || detail.user.email} · Users` : "User" } + const section = await pageTitle("Users", "Utenti") + return { title: detail ? `${detail.user.name || detail.user.email} · ${section}` : await pageTitle("User", "Utente") } } const IMPERSONATE_ERRORS: Record = { @@ -32,6 +36,7 @@ const IMPERSONATE_ERRORS: Record = { export default async function AdminUserDetailPage({ params, searchParams }: PageProps<"/admin/users/[id]">) { const { user: me, session: mySession } = await requireSuperAdminPage() + const it = me.locale === "it" const { id } = await params const sp = await searchParams const detail = await getUserDetail(id) @@ -39,31 +44,31 @@ export default async function AdminUserDetailPage({ params, searchParams }: Page const { user, memberships, sessions, events, soleOwned, activeSuperAdmins } = detail const isSelf = user.id === me.id - const impersonateError = typeof sp.impersonate_error === "string" ? IMPERSONATE_ERRORS[sp.impersonate_error] : undefined + const impersonateError = typeof sp.impersonate_error === "string" ? (it ? ({ self: "Non puoi accedere come te stesso.", disabled: "Non puoi accedere come un account disattivato. Riattiva prima l’account.", super_admin: "Non puoi accedere come un altro super amministratore." }[sp.impersonate_error] ?? IMPERSONATE_ERRORS[sp.impersonate_error]) : IMPERSONATE_ERRORS[sp.impersonate_error]) : undefined const lastActiveSuperAdmin = user.isSuperAdmin && user.status === "active" && activeSuperAdmins <= 1 const superAdminBlocked = user.isSuperAdmin ? isSelf - ? "You can't remove your own super admin access." + ? (it ? "Non puoi rimuovere il tuo accesso da super amministratore." : "You can't remove your own super admin access.") : lastActiveSuperAdmin - ? "This is the last active super admin." + ? (it ? "Questo è l’ultimo super amministratore attivo." : "This is the last active super admin.") : null : user.status !== "active" - ? "Enable the account first." + ? (it ? "Riattiva prima l’account." : "Enable the account first.") : null const statusBlocked = - user.status === "active" ? (isSelf ? "You can't disable your own account." : lastActiveSuperAdmin ? "This is the last active super admin." : null) : null + user.status === "active" ? (isSelf ? (it ? "Non puoi disattivare il tuo account." : "You can't disable your own account.") : lastActiveSuperAdmin ? (it ? "Questo è l’ultimo super amministratore attivo." : "This is the last active super admin.") : null) : null const deleteBlocked = isSelf - ? "You can't delete your own account." + ? (it ? "Non puoi eliminare il tuo account." : "You can't delete your own account.") : lastActiveSuperAdmin - ? "This is the last super admin and can't be deleted." + ? (it ? "Questo è l’ultimo super amministratore e non può essere eliminato." : "This is the last super admin and can't be deleted.") : soleOwned.length - ? "Transfer ownership of these workspaces first:" + ? (it ? "Trasferisci prima la proprietà di questi workspace:" : "Transfer ownership of these workspaces first:") : null const sessionItems: SessionItem[] = sessions.map((s) => ({ id: s.id, - deviceLabel: s.deviceLabel || "Unknown device", + deviceLabel: s.deviceLabel || (it ? "Dispositivo sconosciuto" : "Unknown device"), ip: s.ip, createdAt: s.createdAt.toISOString(), lastUsedAt: s.lastUsedAt.toISOString(), @@ -77,21 +82,21 @@ export default async function AdminUserDetailPage({ params, searchParams }: Page return ( <> {user.isSuperAdmin && ( - Super admin + {it ? "Super amministratore" : "Super admin"} )} - {user.status === "active" ? "Active" : "Disabled"} + {user.status === "active" ? (it ? "Attivo" : "Active") : (it ? "Disattivato" : "Disabled")} - · joined · last seen{" "} - + · {it ? "iscritto" : "joined"} · {it ? "ultimo accesso" : "last seen"}{" "} + } @@ -100,7 +105,7 @@ export default async function AdminUserDetailPage({ params, searchParams }: Page {canImpersonate && ( - Impersonate + {it ? "Accedi come utente" : "Impersonate"} )} @@ -115,32 +120,36 @@ export default async function AdminUserDetailPage({ params, searchParams }: Page
    - + Not set }, - { label: "Time zone", value: user.timezone || Instance default }, - { label: "Created", value: }, + { label: it ? "Nome" : "Name", value: user.name || {it ? "Non impostato" : "Not set"} }, + { label: it ? "Fuso orario" : "Time zone", value: user.timezone || {it ? "Predefinito istanza" : "Instance default"} }, + { label: it ? "Creato" : "Created", value: }, { - label: "Last seen", + label: it ? "Ultimo accesso" : "Last seen", value: user.lastSeenAt ? ( <> () ) : ( - "Never" + it ? "Mai" : "Never" ), }, - ...(user.awayUntil && user.awayUntil > new Date() ? [{ label: "Away until", value: }] : []), + ...(user.awayUntil && user.awayUntil > new Date() ? [{ label: it ? "Assente fino al" : "Away until", value: }] : []), ]} /> - + + + + + {memberships.length === 0 ? ( - + ) : (
      {memberships.map((m) => ( @@ -158,14 +167,14 @@ export default async function AdminUserDetailPage({ params, searchParams }: Page
      {m.orgName}
      - /{m.orgSlug} · joined + /{m.orgSlug} · {it ? "iscritto" : "joined"}
      - {m.suspendedAt && Suspended} - {m.status !== "active" && Membership suspended} + {m.suspendedAt && {it ? "Sospeso" : "Suspended"}} + {m.status !== "active" && {it ? "Appartenenza sospesa" : "Membership suspended"}} - {m.roleName} + {localizeRoleName(m.roleKey, m.roleName, it ? "it" : "en")}
      @@ -176,20 +185,20 @@ export default async function AdminUserDetailPage({ params, searchParams }: Page {sessions.length === 0 ? ( - + ) : ( )} - + {events.length === 0 ? ( - + ) : (
        {events.map((e) => ( @@ -201,7 +210,7 @@ export default async function AdminUserDetailPage({ params, searchParams }: Page {e.orgName} ) : ( - "Instance" + it ? "Istanza" : "Instance" )} {e.targetType && ` · ${e.targetType}`} {e.ip && ` · ${e.ip}`} @@ -215,13 +224,13 @@ export default async function AdminUserDetailPage({ params, searchParams }: Page
    - +
    {!canImpersonate && !isSelf && (

    - {user.isSuperAdmin ? "Super admins can't be impersonated." : "Disabled accounts can't be impersonated."} + {user.isSuperAdmin ? (it ? "Non è possibile accedere come un super amministratore." : "Super admins can't be impersonated.") : (it ? "Non è possibile accedere come un account disattivato." : "Disabled accounts can't be impersonated.")}

    )}
    @@ -229,7 +238,7 @@ export default async function AdminUserDetailPage({ params, searchParams }: Page
    - Danger zone + {it ? "Zona pericolosa" : "Danger zone"}
    { + if (input.email === undefined && input.password === undefined) throw new ApiError(400, "Provide an email or a password.") + const res = await updateUserCredentials(admin.user.id, input.id, input, admin.session.id) + await admin.audit("admin.user_credentials_updated", { + targetType: "user", + targetId: res.user.id, + metadata: { emailChanged: input.email !== undefined, passwordChanged: input.password !== undefined, revokedSessions: res.revokedSessions }, + }) + return { email: res.user.email, revokedSessions: res.revokedSessions } + } +) + export const deleteUserAction = adminAction(z.object({ id, confirm: z.string().trim() }), async (input, admin) => { const check = await userDeletionBlockers(admin.user.id, input.id) if (!check.target) throw new ApiError(404, "User not found") diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index 533f058..8ec9542 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -1,3 +1,4 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import Link from "next/link" import { ChevronRight, Users } from "lucide-react" @@ -8,7 +9,9 @@ import { AdminPageHeader, DataTable, EmptyState, StatusBadge, Td, Th, formatNumb import { FilterSelect, Pager, SearchParamInput } from "@/components/admin/client" import { LocalTime } from "@/components/app/local-time" -export const metadata: Metadata = { title: "Users" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("Users", "Utenti") } +} function first(v: string | string[] | undefined) { return Array.isArray(v) ? v[0] : v @@ -16,6 +19,7 @@ function first(v: string | string[] | undefined) { export default async function AdminUsersPage({ searchParams }: PageProps<"/admin/users">) { const { user: me } = await requireSuperAdminPage() + const it = me.locale === "it" const sp = await searchParams const q = first(sp.q) ?? "" const filter = first(sp.filter) @@ -26,28 +30,28 @@ export default async function AdminUsersPage({ searchParams }: PageProps<"/admin return ( <>
    - +
    {rows.length === 0 ? (
    - +
    ) : ( <> @@ -62,18 +66,18 @@ export default async function AdminUsersPage({ searchParams }: PageProps<"/admin
    {u.name || u.email} - {u.id === me.id && (you)} + {u.id === me.id && ({it ? "tu" : "you"})}
    {u.name &&
    {u.email}
    }
    {u.isSuperAdmin && ( - Super admin + {it ? "Super amministratore" : "Super admin"} )} - {u.status === "disabled" && Disabled} + {u.status === "disabled" && {it ? "Disattivato" : "Disabled"}} - {u.workspaces} {u.workspaces === 1 ? "workspace" : "workspaces"} · seen + {u.workspaces} {it ? (u.workspaces === 1 ? "workspace" : "workspace") : (u.workspaces === 1 ? "workspace" : "workspaces")} · {it ? "visto" : "seen"}
    @@ -86,13 +90,13 @@ export default async function AdminUsersPage({ searchParams }: PageProps<"/admin - User - Workspaces - Role - Status - Last seen - Sessions - Created + {it ? "Utente" : "User"} + Workspace + {it ? "Ruolo" : "Role"} + {it ? "Stato" : "Status"} + {it ? "Ultimo accesso" : "Last seen"} + {it ? "Sessioni" : "Sessions"} + {it ? "Creato" : "Created"} @@ -107,7 +111,7 @@ export default async function AdminUsersPage({ searchParams }: PageProps<"/admin className="block max-w-[260px] truncate font-medium outline-none after:absolute after:inset-0 focus-visible:underline" > {u.name || u.email} - {u.id === me.id && (you)} + {u.id === me.id && ({it ? "tu" : "you"})} {u.name &&
    {u.email}
    }
    @@ -117,17 +121,17 @@ export default async function AdminUsersPage({ searchParams }: PageProps<"/admin {u.isSuperAdmin ? ( - Super admin + {it ? "Super amministratore" : "Super admin"} ) : ( - User + {it ? "Utente" : "User"} )} - {u.status === "active" ? "Active" : "Disabled"} + {u.status === "active" ? (it ? "Attivo" : "Active") : (it ? "Disattivato" : "Disabled")} - + {formatNumber(u.sessions)} diff --git a/src/app/admin/workspaces/[id]/page.tsx b/src/app/admin/workspaces/[id]/page.tsx index ab2c7f5..7e72097 100644 --- a/src/app/admin/workspaces/[id]/page.tsx +++ b/src/app/admin/workspaces/[id]/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next" +import { pageTitle } from "@/server/locale" import Link from "next/link" import { notFound } from "next/navigation" import { AlertTriangle, ArrowUpRight, Inbox, MessagesSquare, Users } from "lucide-react" @@ -27,6 +28,7 @@ import { subscriptionTone, } from "@/components/admin/workspaces/labels" import { WorkspaceInitial } from "@/components/admin/workspaces/workspace-initial" +import { localizeRoleName } from "@/lib/i18n" import { DeleteWorkspaceControl, SubscriptionControls, @@ -38,11 +40,13 @@ export async function generateMetadata({ params }: PageProps<"/admin/workspaces/ await requireSuperAdminPage() const { id } = await params const detail = await getWorkspaceDetail(id) - return { title: detail ? `${detail.org.name} · Workspaces` : "Workspace" } + const section = await pageTitle("Workspaces", "Workspace") + return { title: detail ? `${detail.org.name} · ${section}` : await pageTitle("Workspace", "Workspace") } } export default async function AdminWorkspaceDetailPage({ params }: PageProps<"/admin/workspaces/[id]">) { const { user: me } = await requireSuperAdminPage() + const it = me.locale === "it" const { id } = await params const detail = await getWorkspaceDetail(id) if (!detail) notFound() @@ -62,14 +66,14 @@ export default async function AdminWorkspaceDetailPage({ params }: PageProps<"/a openAction = ( ) } else if (owner && !owner.isSuperAdmin && owner.userStatus === "active") { openAction = ( - Open as owner + {it ? "Apri come proprietario" : "Open as owner"} ) } @@ -77,22 +81,22 @@ export default async function AdminWorkspaceDetailPage({ params }: PageProps<"/a return ( <> - {planLabel(org.plan)} + {it ? ({ free: "Gratuito", cloud: "Cloud", self_hosted: "Installato in autonomia", comped: "Gratuito" }[org.plan] ?? planLabel(org.plan)) : planLabel(org.plan)} {org.suspendedAt ? ( - Suspended + {it ? "Sospeso" : "Suspended"} ) : ( {subscriptionLabel(org.subscriptionStatus)} )} - · created + · {it ? "creato" : "created"} } @@ -112,57 +116,57 @@ export default async function AdminWorkspaceDetailPage({ params }: PageProps<"/a
    - Suspended — the workspace is read-only. + {it ? "Sospeso" : "Suspended"} — {it ? "il workspace è in sola lettura." : "the workspace is read-only."}
    -
    {org.suspendedReason || "No reason given."}
    +
    {org.suspendedReason || (it ? "Nessun motivo indicato." : "No reason given.")}
    )}
    - + - +
    - + {inboxPath} }, { - label: "Owner", + label: it ? "Proprietario" : "Owner", value: owner ? ( {owner.name ? `${owner.name} · ${owner.email}` : owner.email} ) : ( - No owner + {it ? "Nessun proprietario" : "No owner"} ), }, { - label: "Created", + label: it ? "Creato" : "Created", value: ( <> {creator && ( <> {" "} - by{" "} + {it ? "da" : "by"}{" "} {creator.email} @@ -172,28 +176,28 @@ export default async function AdminWorkspaceDetailPage({ params }: PageProps<"/a ), }, { - label: "Onboarding", + label: it ? "Configurazione iniziale" : "Onboarding", value: org.onboardingCompletedAt ? ( <> - Completed + {it ? "Completato" : "Completed"} ) : ( - "Not completed" + it ? "Non completato" : "Not completed" ), }, { - label: "Last activity", - value: , + label: it ? "Ultima attività" : "Last activity", + value: , }, - { label: "Teams", value: formatNumber(stats.teams) }, - { label: "Time zone", value: org.settings?.timezone || "Instance default" }, + { label: it ? "Team" : "Teams", value: formatNumber(stats.teams) }, + { label: it ? "Fuso orario" : "Time zone", value: org.settings?.timezone || (it ? "Predefinito istanza" : "Instance default") }, ]} /> - + {members.length === 0 ? ( - + ) : (
      {members.map((m) => ( @@ -208,23 +212,23 @@ export default async function AdminWorkspaceDetailPage({ params }: PageProps<"/a {m.name || m.email} {m.isSuperAdmin && ( - Super admin + {it ? "Super amministratore" : "Super admin"} )}
    {m.name ? m.email : null} {m.name ? " · " : null} - seen + {it ? "visto" : "seen"}
    {(m.status !== "active" || m.userStatus !== "active") && ( - {m.userStatus !== "active" ? "Disabled" : "Suspended"} + {m.userStatus !== "active" ? (it ? "Disattivato" : "Disabled") : (it ? "Sospeso" : "Suspended")} )} - {m.roleName} + {localizeRoleName(m.roleKey, m.roleName, it ? "it" : "en")}
    @@ -235,12 +239,12 @@ export default async function AdminWorkspaceDetailPage({ params }: PageProps<"/a {inboxes.length === 0 ? ( - + ) : (
      {inboxes.map((a) => ( @@ -250,17 +254,17 @@ export default async function AdminWorkspaceDetailPage({ params }: PageProps<"/a
      {a.name} - {PROVIDER_LABELS[a.provider] ?? a.provider} · {a.personal ? "Personal" : "Shared"} + {PROVIDER_LABELS[a.provider] ?? a.provider} · {a.personal ? (it ? "Personale" : "Personal") : (it ? "Condivisa" : "Shared")}
      {a.email}
    - {a.status[0]!.toUpperCase() + a.status.slice(1)} + {it ? (({ connected: "Connessa", syncing: "In sincronizzazione", error: "Errore", paused: "In pausa", active: "Attiva", pending: "In attesa" } as Record)[a.status] ?? a.status) : a.status[0]!.toUpperCase() + a.status.slice(1)} - synced + {it ? "sincronizzata" : "synced"}
    @@ -277,7 +281,7 @@ export default async function AdminWorkspaceDetailPage({ params }: PageProps<"/a
- + - + - +
- Danger zone + {it ? "Zona pericolosa" : "Danger zone"}

- Deleting removes all data of {org.name} permanently. - {org.stripeSubscriptionId ? " Cancel the Stripe subscription separately." : ""} + {it ? "L’eliminazione rimuoverà definitivamente tutti i dati di " : "Deleting removes all data of "}{org.name}{it ? " in modo permanente." : " permanently."} + {org.stripeSubscriptionId ? (it ? " Annulla separatamente l’abbonamento Stripe." : " Cancel the Stripe subscription separately.") : ""}

diff --git a/src/app/admin/workspaces/page.tsx b/src/app/admin/workspaces/page.tsx index d1dc2eb..9e8ae63 100644 --- a/src/app/admin/workspaces/page.tsx +++ b/src/app/admin/workspaces/page.tsx @@ -1,3 +1,4 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import Link from "next/link" import { Building2, ChevronRight } from "lucide-react" @@ -16,14 +17,17 @@ import { } from "@/components/admin/workspaces/labels" import { WorkspaceInitial } from "@/components/admin/workspaces/workspace-initial" -export const metadata: Metadata = { title: "Workspaces" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("Workspaces", "Workspace") } +} function first(v: string | string[] | undefined) { return Array.isArray(v) ? v[0] : v } export default async function AdminWorkspacesPage({ searchParams }: PageProps<"/admin/workspaces">) { - await requireSuperAdminPage() + const { user } = await requireSuperAdminPage() + const it = user.locale === "it" const sp = await searchParams const q = first(sp.q) ?? "" const plan = first(sp.plan) @@ -35,22 +39,22 @@ export default async function AdminWorkspacesPage({ searchParams }: PageProps<"/ return ( <>
- +
- ({ value: p.value, label: p.label }))} /> + ({ value: p.value, label: it ? ({ Free: "Gratuito", Cloud: "Cloud", "Self-hosted": "Installato in autonomia", Comped: "Gratuito" }[p.label] ?? p.label) : p.label }))} /> s.value !== "incomplete").map((s) => ({ value: s.value, label: s.label })), - { value: "suspended", label: "Suspended" }, + ...SUBSCRIPTION_STATUS_OPTIONS.filter((s) => s.value !== "incomplete").map((s) => ({ value: s.value, label: it ? (({ Active: "Attivo", Trial: "Prova", Trialing: "In prova", None: "Nessuno", "Past due": "Pagamento in ritardo", Canceled: "Annullato", Unpaid: "Non pagato", "No subscription": "Nessun abbonamento" } as Record)[s.label] ?? s.label) : s.label })), + { value: "suspended", label: it ? "Sospeso" : "Suspended" }, ]} />
@@ -60,9 +64,9 @@ export default async function AdminWorkspacesPage({ searchParams }: PageProps<"/
@@ -86,30 +90,30 @@ export default async function AdminWorkspacesPage({ searchParams }: PageProps<"/
- {planLabel(w.plan)} + {it ? ({ free: "Gratuito", cloud: "Cloud", self_hosted: "Installato in autonomia", comped: "Gratuito" }[w.plan] ?? planLabel(w.plan)) : planLabel(w.plan)} {w.suspendedAt ? ( - Suspended + {it ? "Sospeso" : "Suspended"} ) : ( w.subscriptionStatus !== "none" && ( {subscriptionLabel(w.subscriptionStatus)} ) )} - {w.inboxErrors > 0 && {w.inboxErrors} inbox errors} + {w.inboxErrors > 0 && {w.inboxErrors} {it ? "errori caselle" : "inbox errors"}}
- {formatNumber(w.members)} members + {formatNumber(w.members)} {it ? "membri" : "members"} - {formatNumber(w.inboxes)} inboxes + {formatNumber(w.inboxes)} {it ? "caselle" : "inboxes"} - {formatNumber(w.conversations)} convos + {formatNumber(w.conversations)} {it ? "conversazioni" : "convos"}
- {w.ownerEmail ?? "No owner"} · created + {w.ownerEmail ?? (it ? "Nessun proprietario" : "No owner")} · {it ? "creato" : "created"}
@@ -121,14 +125,14 @@ export default async function AdminWorkspacesPage({ searchParams }: PageProps<"/ Workspace - Owner - Members - Inboxes - Convos - Plan - Subscription - Trial ends - Created + {it ? "Proprietario" : "Owner"} + {it ? "Membri" : "Members"} + {it ? "Caselle" : "Inboxes"} + {it ? "Conversazioni" : "Convos"} + {it ? "Piano" : "Plan"} + {it ? "Abbonamento" : "Subscription"} + {it ? "Fine prova" : "Trial ends"} + {it ? "Creato" : "Created"} @@ -163,12 +167,12 @@ export default async function AdminWorkspacesPage({ searchParams }: PageProps<"/ {formatNumber(w.conversations)} - {planLabel(w.plan)} + {it ? ({ free: "Gratuito", cloud: "Cloud", self_hosted: "Installato in autonomia", comped: "Gratuito" }[w.plan] ?? planLabel(w.plan)) : planLabel(w.plan)} {w.suspendedAt ? ( - Suspended + {it ? "Sospeso" : "Suspended"} ) : ( {subscriptionLabel(w.subscriptionStatus)} )} diff --git a/src/app/api/w/[slug]/attachments/[id]/route.ts b/src/app/api/w/[slug]/attachments/[id]/route.ts index e98e0b7..ae9e22d 100644 --- a/src/app/api/w/[slug]/attachments/[id]/route.ts +++ b/src/app/api/w/[slug]/attachments/[id]/route.ts @@ -1,7 +1,8 @@ import { ApiError, route } from "@/server/api" import { assertUuid, inboxContext } from "@/server/conversations/context" import { INLINE_TYPES, getAuthorizedAttachment } from "@/server/conversations/files" -import { getObject } from "@/server/storage" +import { readAttachmentBuffer } from "@/server/mail/remote-attachments" +import { getObject, REMOTE_ATTACHMENT_PREFIX } from "@/server/storage" /** * GET /api/w/[slug]/attachments/[id][?inline=1] — stream an attachment. @@ -12,7 +13,9 @@ export const GET = route<{ slug: string; id: string }>(async (req, { params }) = const { slug, id } = await params const { ctx, scope } = await inboxContext(req, slug) const att = await getAuthorizedAttachment(ctx, scope, assertUuid(id, "Attachment")) - const body = await getObject(att.storageKey) + const remote = att.storageKey.startsWith(REMOTE_ATTACHMENT_PREFIX) + const remoteContent = remote ? await readAttachmentBuffer(att) : null + const body = remote ? (remoteContent ? Uint8Array.from(remoteContent) : null) : await getObject(att.storageKey) if (!body) throw new ApiError(404, "The file is no longer available", "not_found") const inline = req.nextUrl.searchParams.get("inline") === "1" && INLINE_TYPES.has(att.contentType) diff --git a/src/app/api/w/[slug]/calendar/invitations/[attachmentId]/route.ts b/src/app/api/w/[slug]/calendar/invitations/[attachmentId]/route.ts new file mode 100644 index 0000000..cebc081 --- /dev/null +++ b/src/app/api/w/[slug]/calendar/invitations/[attachmentId]/route.ts @@ -0,0 +1,25 @@ +import { z } from "zod" +import { ApiError, json, parseJson, requireApiOrg, route } from "@/server/api" +import { assertPermission, assertWritable } from "@/server/authz" +import { assertUuid } from "@/server/conversations/context" +import { invitation, responseSchema } from "@/server/calendar/invitations" + +type Params = { slug: string; attachmentId: string } + +export const GET = route(async (req, { params }) => { + const { slug, attachmentId } = await params + const ctx = await requireApiOrg(req, slug) + if (ctx.via !== "session") throw new ApiError(403, "Calendar requires a workspace session") + assertPermission(ctx, "calendar.use") + return json(await invitation(ctx, assertUuid(attachmentId))) +}) + +export const POST = route(async (req, { params }) => { + const { slug, attachmentId } = await params + const ctx = await requireApiOrg(req, slug) + if (ctx.via !== "session") throw new ApiError(403, "Calendar requires a workspace session") + assertPermission(ctx, "calendar.use") + assertWritable(ctx) + const { response } = await parseJson(req, z.object({ response: responseSchema })) + return json(await invitation(ctx, assertUuid(attachmentId), response)) +}) diff --git a/src/app/api/w/[slug]/calendar/route.ts b/src/app/api/w/[slug]/calendar/route.ts new file mode 100644 index 0000000..065dbc6 --- /dev/null +++ b/src/app/api/w/[slug]/calendar/route.ts @@ -0,0 +1,20 @@ +import { ApiError, json, parseJson, parseQuery, requireApiOrg, route } from "@/server/api" +import { assertPermission, assertWritable } from "@/server/authz" +import { calendarAction, calendarQuery, getCalendar, mutateCalendar } from "@/server/calendar/service" + +type Params = { slug: string } + +export const GET = route(async (req, { params }) => { + const ctx = await requireApiOrg(req, (await params).slug) + if (ctx.via !== "session") throw new ApiError(403, "Calendar requires a workspace session") + assertPermission(ctx, "calendar.use") + return json(await getCalendar(ctx, parseQuery(req, calendarQuery))) +}) + +export const POST = route(async (req, { params }) => { + const ctx = await requireApiOrg(req, (await params).slug) + if (ctx.via !== "session") throw new ApiError(403, "Calendar requires a workspace session") + assertPermission(ctx, "calendar.use") + assertWritable(ctx) + return json(await mutateCalendar(ctx, await parseJson(req, calendarAction))) +}) diff --git a/src/app/api/w/[slug]/drafts/[id]/route.ts b/src/app/api/w/[slug]/drafts/[id]/route.ts index 662e249..6bbf288 100644 --- a/src/app/api/w/[slug]/drafts/[id]/route.ts +++ b/src/app/api/w/[slug]/drafts/[id]/route.ts @@ -1,6 +1,10 @@ -import { json, parseJson, route } from "@/server/api" +import { after } from "next/server" +import { appendArchiveDraft } from "@/server/mail/archive" +import { rateLimit } from "@/server/rate-limit" +import { z } from "zod" +import { ApiError, json, parseJson, route } from "@/server/api" import { assertUuid, inboxContext } from "@/server/conversations/context" -import { discardDraft, draftSchema, getDraft, saveDraft } from "@/server/conversations/messages" +import { buildDraftCopy, discardDraft, draftSchema, getDraft, readLinkedDraftEdit, saveDraft } from "@/server/conversations/messages" type P = { slug: string; id: string } @@ -20,7 +24,21 @@ export const PUT = route

(async (req, { params }) => { const { slug, id } = await params const { ctx, scope } = await inboxContext(req, slug, { write: true }) const input = await parseJson(req, draftSchema) - return json(await saveDraft(ctx, scope, assertUuid(id, "Draft"), input)) + const saved = await saveDraft(ctx, scope, assertUuid(id, "Draft"), input) + if (saved.syncToMailServer && saved.authorId === ctx.user.id) { + after(async () => { + try { + const latest = await getDraft(ctx, scope, saved.id) + if (!latest.syncToMailServer || latest.accountId !== saved.accountId) return + const { account, source, copyId } = await buildDraftCopy(ctx, scope, latest.id, latest.version) + await appendArchiveDraft(account, source, copyId, { id: latest.id, version: latest.version }) + } catch (err) { + if (err instanceof ApiError && [404, 409].includes(err.status)) return + console.error("[draft-sync] could not copy saved draft", err) + } + }) + } + return json(saved) }) /** DELETE /api/w/[slug]/drafts/[id] → 204 (discard) */ @@ -30,3 +48,23 @@ export const DELETE = route

(async (req, { params }) => { await discardDraft(ctx, scope, assertUuid(id, "Draft")) return new Response(null, { status: 204 }) }) + +export const POST = route

(async (req, { params }) => { + const { slug, id } = await params + const { ctx, scope } = await inboxContext(req, slug, { write: true }) + const { version } = await parseJson(req, z.object({ version: z.number().int().min(1) })) + const limit = await rateLimit(`draft-copy:${ctx.org.id}:${ctx.user.id}`, 8, 60) + if (!limit.ok) throw new ApiError(429, "Too many draft copies. Try again shortly.", "rate_limited") + const { account, source, copyId } = await buildDraftCopy(ctx, scope, assertUuid(id, "Draft"), version) + await appendArchiveDraft(account, source, copyId, { id: assertUuid(id, "Draft"), version }) + return new Response(null, { status: 204 }) +}) + +export const PATCH = route

(async (req, { params }) => { + const { slug, id } = await params + const { ctx, scope } = await inboxContext(req, slug, { write: true }) + const limit = await rateLimit(`draft-pull:${ctx.org.id}:${ctx.user.id}`, 15, 60) + if (!limit.ok) throw new ApiError(429, "Too many draft checks. Try again shortly.", "rate_limited") + const imported = await readLinkedDraftEdit(ctx, scope, assertUuid(id, "Draft")) + return json({ draftId: imported?.id ?? null }, { headers: { "Cache-Control": "private, no-store" } }) +}) diff --git a/src/app/api/w/[slug]/mail-archive/route.ts b/src/app/api/w/[slug]/mail-archive/route.ts new file mode 100644 index 0000000..8b13a1f --- /dev/null +++ b/src/app/api/w/[slug]/mail-archive/route.ts @@ -0,0 +1,205 @@ +import { and, eq, gt, inArray, isNotNull, or, sql } from "drizzle-orm" +import { z } from "zod" +import { ApiError, json, parseJson, parseQuery, route } from "@/server/api" +import { audit } from "@/server/audit" +import { compactMessageAttachments, compactMessageHtml, importRemoteDraft } from "@/server/conversations/messages" +import { markRead } from "@/server/conversations/mutations" +import { inboxContext } from "@/server/conversations/context" +import { atLeast, loadOneAccessible, visibleWhere, type Ctx, type InboxScope } from "@/server/conversations/scope" +import { db, schema } from "@/server/db" +import { importArchiveMessage, readArchive, readArchiveDraft, updateArchiveMessage } from "@/server/mail/archive" +import { REMOTE_ATTACHMENT_PREFIX } from "@/server/storage" +import { publish } from "@/server/realtime" +import { rateLimit } from "@/server/rate-limit" + +const uid = z.coerce.number().int().min(1).max(4294967295) +const folder = z.string().min(1).max(1024).regex(/^[^\x00-\x1f\x7f]+$/) +const validity = z.string().regex(/^\d{1,20}$/) +const query = z.object({ + account: z.string().uuid(), + folder: folder.optional(), + validity: validity.optional(), + uid: uid.optional(), + before: uid.optional(), + part: z.string().max(64).regex(/^\d+(\.\d+)*$/).optional(), +}).refine((value) => + (!(value.uid || value.before) || Boolean(value.folder && value.validity)) && + (!value.part || Boolean(value.uid)) && !(value.uid && value.before) +) + +export const GET = route<{ slug: string }>(async (req, { params }) => { + const { ctx, scope } = await inboxContext(req, (await params).slug) + const input = parseQuery(req, query) + const account = await getAccount(ctx, scope, input.account) + return readArchive(account, input, req.signal, async (messageId) => { + const m = schema.messages + const c = schema.conversations + const [match] = await db.select({ id: c.id }).from(m) + .innerJoin(c, eq(c.id, m.conversationId)) + .where(and(eq(m.orgId, ctx.org.id), eq(m.accountId, account.id), eq(m.messageId, messageId), + inArray(m.status, ["received", "sent"]), visibleWhere(ctx, scope))) + .limit(1) + return match?.id ?? null + }) +}) + +async function getAccount(ctx: Ctx, scope: InboxScope, id: string) { + if (!scope.access.has(id)) throw new ApiError(404, "Inbox not found", "not_found") + const account = await db.query.accounts.findFirst({ + where: and(eq(schema.accounts.id, id), eq(schema.accounts.orgId, ctx.org.id)), + }) + if (!account || account.provider === "demo") throw new ApiError(404, "Inbox not found", "not_found") + const limit = await rateLimit(`mail-archive:${ctx.org.id}:${ctx.user.id}`, 90, 60) + if (!limit.ok) throw new ApiError(429, "Too many requests. Try again shortly.", "rate_limited") + return account +} + +const target = { account: z.string().uuid(), folder, validity, uid } +const mutation = z.discriminatedUnion("action", [ + z.object({ ...target, action: z.literal("seen"), seen: z.boolean() }), + z.object({ ...target, action: z.literal("move"), destination: folder }), + z.object({ ...target, action: z.literal("trash") }), +]) + +export const PATCH = route<{ slug: string }>(async (req, { params }) => { + const { ctx, scope } = await inboxContext(req, (await params).slug, { write: true }) + const input = await parseJson(req, mutation) + const account = await getAccount(ctx, scope, input.account) + const level = scope.access.get(account.id) + if (!level || !atLeast(level, "reply")) throw new ApiError(403, "This mailbox is read only.", "forbidden") + const canTrash = ctx.permissions.has("conversations.delete") + if (input.action === "trash" && !canTrash) throw new ApiError(403, "You don't have permission to trash messages.", "forbidden") + const result = await updateArchiveMessage(account, input, canTrash) + try { + const m = schema.messages + const c = schema.conversations + if (result.messageId && "seen" in result && result.seen !== undefined) { + const seen = result.seen + const [match] = await db.select({ id: c.id }).from(m) + .innerJoin(c, eq(c.id, m.conversationId)) + .where(and(eq(m.orgId, ctx.org.id), eq(m.accountId, account.id), eq(m.messageId, result.messageId), + inArray(m.status, ["received", "sent"]), seen ? eq(c.messageCount, 1) : undefined, visibleWhere(ctx, scope))) + .limit(1) + if (match) { + await db.transaction((tx) => markRead(ctx.org.id, ctx.user.id, [match.id], seen, tx)) + await publish({ orgId: ctx.org.id, type: "conversation.updated", conversationId: match.id, userIds: [ctx.user.id] }) + } + } + if (result.messageId && result.destination) { + const state = schema.mailboxSyncState + const [destinationState] = result.destinationValidity ? await db.select({ validity: state.uidValidity }) + .from(state).where(and(eq(state.accountId, account.id), eq(state.mailbox, result.destination))).limit(1) : [] + const destinationUid = destinationState && String(destinationState.validity) === result.destinationValidity + ? result.destinationUid : null + const destination = result.destination + const messageId = result.messageId + const trashState = result.trashState + const updated = await db.transaction(async (tx) => { + const [message] = await tx.update(m) + .set({ imapMailbox: destination, imapUid: destinationUid }) + .where(and(eq(m.orgId, ctx.org.id), eq(m.accountId, account.id), eq(m.messageId, messageId), + eq(m.imapMailbox, result.source), eq(m.imapUid, input.uid), inArray(m.status, ["received", "sent"]))) + .returning({ conversationId: m.conversationId }) + if (message && trashState !== undefined) { + await tx.update(c).set({ isTrash: trashState }) + .where(and(eq(c.orgId, ctx.org.id), eq(c.accountId, account.id), eq(c.id, message.conversationId), eq(c.messageCount, 1))) + } + return message + }) + if (updated) await publish({ orgId: ctx.org.id, type: "conversation.updated", conversationId: updated.conversationId }) + } + } catch (error) { + // The IMAP operation has already succeeded. Don't suggest retrying it after a local failure. + console.error("[mail-archive] failed to reconcile an IMAP change", error) + } + if (result.destination) { + await audit({ + orgId: ctx.org.id, + actorId: ctx.user.id, + action: input.action === "trash" ? "mail.trashed" : "mail.moved", + targetType: "account", + targetId: account.id, + metadata: { folder: input.folder, uid: input.uid, validity: input.validity, destination: result.destination }, + }) + } + return new Response(null, { status: 204 }) +}) + +export const POST = route<{ slug: string }>(async (req, { params }) => { + const { ctx, scope } = await inboxContext(req, (await params).slug, { write: true }) + const input = await parseJson(req, z.object({ ...target, asDraft: z.boolean().optional() })) + const account = await getAccount(ctx, scope, input.account) + const level = scope.access.get(account.id) + if (!level || !atLeast(level, "reply") || !ctx.permissions.has("conversations.reply")) { + throw new ApiError(403, "You don't have permission to import into this mailbox.", "forbidden") + } + const limit = await rateLimit(`mail-archive-import:${ctx.org.id}:${ctx.user.id}`, 8, 60) + if (!limit.ok) throw new ApiError(429, "Too many imports. Try again shortly.", "rate_limited") + if (input.asDraft) { + const { mail: remote, origin } = await readArchiveDraft(account, input) + const saved = await importRemoteDraft(ctx, scope, account.id, remote, origin) + return json({ draftId: saved.id }, { headers: { "Cache-Control": "private, no-store" } }) + } + const result = await importArchiveMessage(account, input) + if (result.status === "created") { + await publish({ + orgId: ctx.org.id, + type: result.conversationCreated ? "conversation.created" : "conversation.updated", + conversationId: result.conversationId, + data: { accountId: account.id }, + }) + await audit({ + orgId: ctx.org.id, + actorId: ctx.user.id, + action: "mail.imported", + targetType: "conversation", + targetId: result.conversationId, + metadata: { accountId: account.id, folder: input.folder, uid: input.uid, validity: input.validity }, + }) + } + if (!(await loadOneAccessible(ctx, scope, result.conversationId))) { + throw new ApiError(404, "Conversation not found", "not_found") + } + return json({ conversationId: result.conversationId }, { headers: { "Cache-Control": "private, no-store" } }) +}) + +export const PUT = route<{ slug: string }>(async (req, { params }) => { + const { ctx, scope } = await inboxContext(req, (await params).slug, { write: true }) + const input = await parseJson(req, z.object({ account: z.uuid(), after: z.uuid().nullable() })) + if (scope.access.get(input.account) !== "manage") { + throw new ApiError(403, "You can't manage this mailbox.", "forbidden") + } + const account = await getAccount(ctx, scope, input.account) + const m = schema.messages + const c = schema.conversations + const hasHtml = sql`(${m.htmlBody} is not null and nullif(btrim(${m.textBody}), '') is not null)` + const hasAttachments = sql`exists ( + select 1 from attachments a where a.org_id = ${ctx.org.id} + and a.message_id = "messages"."id" and a.storage_key not like ${`${REMOTE_ATTACHMENT_PREFIX}%`} + )` + const [message] = await db.select({ id: m.id, hasHtml, hasAttachments }).from(m) + .innerJoin(c, eq(c.id, m.conversationId)) + .where(and(eq(m.orgId, ctx.org.id), eq(m.accountId, account.id), eq(m.status, "received"), + isNotNull(m.imapMailbox), isNotNull(m.messageId), visibleWhere(ctx, scope), + input.after ? gt(m.id, input.after) : undefined, or(hasHtml, hasAttachments))) + .orderBy(m.id) + .limit(1) + if (!message) return json({ next: null, verified: 0, skipped: 0 }) + + let verified = 0 + let skipped = 0 + const operations = [ + ...(message.hasHtml ? [compactMessageHtml] : []), + ...(message.hasAttachments ? [compactMessageAttachments] : []), + ] + for (const compact of operations) { + try { + await compact(ctx, scope, message.id) + verified++ + } catch (err) { + if (!(err instanceof ApiError) || err.status === 403) throw err + skipped++ + } + } + return json({ next: message.id, verified, skipped }) +}) diff --git a/src/app/api/w/[slug]/messages/[id]/route.ts b/src/app/api/w/[slug]/messages/[id]/route.ts index d6b0d36..72dcf9f 100644 --- a/src/app/api/w/[slug]/messages/[id]/route.ts +++ b/src/app/api/w/[slug]/messages/[id]/route.ts @@ -1,6 +1,7 @@ -import { json, route } from "@/server/api" +import { ApiError, json, route } from "@/server/api" import { assertUuid, inboxContext } from "@/server/conversations/context" -import { getMessageBody } from "@/server/conversations/messages" +import { compactMessageAttachments, compactMessageHtml, getMessageBody } from "@/server/conversations/messages" +import { rateLimit } from "@/server/rate-limit" /** * GET /api/w/[slug]/messages/[id]?images=1 → MessageBody { html, hasRemoteImages, imagesLoaded } @@ -11,5 +12,16 @@ export const GET = route<{ slug: string; id: string }>(async (req, { params }) = const { slug, id } = await params const { ctx, scope } = await inboxContext(req, slug) const body = await getMessageBody(ctx, scope, assertUuid(id, "Message"), req.nextUrl.searchParams.get("images") === "1") - return json(body, { headers: { "Cache-Control": "private, max-age=300" } }) + return json(body, { headers: { "Cache-Control": body.remoteHtmlUnavailable ? "private, no-store" : "private, max-age=300" } }) +}) + +export const POST = route<{ slug: string; id: string }>(async (req, { params }) => { + const { slug, id } = await params + const { ctx, scope } = await inboxContext(req, slug, { write: true }) + const limit = await rateLimit(`mail-release-html:${ctx.org.id}:${ctx.user.id}`, 10, 60) + if (!limit.ok) throw new ApiError(429, "Too many requests. Try again shortly.", "rate_limited") + const messageId = assertUuid(id, "Message") + if (req.nextUrl.searchParams.get("attachments") === "1") await compactMessageAttachments(ctx, scope, messageId) + else await compactMessageHtml(ctx, scope, messageId) + return new Response(null, { status: 204 }) }) diff --git a/src/app/auth/verify/page.tsx b/src/app/auth/verify/page.tsx index 1459af0..b4795ad 100644 --- a/src/app/auth/verify/page.tsx +++ b/src/app/auth/verify/page.tsx @@ -1,3 +1,4 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import Link from "next/link" import { LinkIcon, MailX } from "lucide-react" @@ -8,10 +9,12 @@ import { AuthCard, AuthHeading, CenteredAuthLayout } from "@/components/auth/aut import { VerifyForm } from "@/components/auth/verify-form" import { Button } from "@/components/ui/button" -export const metadata: Metadata = { - title: "Confirm sign-in", - robots: { index: false, follow: false }, - referrer: "no-referrer", +export async function generateMetadata(): Promise { + return { + title: await pageTitle("Confirm sign-in", "Conferma accesso"), + robots: { index: false, follow: false }, + referrer: "no-referrer", + } } /** @@ -28,6 +31,7 @@ export default async function VerifyPage({ searchParams }: PageProps<"/auth/veri getCurrentSession(), ]) const productName = branding.productName || general.instanceName + const it = session?.user.locale === "it" return ( @@ -38,25 +42,23 @@ export default async function VerifyPage({ searchParams }: PageProps<"/auth/veri

- Continue as {peek.email}. This link can only be used - once. + {it ? "Continua come" : "Continue as"} {peek.email}. {it ? "Questo link può essere usato una sola volta." : "This link can only be used once."} } /> {session && session.user.email.toLowerCase() !== peek.email && (

- You're currently signed in as {session.user.email} on this - device. Continuing adds a session for {peek.email}. + {it ? "Hai già effettuato l’accesso come" : "You’re currently signed in as"} {session.user.email} {it ? "su questo dispositivo. Continuando aggiungerai una sessione per" : "on this device. Continuing adds a session for"} {peek.email}.

)}

- Didn't request this? You can close this page — nothing happens until you click continue. + {it ? "Non hai richiesto questo accesso? Puoi chiudere la pagina: non succede nulla finché non fai clic su Continua." : "Didn’t request this? You can close this page — nothing happens until you click continue."}

) : ( @@ -65,13 +67,13 @@ export default async function VerifyPage({ searchParams }: PageProps<"/auth/veri
)} diff --git a/src/app/error.tsx b/src/app/error.tsx index 9bea2d2..9e322bd 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -4,8 +4,11 @@ import { useEffect } from "react" import Link from "next/link" import { LogoMark } from "@/components/brand/logo" import { Button } from "@/components/ui/button" +import { useI18n } from "@/lib/i18n" export default function ErrorPage({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + const { locale } = useI18n() + const it = locale === "it" useEffect(() => { console.error(error) }, [error]) @@ -13,17 +16,17 @@ export default function ErrorPage({ error, reset }: { error: Error & { digest?: return (
-

Something went wrong

+

{it ? "Si è verificato un errore" : "Something went wrong"}

- That didn't work. Please try again. + {it ? "Operazione non riuscita." : "That didn’t work."} {it ? "Riprova." : "Please try again."}

{error.digest &&

Reference: {error.digest}

}
diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index da1a55f..b9abd27 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -1,8 +1,11 @@ "use client" +import { useState } from "react" + export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + const [italian] = useState(() => typeof document !== "undefined" && (document.cookie.split(";").some((cookie) => cookie.trim() === "dispatch-locale=it") || navigator.language.toLowerCase().startsWith("it"))) return ( - +
-

Dispatch hit an unexpected error.

- {error.digest &&

Reference: {error.digest}

} +

{italian ? "Dispatch ha riscontrato un errore imprevisto." : "Dispatch hit an unexpected error."}

+ {error.digest &&

{italian ? "Riferimento" : "Reference"}: {error.digest}

}
diff --git a/src/app/globals.css b/src/app/globals.css index c44c0cf..63e3c49 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -192,6 +192,14 @@ * { @apply border-border outline-ring/50; } + @media (hover: none) and (pointer: coarse) { + input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]), + textarea, + select, + [contenteditable="true"] { + font-size: max(16px, 1em) !important; + } + } html { @apply font-sans; -webkit-tap-highlight-color: transparent; diff --git a/src/app/invite/[token]/actions.ts b/src/app/invite/[token]/actions.ts index 2d11d38..0dfb423 100644 --- a/src/app/invite/[token]/actions.ts +++ b/src/app/invite/[token]/actions.ts @@ -1,7 +1,7 @@ "use server" import { redirect, RedirectType } from "next/navigation" -import { and, eq } from "drizzle-orm" +import { and, eq, sql } from "drizzle-orm" import { z } from "zod" import { db, schema } from "@/server/db" import { action } from "@/server/action" @@ -12,8 +12,11 @@ import { clearSessionCookie } from "@/server/auth/session" import { hashToken } from "@/server/crypto" import { acceptInvitation } from "@/server/orgs" import { getRequestMeta } from "@/server/request" +import { PASSWORD_MIN_LENGTH, passwordHash } from "@/server/auth/password" +import { establishSession } from "@/app/login/complete-login" const tokenSchema = z.object({ token: z.string().min(20).max(200) }) +const passwordInviteSchema = tokenSchema.extend({ password: z.string().min(PASSWORD_MIN_LENGTH, `Use at least ${PASSWORD_MIN_LENGTH} characters`).max(4000) }) async function findInvitation(token: string) { return db.query.invitations.findFirst({ where: eq(schema.invitations.tokenHash, hashToken(token)) }) @@ -47,6 +50,30 @@ export const acceptInviteAction = action(tokenSchema, async ({ token }) => { redirect(`/w/${org.slug}/inbox`, RedirectType.replace) }) +/** Create/set a local password directly from a valid invitation, then join and sign in. */ +export const setInvitePasswordAction = action(passwordInviteSchema, async ({ token, password }) => { + const inv = await findInvitation(token) + if (!inv || inv.revokedAt) throw new ApiError(404, "This invitation is no longer valid.") + if (inv.acceptedAt) throw new ApiError(410, "This invitation has already been used.") + if (inv.expiresAt < new Date()) throw new ApiError(410, "This invitation has expired. Ask for a new one.") + const email = inv.email.toLowerCase() + let user = await db.query.users.findFirst({ where: sql`lower(${schema.users.email}) = ${email}` }) + if (!user) { + const [created] = await db.insert(schema.users).values({ email, name: null, passwordHash: passwordHash(password) }).returning() + user = created + } else { + const [updated] = await db.update(schema.users).set({ passwordHash: passwordHash(password), status: "active" }).where(eq(schema.users.id, user.id)).returning() + user = updated + } + if (!user) throw new ApiError(500, "Could not create the account.") + await acceptInvitation(inv, user.id) + const org = await db.query.organizations.findFirst({ where: eq(schema.organizations.id, inv.orgId) }) + if (!org) throw new ApiError(404, "This workspace no longer exists.") + await db.update(schema.users).set({ preferences: { ...user.preferences, lastOrgSlug: org.slug } }).where(eq(schema.users.id, user.id)) + const redirectTo = await establishSession({ userId: user.id, email: user.email, method: "password", redirectTo: `/w/${org.slug}/inbox` }) + redirect(redirectTo, RedirectType.replace) +}) + /** Signed in with a different account: sign out this device and continue with the invited email. */ export const switchAccountAction = action(tokenSchema, async ({ token }) => { const s = await requireUser() diff --git a/src/app/invite/[token]/page.tsx b/src/app/invite/[token]/page.tsx index cfe8bd6..9669589 100644 --- a/src/app/invite/[token]/page.tsx +++ b/src/app/invite/[token]/page.tsx @@ -1,28 +1,37 @@ +import { pageTitle } from "@/server/locale" /* eslint-disable @next/next/no-img-element -- workspace logos are user-provided URLs/uploads, not optimizable assets */ import type { Metadata } from "next" import Link from "next/link" import { redirect } from "next/navigation" +import { cookies, headers } from "next/headers" import { and, eq } from "drizzle-orm" import { CalendarX2, MailX, ShieldOff } from "lucide-react" import { db, schema } from "@/server/db" import { hashToken } from "@/server/crypto" import { getSettings } from "@/server/settings" import { getCurrentSession } from "@/server/auth/session" -import { AuthCard, AuthHeading, CenteredAuthLayout, ImpersonationBanner } from "@/components/auth/auth-layout" +import { AuthCard, AuthHeading, CenteredAuthLayout } from "@/components/auth/auth-layout" +import { ImpersonationBanner } from "@/components/auth/impersonation-banner" import { InviteActions } from "@/components/auth/invite-actions" import { UserAvatar } from "@/components/app/user-avatar" import { Button } from "@/components/ui/button" import { LocalTime } from "@/components/app/local-time" +import { InvitePasswordForm } from "@/components/auth/invite-password-form" -export const metadata: Metadata = { - title: "Invitation", - robots: { index: false, follow: false }, - referrer: "no-referrer", +export async function generateMetadata(): Promise { + return { + title: await pageTitle("Invitation", "Invito"), + robots: { index: false, follow: false }, + referrer: "no-referrer", + } } export default async function InvitePage({ params }: PageProps<"/invite/[token]">) { const { token } = await params const [general, branding, session] = await Promise.all([getSettings("general"), getSettings("branding"), getCurrentSession()]) + const requestHeaders = await headers() + const preferredLocale = (await cookies()).get("dispatch-locale")?.value + const italian = session?.user.locale === "it" || (!session && (preferredLocale === "it" || (!preferredLocale && requestHeaders.get("accept-language")?.toLowerCase().startsWith("it")))) const productName = branding.productName || general.instanceName const rows = @@ -56,25 +65,72 @@ export default async function InvitePage({ params }: PageProps<"/invite/[token]" const banner = session?.session.impersonatorId ? : undefined const homeHref = "/login" + const copy = italian + ? { + invitation: "Invito", + notFound: "Invito non trovato", + invalidLink: "Questo link di invito non è valido. Controlla di aver copiato il link completo oppure chiedi un nuovo invito.", + withdrawn: "Questo invito è stato ritirato", + revoked: (workspace: string) => `Un amministratore di ${workspace} lo ha revocato. Chiedi di essere invitato di nuovo.`, + alreadyUsed: "Questo invito è già stato utilizzato", + signInToOpen: (email: string, workspace: string) => `Accedi con ${email} per aprire ${workspace}.`, + expired: "Questo invito è scaduto", + expiredText: (workspace: string) => `Gli inviti sono validi per 14 giorni. Chiedi a un amministratore di ${workspace} di inviarne uno nuovo.`, + goWorkspace: "Vai al tuo workspace", + signIn: "Accedi", + join: "Unisciti a", + on: "su", + invited: "ha invitato", + collaborate: "a collaborare: caselle condivise, commenti interni e assegnazioni, tutto in un unico posto.", + existingPassword: "Hai già una password?", + signInInstead: "Accedi invece", + signedAs: "Hai effettuato l'accesso come", + butInvitation: "ma questo invito è per", + switchAccount: "Esci da questo dispositivo e continua con l'indirizzo invitato.", + validUntil: "Invito valido fino al", + } + : { + invitation: "Invitation", + notFound: "Invitation not found", + invalidLink: "This invitation link is invalid. Check that you copied the whole link, or ask for a new invitation.", + withdrawn: "This invitation was withdrawn", + revoked: (workspace: string) => `An admin of ${workspace} revoked it. Ask them to invite you again.`, + alreadyUsed: "This invitation was already used", + signInToOpen: (email: string, workspace: string) => `Sign in with ${email} to open ${workspace}.`, + expired: "This invitation has expired", + expiredText: (workspace: string) => `Invitations are valid for 14 days. Ask an admin of ${workspace} to send a new one.`, + goWorkspace: "Go to your workspace", + signIn: "Sign in", + join: "Join", + on: "on", + invited: "invited", + collaborate: "to collaborate: shared inboxes, internal comments and assignments — all in one place.", + existingPassword: "Already have a password?", + signInInstead: "Sign in instead", + signedAs: "You're signed in as", + butInvitation: "but this invitation is for", + switchAccount: "Sign out on this device and continue with the invited address.", + validUntil: "Invitation valid until", + } if (!row || row.inv.revokedAt || row.inv.acceptedAt || row.inv.expiresAt < new Date()) { const state = !row ? "missing" : row.inv.revokedAt ? "revoked" : row.inv.acceptedAt ? "accepted" : "expired" - const copy = { - missing: { icon: MailX, title: "Invitation not found", quiet: "", text: "This invitation link is invalid. Check that you copied the whole link, or ask for a new invitation." }, - revoked: { icon: ShieldOff, title: "This invitation was withdrawn", quiet: "", text: `An admin of ${row?.org.name ?? "the workspace"} revoked it. Ask them to invite you again.` }, - accepted: { icon: MailX, title: "This invitation was already used", quiet: "", text: `Sign in with ${row?.inv.email ?? "the invited address"} to open ${row?.org.name ?? "the workspace"}.` }, - expired: { icon: CalendarX2, title: "This invitation has expired", quiet: "", text: `Invitations are valid for 14 days. Ask an admin of ${row?.org.name ?? "the workspace"} to send a new one.` }, + const stateCopy = { + missing: { icon: MailX, title: copy.notFound, quiet: "", text: copy.invalidLink }, + revoked: { icon: ShieldOff, title: copy.withdrawn, quiet: "", text: copy.revoked(row?.org.name ?? (italian ? "workspace" : "the workspace")) }, + accepted: { icon: MailX, title: copy.alreadyUsed, quiet: "", text: copy.signInToOpen(row?.inv.email ?? (italian ? "l'indirizzo invitato" : "the invited address"), row?.org.name ?? (italian ? "il workspace" : "the workspace")) }, + expired: { icon: CalendarX2, title: copy.expired, quiet: "", text: copy.expiredText(row?.org.name ?? (italian ? "il workspace" : "the workspace")) }, }[state] return (
- +
- +
@@ -84,6 +140,7 @@ export default async function InvitePage({ params }: PageProps<"/invite/[token]" const { inv, org, role, inviter } = row const inviterName = inviter?.name || inviter?.email || "A teammate" + const translatedRole = italian ? ({ owner: "Proprietario", admin: "Amministratore", member: "Membro", guest: "Ospite" }[role.key ?? ""] ?? role.name) : role.name const signedInAs = session?.user.email.toLowerCase() const matches = signedInAs === inv.email.toLowerCase() @@ -101,30 +158,25 @@ export default async function InvitePage({ params }: PageProps<"/invite/[token]" )}
- {inviterName} invited{" "} + {inviterName} {copy.invited}{" "} {inv.email} to collaborate as{" "} - {role.name} - - . Shared inboxes, internal comments and assignments — all in one place. + {translatedRole} + {" "}{copy.collaborate} } /> {!session ? ( <> - +

- No password needed — we'll email a sign-in code to {inv.email}. + {copy.existingPassword}{" "}{copy.signInInstead}.

) : matches ? ( @@ -132,13 +184,13 @@ export default async function InvitePage({ params }: PageProps<"/invite/[token]" ) : ( <>
- You're signed in as {session.user.email}, but this invitation is for{" "} - {inv.email}. Sign out on this device and continue with the invited address. + {copy.signedAs} {session.user.email}, {copy.butInvitation}{" "} + {inv.email}. {copy.switchAccount}
)} -

Invitation valid until .

+

{copy.validUntil} .

) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index bf80d53..d2c06ee 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,8 +1,11 @@ import type { Metadata, Viewport } from "next" import { headers } from "next/headers" +import { cookies } from "next/headers" import { Geist, Geist_Mono } from "next/font/google" import { Providers } from "@/components/providers" import { getAppUrl } from "@/server/env" +import { getCurrentSession } from "@/server/auth/session" +import type { Locale } from "@/lib/i18n" import "./globals.css" const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] }) @@ -37,10 +40,15 @@ export const viewport: Viewport = { export default async function RootLayout({ children }: LayoutProps<"/">) { // Per-request CSP nonce from src/proxy.ts (also makes every page dynamic, which nonces require) const nonce = (await headers()).get("x-nonce") ?? undefined + const session = await getCurrentSession() + const requestHeaders = await headers() + const acceptLanguage = requestHeaders.get("accept-language") ?? "" + const preferredCookie = (await cookies()).get("dispatch-locale")?.value + const locale: Locale = session?.user.locale === "it" || (!session && (preferredCookie === "it" || (!preferredCookie && acceptLanguage.toLowerCase().startsWith("it")))) ? "it" : "en" return ( - + - {children} + {children} ) diff --git a/src/app/login/actions.ts b/src/app/login/actions.ts index 4573ece..9588eae 100644 --- a/src/app/login/actions.ts +++ b/src/app/login/actions.ts @@ -7,6 +7,7 @@ import { ApiError } from "@/server/api" import { normalizeEmail, requestMagicLink, verifyLoginCode, verifyMagicToken } from "@/server/auth/magic-link" import { getRequestMeta } from "@/server/request" import { establishSession } from "./complete-login" +import { authenticatePassword } from "@/server/auth/password" const nextSchema = z.string().max(2000).optional() @@ -21,6 +22,21 @@ export const requestLoginCodeAction = action( } ) +export const loginWithPasswordAction = action( + z.object({ + email: z.string().trim().min(3).max(320), + password: z.string().min(1).max(4000), + next: nextSchema, + }), + async ({ email, password, next }) => { + const meta = await getRequestMeta() + const res = await authenticatePassword({ email, password, ip: meta.ip, userAgent: meta.userAgent }) + if (!res.ok) throw new ApiError(401, res.error) + const redirectTo = await establishSession({ userId: res.userId, email: res.email, method: "password", redirectTo: next }) + redirect(redirectTo, RedirectType.replace) + } +) + /** Step 2: the 6-digit code from the email → session on this device, then navigate to the destination. */ export const verifyLoginCodeAction = action( z.object({ diff --git a/src/app/login/complete-login.ts b/src/app/login/complete-login.ts index 96f9cff..b96382e 100644 --- a/src/app/login/complete-login.ts +++ b/src/app/login/complete-login.ts @@ -15,7 +15,7 @@ import { getRequestMeta } from "@/server/request" export async function establishSession(opts: { userId: string email: string - method: "code" | "link" | "setup" + method: "code" | "link" | "password" | "setup" isNewUser?: boolean redirectTo?: string | null }): Promise { diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 8b90732..1d3c3e5 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,3 +1,4 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import Link from "next/link" import { redirect } from "next/navigation" @@ -7,12 +8,15 @@ import { safeRedirect } from "@/server/auth/magic-link" import { resolveHomePath } from "@/server/orgs" import { isEmailDeliveryConfigured } from "@/server/mail/system-mailer" import { unsign } from "@/server/crypto" +import { headers } from "next/headers" import { SplitAuthLayout } from "@/components/auth/auth-layout" import { LoginForm, type LoginNotice } from "@/components/auth/login-form" -export const metadata: Metadata = { - title: "Sign in", - robots: { index: false, follow: false }, +export async function generateMetadata(): Promise { + return { + title: await pageTitle("Sign in", "Accedi"), + robots: { index: false, follow: false }, + } } /** @@ -35,12 +39,27 @@ const ERRORS: Record = { session_expired: "Your session has expired. Please sign in again.", } +const ERRORS_IT: Record = { + access_denied: "Accesso annullato.", + oauth_denied: "Accesso annullato.", + oauth_failed: "Impossibile completare l’accesso con questo provider. Riprova oppure usa la tua email.", + oauth_state: "La sessione di accesso è scaduta. Riprova.", + oauth_disabled: "Questo metodo di accesso non è attivo in questa istanza.", + email_unverified: "Il provider non ha verificato il tuo indirizzo email. Accedi usando la tua email.", + not_allowed: "L’accesso a questa istanza è solo su invito. Chiedi a un amministratore del workspace di invitarti.", + domain: "Le registrazioni sono limitate ai domini email approvati.", + invite_only: "L’accesso a questa istanza è solo su invito. Chiedi a un amministratore del workspace di invitarti.", + disabled: "Questo account è stato disattivato. Contatta l’amministratore.", + session_expired: "La sessione è scaduta. Accedi di nuovo.", +} + function first(v: string | string[] | undefined) { return Array.isArray(v) ? v[0] : v } export default async function LoginPage({ searchParams }: PageProps<"/login">) { const sp = await searchParams + const italian = ((await headers()).get("accept-language") ?? "").toLowerCase().startsWith("it") const next = safeRedirect(first(sp.next), "") || undefined const status = await getSetupStatus() @@ -64,16 +83,16 @@ export default async function LoginPage({ searchParams }: PageProps<"/login">) { let notice: LoginNotice | null = null if (errorCode) { const signed = errorCode.length <= 600 ? unsign(errorCode, "login-error") : null - notice = { tone: "error", text: signed?.slice(0, 300) || ERRORS[errorCode] || "Sign-in failed. Please try again." } + notice = { tone: "error", text: signed?.slice(0, 300) || (italian ? ERRORS_IT[errorCode] : ERRORS[errorCode]) || (italian ? "Accesso non riuscito. Riprova." : "Sign-in failed. Please try again.") } } - else if (first(sp.signed_out)) notice = { tone: "success", text: "You've been signed out on this device." } + else if (first(sp.signed_out)) notice = { tone: "success", text: italian ? "Hai effettuato la disconnessione su questo dispositivo." : "You've been signed out on this device." } const signupHint = auth.signupMode === "open" - ? "New here? Enter your email — we'll create your account." + ? italian ? "È la prima volta? Inserisci la tua email: creeremo il tuo account." : "New here? Enter your email — we'll create your account." : auth.signupMode === "domains" - ? `New here? Sign up with your ${auth.allowedSignupDomains.map((d) => `@${d}`).join(", ")} email address.` - : "New here? You'll need an invitation from a workspace admin." + ? italian ? `È la prima volta? Usa un indirizzo email ${auth.allowedSignupDomains.map((d) => `@${d}`).join(", ")}.` : `New here? Sign up with your ${auth.allowedSignupDomains.map((d) => `@${d}`).join(", ")} email address.` + : italian ? "È la prima volta? Ti serve un invito da un amministratore del workspace." : "New here? You'll need an invitation from a workspace admin." const showLegal = Boolean(legal.terms.trim() || legal.privacy.trim()) const marketing = general.mode === "saas" && general.marketingSite @@ -85,22 +104,22 @@ export default async function LoginPage({ searchParams }: PageProps<"/login">) { footer={ showLegal ? ( <> - By continuing you agree to the{" "} + {italian ? "Continuando accetti i " : "By continuing you agree to the "} {legal.terms.trim() && ( - Terms + {italian ? "Termini" : "Terms"} )} {legal.terms.trim() && legal.privacy.trim() && " and "} {legal.privacy.trim() && ( - Privacy Policy + {italian ? "Informativa privacy" : "Privacy Policy"} )} . ) : ( - <>Passwordless sign-in · sessions stay active for {auth.sessionDays >= 365 ? "a year" : `${auth.sessionDays} days`} + <>{italian ? "Accesso con password o codice email · sessioni attive per " : "Sign in with a password or email code · sessions stay active for "}{auth.sessionDays >= 365 ? (italian ? "un anno" : "a year") : `${auth.sessionDays} ${italian ? "giorni" : "days"}`} ) } > diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index c67b002..c2226ba 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,24 +1,28 @@ import Link from "next/link" import { LogoMark } from "@/components/brand/logo" import { Button } from "@/components/ui/button" +import { cookies, headers } from "next/headers" -export default function NotFound() { +export default async function NotFound() { + const cookieStore = await cookies() + const requestHeaders = await headers() + const it = cookieStore.get("dispatch-locale")?.value === "it" || (!cookieStore.get("dispatch-locale") && requestHeaders.get("accept-language")?.toLowerCase().startsWith("it")) return (

Error 404

- Lost in the inbox. This page doesn't exist. + {it ? "Ti sei perso nella posta." : "Lost in the inbox."} {it ? "Questa pagina non esiste." : "This page doesn’t exist."}

- The link may be broken, or you might not have access to this workspace. + {it ? "Il link potrebbe essere errato oppure potresti non avere accesso a questo workspace." : "The link may be broken, or you might not have access to this workspace."}

diff --git a/src/app/onboarding/page.tsx b/src/app/onboarding/page.tsx index 45f3b18..0244afb 100644 --- a/src/app/onboarding/page.tsx +++ b/src/app/onboarding/page.tsx @@ -1,3 +1,4 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import { redirect } from "next/navigation" import { Clock3, LogOut, MailPlus } from "lucide-react" @@ -7,14 +8,17 @@ import { can, loadOrgContext, requireUserPage } from "@/server/authz" import { hasUsedTrial, listPendingInvitations, listUserOrganizations } from "@/server/orgs" import { canCreateWorkspace } from "@/server/setup" import { isEmailDeliveryConfigured } from "@/server/mail/system-mailer" -import { AuthCard, AuthHeading, CenteredAuthLayout, ImpersonationBanner } from "@/components/auth/auth-layout" +import { AuthCard, AuthHeading, CenteredAuthLayout } from "@/components/auth/auth-layout" +import { ImpersonationBanner } from "@/components/auth/impersonation-banner" import { OnboardingWizard } from "@/components/onboarding/onboarding-wizard" import { PendingInvitations } from "@/components/onboarding/pending-invitations" import { formatMoney } from "@/components/admin/ui" -export const metadata: Metadata = { - title: "Create your workspace", - robots: { index: false, follow: false }, +export async function generateMetadata(): Promise { + return { + title: await pageTitle("Create your workspace", "Crea il tuo workspace"), + robots: { index: false, follow: false }, + } } /** diff --git a/src/app/setup/actions.ts b/src/app/setup/actions.ts index fe63e31..79a7e75 100644 --- a/src/app/setup/actions.ts +++ b/src/app/setup/actions.ts @@ -20,6 +20,7 @@ import { verifySetupCode, } from "@/server/setup" import { establishSession } from "@/app/login/complete-login" +import { PASSWORD_MIN_LENGTH } from "@/server/auth/password" async function auditSetup(userId: string, email: string, actionName: string, metadata: Record = {}) { const meta = await getRequestMeta() @@ -42,8 +43,9 @@ export const createOwnerAction = action( setupCode: z.string().max(40).optional(), name: z.string().trim().min(1, "Enter your name").max(80), email: emailSchema, + password: z.string().min(PASSWORD_MIN_LENGTH, `Use at least ${PASSWORD_MIN_LENGTH} characters`).max(4000), }), - async ({ setupCode, name, email }) => { + async ({ setupCode, name, email, password }) => { const status = await getSetupStatus() if (status.completed) throw new ApiError(403, "Setup has already been completed.") if (status.hasSuperAdmin) throw new ApiError(409, "An instance owner already exists. Sign in to continue setup.") @@ -58,7 +60,7 @@ export const createOwnerAction = action( let user try { - user = await createFirstSuperAdmin({ name, email }) + user = await createFirstSuperAdmin({ name, email, password }) } catch (err) { if (err instanceof SetupError) throw new ApiError(409, err.message) throw err diff --git a/src/app/setup/page.tsx b/src/app/setup/page.tsx index 9ff30f9..fec998b 100644 --- a/src/app/setup/page.tsx +++ b/src/app/setup/page.tsx @@ -1,3 +1,4 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import { redirect } from "next/navigation" import { ShieldCheck } from "lucide-react" @@ -12,9 +13,11 @@ import { AuthCard, CenteredAuthLayout } from "@/components/auth/auth-layout" import { LoginForm } from "@/components/auth/login-form" import { SetupWizard } from "@/components/onboarding/setup-wizard" -export const metadata: Metadata = { - title: "Set up Dispatch", - robots: { index: false, follow: false }, +export async function generateMetadata(): Promise { + return { + title: await pageTitle("Set up Dispatch", "Configura Dispatch"), + robots: { index: false, follow: false }, + } } /** diff --git a/src/app/w/[slug]/(mail)/[box]/[conversationId]/page.tsx b/src/app/w/[slug]/(mail)/[box]/[conversationId]/page.tsx index 6bd9b92..92ce7a0 100644 --- a/src/app/w/[slug]/(mail)/[box]/[conversationId]/page.tsx +++ b/src/app/w/[slug]/(mail)/[box]/[conversationId]/page.tsx @@ -1,9 +1,12 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import { notFound } from "next/navigation" import { isUuid, parseBox, boxKey } from "@/lib/inbox/boxes" import { ConversationView } from "@/components/inbox/conversation/conversation-view" -export const metadata: Metadata = { title: "Conversation" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("Conversation", "Conversazione") } +} export default async function ConversationPage({ params }: { params: Promise<{ box: string; conversationId: string }> }) { const { box, conversationId } = await params diff --git a/src/app/w/[slug]/(mail)/[box]/layout.tsx b/src/app/w/[slug]/(mail)/[box]/layout.tsx index d10d31e..30fe965 100644 --- a/src/app/w/[slug]/(mail)/[box]/layout.tsx +++ b/src/app/w/[slug]/(mail)/[box]/layout.tsx @@ -1,11 +1,16 @@ -import { notFound } from "next/navigation" +import { notFound, redirect } from "next/navigation" +import { requireOrgPage } from "@/server/authz" import { boxKey, parseBox } from "@/lib/inbox/boxes" import { MailPanes } from "@/components/inbox/mail-panes" /** List pane + conversation pane for a mailbox (`/w/[slug]/[box]`). */ export default async function BoxLayout({ children, params }: { children: React.ReactNode; params: Promise<{ slug: string; box: string }> }) { - const { box } = await params + const { slug, box } = await params const parsed = parseBox(box) if (!parsed) notFound() + if (parsed.kind === "static" && parsed.id === "chats") { + const ctx = await requireOrgPage(slug) + if (ctx.org.settings.features?.chat === false) redirect(`/w/${slug}/inbox`) + } return {children} } diff --git a/src/app/w/[slug]/(mail)/[box]/page.tsx b/src/app/w/[slug]/(mail)/[box]/page.tsx index 8e8ec0b..72ca0a8 100644 --- a/src/app/w/[slug]/(mail)/[box]/page.tsx +++ b/src/app/w/[slug]/(mail)/[box]/page.tsx @@ -1,10 +1,36 @@ import type { Metadata } from "next" +import { cookies, headers } from "next/headers" import { parseBox, STATIC_BOX_META } from "@/lib/inbox/boxes" +import { getCurrentSession } from "@/server/auth/session" import { NoConversation } from "@/components/inbox/conversation/no-conversation" +const BOX_TITLES_IT: Record = { + inbox: "Posta in arrivo", + assigned: "Assegnate a me", + mentions: "Menzioni", + starred: "Speciali", + snoozed: "Posticipate", + drafts: "Bozze", + scheduled: "Programmate", + sent: "Inviate", + unassigned: "Non assegnate", + all: "Tutte le conversazioni", + closed: "Chiuse", + spam: "Spam", + trash: "Cestino", + chats: "Chat", + search: "Ricerca avanzata", +} + export async function generateMetadata({ params }: { params: Promise<{ box: string }> }): Promise { const box = parseBox((await params).box) - return { title: box?.kind === "static" ? STATIC_BOX_META[box.id].title : "Inbox" } + if (box?.kind !== "static") return { title: "Inbox" } + // Same locale resolution as the root layout, so the server-rendered tab title matches the UI language. + const session = await getCurrentSession() + const acceptLanguage = (await headers()).get("accept-language") ?? "" + const preferredCookie = (await cookies()).get("dispatch-locale")?.value + const it = session?.user.locale === "it" || (!session && (preferredCookie === "it" || (!preferredCookie && acceptLanguage.toLowerCase().startsWith("it")))) + return { title: it ? (BOX_TITLES_IT[box.id] ?? STATIC_BOX_META[box.id].title) : STATIC_BOX_META[box.id].title } } export default function BoxPage() { diff --git a/src/app/w/[slug]/(mail)/mail-archive/page.tsx b/src/app/w/[slug]/(mail)/mail-archive/page.tsx new file mode 100644 index 0000000..57fc702 --- /dev/null +++ b/src/app/w/[slug]/(mail)/mail-archive/page.tsx @@ -0,0 +1,5 @@ +import { MailArchive } from "@/components/inbox/mail-archive" + +export default function MailArchivePage() { + return +} diff --git a/src/app/w/[slug]/analytics/layout.tsx b/src/app/w/[slug]/analytics/layout.tsx index fb048f8..dff49ed 100644 --- a/src/app/w/[slug]/analytics/layout.tsx +++ b/src/app/w/[slug]/analytics/layout.tsx @@ -1,8 +1,11 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import { requireOrgPage, requirePagePermission } from "@/server/authz" import { AnalyticsTopBar } from "@/components/analytics/analytics-top-bar" -export const metadata: Metadata = { title: "Analytics" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("Analytics", "Statistiche") } +} export default async function AnalyticsLayout({ children, params }: LayoutProps<"/w/[slug]/analytics">) { const { slug } = await params diff --git a/src/app/w/[slug]/analytics/loading.tsx b/src/app/w/[slug]/analytics/loading.tsx index 5998654..17f72a7 100644 --- a/src/app/w/[slug]/analytics/loading.tsx +++ b/src/app/w/[slug]/analytics/loading.tsx @@ -1,8 +1,12 @@ +"use client" + import { Skeleton } from "@/components/ui/skeleton" +import { useI18n } from "@/lib/i18n" export default function AnalyticsLoading() { + const { locale } = useI18n() return ( -
+
diff --git a/src/app/w/[slug]/analytics/page.tsx b/src/app/w/[slug]/analytics/page.tsx index 6a2a476..ba1df29 100644 --- a/src/app/w/[slug]/analytics/page.tsx +++ b/src/app/w/[slug]/analytics/page.tsx @@ -31,6 +31,7 @@ export default async function AnalyticsPage({ params, searchParams }: PageProps< const { slug } = await params const ctx = await requireOrgPage(slug) requirePagePermission(ctx, "analytics.view") + const it = ctx.user.locale === "it" const sp = await searchParams const [tz, options, hasData] = await Promise.all([ @@ -69,12 +70,12 @@ export default async function AnalyticsPage({ params, searchParams }: PageProps< return (
- Reports + {it ? "Report" : "Reports"}

- Analytics — how your team is doing + {it ? "Analisi" : "Analytics"} {it ? "— andamento del team" : "— how your team is doing"}

- Volume, responsiveness and workload across your shared inboxes. Times are shown in{" "} + {it ? "Volumi, tempi di risposta e carico di lavoro delle caselle condivise. Orari nel fuso " : "Volume, responsiveness and workload across your shared inboxes. Times are shown in "} {tz.replace(/_/g, " ")}.

@@ -82,17 +83,17 @@ export default async function AnalyticsPage({ params, searchParams }: PageProps< {!report ? ( } - title={options.accounts.length === 0 ? "Connect a shared inbox to see analytics" : "No conversations yet"} + title={options.accounts.length === 0 ? (it ? "Collega una casella condivisa per vedere le analisi" : "Connect a shared inbox to see analytics") : (it ? "Nessuna conversazione" : "No conversations yet")} description={ options.accounts.length === 0 - ? "Analytics cover your team's shared inboxes. Once a shared inbox is connected and emails arrive, you'll see volume, response times and workload here." - : "As soon as emails arrive in your shared inboxes, you'll see volume, response times, busiest hours and per-teammate stats here." + ? (it ? "Le analisi riguardano le caselle condivise del team. Collega una casella e, all’arrivo delle email, vedrai qui volumi, tempi di risposta e carico di lavoro." : "Analytics cover your team's shared inboxes. Once a shared inbox is connected and emails arrive, you'll see volume, response times and workload here.") + : (it ? "Quando arrivano email nelle caselle condivise, qui vedrai volumi, tempi di risposta, fasce orarie più intense e statistiche per collega." : "As soon as emails arrive in your shared inboxes, you'll see volume, response times, busiest hours and per-teammate stats here.") } className="py-16" > {can(ctx, "inboxes.manage") && ( )} diff --git a/src/app/w/[slug]/calendar/page.tsx b/src/app/w/[slug]/calendar/page.tsx new file mode 100644 index 0000000..bfa9134 --- /dev/null +++ b/src/app/w/[slug]/calendar/page.tsx @@ -0,0 +1,15 @@ +import { Suspense } from "react" +import { requireOrgPage, requirePagePermission } from "@/server/authz" +import { pageTitle } from "@/server/locale" +import { CalendarView } from "@/components/calendar/calendar-view" + +export async function generateMetadata() { + return { title: await pageTitle("Calendar", "Calendario") } +} + +export default async function CalendarPage({ params }: PageProps<"/w/[slug]/calendar">) { + const { slug } = await params + const ctx = await requireOrgPage(slug) + requirePagePermission(ctx, "calendar.use") + return +} diff --git a/src/app/w/[slug]/contacts/page.tsx b/src/app/w/[slug]/contacts/page.tsx index e4807af..813f239 100644 --- a/src/app/w/[slug]/contacts/page.tsx +++ b/src/app/w/[slug]/contacts/page.tsx @@ -1,9 +1,12 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import { Suspense } from "react" import { requireOrgPage } from "@/server/authz" import { ContactsView } from "@/components/contacts/contacts-view" -export const metadata: Metadata = { title: "Contacts" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("Contacts", "Contatti") } +} export default async function ContactsPage({ params }: PageProps<"/w/[slug]/contacts">) { const { slug } = await params diff --git a/src/app/w/[slug]/layout.tsx b/src/app/w/[slug]/layout.tsx index 5c350aa..97c401a 100644 --- a/src/app/w/[slug]/layout.tsx +++ b/src/app/w/[slug]/layout.tsx @@ -37,6 +37,11 @@ export default async function WorkspaceLayout({ children, params }: LayoutProps< plan: ctx.org.plan, subscriptionStatus: ctx.org.subscriptionStatus, trialEndsAt: ctx.org.trialEndsAt?.toISOString() ?? null, + features: { chat: ctx.org.settings.features?.chat !== false }, + navigation: { + hiddenItems: ctx.org.settings.navigation?.hiddenItems ?? [], + hiddenSections: ctx.org.settings.navigation?.hiddenSections ?? [], + }, }, user: { id: ctx.user.id, @@ -54,24 +59,34 @@ export default async function WorkspaceLayout({ children, params }: LayoutProps< workspaces: workspaces.map((w) => ({ id: w.id, name: w.name, slug: w.slug, logoUrl: w.logoUrl })), productName: branding.productName || general.instanceName, } + const it = ctx.user.locale === "it" + const lockedMessage = !ctx.locked ? null : it + ? ctx.locked.message === "Your free trial has ended. Subscribe to keep using this workspace." + ? "Il periodo di prova è terminato. Abbonati per continuare a usare questo workspace." + : ctx.locked.message === "This workspace has no active subscription. Subscribe to continue." + ? "Questo workspace non ha un abbonamento attivo. Abbonati per continuare." + : ctx.locked.message === "This workspace has been suspended by the instance administrator." + ? "Questo workspace è stato sospeso dall’amministratore dell’istanza." + : ctx.locked.message + : ctx.locked.message return (
{value.impersonating && (
- You are impersonating {ctx.user.email}. + {it ? `Stai impersonando ${ctx.user.email}.` : `You are impersonating ${ctx.user.email}.`}
- +
)} {ctx.locked && (
- {ctx.locked.message} The workspace is read-only. + {lockedMessage} {it ? "Il workspace è in sola lettura." : "The workspace is read-only."} {ctx.locked.reason === "billing" && ctx.permissions.has("billing.manage") && ( - Subscribe now + {it ? "Abbonati ora" : "Subscribe now"} )}
diff --git a/src/app/w/[slug]/settings/audit-log/page.tsx b/src/app/w/[slug]/settings/audit-log/page.tsx index 601e3c3..19115a5 100644 --- a/src/app/w/[slug]/settings/audit-log/page.tsx +++ b/src/app/w/[slug]/settings/audit-log/page.tsx @@ -1,10 +1,13 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import { requireOrgPage, requirePagePermission } from "@/server/authz" import { auditFilterSchema, listAuditFacets, listAuditLogs } from "@/server/workspace/queries/audit" import { SettingsPage } from "@/components/settings/settings-ui" import { AuditLog } from "@/components/settings/audit/audit-log" -export const metadata: Metadata = { title: "Audit log" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("Audit log", "Registro attività") } +} export default async function AuditLogPage({ params, searchParams }: PageProps<"/w/[slug]/settings/audit-log">) { const { slug } = await params @@ -24,10 +27,10 @@ export default async function AuditLogPage({ params, searchParams }: PageProps<" return ( { + return { title: await pageTitle("Billing", "Fatturazione") } +} export default async function BillingPage({ params, searchParams }: PageProps<"/w/[slug]/settings/billing">) { const { slug } = await params @@ -16,19 +19,20 @@ export default async function BillingPage({ params, searchParams }: PageProps<"/ // Only known error codes are rendered — never free text from the URL const code = typeof sp.error === "string" ? sp.error : null - const error = code && Object.hasOwn(BILLING_ERRORS, code) ? BILLING_ERRORS[code as BillingErrorCode] : code ? "Something went wrong with billing. Please try again." : null + const it = ctx.user.locale === "it" + const error = code && Object.hasOwn(BILLING_ERRORS, code) ? BILLING_ERRORS[code as BillingErrorCode] : code ? (it ? "Si è verificato un errore con la fatturazione. Riprova." : "Something went wrong with billing. Please try again.") : null const checkout = sp.checkout === "success" || sp.checkout === "canceled" ? sp.checkout : null const now = new Date().getTime() return ( diff --git a/src/app/w/[slug]/settings/general/page.tsx b/src/app/w/[slug]/settings/general/page.tsx index 860ba18..7813060 100644 --- a/src/app/w/[slug]/settings/general/page.tsx +++ b/src/app/w/[slug]/settings/general/page.tsx @@ -1,3 +1,4 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import { isOwner, requirePagePermission, requireOrgPage } from "@/server/authz" import { getSettings, readSecret } from "@/server/settings" @@ -11,11 +12,15 @@ import { PUBLIC_EMAIL_DOMAINS, verifiedAdminDomains } from "@/server/workspace/s import { SettingsPage } from "@/components/settings/settings-ui" import { IdentitySection } from "@/components/settings/general/identity-section" import { BusinessHoursSection, ConversationSection } from "@/components/settings/general/behaviour-sections" +import { WorkspaceFeaturesSection } from "@/components/settings/general/features-section" +import { WorkspaceNavigationSection } from "@/components/settings/general/navigation-section" import { AccessSection } from "@/components/settings/general/access-section" import { AiSection, type AiMode } from "@/components/settings/general/ai-section" import { DangerZone, DemoDataSection } from "@/components/settings/general/danger-zone" -export const metadata: Metadata = { title: "General" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("General", "Generale") } +} export default async function GeneralSettingsPage({ params }: PageProps<"/w/[slug]/settings/general">) { const { slug } = await params @@ -40,7 +45,7 @@ export default async function GeneralSettingsPage({ params }: PageProps<"/w/[slu const defaultRoleId = selectableRoles.some((r) => r.id === s.defaultRoleId) ? s.defaultRoleId! : (memberRole?.id ?? selectableRoles[0]?.id ?? "") return ( - + + + { + return { title: await pageTitle("Inbox settings", "Impostazioni casella") } +} export default async function InboxSettingsPage({ params }: PageProps<"/w/[slug]/settings/inboxes/[id]">) { const { slug, id } = await params diff --git a/src/app/w/[slug]/settings/inboxes/page.tsx b/src/app/w/[slug]/settings/inboxes/page.tsx index 2dfd193..21bc409 100644 --- a/src/app/w/[slug]/settings/inboxes/page.tsx +++ b/src/app/w/[slug]/settings/inboxes/page.tsx @@ -1,3 +1,4 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import { redirect } from "next/navigation" import { and, eq } from "drizzle-orm" @@ -9,7 +10,9 @@ import { ConnectInboxWizard } from "@/components/settings/inboxes/connect-wizard import { ConnectResultBanner } from "@/components/settings/inboxes/connect-banner" import { InboxList } from "@/components/settings/inboxes/inbox-list" -export const metadata: Metadata = { title: "Inboxes" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("Inboxes", "Caselle") } +} export default async function InboxesPage({ params, searchParams }: PageProps<"/w/[slug]/settings/inboxes">) { const { slug } = await params @@ -37,10 +40,10 @@ export default async function InboxesPage({ params, searchParams }: PageProps<"/ return ( 0 ? : undefined} > { + return { title: await pageTitle("API & webhooks", "API e webhook") } +} export default async function IntegrationsPage({ params }: PageProps<"/w/[slug]/settings/integrations">) { const { slug } = await params @@ -21,10 +24,10 @@ export default async function IntegrationsPage({ params }: PageProps<"/w/[slug]/ return ( diff --git a/src/app/w/[slug]/settings/labels/page.tsx b/src/app/w/[slug]/settings/labels/page.tsx index 5cbc050..81e7bea 100644 --- a/src/app/w/[slug]/settings/labels/page.tsx +++ b/src/app/w/[slug]/settings/labels/page.tsx @@ -1,9 +1,12 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import { can, requireOrgPage } from "@/server/authz" import { loadLabelsPage } from "@/server/workspace/queries/labels" import { LabelsSettings } from "@/components/settings/labels/labels-settings" -export const metadata: Metadata = { title: "Labels" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("Labels", "Etichette") } +} export default async function LabelsPage({ params }: PageProps<"/w/[slug]/settings/labels">) { const { slug } = await params diff --git a/src/app/w/[slug]/settings/layout.tsx b/src/app/w/[slug]/settings/layout.tsx index 97008a4..4a445bf 100644 --- a/src/app/w/[slug]/settings/layout.tsx +++ b/src/app/w/[slug]/settings/layout.tsx @@ -1,8 +1,12 @@ import type { Metadata } from "next" import { requireOrgPage } from "@/server/authz" +import { requestLocale } from "@/server/locale" import { SettingsShell } from "@/components/settings/settings-shell" -export const metadata: Metadata = { title: { template: "%s · Settings", default: "Settings" } } +export async function generateMetadata(): Promise { + const it = (await requestLocale()) === "it" + return { title: { template: it ? "%s · Impostazioni" : "%s · Settings", default: it ? "Impostazioni" : "Settings" } } +} export default async function SettingsLayout({ children, params }: LayoutProps<"/w/[slug]/settings">) { const { slug } = await params diff --git a/src/app/w/[slug]/settings/members/page.tsx b/src/app/w/[slug]/settings/members/page.tsx index 84c81d2..8c7c5d6 100644 --- a/src/app/w/[slug]/settings/members/page.tsx +++ b/src/app/w/[slug]/settings/members/page.tsx @@ -1,10 +1,13 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import { notFound } from "next/navigation" import { can, requireOrgPage } from "@/server/authz" import { loadMembersPage } from "@/server/workspace/queries/members" import { MembersSettings } from "@/components/settings/members/members-settings" -export const metadata: Metadata = { title: "Members" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("Members", "Membri") } +} export default async function MembersPage({ params }: PageProps<"/w/[slug]/settings/members">) { const { slug } = await params diff --git a/src/app/w/[slug]/settings/notifications/page.tsx b/src/app/w/[slug]/settings/notifications/page.tsx index feed62d..32e482a 100644 --- a/src/app/w/[slug]/settings/notifications/page.tsx +++ b/src/app/w/[slug]/settings/notifications/page.tsx @@ -1,9 +1,12 @@ import type { Metadata } from "next" import { requireOrgPage } from "@/server/authz" +import { pageTitle } from "@/server/locale" import { SettingsPage } from "@/components/settings/settings-ui" import { NotificationsForm } from "@/components/settings/personal/notifications-form" -export const metadata: Metadata = { title: "Notifications" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("Notifications", "Notifiche") } +} export default async function NotificationsPage({ params }: PageProps<"/w/[slug]/settings/notifications">) { const { slug } = await params @@ -11,10 +14,10 @@ export default async function NotificationsPage({ params }: PageProps<"/w/[slug] const n = ctx.user.preferences?.notifications ?? {} return ( { + return { title: await pageTitle("Inbox settings", "Impostazioni casella") } +} export default async function PersonalInboxSettingsPage({ params }: PageProps<"/w/[slug]/settings/personal-inboxes/[id]">) { const { slug, id } = await params diff --git a/src/app/w/[slug]/settings/personal-inboxes/page.tsx b/src/app/w/[slug]/settings/personal-inboxes/page.tsx index 39c4d71..20ee8c6 100644 --- a/src/app/w/[slug]/settings/personal-inboxes/page.tsx +++ b/src/app/w/[slug]/settings/personal-inboxes/page.tsx @@ -1,3 +1,4 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import { requireOrgPage, requirePagePermission } from "@/server/authz" import { getConnectOptions, listInboxes } from "@/server/workspace/queries/inboxes" @@ -6,7 +7,9 @@ import { ConnectInboxWizard } from "@/components/settings/inboxes/connect-wizard import { ConnectResultBanner } from "@/components/settings/inboxes/connect-banner" import { InboxList } from "@/components/settings/inboxes/inbox-list" -export const metadata: Metadata = { title: "My inboxes" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("My inboxes", "Le mie caselle") } +} export default async function PersonalInboxesPage({ params, searchParams }: PageProps<"/w/[slug]/settings/personal-inboxes">) { const { slug } = await params @@ -17,10 +20,10 @@ export default async function PersonalInboxesPage({ params, searchParams }: Page return ( 0 ? : undefined} > { + return { title: await pageTitle("Preferences", "Preferenze") } +} export default async function PreferencesPage({ params }: PageProps<"/w/[slug]/settings/preferences">) { const { slug } = await params @@ -12,7 +15,12 @@ export default async function PreferencesPage({ params }: PageProps<"/w/[slug]/s const security = await getSettings("security") const p = ctx.user.preferences ?? {} return ( - + { + return { title: await pageTitle("Profile", "Profilo") } +} export default async function ProfilePage({ params }: PageProps<"/w/[slug]/settings/profile">) { const { slug } = await params @@ -14,41 +14,22 @@ export default async function ProfilePage({ params }: PageProps<"/w/[slug]/setti const general = await getSettings("general") const { user } = ctx return ( - - new Date()), - }} - /> - - - {ctx.org.name} - - - {ctx.role.name} - - - } - description={ - <> - Member since . - - } - > - - - - + new Date()), + }} + role={{ key: ctx.role.key, name: ctx.role.name, color: ctx.role.color }} + memberSince={ctx.membership.createdAt.toISOString()} + /> ) } diff --git a/src/app/w/[slug]/settings/responses/page.tsx b/src/app/w/[slug]/settings/responses/page.tsx index f2b24d5..d3e2024 100644 --- a/src/app/w/[slug]/settings/responses/page.tsx +++ b/src/app/w/[slug]/settings/responses/page.tsx @@ -1,9 +1,12 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import { can, requireOrgPage } from "@/server/authz" import { loadResponsesPage } from "@/server/workspace/queries/responses" import { ResponsesSettings } from "@/components/settings/responses/responses-settings" -export const metadata: Metadata = { title: "Canned responses" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("Canned responses", "Risposte predefinite") } +} export default async function ResponsesPage({ params }: PageProps<"/w/[slug]/settings/responses">) { const { slug } = await params diff --git a/src/app/w/[slug]/settings/roles/page.tsx b/src/app/w/[slug]/settings/roles/page.tsx index 9cfffca..916b13e 100644 --- a/src/app/w/[slug]/settings/roles/page.tsx +++ b/src/app/w/[slug]/settings/roles/page.tsx @@ -1,9 +1,12 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import { requireOrgPage, requirePagePermission } from "@/server/authz" import { loadRolesPage } from "@/server/workspace/queries/roles" import { RolesSettings } from "@/components/settings/roles/roles-settings" -export const metadata: Metadata = { title: "Roles & permissions" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("Roles & permissions", "Ruoli e permessi") } +} export default async function RolesPage({ params }: PageProps<"/w/[slug]/settings/roles">) { const { slug } = await params diff --git a/src/app/w/[slug]/settings/rules/[id]/page.tsx b/src/app/w/[slug]/settings/rules/[id]/page.tsx index 936206d..d71e1f3 100644 --- a/src/app/w/[slug]/settings/rules/[id]/page.tsx +++ b/src/app/w/[slug]/settings/rules/[id]/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next" +import { pageTitle } from "@/server/locale" import { notFound } from "next/navigation" import { and, eq } from "drizzle-orm" import { db, schema } from "@/server/db" @@ -26,7 +27,7 @@ async function loadRule(slug: string, id: string) { export async function generateMetadata({ params }: PageProps<"/w/[slug]/settings/rules/[id]">): Promise { const { slug, id } = await params const rule = await loadRule(slug, id) - return { title: rule ? rule.name : "Rule" } + return { title: rule ? rule.name : await pageTitle("Rule", "Regola") } } export default async function EditRulePage({ params }: PageProps<"/w/[slug]/settings/rules/[id]">) { diff --git a/src/app/w/[slug]/settings/rules/new/page.tsx b/src/app/w/[slug]/settings/rules/new/page.tsx index 4917dcf..be56d10 100644 --- a/src/app/w/[slug]/settings/rules/new/page.tsx +++ b/src/app/w/[slug]/settings/rules/new/page.tsx @@ -1,3 +1,5 @@ +import type { Metadata } from "next" +import { pageTitle } from "@/server/locale" import { notFound } from "next/navigation" import { requireOrgPage } from "@/server/authz" import { loadRuleOptions } from "@/server/workspace/queries/rules" @@ -5,7 +7,9 @@ import { canUseScope } from "@/server/workspace/services/rules" import { RuleBuilder } from "@/components/settings/rules/rule-builder" import type { RuleScope } from "@/components/settings/rules/definitions" -export const metadata = { title: "New rule" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("New rule", "Nuova regola") } +} export default async function NewRulePage({ params, searchParams }: PageProps<"/w/[slug]/settings/rules/new">) { const { slug } = await params diff --git a/src/app/w/[slug]/settings/rules/page.tsx b/src/app/w/[slug]/settings/rules/page.tsx index 22c677f..44c6ee2 100644 --- a/src/app/w/[slug]/settings/rules/page.tsx +++ b/src/app/w/[slug]/settings/rules/page.tsx @@ -1,3 +1,5 @@ +import type { Metadata } from "next" +import { pageTitle } from "@/server/locale" import { notFound } from "next/navigation" import { can, requireOrgPage } from "@/server/authz" import { listRules, loadRuleLookup } from "@/server/workspace/queries/rules" @@ -6,7 +8,9 @@ import { SettingsPage } from "@/components/settings/settings-ui" import { RulesManager, type RuleScopeData } from "@/components/settings/rules/rules-manager" import type { RuleScope } from "@/components/settings/rules/definitions" -export const metadata = { title: "Rules" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("Rules", "Regole") } +} export default async function RulesPage({ params, searchParams }: PageProps<"/w/[slug]/settings/rules">) { const { slug } = await params @@ -30,9 +34,9 @@ export default async function RulesPage({ params, searchParams }: PageProps<"/w/ return ( { + return { title: await pageTitle("Security & devices", "Sicurezza e dispositivi") } +} export default async function SecurityPage({ params }: PageProps<"/w/[slug]/settings/security">) { const { slug } = await params @@ -15,10 +18,10 @@ export default async function SecurityPage({ params }: PageProps<"/w/[slug]/sett ]) return ( { + return { title: await pageTitle("Signatures", "Firme") } +} export default async function SignaturesPage({ params }: PageProps<"/w/[slug]/settings/signatures">) { const { slug } = await params diff --git a/src/app/w/[slug]/settings/teams/page.tsx b/src/app/w/[slug]/settings/teams/page.tsx index 9f14664..656bf81 100644 --- a/src/app/w/[slug]/settings/teams/page.tsx +++ b/src/app/w/[slug]/settings/teams/page.tsx @@ -1,9 +1,12 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import { requireOrgPage, requirePagePermission } from "@/server/authz" import { loadTeamsPage } from "@/server/workspace/queries/teams" import { TeamsSettings } from "@/components/settings/teams/teams-settings" -export const metadata: Metadata = { title: "Teams" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("Teams", "Team") } +} export default async function TeamsPage({ params }: PageProps<"/w/[slug]/settings/teams">) { const { slug } = await params diff --git a/src/app/w/[slug]/tasks/page.tsx b/src/app/w/[slug]/tasks/page.tsx index f0aecb8..449ec04 100644 --- a/src/app/w/[slug]/tasks/page.tsx +++ b/src/app/w/[slug]/tasks/page.tsx @@ -1,9 +1,12 @@ +import { pageTitle } from "@/server/locale" import type { Metadata } from "next" import { Suspense } from "react" import { requireOrgPage } from "@/server/authz" import { TasksView } from "@/components/tasks/tasks-view" -export const metadata: Metadata = { title: "Tasks" } +export async function generateMetadata(): Promise { + return { title: await pageTitle("Tasks", "Attività") } +} export default async function TasksPage({ params }: PageProps<"/w/[slug]/tasks">) { const { slug } = await params diff --git a/src/components/admin/admin-shell.tsx b/src/components/admin/admin-shell.tsx index 126fae1..8e49bd9 100644 --- a/src/components/admin/admin-shell.tsx +++ b/src/components/admin/admin-shell.tsx @@ -23,6 +23,7 @@ import { } from "@/components/ui/dropdown-menu" import { ADMIN_NAV, SETTINGS_SECTIONS, isActivePath } from "@/components/admin/nav" import { cn } from "@/lib/utils" +import { useI18n } from "@/lib/i18n" export type AdminShellProps = { instanceName: string @@ -34,6 +35,8 @@ export type AdminShellProps = { /** Chrome of the super admin panel: sidebar (desktop), sheet navigation (mobile), top bar. */ export function AdminShell({ instanceName, productName, homePath, user, children }: AdminShellProps) { + const { locale } = useI18n() + const it = locale === "it" const [open, setOpen] = useState(false) return (
@@ -46,13 +49,13 @@ export function AdminShell({ instanceName, productName, homePath, user, children
- - Admin navigation + {it ? "Navigazione amministrazione" : "Admin navigation"} setOpen(false)} /> @@ -61,13 +64,13 @@ export function AdminShell({ instanceName, productName, homePath, user, children
{instanceName} - Instance admin + {it ? "Amministrazione istanza" : "Instance admin"}
@@ -93,6 +96,10 @@ function SidebarContents({ onNavigate?: () => void }) { const pathname = usePathname() + const { locale } = useI18n() + const it = locale === "it" + const navLabel = (label: string) => it ? ({ Instance: "Istanza", Overview: "Panoramica", Workspaces: "Workspace", Users: "Utenti", Operations: "Operazioni", "Audit log": "Registro attività", System: "Sistema" }[label] ?? label) : label + const settingsLabel = (slug: string, label: string) => it ? ({ general: "Generale", authentication: "Accesso", email: "Invio email", oauth: "App OAuth", billing: "Fatturazione", storage: "Archiviazione", ai: "IA", branding: "Marchio", security: "Sicurezza", legal: "Note legali" }[slug] ?? label) : label const settingsActive = isActivePath(pathname, "/admin/settings") return (
@@ -105,19 +112,19 @@ function SidebarContents({
-
@@ -211,34 +218,36 @@ function NavLink({ function AdminUserMenu({ user, homePath }: { user: AdminShellProps["user"]; homePath: string }) { const { theme, setTheme } = useTheme() + const { locale } = useI18n() + const it = locale === "it" return ( -
{user.name || "Instance owner"}
+
{user.name || (it ? "Proprietario istanza" : "Instance owner")}
{user.email}
- Open app + {it ? "Apri Dispatch" : "Open app"} - {theme === "dark" ? : theme === "light" ? : } Theme + {theme === "dark" ? : theme === "light" ? : } {it ? "Tema" : "Theme"} {[ - { v: "light", label: "Light", icon: Sun }, - { v: "dark", label: "Dark", icon: Moon }, - { v: "system", label: "System", icon: Monitor }, + { v: "light", label: it ? "Chiaro" : "Light", icon: Sun }, + { v: "dark", label: it ? "Scuro" : "Dark", icon: Moon }, + { v: "system", label: it ? "Sistema" : "System", icon: Monitor }, ].map((t) => ( setTheme(t.v)}> {t.label} @@ -249,7 +258,7 @@ function AdminUserMenu({ user, homePath }: { user: AdminShellProps["user"]; home - Sign out + {it ? "Esci" : "Sign out"}
diff --git a/src/components/admin/audit/audit-filters.tsx b/src/components/admin/audit/audit-filters.tsx index a45d549..185f62e 100644 --- a/src/components/admin/audit/audit-filters.tsx +++ b/src/components/admin/audit/audit-filters.tsx @@ -1,11 +1,13 @@ "use client" import { useEffect, useRef, useState } from "react" +import { DateField } from "@/components/ui/date-field" import { usePathname, useRouter, useSearchParams } from "next/navigation" import { X } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { FilterSelect, SearchParamInput } from "@/components/admin/client" +import { useI18n } from "@/lib/i18n" const FILTER_PARAMS = ["org", "actor", "action", "from", "to", "actorId", "targetType", "targetId", "page"] @@ -27,6 +29,7 @@ function useUpdateParams() { /** Free-text action prefix with suggestions (prefixes + actions seen so far). */ function ActionPrefixInput({ suggestions }: { suggestions: string[] }) { + const { t, locale } = useI18n() const params = useSearchParams() const update = useUpdateParams() const [value, setValue] = useState(params.get("action") ?? "") @@ -42,8 +45,8 @@ function ActionPrefixInput({ suggestions }: { suggestions: string[] }) { { setValue("") @@ -84,12 +87,11 @@ function DateInput({ param, label }: { param: "from" | "to"; label: string }) { {label} - update({ [param]: e.target.value || null })} + onChange={(v) => update({ [param]: v || null })} className="w-full bg-card sm:w-[150px]" /> @@ -97,6 +99,8 @@ function DateInput({ param, label }: { param: "from" | "to"; label: string }) { } export function AuditFilters({ workspaces, suggestions }: { workspaces: { id: string; name: string }[]; suggestions: string[] }) { + const { t, locale } = useI18n() + const it = locale === "it" const params = useSearchParams() const update = useUpdateParams() // Remount the debounced text inputs after "Clear filters" so their local state resets @@ -105,17 +109,17 @@ export function AuditFilters({ workspaces, suggestions }: { workspaces: { id: st const scoped = [ params.get("actorId") && { key: "actorId", - label: "Actor", + label: it ? "Autore" : "Actor", value: params.get("actorId")!, }, params.get("targetType") && { key: "targetType", - label: "Target type", + label: it ? "Tipo elemento" : "Target type", value: params.get("targetType")!, }, params.get("targetId") && { key: "targetId", - label: "Target", + label: it ? "Elemento" : "Target", value: params.get("targetId")!, }, ].filter(Boolean) as { key: string; label: string; value: string }[] @@ -123,20 +127,20 @@ export function AuditFilters({ workspaces, suggestions }: { workspaces: { id: st return (
- + ({ value: w.id, label: w.name })), ]} />
- - + +
{active && ( )}
@@ -163,7 +167,7 @@ export function AuditFilters({ workspaces, suggestions }: { workspaces: { id: st {s.value}