summaryrefslogtreecommitdiff
path: root/db/schema/permissions.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-26 09:57:24 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-26 09:57:24 +0000
commit8b23b471638a155fd1bfa3a8c853b26d9315b272 (patch)
tree47353e9dd342011cb2f1dcd24b09661707a8421b /db/schema/permissions.ts
parentd62368d2b68d73da895977e60a18f9b1286b0545 (diff)
(대표님) 권한관리, 문서업로드, rfq첨부, SWP문서룰 등
(최겸) 입찰
Diffstat (limited to 'db/schema/permissions.ts')
-rw-r--r--db/schema/permissions.ts204
1 files changed, 204 insertions, 0 deletions
diff --git a/db/schema/permissions.ts b/db/schema/permissions.ts
new file mode 100644
index 00000000..5f641f83
--- /dev/null
+++ b/db/schema/permissions.ts
@@ -0,0 +1,204 @@
+// 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),
+})); \ No newline at end of file