Add vault secret manager backend with AES-256-GCM encryption
Secrets are encrypted at rest in the database. The derived encryption key is held in memory only — on reboot, an authorized user must enter the master password to unlock. Admins can also manually lock the vault. - vault_config, vault_category, vault_category_permission, vault_entry tables - AES-256-GCM encryption with PBKDF2-derived key + per-entry IV - Master password initialize/unlock/lock/change lifecycle - Category CRUD with role/user permission model (view/edit/admin) - Entry CRUD with reveal endpoint (POST to avoid caching) - Secret values never returned in list/detail responses - vault.view/edit/admin RBAC permissions seeded - 19 API integration tests covering full lifecycle
This commit is contained in:
57
packages/backend/src/db/schema/vault.ts
Normal file
57
packages/backend/src/db/schema/vault.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { pgTable, uuid, varchar, text, timestamp, boolean } from 'drizzle-orm/pg-core'
|
||||
import { users } from './users.js'
|
||||
import { roles } from './rbac.js'
|
||||
import { storageFolderAccessEnum } from './storage.js'
|
||||
|
||||
export const vaultConfig = pgTable('vault_config', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
masterKeySalt: text('master_key_salt').notNull(),
|
||||
masterKeyHash: text('master_key_hash').notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
})
|
||||
|
||||
export const vaultCategories = pgTable('vault_category', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
description: text('description'),
|
||||
createdBy: uuid('created_by').references(() => users.id),
|
||||
isPublic: boolean('is_public').notNull().default(false),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
})
|
||||
|
||||
export const vaultCategoryPermissions = pgTable('vault_category_permission', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
categoryId: uuid('category_id')
|
||||
.notNull()
|
||||
.references(() => vaultCategories.id, { onDelete: 'cascade' }),
|
||||
roleId: uuid('role_id').references(() => roles.id),
|
||||
userId: uuid('user_id').references(() => users.id),
|
||||
accessLevel: storageFolderAccessEnum('access_level').notNull().default('view'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
})
|
||||
|
||||
export const vaultEntries = pgTable('vault_entry', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
categoryId: uuid('category_id')
|
||||
.notNull()
|
||||
.references(() => vaultCategories.id, { onDelete: 'cascade' }),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
username: varchar('username', { length: 255 }),
|
||||
url: varchar('url', { length: 1000 }),
|
||||
notes: text('notes'),
|
||||
encryptedValue: text('encrypted_value'),
|
||||
iv: text('iv'),
|
||||
createdBy: uuid('created_by').references(() => users.id),
|
||||
updatedBy: uuid('updated_by').references(() => users.id),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
})
|
||||
|
||||
export type VaultConfig = typeof vaultConfig.$inferSelect
|
||||
export type VaultCategory = typeof vaultCategories.$inferSelect
|
||||
export type VaultCategoryInsert = typeof vaultCategories.$inferInsert
|
||||
export type VaultCategoryPermission = typeof vaultCategoryPermissions.$inferSelect
|
||||
export type VaultEntry = typeof vaultEntries.$inferSelect
|
||||
export type VaultEntryInsert = typeof vaultEntries.$inferInsert
|
||||
Reference in New Issue
Block a user