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:
@@ -0,0 +1,32 @@
|
||||
CREATE TYPE "storage_folder_access" AS ENUM ('view', 'edit', 'admin');
|
||||
|
||||
CREATE TABLE "storage_folder" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"parent_id" uuid,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"path" text NOT NULL DEFAULT '/',
|
||||
"created_by" uuid REFERENCES "user"("id"),
|
||||
"is_public" boolean NOT NULL DEFAULT true,
|
||||
"created_at" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"updated_at" timestamp with time zone NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE "storage_folder_permission" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"folder_id" uuid NOT NULL REFERENCES "storage_folder"("id") ON DELETE CASCADE,
|
||||
"role_id" uuid REFERENCES "role"("id"),
|
||||
"user_id" uuid REFERENCES "user"("id"),
|
||||
"access_level" "storage_folder_access" NOT NULL DEFAULT 'view',
|
||||
"created_at" timestamp with time zone NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE "storage_file" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"folder_id" uuid NOT NULL REFERENCES "storage_folder"("id") ON DELETE CASCADE,
|
||||
"filename" varchar(255) NOT NULL,
|
||||
"path" varchar(1000) NOT NULL,
|
||||
"content_type" varchar(100) NOT NULL,
|
||||
"size_bytes" integer NOT NULL,
|
||||
"uploaded_by" uuid REFERENCES "user"("id"),
|
||||
"created_at" timestamp with time zone NOT NULL DEFAULT now()
|
||||
);
|
||||
@@ -155,6 +155,13 @@
|
||||
"when": 1774810000000,
|
||||
"tag": "0021_remove_company_scoping",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 22,
|
||||
"version": "7",
|
||||
"when": 1774820000000,
|
||||
"tag": "0022_shared_file_storage",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
55
packages/backend/src/db/schema/storage.ts
Normal file
55
packages/backend/src/db/schema/storage.ts
Normal 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
|
||||
Reference in New Issue
Block a user