diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-27 01:16:20 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-27 01:16:20 +0000 |
| commit | e9897d416b3e7327bbd4d4aef887eee37751ae82 (patch) | |
| tree | bd20ce6eadf9b21755bd7425492d2d31c7700a0e /db/schema/users.ts | |
| parent | 3bf1952c1dad9d479bb8b22031b06a7434d37c37 (diff) | |
(대표님) 20250627 오전 10시 작업사항
Diffstat (limited to 'db/schema/users.ts')
| -rw-r--r-- | db/schema/users.ts | 161 |
1 files changed, 157 insertions, 4 deletions
diff --git a/db/schema/users.ts b/db/schema/users.ts index f6a66a8f..ad1224d2 100644 --- a/db/schema/users.ts +++ b/db/schema/users.ts @@ -1,4 +1,5 @@ -import { integer, boolean,serial, pgTable, varchar,timestamp,pgEnum ,pgView, text, primaryKey} from "drizzle-orm/pg-core"; +import { integer, boolean,serial, pgTable, varchar,timestamp,pgEnum ,pgView, text, primaryKey, index, + uniqueIndex,} from "drizzle-orm/pg-core"; import { eq , sql} from "drizzle-orm"; import { vendors } from "./vendors"; import { techVendors } from "./techVendors"; @@ -11,17 +12,168 @@ export const users = pgTable("users", { name: varchar("name", { length: 255 }).notNull(), email: varchar("email", { length: 255 }).notNull().unique(), companyId: integer("company_id") - .references(() => vendors.id, { onDelete: "set null" }), + .references(() => vendors.id, { onDelete: "set null" }), techCompanyId: integer("tech_company_id") - .references(() => techVendors.id, { onDelete: "set null" }), + .references(() => techVendors.id, { onDelete: "set null" }), domain: userDomainEnum("domain").notNull().default("partners"), createdAt: timestamp("created_at", { withTimezone: true }) .defaultNow() .notNull(), imageUrl: varchar("image_url", { length: 1024 }), - language: varchar("language", { length: 10 }).default("en"), // 언어 필드 추가 (기본값: 영어) + language: varchar("language", { length: 10 }).default("en"), + + // MFA 관련 새 컬럼들 + phone: varchar("phone", { length: 20 }), // 국제 형식 전화번호 (+82-10-1234-5678) + mfaEnabled: boolean("mfa_enabled").default(false).notNull(), + mfaSecret: varchar("mfa_secret", { length: 32 }), // TOTP secret (나중에 사용) + + // 계정 보안 관련 + isLocked: boolean("is_locked").default(false).notNull(), + lockoutUntil: timestamp("lockout_until", { withTimezone: true }), + failedLoginAttempts: integer("failed_login_attempts").default(0).notNull(), + lastLoginAt: timestamp("last_login_at", { withTimezone: true }), + passwordChangeRequired: boolean("password_change_required").default(false).notNull(), + + // 비활성화 관련 새 필드들 + isActive: boolean("is_active").default(true).notNull(), + deactivatedAt: timestamp("deactivated_at", { withTimezone: true }), + deactivationReason: varchar("deactivation_reason", { length: 50 }), // 'INACTIVE', 'ADMIN', 'GDPR' 등 + +}, (table) => { + return { + emailIdx: uniqueIndex("users_email_idx").on(table.email), + phoneIdx: index("users_phone_idx").on(table.phone), + }; +}); + +// 패스워드 테이블 (현재 활성 패스워드) +export const passwords = pgTable("passwords", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + userId: integer("user_id") + .references(() => users.id, { onDelete: "cascade" }) + .notNull(), + passwordHash: varchar("password_hash", { length: 255 }).notNull(), // bcrypt hash + salt: varchar("salt", { length: 255 }).notNull(), // 추가 salt + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true }), // 패스워드 만료일 + isActive: boolean("is_active").default(true).notNull(), + + // 패스워드 메타데이터 + strength: integer("strength").notNull(), // 1-5 강도 점수 + hasUppercase: boolean("has_uppercase").notNull(), + hasLowercase: boolean("has_lowercase").notNull(), + hasNumbers: boolean("has_numbers").notNull(), + hasSymbols: boolean("has_symbols").notNull(), + length: integer("length").notNull(), +}, (table) => { + return { + userIdIdx: index("passwords_user_id_idx").on(table.userId), + activeIdx: index("passwords_active_idx").on(table.isActive), + }; +}); + +// 패스워드 히스토리 (재사용 방지) +export const passwordHistory = pgTable("password_history", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + userId: integer("user_id") + .references(() => users.id, { onDelete: "cascade" }) + .notNull(), + passwordHash: varchar("password_hash", { length: 255 }).notNull(), + salt: varchar("salt", { length: 255 }).notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + replacedAt: timestamp("replaced_at", { withTimezone: true }), // 언제 교체되었는지 +}, (table) => { + return { + userIdIdx: index("password_history_user_id_idx").on(table.userId), + createdAtIdx: index("password_history_created_at_idx").on(table.createdAt), + }; +}); + +// 로그인 시도 로그 (보안 감사) +export const loginAttempts = pgTable("login_attempts", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + email: varchar("email", { length: 255 }).notNull(), + userId: integer("user_id").references(() => users.id, { onDelete: "set null" }), + success: boolean("success").notNull(), + ipAddress: varchar("ip_address", { length: 45 }).notNull(), // IPv6 지원 + userAgent: text("user_agent"), + failureReason: varchar("failure_reason", { length: 100 }), // 실패 이유 + attemptedAt: timestamp("attempted_at", { withTimezone: true }) + .defaultNow() + .notNull(), + + // 지리적 정보 (옵셔널) + country: varchar("country", { length: 2 }), + city: varchar("city", { length: 100 }), +}, (table) => { + return { + emailIdx: index("login_attempts_email_idx").on(table.email), + attemptedAtIdx: index("login_attempts_attempted_at_idx").on(table.attemptedAt), + ipAddressIdx: index("login_attempts_ip_address_idx").on(table.ipAddress), + }; +}); +// MFA 토큰 테이블 (SMS, TOTP 등) +export const mfaTokens = pgTable("mfa_tokens", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + userId: integer("user_id") + .references(() => users.id, { onDelete: "cascade" }) + .notNull(), + token: varchar("token", { length: 255 }).notNull(), // SMS 코드나 TOTP + type: varchar("type", { length: 20 }).notNull(), // 'sms', 'totp', 'backup' + expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), + usedAt: timestamp("used_at", { withTimezone: true }), + isActive: boolean("is_active").default(true).notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + + // SMS 관련 추가 정보 + phoneNumber: varchar("phone_number", { length: 20 }), // 전송된 전화번호 + attempts: integer("attempts").default(0).notNull(), // 시도 횟수 +}, (table) => { + return { + userIdIdx: index("mfa_tokens_user_id_idx").on(table.userId), + tokenIdx: index("mfa_tokens_token_idx").on(table.token), + expiresAtIdx: index("mfa_tokens_expires_at_idx").on(table.expiresAt), + }; +}); +// 보안 설정 테이블 +export const securitySettings = pgTable("security_settings", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + + // 패스워드 정책 + minPasswordLength: integer("min_password_length").default(8).notNull(), + requireUppercase: boolean("require_uppercase").default(true).notNull(), + requireLowercase: boolean("require_lowercase").default(true).notNull(), + requireNumbers: boolean("require_numbers").default(true).notNull(), + requireSymbols: boolean("require_symbols").default(true).notNull(), + passwordExpiryDays: integer("password_expiry_days").default(90), // null이면 만료 없음 + passwordHistoryCount: integer("password_history_count").default(5).notNull(), + + // 계정 잠금 정책 + maxFailedAttempts: integer("max_failed_attempts").default(5).notNull(), + lockoutDurationMinutes: integer("lockout_duration_minutes").default(30).notNull(), + + // MFA 정책 + requireMfaForPartners: boolean("require_mfa_for_partners").default(true).notNull(), + smsTokenExpiryMinutes: integer("sms_token_expiry_minutes").default(5).notNull(), + maxSmsAttemptsPerDay: integer("max_sms_attempts_per_day").default(10).notNull(), + + // 세션 관리 + sessionTimeoutMinutes: integer("session_timeout_minutes").default(480).notNull(), // 8시간 + + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), }); @@ -105,6 +257,7 @@ export const userView = pgView("user_view").as((qb) => { // 2) userName: string user_name: sql<string>`${users.name}`.as("user_name"), + user_phone: sql<string>`${users.phone}`.as("user_phone"), // 3) userEmail: string user_email: sql<string>`${users.email}`.as("user_email"), |
