Add shared file storage with folder tree, permissions, and file manager UI

New document hub for centralized file storage — replaces scattered
drives and USB sticks for non-technical SMBs. Three new tables:
storage_folder (nested hierarchy), storage_folder_permission (role
and user-level access control), storage_file.

Backend: folder CRUD with nested paths, file upload/download via
signed URLs, permission checks (view/edit/admin with inheritance
from parent folders), public/private toggle, breadcrumb navigation,
file search.

Frontend: two-panel file manager — collapsible folder tree on left,
icon grid view on right. Folder icons by type, file size display,
upload button, context menu for download/delete. Breadcrumb nav.
Files sidebar link added.
This commit is contained in:
Ryan Moon
2026-03-29 15:31:20 -05:00
parent d36c6f7135
commit 0f6cc104d2
13 changed files with 1093 additions and 1 deletions

View File

@@ -0,0 +1,55 @@
import {
pgTable,
uuid,
varchar,
text,
timestamp,
boolean,
integer,
pgEnum,
} from 'drizzle-orm/pg-core'
import { users } from './users.js'
import { roles } from './rbac.js'
export const storageFolderAccessEnum = pgEnum('storage_folder_access', ['view', 'edit', 'admin'])
export const storageFolders = pgTable('storage_folder', {
id: uuid('id').primaryKey().defaultRandom(),
parentId: uuid('parent_id'),
name: varchar('name', { length: 255 }).notNull(),
path: text('path').notNull().default('/'),
createdBy: uuid('created_by').references(() => users.id),
isPublic: boolean('is_public').notNull().default(true),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
})
export const storageFolderPermissions = pgTable('storage_folder_permission', {
id: uuid('id').primaryKey().defaultRandom(),
folderId: uuid('folder_id')
.notNull()
.references(() => storageFolders.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 storageFiles = pgTable('storage_file', {
id: uuid('id').primaryKey().defaultRandom(),
folderId: uuid('folder_id')
.notNull()
.references(() => storageFolders.id, { onDelete: 'cascade' }),
filename: varchar('filename', { length: 255 }).notNull(),
path: varchar('path', { length: 1000 }).notNull(),
contentType: varchar('content_type', { length: 100 }).notNull(),
sizeBytes: integer('size_bytes').notNull(),
uploadedBy: uuid('uploaded_by').references(() => users.id),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
export type StorageFolder = typeof storageFolders.$inferSelect
export type StorageFolderInsert = typeof storageFolders.$inferInsert
export type StorageFolderPermission = typeof storageFolderPermissions.$inferSelect
export type StorageFile = typeof storageFiles.$inferSelect
export type StorageFileInsert = typeof storageFiles.$inferInsert