// db/schema/permissions.ts import {unique, pgTable, varchar, text, timestamp, boolean, integer, uniqueIndex, index, primaryKey, pgEnum } from "drizzle-orm/pg-core"; import { userDomainEnum, users, roles, menuAssignments } from "@/db/schema"; import { sql } from "drizzle-orm"; // 권한 타입 enum export const permissionTypeEnum = pgEnum("permission_type", [ "menu_access", // 메뉴 접근 "action", // 액션 실행 (버튼 클릭 등) "data_read", // 데이터 읽기 "data_write", // 데이터 쓰기 "data_delete", // 데이터 삭제 "approve", // 승인 "export", // 내보내기 "import" // 가져오기 ]); // 권한 범위 enum export const permissionScopeEnum = pgEnum("permission_scope", [ "all", // 전체 데이터 "domain", // 도메인 내 전체 "assigned", // 담당자 할당된 것만 "own", // 본인 것만 "department", // 부서 내 "company" // 회사 내 ]); // 확장된 권한 테이블 export const permissions = pgTable("permissions", { id: integer("id").primaryKey().generatedAlwaysAsIdentity(), permissionKey: varchar("permission_key", { length: 255 }).notNull().unique(), permissionType: permissionTypeEnum("permission_type").notNull(), resource: varchar("resource", { length: 100 }).notNull(), action: varchar("action", { length: 50 }).notNull(), scope: permissionScopeEnum("permission_scope").default("own"), // 메뉴 연관 menuPath: varchar("menu_path", { length: 255 }) .references(() => menuAssignments.menuPath, { onDelete: "set null" }), // UI 요소 매핑 uiElement: varchar("ui_element", { length: 255 }), // 버튼 ID, 컴포넌트 이름 등 name: varchar("name", { length: 255 }).notNull(), description: text("description"), isSystem: boolean("is_system").default(false).notNull(), // 시스템 권한 여부 isActive: boolean("is_active").default(true).notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }, (table) => ({ permissionKeyIdx: uniqueIndex("permissions_key_idx").on(table.permissionKey), resourceActionIdx: index("permissions_resource_action_idx").on(table.resource, table.action), menuPathIdx: index("permissions_menu_path_idx").on(table.menuPath), typeIdx: index("permissions_type_idx").on(table.permissionType), })); // 권한 그룹 (권한 묶음) export const permissionGroups = pgTable("permission_groups", { id: integer("id").primaryKey().generatedAlwaysAsIdentity(), groupKey: varchar("group_key", { length: 100 }).notNull().unique(), name: varchar("name", { length: 255 }).notNull(), description: text("description"), domain: userDomainEnum("domain"), isActive: boolean("is_active").default(true).notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }, (table) => ({ groupKeyIdx: uniqueIndex("permission_groups_key_idx").on(table.groupKey), domainIdx: index("permission_groups_domain_idx").on(table.domain), })); // 권한 그룹 멤버십 export const permissionGroupMembers = pgTable("permission_group_members", { groupId: integer("group_id") .references(() => permissionGroups.id, { onDelete: "cascade" }) .notNull(), permissionId: integer("permission_id") .references(() => permissions.id, { onDelete: "cascade" }) .notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), }, (table) => ({ pk: primaryKey({ columns: [table.groupId, table.permissionId] }), groupIdx: index("pgm_group_idx").on(table.groupId), permissionIdx: index("pgm_permission_idx").on(table.permissionId), })); // 역할-권한 매핑 (기존 rolePermissions 대체) export const rolePermissions = pgTable("role_permissions", { roleId: integer("role_id") .references(() => roles.id, { onDelete: "cascade" }) .notNull(), permissionId: integer("permission_id") .references(() => permissions.id, { onDelete: "cascade" }), permissionGroupId: integer("permission_group_id") .references(() => permissionGroups.id, { onDelete: "cascade" }), // 권한 부여 정보 grantedBy: integer("granted_by") .references(() => users.id, { onDelete: "set null" }), grantedAt: timestamp("granted_at").defaultNow().notNull(), expiresAt: timestamp("expires_at"), // 권한 만료일 isActive: boolean("is_active").default(true).notNull(), notes: text("notes"), }, (table) => ({ pk: primaryKey({ columns: [table.roleId, table.permissionId, table.permissionGroupId] }), roleIdx: index("role_permissions_role_idx").on(table.roleId), permissionIdx: index("role_permissions_permission_idx").on(table.permissionId), groupIdx: index("role_permissions_group_idx").on(table.permissionGroupId), })); // 사용자 직접 권한 (역할 외 추가 권한) export const userPermissions = pgTable("user_permissions", { userId: integer("user_id") .references(() => users.id, { onDelete: "cascade" }) .notNull(), // 직접 부여를 그룹 없이도 가능하게 하려면 permissionId는 PK에 포함되므로 notNull 권장 permissionId: integer("permission_id") .references(() => permissions.id, { onDelete: "cascade" }) .notNull(), // 이제 nullable (직접 부여 시 NULL) permissionGroupId: integer("permission_group_id") .references(() => permissionGroups.id, { onDelete: "cascade" }), // 권한 타입 isGrant: boolean("is_grant").default(true).notNull(), // true: 부여, false: 제한 // 권한 부여 정보 grantedBy: integer("granted_by") .references(() => users.id, { onDelete: "set null" }), grantedAt: timestamp("granted_at").defaultNow().notNull(), expiresAt: timestamp("expires_at"), isActive: boolean("is_active").default(true).notNull(), reason: text("reason"), }, (table) => ({ // PK에서 group 제거 → (user_id, permission_id) pk: primaryKey({ columns: [table.userId, table.permissionId] }), // 그룹이 있는 행에 대해서만 유니크 보장 (NULL은 유니크 고려에서 제외됨) userPermGroupUnique: uniqueIndex("user_permissions_user_perm_group_unique") .on(table.userId, table.permissionId, table.permissionGroupId) .where(sql`permission_group_id IS NOT NULL`), userIdx: index("user_permissions_user_idx").on(table.userId), permissionIdx: index("user_permissions_permission_idx").on(table.permissionId), groupIdx: index("user_permissions_group_idx").on(table.permissionGroupId), })); // 메뉴별 기본 필요 권한 export const menuRequiredPermissions = pgTable("menu_required_permissions", { id: integer("id").primaryKey().generatedAlwaysAsIdentity(), menuPath: varchar("menu_path", { length: 255 }) .references(() => menuAssignments.menuPath, { onDelete: "cascade" }) .notNull(), permissionId: integer("permission_id") .references(() => permissions.id, { onDelete: "cascade" }) .notNull(), isRequired: boolean("is_required").default(true).notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), }, (table) => ({ uq: unique("uq_menu_path_permission_id").on(table.menuPath, table.permissionId), menuPathIdx: index("mrp_menu_path_idx").on(table.menuPath), permissionIdx: index("mrp_permission_idx").on(table.permissionId), })); // 권한 감사 로그 export const permissionAuditLogs = pgTable("permission_audit_logs", { id: integer("id").primaryKey().generatedAlwaysAsIdentity(), // 대상 targetType: varchar("target_type", { length: 50 }).notNull(), // 'user', 'role' targetId: integer("target_id").notNull(), // 권한 permissionId: integer("permission_id") .references(() => permissions.id, { onDelete: "set null" }), permissionGroupId: integer("permission_group_id") .references(() => permissionGroups.id, { onDelete: "set null" }), // 액션 action: varchar("action", { length: 50 }).notNull(), // 'grant', 'revoke', 'expire' // 실행자 performedBy: integer("performed_by") .references(() => users.id, { onDelete: "set null" }), performedAt: timestamp("performed_at").defaultNow().notNull(), // 추가 정보 reason: text("reason"), metadata: text("metadata"), // JSON 형태의 추가 정보 ipAddress: varchar("ip_address", { length: 45 }), }, (table) => ({ targetIdx: index("pal_target_idx").on(table.targetType, table.targetId), performedByIdx: index("pal_performed_by_idx").on(table.performedBy), performedAtIdx: index("pal_performed_at_idx").on(table.performedAt), }));