From 41ba63c75ed4bb52bf5188a10079ac8e77ea8ab7 Mon Sep 17 00:00:00 2001 From: Maxim Date: Thu, 25 Jun 2026 18:33:58 +0300 Subject: [PATCH 1/7] fix(oauth): adjust cookie signing settings --- src/auth/application/controllers/oauth/controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/auth/application/controllers/oauth/controller.ts b/src/auth/application/controllers/oauth/controller.ts index 0bce2f1f..dbc9ac97 100644 --- a/src/auth/application/controllers/oauth/controller.ts +++ b/src/auth/application/controllers/oauth/controller.ts @@ -152,6 +152,7 @@ export class OAuthController { private setRefreshCookie(res: FastifyReply, refreshToken: string, expires: Date) { res.setCookie('refresh', refreshToken, { + signed: false, expires, }); } From b29a9036d993faaa4da135afa2c206cd3366ce2a Mon Sep 17 00:00:00 2001 From: Maxim Date: Thu, 25 Jun 2026 18:35:35 +0300 Subject: [PATCH 2/7] fix(team): fix user invites response data --- src/team/application/dtos/member.dto.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/team/application/dtos/member.dto.ts b/src/team/application/dtos/member.dto.ts index f8b0e1c9..e67929b7 100644 --- a/src/team/application/dtos/member.dto.ts +++ b/src/team/application/dtos/member.dto.ts @@ -77,9 +77,7 @@ export const UserInviteSchema = z.object({ .describe('Дата истечения'), }); -export class UserInviteResponse extends createZodDto(UserInviteSchema) {} - -export class UserInvitesResponse extends createZodDto(UserInviteSchema) {} +export class UserInvitesResponse extends createZodDto(z.array(UserInviteSchema)) {} export const TeamMembersQuerySchema = z .object({}) From a540cefc4806d28e5eed02649f048662c285d74c Mon Sep 17 00:00:00 2001 From: Maxim Date: Fri, 26 Jun 2026 14:20:39 +0300 Subject: [PATCH 3/7] feat(project): add description field to project list response and DTO --- src/project/application/dtos/project.dto.ts | 1 + src/project/application/mappers/project.mapper.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/project/application/dtos/project.dto.ts b/src/project/application/dtos/project.dto.ts index 1051d801..4221878c 100644 --- a/src/project/application/dtos/project.dto.ts +++ b/src/project/application/dtos/project.dto.ts @@ -161,6 +161,7 @@ export const ProjectListItemSchema = z.object({ id: z.string().describe('ID проекта'), slug: z.string().describe('Slug проекта (URL-идентификатор)'), name: z.string().describe('Название проекта'), + description: z.string().describe('Описание проекта'), status: ProjectStatusSchema.default('active').describe('Текущий статус проекта'), color: z.string().describe('Цвет проекта'), icon: z.string().nullish().describe('Иконка проекта'), diff --git a/src/project/application/mappers/project.mapper.ts b/src/project/application/mappers/project.mapper.ts index e4aeaa21..3fd7c55c 100644 --- a/src/project/application/mappers/project.mapper.ts +++ b/src/project/application/mappers/project.mapper.ts @@ -42,12 +42,13 @@ export class ProjectMapper { } public static toListResponse(project: Project, member: RawMemberRow) { - const { id, slug, name, status, color, icon, createdAt } = project; + const { id, slug, name, description, status, color, icon, createdAt } = project; return { id, slug, name, + description, status, color: color || '#3b82f6', icon, From 81ef622ef1f4f65857c8959061c34b966c2c351e Mon Sep 17 00:00:00 2001 From: Maxim Date: Fri, 26 Jun 2026 21:23:09 +0300 Subject: [PATCH 4/7] refactor(issue): remove areaId from issue creating schema --- .../persistence/models/state.model.ts | 4 ++- src/issue/application/dtos/issue.dto.ts | 1 + .../use-cases/base/create.use-case.ts | 28 +++++++++---------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/area/infrastructure/persistence/models/state.model.ts b/src/area/infrastructure/persistence/models/state.model.ts index 93ba3f42..34d22667 100644 --- a/src/area/infrastructure/persistence/models/state.model.ts +++ b/src/area/infrastructure/persistence/models/state.model.ts @@ -20,7 +20,9 @@ export const states = baseSchema.table( id: text('id') .primaryKey() .$defaultFn(() => createId()), - areaId: text('area_id').references(() => areas.id, { onDelete: 'cascade' }), + areaId: text('area_id') + .notNull() + .references(() => areas.id, { onDelete: 'cascade' }), title: text('title').notNull(), description: text('description'), stateType: stateTypeEnum('state_type').notNull().default('custom'), diff --git a/src/issue/application/dtos/issue.dto.ts b/src/issue/application/dtos/issue.dto.ts index 0cee3ce8..968e20bb 100644 --- a/src/issue/application/dtos/issue.dto.ts +++ b/src/issue/application/dtos/issue.dto.ts @@ -125,6 +125,7 @@ export const IssueSchema = z.object({ export const CreateIssueSchema = IssueSchema.omit({ id: true, + areaId: true, assignee: true, reporter: true, parent: true, diff --git a/src/issue/application/use-cases/base/create.use-case.ts b/src/issue/application/use-cases/base/create.use-case.ts index 9ff89c1e..ad33bcc1 100644 --- a/src/issue/application/use-cases/base/create.use-case.ts +++ b/src/issue/application/use-cases/base/create.use-case.ts @@ -28,10 +28,21 @@ export class CreateIssueUseCase { 'admin', ]); - await this.validateContext(dto, userId, project.id, key); + let areaId; - const data: CreateIssueDto = { + if (dto.stateId) { + const state = await this.getState.execute(key, dto.stateId, userId); + areaId = state.areaId; + } else { + const area = await this.getArea.execute({ key }, userId); + areaId = area.id; + } + + await this.validateContext(dto, userId, project.id); + + const data = { ...dto, + areaId, type: dto.type ?? ISSUE_TYPE.TASK, priority: dto.priority ?? PRIORITY.MEDIUM, }; @@ -58,18 +69,7 @@ export class CreateIssueUseCase { } } - private async validateContext( - dto: CreateIssueDto, - userId: string, - projectId: string, - key: string, - ) { - if (dto.stateId) { - await this.getState.execute(key, dto.stateId, userId); - } else { - await this.getArea.execute({ key }, userId); - } - + private async validateContext(dto: CreateIssueDto, userId: string, projectId: string) { if (dto.assigneeId) { const projectMember = await this.getProjectMember.execute(projectId, dto.assigneeId); From 6206c4c3aa92b123919de0652ee2b2245d5a14f0 Mon Sep 17 00:00:00 2001 From: Maxim Date: Sat, 27 Jun 2026 19:02:43 +0300 Subject: [PATCH 5/7] refactor(states): enforce non-null constraint on area_id in states table --- migrations/0008_illegal_vapor.sql | 3 + migrations/meta/0008_snapshot.json | 2390 ++++++++++++++++++++++++++++ migrations/meta/_journal.json | 7 + 3 files changed, 2400 insertions(+) create mode 100644 migrations/0008_illegal_vapor.sql create mode 100644 migrations/meta/0008_snapshot.json diff --git a/migrations/0008_illegal_vapor.sql b/migrations/0008_illegal_vapor.sql new file mode 100644 index 00000000..cc1da5d4 --- /dev/null +++ b/migrations/0008_illegal_vapor.sql @@ -0,0 +1,3 @@ +UPDATE "base"."states" SET "area_id" = 'corrupted_id' WHERE "area_id" IS NULL; + +ALTER TABLE "base"."states" ALTER COLUMN "area_id" SET NOT NULL; \ No newline at end of file diff --git a/migrations/meta/0008_snapshot.json b/migrations/meta/0008_snapshot.json new file mode 100644 index 00000000..8a41c22e --- /dev/null +++ b/migrations/meta/0008_snapshot.json @@ -0,0 +1,2390 @@ +{ + "id": "e952bee9-3c54-4f13-bca3-450a16cfb9c2", + "prevId": "46748910-a0ec-4fd3-bae5-acefb5e6660c", + "version": "7", + "dialect": "postgresql", + "tables": { + "base.user_activity": { + "name": "user_activity", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_activity_user_id_users_id_fk": { + "name": "user_activity_user_id_users_id_fk", + "tableFrom": "user_activity", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_notifications": { + "name": "user_notifications", + "schema": "base", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"email\":{\"task_assigned\":true,\"mentions\":true,\"daily_summary\":false},\"push\":{\"task_assigned\":true,\"reminders\":true}}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_notifications_user_id_users_id_fk": { + "name": "user_notifications_user_id_users_id_fk", + "tableFrom": "user_notifications", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_preferences": { + "name": "user_preferences", + "schema": "base", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'system'" + }, + "timezone": { + "name": "timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "language": { + "name": "language", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'ru'" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_security": { + "name": "user_security", + "schema": "base", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "recovery_email": { + "name": "recovery_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "is_2fa_enabled": { + "name": "is_2fa_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "two_factor_secret": { + "name": "two_factor_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_login_at": { + "name": "last_login_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_password_change": { + "name": "last_password_change", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_security_user_id_users_id_fk": { + "name": "user_security_user_id_users_id_fk", + "tableFrom": "user_security", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.users": { + "name": "users", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "headline": { + "name": "headline", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "last_name": { + "name": "last_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "middle_name": { + "name": "middle_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "vacation_start": { + "name": "vacation_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "vacation_end": { + "name": "vacation_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "vacation_message": { + "name": "vacation_message", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'none'" + }, + "pronouns": { + "name": "pronouns", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'none'" + }, + "pronouns_custom": { + "name": "pronouns_custom", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_team_id": { + "name": "last_team_id", + "type": "text", + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.sessions": { + "name": "sessions", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "browser": { + "name": "browser", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "os": { + "name": "os", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip": { + "name": "ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true + }, + "city": { + "name": "city", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "varchar(5)", + "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()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "is_revoked": { + "name": "is_revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_identities": { + "name": "user_identities", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "provider_user_id": { + "name": "provider_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_identities_user_id_users_id_fk": { + "name": "user_identities_user_id_users_id_fk", + "tableFrom": "user_identities", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_user_id_idx": { + "name": "provider_user_id_idx", + "nullsNotDistinct": false, + "columns": [ + "provider", + "provider_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.team_members": { + "name": "team_members", + "schema": "base", + "columns": { + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "team_role", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "member_status", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'inactive'" + }, + "joined_at": { + "name": "joined_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": { + "member_status_idx": { + "name": "member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_role_idx": { + "name": "member_role_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "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", + "schemaTo": "base", + "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", + "schemaTo": "base", + "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 + }, + "base.teams": { + "name": "teams", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_url": { + "name": "cover_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "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()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "team_owner_idx": { + "name": "team_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_deleted_at_idx": { + "name": "team_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_owner_id_users_id_fk": { + "name": "teams_owner_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.project_members": { + "name": "project_members", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_member_unique_idx": { + "name": "project_member_unique_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_member_user_idx": { + "name": "project_member_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_member_project_idx": { + "name": "project_member_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_members_project_id_projects_id_fk": { + "name": "project_members_project_id_projects_id_fk", + "tableFrom": "project_members", + "tableTo": "projects", + "schemaTo": "base", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_members_user_id_users_id_fk": { + "name": "project_members_user_id_users_id_fk", + "tableFrom": "project_members", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_members_added_by_users_id_fk": { + "name": "project_members_added_by_users_id_fk", + "tableFrom": "project_members", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "added_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.project_settings": { + "name": "project_settings", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_view": { + "name": "default_view", + "type": "layout_type", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'kanban'" + }, + "task_prefix": { + "name": "task_prefix", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "auto_close_days": { + "name": "auto_close_days", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_tasks_per_area": { + "name": "max_tasks_per_area", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_members": { + "name": "max_members", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_areas": { + "name": "max_areas", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "allow_guests": { + "name": "allow_guests", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "time_tracking": { + "name": "time_tracking", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "time_tracking_mode": { + "name": "time_tracking_mode", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'optional'" + }, + "default_assignee_id": { + "name": "default_assignee_id", + "type": "text", + "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": { + "project_settings_project_idx": { + "name": "project_settings_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_settings_project_id_projects_id_fk": { + "name": "project_settings_project_id_projects_id_fk", + "tableFrom": "project_settings", + "tableTo": "projects", + "schemaTo": "base", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_settings_default_assignee_id_users_id_fk": { + "name": "project_settings_default_assignee_id_users_id_fk", + "tableFrom": "project_settings", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "default_assignee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "project_settings_project_id_unique": { + "name": "project_settings_project_id_unique", + "nullsNotDistinct": false, + "columns": [ + "project_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.project_shares": { + "name": "project_shares", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "token_idx": { + "name": "token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_share_project_id_idx": { + "name": "project_share_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_shares_project_id_projects_id_fk": { + "name": "project_shares_project_id_projects_id_fk", + "tableFrom": "project_shares", + "tableTo": "projects", + "schemaTo": "base", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_shares_created_by_users_id_fk": { + "name": "project_shares_created_by_users_id_fk", + "tableFrom": "project_shares", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "project_shares_token_unique": { + "name": "project_shares_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.projects": { + "name": "projects", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "descriptionHtml": { + "name": "descriptionHtml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "project_status", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "project_visibility", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "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()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "project_team_slug_idx": { + "name": "project_team_slug_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"base\".\"projects\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_owner_id_idx": { + "name": "project_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_team_id_idx": { + "name": "project_team_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_team_id_teams_id_fk": { + "name": "projects_team_id_teams_id_fk", + "tableFrom": "projects", + "tableTo": "teams", + "schemaTo": "base", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "projects_owner_id_users_id_fk": { + "name": "projects_owner_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "projects_slug_unique": { + "name": "projects_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.areas": { + "name": "areas", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description_html": { + "name": "description_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "tasks_count": { + "name": "tasks_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "default_view": { + "name": "default_view", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'kanban'" + }, + "icon": { + "name": "icon", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_tasks_limit": { + "name": "max_tasks_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_locked": { + "name": "is_locked", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "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()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_areas_slug": { + "name": "idx_areas_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_areas_project_active": { + "name": "idx_areas_project_active", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"base\".\"areas\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_areas_created_by": { + "name": "idx_areas_created_by", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"base\".\"areas\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_areas_deleted_at": { + "name": "idx_areas_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"base\".\"areas\".\"deleted_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "areas_project_id_projects_id_fk": { + "name": "areas_project_id_projects_id_fk", + "tableFrom": "areas", + "tableTo": "projects", + "schemaTo": "base", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "areas_created_by_users_id_fk": { + "name": "areas_created_by_users_id_fk", + "tableFrom": "areas", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "areas_slug_unique": { + "name": "areas_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.states": { + "name": "states", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "area_id": { + "name": "area_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_type": { + "name": "state_type", + "type": "state_type", + "primaryKey": false, + "notNull": true, + "default": "'custom'" + }, + "category": { + "name": "category", + "type": "state_category", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "color": { + "name": "color", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_visible": { + "name": "is_visible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "max_tasks_limit": { + "name": "max_tasks_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_transition_to": { + "name": "auto_transition_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notify_on_enter": { + "name": "notify_on_enter", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "notify_on_exit": { + "name": "notify_on_exit", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_locked": { + "name": "is_locked", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "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()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_states_position": { + "name": "idx_states_position", + "columns": [ + { + "expression": "area_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_states_title": { + "name": "idx_states_title", + "columns": [ + { + "expression": "area_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_states_created_at": { + "name": "idx_states_created_at", + "columns": [ + { + "expression": "area_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_states_search": { + "name": "idx_states_search", + "columns": [ + { + "expression": "area_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_states_unique_title": { + "name": "idx_states_unique_title", + "columns": [ + { + "expression": "area_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"base\".\"states\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_states_deleted_at": { + "name": "idx_states_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"base\".\"states\".\"deleted_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "states_area_id_areas_id_fk": { + "name": "states_area_id_areas_id_fk", + "tableFrom": "states", + "tableTo": "areas", + "schemaTo": "base", + "columnsFrom": [ + "area_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "states_created_by_users_id_fk": { + "name": "states_created_by_users_id_fk", + "tableFrom": "states", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.issues": { + "name": "issues", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description_html": { + "name": "description_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "priority", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "type": { + "name": "type", + "type": "issue_type", + "primaryKey": false, + "notNull": true, + "default": "'task'" + }, + "area_id": { + "name": "area_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_id": { + "name": "state_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "assignee_id": { + "name": "assignee_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reporter_id": { + "name": "reporter_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "story_points": { + "name": "story_points", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "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()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_issue_area_state": { + "name": "idx_issue_area_state", + "columns": [ + { + "expression": "area_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"base\".\"issues\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issue_assignee": { + "name": "idx_issue_assignee", + "columns": [ + { + "expression": "assignee_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"base\".\"issues\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issue_parent": { + "name": "idx_issue_parent", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"base\".\"issues\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issue_priority": { + "name": "idx_issue_priority", + "columns": [ + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"base\".\"issues\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issue_type": { + "name": "idx_issue_type", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"base\".\"issues\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issue_search": { + "name": "idx_issue_search", + "columns": [ + { + "expression": "to_tsvector('english', COALESCE(\"title\", '') || ' ' || COALESCE(\"description\", ''))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"base\".\"issues\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "issues_area_id_areas_id_fk": { + "name": "issues_area_id_areas_id_fk", + "tableFrom": "issues", + "tableTo": "areas", + "schemaTo": "base", + "columnsFrom": [ + "area_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issues_state_id_states_id_fk": { + "name": "issues_state_id_states_id_fk", + "tableFrom": "issues", + "tableTo": "states", + "schemaTo": "base", + "columnsFrom": [ + "state_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_assignee_id_users_id_fk": { + "name": "issues_assignee_id_users_id_fk", + "tableFrom": "issues", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "assignee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_reporter_id_users_id_fk": { + "name": "issues_reporter_id_users_id_fk", + "tableFrom": "issues", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "reporter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_parent_id_issues_id_fk": { + "name": "issues_parent_id_issues_id_fk", + "tableFrom": "issues", + "tableTo": "issues", + "schemaTo": "base", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "no_self_parent": { + "name": "no_self_parent", + "value": "\"base\".\"issues\".\"parent_id\" IS NULL OR \"base\".\"issues\".\"parent_id\" != \"base\".\"issues\".\"id\"" + } + }, + "isRLSEnabled": false + } + }, + "enums": { + "base.team_role": { + "name": "team_role", + "schema": "base", + "values": [ + "owner", + "admin", + "member", + "viewer" + ] + }, + "base.member_status": { + "name": "member_status", + "schema": "base", + "values": [ + "active", + "banned", + "inactive" + ] + }, + "base.layout_type": { + "name": "layout_type", + "schema": "base", + "values": [ + "kanban", + "list", + "calendar", + "gantt" + ] + }, + "base.project_status": { + "name": "project_status", + "schema": "base", + "values": [ + "active", + "archived", + "template", + "deleted" + ] + }, + "base.project_visibility": { + "name": "project_visibility", + "schema": "base", + "values": [ + "public", + "private" + ] + }, + "base.state_category": { + "name": "state_category", + "schema": "base", + "values": [ + "backlog", + "active", + "review", + "completed", + "archived" + ] + }, + "base.state_type": { + "name": "state_type", + "schema": "base", + "values": [ + "backlog", + "todo", + "in_progress", + "review", + "done", + "archived", + "custom" + ] + }, + "base.issue_type": { + "name": "issue_type", + "schema": "base", + "values": [ + "bug", + "task", + "epic" + ] + }, + "base.priority": { + "name": "priority", + "schema": "base", + "values": [ + "critical", + "low", + "medium", + "high" + ] + } + }, + "schemas": { + "base": "base" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index ffa45f72..36b7db17 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -57,6 +57,13 @@ "when": 1782216398515, "tag": "0007_add_team_members_roles", "breakpoints": false + }, + { + "idx": 8, + "version": "7", + "when": 1782575945926, + "tag": "0008_illegal_vapor", + "breakpoints": false } ] } \ No newline at end of file From 8e90dcf3482d709a1e76f179eafd8605269bc027 Mon Sep 17 00:00:00 2001 From: Maxim Date: Sun, 28 Jun 2026 16:35:34 +0300 Subject: [PATCH 6/7] fix(auth): reuse cache key constant and handle already-verified reset codes --- .../password/confirm-reset-password.use-case.ts | 3 ++- .../password/verify-reset-password.use-case.ts | 13 +++++++------ src/auth/domain/errors/auth.error.ts | 2 ++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/auth/application/use-cases/password/confirm-reset-password.use-case.ts b/src/auth/application/use-cases/password/confirm-reset-password.use-case.ts index 4cba5968..78b96d60 100644 --- a/src/auth/application/use-cases/password/confirm-reset-password.use-case.ts +++ b/src/auth/application/use-cases/password/confirm-reset-password.use-case.ts @@ -1,3 +1,4 @@ +import { RESET_PASSWORD_CACHE_KEY } from '@core/auth/infrastructure/constants'; import { UpdatePasswordUseCase } from '@core/user'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; @@ -17,7 +18,7 @@ export class ConfirmResetPasswordUseCase { ) {} async execute(dto: PasswordResetConfirmDto) { - const redisKey = `pass:reset:${dto.email}`; + const redisKey = RESET_PASSWORD_CACHE_KEY(dto.email); const cachedData = await this.cacheService.getOne(redisKey); if (!cachedData) { diff --git a/src/auth/application/use-cases/password/verify-reset-password.use-case.ts b/src/auth/application/use-cases/password/verify-reset-password.use-case.ts index 937e0ed0..59aa3021 100644 --- a/src/auth/application/use-cases/password/verify-reset-password.use-case.ts +++ b/src/auth/application/use-cases/password/verify-reset-password.use-case.ts @@ -1,3 +1,4 @@ +import { RESET_PASSWORD_CACHE_KEY } from '@core/auth/infrastructure/constants'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; import { ICacheService } from '@shared/adapters/cache/ports'; @@ -15,7 +16,7 @@ export class VerifyResetPasswordUseCase { ) {} async execute(dto: VerifyResetCodeDto) { - const redisKey = `pass:reset:${dto.email}`; + const redisKey = RESET_PASSWORD_CACHE_KEY(dto.email); const cachedData = await this.cacheService.getOne(redisKey); if (!cachedData) { @@ -43,14 +44,14 @@ export class VerifyResetPasswordUseCase { ); } - if (!resetSession.isVerified) { + if (resetSession.isVerified) { throw new BaseException( { - code: AuthErrorCodes.CODE_NOT_VERIFIED, - message: AuthErrorMessages[AuthErrorCodes.CODE_NOT_VERIFIED], - details: [{ target: 'code', message: 'Код подтверждения не верифицирован' }], + code: AuthErrorCodes.CODE_ALREADY_VERIFIED, + message: AuthErrorMessages[AuthErrorCodes.CODE_ALREADY_VERIFIED], + details: [{ target: 'code', message: 'Код подтверждения уже верифицирован' }], }, - HttpStatus.FORBIDDEN, + HttpStatus.BAD_REQUEST, ); } diff --git a/src/auth/domain/errors/auth.error.ts b/src/auth/domain/errors/auth.error.ts index 91cf0c94..9c1fbba6 100644 --- a/src/auth/domain/errors/auth.error.ts +++ b/src/auth/domain/errors/auth.error.ts @@ -10,6 +10,7 @@ export const AuthErrorCodes = { TOKEN_INVALID: 'AUTH.TOKEN_INVALID', REFRESH_FAILED: 'AUTH.REFRESH_FAILED', CODE_NOT_VERIFIED: 'AUTH.CODE_NOT_VERIFIED', + CODE_ALREADY_VERIFIED: 'AUTH.CODE_ALREADY_VERIFIED', SIGNUP_FAILED: 'AUTH.SIGNUP_FAILED', UNAUTHORIZED: 'AUTH.UNAUTHORIZED', RESET_SESSION_NOT_FOUND: 'AUTH.RESET_SESSION_NOT_FOUND', @@ -39,6 +40,7 @@ export const AuthErrorMessages: Record = { [AuthErrorCodes.SESSION_REVOKED]: 'Сессия была отозвана. Выполните вход заново', [AuthErrorCodes.STRATEGY_NOT_FOUND]: 'Неизвестный контекст операции', [AuthErrorCodes.CODE_NOT_VERIFIED]: 'Код подтверждения еще не был верифицирован', + [AuthErrorCodes.CODE_ALREADY_VERIFIED]: 'Код подтверждения уже верифицирован', [AuthErrorCodes.UNAUTHORIZED]: 'Необходимо выполнить вход', [AuthErrorCodes.RESET_SESSION_NOT_FOUND]: 'Запрос на сброс пароля не найден', [AuthErrorCodes.REGISTRATION_EXPIRED]: 'Регистрация истекла. Зарегистрируйтесь заново', From 1ea9aeabe6f929c0453ac13bf0bca55710d06458 Mon Sep 17 00:00:00 2001 From: Maxim Date: Sun, 28 Jun 2026 18:46:27 +0300 Subject: [PATCH 7/7] fix(user): remove default values for gender and pronouns in DTO and fix preference handling --- src/user/application/dtos/user.dto.ts | 2 -- src/user/application/use-cases/update-profile.use-case.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/user/application/dtos/user.dto.ts b/src/user/application/dtos/user.dto.ts index 67a70dc0..cf9869d6 100644 --- a/src/user/application/dtos/user.dto.ts +++ b/src/user/application/dtos/user.dto.ts @@ -127,7 +127,6 @@ export const UpdateProfileSchema = z phone: z.string().describe('Номер телефона (для связи)').nullish(), gender: z .enum(['none', 'male', 'female', 'non_binary', 'other', 'prefer_not_to_say']) - .default('none') .optional() .describe( 'Пол пользователя: none - не указан, male - мужской, female - женский, non_binary - небинарный, other - другой, prefer_not_to_say - предпочитаю не указывать', @@ -137,7 +136,6 @@ export const UpdateProfileSchema = z vacationMessage: z.string().nullish().describe('Сообщение автоответчика на время отпуска'), pronouns: z .enum(['he_him', 'she_her', 'they_them', 'other', 'none']) - .default('none') .optional() .describe( 'Предпочитаемые местоимения: he_him - он/его, she_her - она/ее, they_them - они/их, other - другие, none - не указаны', diff --git a/src/user/application/use-cases/update-profile.use-case.ts b/src/user/application/use-cases/update-profile.use-case.ts index a1c23028..40dec7b9 100644 --- a/src/user/application/use-cases/update-profile.use-case.ts +++ b/src/user/application/use-cases/update-profile.use-case.ts @@ -41,7 +41,7 @@ export class UpdateProfileUseCase { const result = await this.userRepo.updateProfile( entity.user.id, removeUndefined(profile), - preferences, + removeUndefined(preferences), ); await this.userRepo.logActivity({