From 085abddec2d47ff8ab3b01a6cb2eba68f3e40d98 Mon Sep 17 00:00:00 2001 From: flipvanhaaren Date: Wed, 22 Oct 2025 11:37:45 +0200 Subject: [PATCH 01/27] progress --- backend/drizzle/0001_absurd_whiplash.sql | 3 + backend/drizzle/meta/0001_snapshot.json | 1617 +++++++++++++++++ backend/drizzle/meta/_journal.json | 7 + backend/emails/account-security.tsx | 10 +- backend/emails/create-password.tsx | 3 + backend/emails/email-verification.tsx | 3 + backend/emails/member-invite-with-token.tsx | 62 + backend/emails/member-invite.tsx | 7 +- backend/emails/newsletter.tsx | 3 + backend/emails/oauth-verification.tsx | 3 + backend/emails/request-was-sent.tsx | 3 + backend/emails/system-invite.tsx | 3 + backend/mocks/basic.ts | 1 - backend/src/db/schema/inactive-memberships.ts | 32 + backend/src/db/schema/memberships.ts | 5 +- backend/src/db/schema/tokens.ts | 33 +- .../utils/generate-context-entity-fields.ts | 2 +- backend/src/db/utils/pick-columns.ts | 12 + backend/src/lib/docs.ts | 8 +- .../src/middlewares/guard/is-authenticated.ts | 4 +- backend/src/modules/auth/general/handlers.ts | 104 +- .../src/modules/auth/general/helpers/mfa.ts | 12 +- .../modules/auth/general/helpers/session.ts | 4 +- .../src/modules/auth/general/helpers/user.ts | 18 +- backend/src/modules/auth/general/routes.ts | 24 +- backend/src/modules/auth/general/schema.ts | 6 +- backend/src/modules/auth/oauth/handlers.ts | 1 - .../modules/auth/oauth/helpers/initiation.ts | 14 +- .../src/modules/auth/passwords/handlers.ts | 18 +- backend/src/modules/entities/handlers.ts | 25 +- .../modules/entities/helpers/counts/index.ts | 4 +- .../modules/entities/helpers/counts/member.ts | 47 +- .../helpers/counts/related-entities.ts | 6 +- .../entities/helpers/get-related-entities.ts | 5 +- .../src/modules/entities/helpers/select.ts | 18 + backend/src/modules/entities/routes.ts | 3 +- backend/src/modules/entities/schema-base.ts | 16 + backend/src/modules/entities/schema.ts | 23 +- backend/src/modules/me/handlers.ts | 44 +- .../me/helpers/get-user-menu-entities.ts | 4 +- backend/src/modules/me/routes.ts | 12 +- backend/src/modules/me/schema.ts | 25 +- backend/src/modules/memberships/handlers.ts | 551 +++--- .../src/modules/memberships/helpers/index.ts | 211 ++- .../src/modules/memberships/helpers/select.ts | 41 +- backend/src/modules/memberships/routes.ts | 56 +- backend/src/modules/memberships/schema.ts | 33 +- backend/src/modules/organizations/handlers.ts | 16 +- backend/src/modules/system/handlers.ts | 19 +- backend/src/modules/users/handlers.ts | 20 +- backend/src/modules/users/helpers/select.ts | 16 +- backend/src/modules/users/schema-base.ts | 13 + backend/src/utils/get-valid-token.ts | 8 +- backend/tests/setup.ts | 8 +- config/default.ts | 18 +- config/development.ts | 2 +- config/index.ts | 5 +- config/production.ts | 2 +- config/staging.ts | 2 +- config/test.ts | 2 +- config/tunnel.ts | 4 +- config/types.ts | 16 + config/utils.ts | 2 +- frontend/src/api.gen/sdk.gen.ts | 78 +- frontend/src/api.gen/types.gen.ts | 237 +-- frontend/src/api.gen/zod.gen.ts | 158 +- .../hooks/use-local-sync-attachments.tsx | 2 +- .../hooks/use-merge-local-attachments.ts | 2 +- frontend/src/modules/auth/auth-error-page.tsx | 4 +- .../src/modules/auth/authenticate-page.tsx | 2 +- frontend/src/modules/auth/steps/sign-up.tsx | 12 +- .../dropdowner/dropdown-action-item.tsx | 2 +- frontend/src/modules/common/error-notice.tsx | 14 +- frontend/src/modules/common/page/header.tsx | 1 + .../entities/use-get-entity-base-data.tsx | 2 + .../src/modules/me/entity-invitations.tsx | 31 +- .../modules/memberships/members-table/bar.tsx | 4 +- .../modules/memberships/pending-table/bar.tsx | 2 +- .../memberships/pending-table/columns.tsx | 21 +- .../memberships/pending-table/index.tsx | 31 +- ...nvitations.tsx => pending-memberships.tsx} | 8 +- frontend/src/modules/memberships/query.ts | 22 +- ...ation.tsx => resend-invitation-button.tsx} | 13 +- frontend/src/modules/memberships/types.ts | 4 +- .../modules/navigation/menu-sheet/index.tsx | 2 +- frontend/src/query/utils/prefetch-query.ts | 2 +- frontend/src/routes/auth-routes.tsx | 1 - frontend/vite.config.ts | 10 +- lefthook.yaml | 16 +- 89 files changed, 2860 insertions(+), 1120 deletions(-) create mode 100644 backend/drizzle/0001_absurd_whiplash.sql create mode 100644 backend/drizzle/meta/0001_snapshot.json create mode 100644 backend/emails/member-invite-with-token.tsx create mode 100644 backend/src/db/schema/inactive-memberships.ts create mode 100644 backend/src/db/utils/pick-columns.ts create mode 100644 backend/src/modules/entities/helpers/select.ts create mode 100644 backend/src/modules/entities/schema-base.ts create mode 100644 backend/src/modules/users/schema-base.ts create mode 100644 config/types.ts rename frontend/src/modules/memberships/pending-table/{pending-invitations.tsx => pending-memberships.tsx} (88%) rename frontend/src/modules/memberships/{resend-membership-invitation.tsx => resend-invitation-button.tsx} (75%) diff --git a/backend/drizzle/0001_absurd_whiplash.sql b/backend/drizzle/0001_absurd_whiplash.sql new file mode 100644 index 000000000..6ddc36461 --- /dev/null +++ b/backend/drizzle/0001_absurd_whiplash.sql @@ -0,0 +1,3 @@ +ALTER TABLE "memberships" DROP CONSTRAINT "memberships_token_id_tokens_id_fk"; +--> statement-breakpoint +ALTER TABLE "memberships" DROP COLUMN "token_id"; \ No newline at end of file diff --git a/backend/drizzle/meta/0001_snapshot.json b/backend/drizzle/meta/0001_snapshot.json new file mode 100644 index 000000000..fcdd7e916 --- /dev/null +++ b/backend/drizzle/meta/0001_snapshot.json @@ -0,0 +1,1617 @@ +{ + "id": "75cf029a-b353-4de8-bf6a-7a58f15def8a", + "prevId": "eb63c351-3bd9-470d-a7ae-afb456ca323a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.attachments": { + "name": "attachments", + "schema": "", + "columns": { + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'attachment'" + }, + "entity_type": { + "name": "entity_type", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'attachment'" + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bucket_name": { + "name": "bucket_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "converted_content_type": { + "name": "converted_content_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "original_key": { + "name": "original_key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "converted_key": { + "name": "converted_key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "thumbnail_key": { + "name": "thumbnail_key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "modified_at": { + "name": "modified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "modified_by": { + "name": "modified_by", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "attachments_organization_id_index": { + "name": "attachments_organization_id_index", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "attachments_created_by_users_id_fk": { + "name": "attachments_created_by_users_id_fk", + "tableFrom": "attachments", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "attachments_modified_by_users_id_fk": { + "name": "attachments_modified_by_users_id_fk", + "tableFrom": "attachments", + "tableTo": "users", + "columnsFrom": [ + "modified_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "attachments_organization_id_organizations_id_fk": { + "name": "attachments_organization_id_organizations_id_fk", + "tableFrom": "attachments", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.emails": { + "name": "emails", + "schema": "", + "columns": { + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "token_id": { + "name": "token_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "emails_token_id_tokens_id_fk": { + "name": "emails_token_id_tokens_id_fk", + "tableFrom": "emails", + "tableTo": "tokens", + "columnsFrom": [ + "token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "emails_user_id_users_id_fk": { + "name": "emails_user_id_users_id_fk", + "tableFrom": "emails", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "emails_email_unique": { + "name": "emails_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memberships": { + "name": "memberships", + "schema": "", + "columns": { + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "context_type": { + "name": "context_type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "activated_at": { + "name": "activated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "modified_at": { + "name": "modified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "modified_by": { + "name": "modified_by", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "muted": { + "name": "muted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "order": { + "name": "order", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "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_created_by_users_id_fk": { + "name": "memberships_created_by_users_id_fk", + "tableFrom": "memberships", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "memberships_modified_by_users_id_fk": { + "name": "memberships_modified_by_users_id_fk", + "tableFrom": "memberships", + "tableTo": "users", + "columnsFrom": [ + "modified_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "memberships_organization_id_organizations_id_fk": { + "name": "memberships_organization_id_organizations_id_fk", + "tableFrom": "memberships", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_accounts": { + "name": "oauth_accounts", + "schema": "", + "columns": { + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "provider_user_id": { + "name": "provider_user_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_accounts_user_id_users_id_fk": { + "name": "oauth_accounts_user_id_users_id_fk", + "tableFrom": "oauth_accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_accounts_provider_providerUserId_email_unique": { + "name": "oauth_accounts_provider_providerUserId_email_unique", + "nullsNotDistinct": false, + "columns": [ + "provider", + "provider_user_id", + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "banner_url": { + "name": "banner_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "short_name": { + "name": "short_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "default_language": { + "name": "default_language", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "languages": { + "name": "languages", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[\"en\"]'::json" + }, + "restrictions": { + "name": "restrictions", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"user\":1000,\"attachment\":100}'::json" + }, + "notification_email": { + "name": "notification_email", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "email_domains": { + "name": "email_domains", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[]'::json" + }, + "color": { + "name": "color", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "logo_url": { + "name": "logo_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "welcome_text": { + "name": "welcome_text", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "auth_strategies": { + "name": "auth_strategies", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[]'::json" + }, + "chat_support": { + "name": "chat_support", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "modified_at": { + "name": "modified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "modified_by": { + "name": "modified_by", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organizations_name_index": { + "name": "organizations_name_index", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organizations_created_at_index": { + "name": "organizations_created_at_index", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organizations_created_by_users_id_fk": { + "name": "organizations_created_by_users_id_fk", + "tableFrom": "organizations", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "organizations_modified_by_users_id_fk": { + "name": "organizations_modified_by_users_id_fk", + "tableFrom": "organizations", + "tableTo": "users", + "columnsFrom": [ + "modified_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkeys": { + "name": "passkeys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "device_name": { + "name": "device_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "device_type": { + "name": "device_type", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'desktop'" + }, + "device_os": { + "name": "device_os", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "browser": { + "name": "browser", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "name_on_device": { + "name": "name_on_device", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "passkeys_user_id_users_id_fk": { + "name": "passkeys_user_id_users_id_fk", + "tableFrom": "passkeys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passwords": { + "name": "passwords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "hashed_password": { + "name": "hashed_password", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "modified_at": { + "name": "modified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "passwords_user_id_users_id_fk": { + "name": "passwords_user_id_users_id_fk", + "tableFrom": "passwords", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "passwords_userId_unique": { + "name": "passwords_userId_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.requests": { + "name": "requests", + "schema": "", + "columns": { + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "message": { + "name": "message", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "token_id": { + "name": "token_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "requests_emails": { + "name": "requests_emails", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "requests_created_at": { + "name": "requests_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "requests_token_id_tokens_id_fk": { + "name": "requests_token_id_tokens_id_fk", + "tableFrom": "requests", + "tableTo": "tokens", + "columnsFrom": [ + "token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'regular'" + }, + "user_id": { + "name": "user_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "device_name": { + "name": "device_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "device_type": { + "name": "device_type", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'desktop'" + }, + "device_os": { + "name": "device_os", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "browser": { + "name": "browser", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "auth_strategy": { + "name": "auth_strategy", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "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.tokens": { + "name": "tokens", + "schema": "", + "columns": { + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "single_use_token": { + "name": "single_use_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "oauth_account_id": { + "name": "oauth_account_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "invoked_at": { + "name": "invoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "tokens_user_id_users_id_fk": { + "name": "tokens_user_id_users_id_fk", + "tableFrom": "tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tokens_oauth_account_id_oauth_accounts_id_fk": { + "name": "tokens_oauth_account_id_oauth_accounts_id_fk", + "tableFrom": "tokens", + "tableTo": "oauth_accounts", + "columnsFrom": [ + "oauth_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tokens_created_by_users_id_fk": { + "name": "tokens_created_by_users_id_fk", + "tableFrom": "tokens", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tokens_organization_id_organizations_id_fk": { + "name": "tokens_organization_id_organizations_id_fk", + "tableFrom": "tokens", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.totps": { + "name": "totps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "totps_user_id_users_id_fk": { + "name": "totps_user_id_users_id_fk", + "tableFrom": "totps", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.unsubscribe_tokens": { + "name": "unsubscribe_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_token_index": { + "name": "users_token_index", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "unsubscribe_tokens_user_id_users_id_fk": { + "name": "unsubscribe_tokens_user_id_users_id_fk", + "tableFrom": "unsubscribe_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unsubscribe_tokens_userId_unique": { + "name": "unsubscribe_tokens_userId_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "unsubscribe_tokens_token_unique": { + "name": "unsubscribe_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "banner_url": { + "name": "banner_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "mfa_required": { + "name": "mfa_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "first_name": { + "name": "first_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "newsletter": { + "name": "newsletter", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "user_flags": { + "name": "user_flags", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "modified_at": { + "name": "modified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sign_in_at": { + "name": "last_sign_in_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "modified_by": { + "name": "modified_by", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_name_index": { + "name": "users_name_index", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_email_index": { + "name": "users_email_index", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_created_at_index": { + "name": "users_created_at_index", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_modified_by_users_id_fk": { + "name": "users_modified_by_users_id_fk", + "tableFrom": "users", + "tableTo": "users", + "columnsFrom": [ + "modified_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_slug_unique": { + "name": "users_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/backend/drizzle/meta/_journal.json b/backend/drizzle/meta/_journal.json index dc9770bc7..80a627c15 100644 --- a/backend/drizzle/meta/_journal.json +++ b/backend/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1758835199217, "tag": "0000_modern_black_queen", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1760949896936, + "tag": "0001_absurd_whiplash", + "breakpoints": true } ] } \ No newline at end of file diff --git a/backend/emails/account-security.tsx b/backend/emails/account-security.tsx index 1dab77e11..ecb21bd88 100644 --- a/backend/emails/account-security.tsx +++ b/backend/emails/account-security.tsx @@ -19,12 +19,10 @@ export interface AccountSecurityProps extends BasicTemplateType { details?: Record; // Optional extra details for dynamic messages } -export const AccountSecurity = ({ - lng, - name, - type, - details, -}: AccountSecurityProps) => { +/** + * Email template for account security notifications. + */ +export const AccountSecurity = ({ lng, name, type, details }: AccountSecurityProps) => { // Base properties for all i18n calls const baseProps = { lng, appName: appConfig.name }; diff --git a/backend/emails/create-password.tsx b/backend/emails/create-password.tsx index cf0f53d55..c7a80254e 100644 --- a/backend/emails/create-password.tsx +++ b/backend/emails/create-password.tsx @@ -18,6 +18,9 @@ const baseUrl = appConfig.frontendUrl; const createPasswordUrl = `${baseUrl}/auth/request-password`; const appName = appConfig.name; +/** + * Email template for users to create a password for their account. + */ export const CreatePasswordEmail = ({ name, lng, createPasswordLink }: CreatePasswordEmailProps) => { return ( diff --git a/backend/emails/email-verification.tsx b/backend/emails/email-verification.tsx index dcfd65b6a..659a7126d 100644 --- a/backend/emails/email-verification.tsx +++ b/backend/emails/email-verification.tsx @@ -17,6 +17,9 @@ export interface EmailVerificationEmailProps extends BasicTemplateType { email: string; } +/** + * Email template for users to verify ownership of this email address. + */ export const EmailVerificationEmail = ({ lng, verificationLink, email, name }: EmailVerificationEmailProps) => { return ( diff --git a/backend/emails/member-invite-with-token.tsx b/backend/emails/member-invite-with-token.tsx new file mode 100644 index 000000000..71e763a04 --- /dev/null +++ b/backend/emails/member-invite-with-token.tsx @@ -0,0 +1,62 @@ +import { appConfig } from 'config'; +import i18n from 'i18next'; +import { Column, Row, Text } from 'jsx-email'; + +import type { BasicTemplateType } from '../src/lib/mailer'; +import { AppLogo } from './components/app-logo'; +import { Avatar } from './components/avatar'; +import { EmailContainer } from './components/container'; +import { EmailBody } from './components/email-body'; +import { EmailButton } from './components/email-button'; +import { EmailHeader } from './components/email-header'; +import { Footer } from './components/footer'; + +export interface MemberInviteWithTokenEmailProps extends BasicTemplateType { + memberInviteLink: string; + senderName: string; + entityName: string; + role: (typeof appConfig.roles.entityRoles)[number]; +} + +const appName = appConfig.name; + +/** + * Email template for new users that receive a new membership invitation with a token. + */ +export const MemberInviteWithTokenEmail = ({ name, lng, senderName, role, entityName, memberInviteLink }: MemberInviteWithTokenEmailProps) => { + return ( + + {senderName && ( + + + + + + )} + + + } + /> + + +

{name && i18n.t('backend:email.hi', { lng, name })}

+ +
+ + + + + {i18n.t('backend:email.invite_expires', { lng })} + +
+ + +