mirror of
https://github.com/khairul169/code-share.git
synced 2025-04-28 16:49:36 +07:00
feat: add project fork id, add project permission check
This commit is contained in:
parent
543260479a
commit
110bdd88e6
@ -6,7 +6,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"dev": "HOST=0.0.0.0 tsx server/index.ts",
|
"dev": "HOST=0.0.0.0 PORT=3001 tsx server/index.ts",
|
||||||
"dev:watch": "tsx watch --ignore *.mjs server/index.ts",
|
"dev:watch": "tsx watch --ignore *.mjs server/index.ts",
|
||||||
"start": "NODE_ENV=production tsx server/index.ts",
|
"start": "NODE_ENV=production tsx server/index.ts",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
@ -23,6 +23,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@swc/cli": "^0.3.9",
|
"@swc/cli": "^0.3.9",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
"@types/better-sqlite3": "^7.6.9",
|
||||||
"@types/cookie-parser": "^1.4.6",
|
"@types/cookie-parser": "^1.4.6",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/jsonwebtoken": "^9.0.5",
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
|
@ -17,11 +17,9 @@ import { useData } from "~/renderer/hooks";
|
|||||||
import { Data } from "../+data";
|
import { Data } from "../+data";
|
||||||
import { useBreakpoint } from "~/hooks/useBreakpoint";
|
import { useBreakpoint } from "~/hooks/useBreakpoint";
|
||||||
import StatusBar from "./status-bar";
|
import StatusBar from "./status-bar";
|
||||||
import { FiServer, FiTerminal } from "react-icons/fi";
|
import { FiTerminal } from "react-icons/fi";
|
||||||
import SettingsDialog from "./settings-dialog";
|
import SettingsDialog from "./settings-dialog";
|
||||||
import FileIcon from "~/components/ui/file-icon";
|
import FileIcon from "~/components/ui/file-icon";
|
||||||
import APIManager from "./api-manager";
|
|
||||||
import { api } from "~/lib/api";
|
|
||||||
|
|
||||||
const Editor = () => {
|
const Editor = () => {
|
||||||
const { project, initialFiles } = useData<Data>();
|
const { project, initialFiles } = useData<Data>();
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { getFileExt } from "~/lib/utils";
|
import { getFileExt } from "~/lib/utils";
|
||||||
import CodeEditor from "../../../../components/ui/code-editor";
|
|
||||||
import trpc from "~/lib/trpc";
|
import trpc from "~/lib/trpc";
|
||||||
import { useData } from "~/renderer/hooks";
|
import { useData } from "~/renderer/hooks";
|
||||||
import { Data } from "../+data";
|
import { Data } from "../+data";
|
||||||
import Spinner from "~/components/ui/spinner";
|
import Spinner from "~/components/ui/spinner";
|
||||||
import { previewStore } from "../stores/web-preview";
|
import { previewStore } from "../stores/web-preview";
|
||||||
import { useProjectContext } from "../context/project";
|
import { useProjectContext } from "../context/project";
|
||||||
|
import { Suspense, lazy } from "react";
|
||||||
|
const CodeEditor = lazy(() => import("~/components/ui/code-editor"));
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -46,14 +47,20 @@ const FileViewer = ({ id }: Props) => {
|
|||||||
const ext = getFileExt(filename);
|
const ext = getFileExt(filename);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeEditor
|
<Suspense fallback={<LoadingLayout />}>
|
||||||
lang={ext}
|
<CodeEditor
|
||||||
value={data?.content || ""}
|
lang={ext}
|
||||||
formatOnSave
|
value={data?.content || ""}
|
||||||
onChange={(val) =>
|
formatOnSave
|
||||||
updateFileContent.mutate({ projectId: project.id, id, content: val })
|
onChange={(val) =>
|
||||||
}
|
updateFileContent.mutate({
|
||||||
/>
|
projectId: project.id,
|
||||||
|
id,
|
||||||
|
content: val,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@ -94,7 +94,7 @@ dependencies:
|
|||||||
version: 16.4.5
|
version: 16.4.5
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.29.3
|
specifier: ^0.29.3
|
||||||
version: 0.29.3(@types/react@18.2.57)(better-sqlite3@9.4.2)(react@18.2.0)
|
version: 0.29.3(@types/better-sqlite3@7.6.9)(@types/react@18.2.57)(better-sqlite3@9.4.2)(react@18.2.0)
|
||||||
drizzle-zod:
|
drizzle-zod:
|
||||||
specifier: ^0.5.1
|
specifier: ^0.5.1
|
||||||
version: 0.5.1(drizzle-orm@0.29.3)(zod@3.22.4)
|
version: 0.5.1(drizzle-orm@0.29.3)(zod@3.22.4)
|
||||||
@ -172,6 +172,9 @@ devDependencies:
|
|||||||
'@types/bcrypt':
|
'@types/bcrypt':
|
||||||
specifier: ^5.0.2
|
specifier: ^5.0.2
|
||||||
version: 5.0.2
|
version: 5.0.2
|
||||||
|
'@types/better-sqlite3':
|
||||||
|
specifier: ^7.6.9
|
||||||
|
version: 7.6.9
|
||||||
'@types/cookie-parser':
|
'@types/cookie-parser':
|
||||||
specifier: ^1.4.6
|
specifier: ^1.4.6
|
||||||
version: 1.4.6
|
version: 1.4.6
|
||||||
@ -2267,6 +2270,11 @@ packages:
|
|||||||
'@types/node': 20.11.19
|
'@types/node': 20.11.19
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/better-sqlite3@7.6.9:
|
||||||
|
resolution: {integrity: sha512-FvktcujPDj9XKMJQWFcl2vVl7OdRIqsSRX9b0acWwTmwLK9CF2eqo/FRcmMLNpugKoX/avA6pb7TorDLmpgTnQ==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.11.19
|
||||||
|
|
||||||
/@types/body-parser@1.19.5:
|
/@types/body-parser@1.19.5:
|
||||||
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
|
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -3489,7 +3497,7 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/drizzle-orm@0.29.3(@types/react@18.2.57)(better-sqlite3@9.4.2)(react@18.2.0):
|
/drizzle-orm@0.29.3(@types/better-sqlite3@7.6.9)(@types/react@18.2.57)(better-sqlite3@9.4.2)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-uSE027csliGSGYD0pqtM+SAQATMREb3eSM/U8s6r+Y0RFwTKwftnwwSkqx3oS65UBgqDOM0gMTl5UGNpt6lW0A==}
|
resolution: {integrity: sha512-uSE027csliGSGYD0pqtM+SAQATMREb3eSM/U8s6r+Y0RFwTKwftnwwSkqx3oS65UBgqDOM0gMTl5UGNpt6lW0A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@aws-sdk/client-rds-data': '>=3'
|
'@aws-sdk/client-rds-data': '>=3'
|
||||||
@ -3560,6 +3568,7 @@ packages:
|
|||||||
sqlite3:
|
sqlite3:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@types/better-sqlite3': 7.6.9
|
||||||
'@types/react': 18.2.57
|
'@types/react': 18.2.57
|
||||||
better-sqlite3: 9.4.2
|
better-sqlite3: 9.4.2
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
@ -3571,7 +3580,7 @@ packages:
|
|||||||
drizzle-orm: '>=0.23.13'
|
drizzle-orm: '>=0.23.13'
|
||||||
zod: '*'
|
zod: '*'
|
||||||
dependencies:
|
dependencies:
|
||||||
drizzle-orm: 0.29.3(@types/react@18.2.57)(better-sqlite3@9.4.2)(react@18.2.0)
|
drizzle-orm: 0.29.3(@types/better-sqlite3@7.6.9)(@types/react@18.2.57)(better-sqlite3@9.4.2)(react@18.2.0)
|
||||||
zod: 3.22.4
|
zod: 3.22.4
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
44
server/db/drizzle/0000_swift_mandroid.sql
Normal file
44
server/db/drizzle/0000_swift_mandroid.sql
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
CREATE TABLE `files` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`project_id` integer NOT NULL,
|
||||||
|
`parent_id` integer,
|
||||||
|
`path` text NOT NULL,
|
||||||
|
`filename` text NOT NULL,
|
||||||
|
`is_directory` integer DEFAULT false NOT NULL,
|
||||||
|
`is_file` integer DEFAULT false NOT NULL,
|
||||||
|
`is_pinned` integer DEFAULT false NOT NULL,
|
||||||
|
`content` text,
|
||||||
|
`created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
`deleted_at` text,
|
||||||
|
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE no action,
|
||||||
|
FOREIGN KEY (`parent_id`) REFERENCES `files`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `projects` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`fork_id` integer;
|
||||||
|
`user_id` integer NOT NULL,
|
||||||
|
`slug` text NOT NULL,
|
||||||
|
`title` text NOT NULL,
|
||||||
|
`visibility` text DEFAULT 'private',
|
||||||
|
`settings` text DEFAULT [object Object],
|
||||||
|
`created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
`deleted_at` text,
|
||||||
|
FOREIGN KEY (`fork_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE no action,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`email` text NOT NULL,
|
||||||
|
`password` text NOT NULL,
|
||||||
|
`created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
`deleted_at` text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX `file_path_idx` ON `files` (`path`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `file_name_idx` ON `files` (`filename`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `projects_slug_unique` ON `projects` (`slug`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `project_visibility_idx` ON `projects` (`visibility`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);
|
332
server/db/drizzle/meta/0000_snapshot.json
Normal file
332
server/db/drizzle/meta/0000_snapshot.json
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "4d5af5ab-31e5-4202-9c7e-3264e985bf20",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"tables": {
|
||||||
|
"files": {
|
||||||
|
"name": "files",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"parent_id": {
|
||||||
|
"name": "parent_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"name": "path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"filename": {
|
||||||
|
"name": "filename",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"is_directory": {
|
||||||
|
"name": "is_directory",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"is_file": {
|
||||||
|
"name": "is_file",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"is_pinned": {
|
||||||
|
"name": "is_pinned",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "CURRENT_TIMESTAMP"
|
||||||
|
},
|
||||||
|
"deleted_at": {
|
||||||
|
"name": "deleted_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"file_path_idx": {
|
||||||
|
"name": "file_path_idx",
|
||||||
|
"columns": [
|
||||||
|
"path"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"file_name_idx": {
|
||||||
|
"name": "file_name_idx",
|
||||||
|
"columns": [
|
||||||
|
"filename"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"files_project_id_projects_id_fk": {
|
||||||
|
"name": "files_project_id_projects_id_fk",
|
||||||
|
"tableFrom": "files",
|
||||||
|
"tableTo": "projects",
|
||||||
|
"columnsFrom": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"parent_id_fk": {
|
||||||
|
"name": "parent_id_fk",
|
||||||
|
"tableFrom": "files",
|
||||||
|
"tableTo": "files",
|
||||||
|
"columnsFrom": [
|
||||||
|
"parent_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"name": "projects",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"fork_id": {
|
||||||
|
"name": "fork_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"name": "visibility",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'private'"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"name": "settings",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": {
|
||||||
|
"css": {
|
||||||
|
"preprocessor": null,
|
||||||
|
"tailwindcss": false
|
||||||
|
},
|
||||||
|
"js": {
|
||||||
|
"transpiler": null,
|
||||||
|
"packages": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "CURRENT_TIMESTAMP"
|
||||||
|
},
|
||||||
|
"deleted_at": {
|
||||||
|
"name": "deleted_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"projects_slug_unique": {
|
||||||
|
"name": "projects_slug_unique",
|
||||||
|
"columns": [
|
||||||
|
"slug"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
},
|
||||||
|
"project_visibility_idx": {
|
||||||
|
"name": "project_visibility_idx",
|
||||||
|
"columns": [
|
||||||
|
"visibility"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"projects_user_id_users_id_fk": {
|
||||||
|
"name": "projects_user_id_users_id_fk",
|
||||||
|
"tableFrom": "projects",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"project_fork_id_fk": {
|
||||||
|
"name": "project_fork_id_fk",
|
||||||
|
"tableFrom": "projects",
|
||||||
|
"tableTo": "projects",
|
||||||
|
"columnsFrom": [
|
||||||
|
"fork_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "CURRENT_TIMESTAMP"
|
||||||
|
},
|
||||||
|
"deleted_at": {
|
||||||
|
"name": "deleted_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
}
|
||||||
|
}
|
1
server/db/drizzle/meta/_journal.json
Normal file
1
server/db/drizzle/meta/_journal.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":"5","dialect":"sqlite","entries":[{"idx":0,"version":"5","when":1709221275637,"tag":"0000_swift_mandroid","breakpoints":true}]}
|
@ -1,5 +1,9 @@
|
|||||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||||
import db from "./index";
|
import db from "./index";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
migrate(db, { migrationsFolder: __dirname + "/drizzle" });
|
migrate(db, { migrationsFolder: __dirname + "/drizzle" });
|
||||||
process.exit();
|
process.exit();
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import { relations, sql } from "drizzle-orm";
|
import { relations, sql } from "drizzle-orm";
|
||||||
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
import {
|
||||||
|
foreignKey,
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
sqliteTable,
|
||||||
|
text,
|
||||||
|
} from "drizzle-orm/sqlite-core";
|
||||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { user } from "./user";
|
import { user } from "./user";
|
||||||
@ -23,6 +29,7 @@ export const project = sqliteTable(
|
|||||||
userId: integer("user_id")
|
userId: integer("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => user.id),
|
.references(() => user.id),
|
||||||
|
forkId: integer("fork_id"),
|
||||||
slug: text("slug").notNull().unique(),
|
slug: text("slug").notNull().unique(),
|
||||||
title: text("title").notNull(),
|
title: text("title").notNull(),
|
||||||
|
|
||||||
@ -40,12 +47,21 @@ export const project = sqliteTable(
|
|||||||
deletedAt: text("deleted_at"),
|
deletedAt: text("deleted_at"),
|
||||||
},
|
},
|
||||||
(table) => ({
|
(table) => ({
|
||||||
|
forkIdFk: foreignKey({
|
||||||
|
columns: [table.forkId],
|
||||||
|
foreignColumns: [table.id],
|
||||||
|
name: "project_fork_id_fk",
|
||||||
|
}),
|
||||||
visibilityEnum: index("project_visibility_idx").on(table.visibility),
|
visibilityEnum: index("project_visibility_idx").on(table.visibility),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export const projectRelations = relations(project, ({ one, many }) => ({
|
export const projectRelations = relations(project, ({ one, many }) => ({
|
||||||
files: many(file),
|
files: many(file),
|
||||||
|
fork: one(project, {
|
||||||
|
fields: [project.forkId],
|
||||||
|
references: [project.id],
|
||||||
|
}),
|
||||||
user: one(user, {
|
user: one(user, {
|
||||||
fields: [project.userId],
|
fields: [project.userId],
|
||||||
references: [user.id],
|
references: [user.id],
|
||||||
|
@ -4,6 +4,7 @@ import { procedure, router } from "../api/trpc";
|
|||||||
import { file, insertFileSchema, selectFileSchema } from "../db/schema/file";
|
import { file, insertFileSchema, selectFileSchema } from "../db/schema/file";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { getProjectById, hasPermission } from "./project";
|
||||||
|
|
||||||
const fileRouter = router({
|
const fileRouter = router({
|
||||||
getAll: procedure
|
getAll: procedure
|
||||||
@ -14,7 +15,13 @@ const fileRouter = router({
|
|||||||
isPinned: z.boolean().optional(),
|
isPinned: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.query(async ({ input: opt }) => {
|
.query(async ({ ctx, input: opt }) => {
|
||||||
|
const project = await getProjectById(opt.projectId);
|
||||||
|
|
||||||
|
if (!hasPermission(ctx, project, "r")) {
|
||||||
|
throw new TRPCError({ code: "FORBIDDEN" });
|
||||||
|
}
|
||||||
|
|
||||||
const files = await db.query.file.findMany({
|
const files = await db.query.file.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
eq(file.projectId, opt.projectId),
|
eq(file.projectId, opt.projectId),
|
||||||
@ -29,10 +36,21 @@ const fileRouter = router({
|
|||||||
return files;
|
return files;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getById: procedure.input(z.number()).query(async ({ input }) => {
|
getById: procedure.input(z.number()).query(async ({ ctx, input }) => {
|
||||||
return db.query.file.findFirst({
|
const result = await db.query.file.findFirst({
|
||||||
where: and(eq(file.id, input), isNull(file.deletedAt)),
|
where: and(eq(file.id, input), isNull(file.deletedAt)),
|
||||||
|
with: { project: { columns: { visibility: true, userId: true } } },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPermission(ctx, result?.project, "r")) {
|
||||||
|
throw new TRPCError({ code: "FORBIDDEN" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
create: procedure
|
create: procedure
|
||||||
@ -44,7 +62,13 @@ const fileRouter = router({
|
|||||||
isDirectory: true,
|
isDirectory: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const project = await getProjectById(input.projectId);
|
||||||
|
|
||||||
|
if (!hasPermission(ctx, project, "w")) {
|
||||||
|
throw new TRPCError({ code: "FORBIDDEN" });
|
||||||
|
}
|
||||||
|
|
||||||
let basePath = "";
|
let basePath = "";
|
||||||
if (input.parentId) {
|
if (input.parentId) {
|
||||||
const parent = await db.query.file.findFirst({
|
const parent = await db.query.file.findFirst({
|
||||||
@ -58,7 +82,7 @@ const fileRouter = router({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data: z.infer<typeof insertFileSchema> = {
|
const data: z.infer<typeof insertFileSchema> = {
|
||||||
projectId: input.projectId,
|
projectId: project.id,
|
||||||
parentId: input.parentId,
|
parentId: input.parentId,
|
||||||
path: basePath + input.filename,
|
path: basePath + input.filename,
|
||||||
filename: input.filename,
|
filename: input.filename,
|
||||||
@ -71,17 +95,22 @@ const fileRouter = router({
|
|||||||
|
|
||||||
update: procedure
|
update: procedure
|
||||||
.input(selectFileSchema.partial().required({ id: true, projectId: true }))
|
.input(selectFileSchema.partial().required({ id: true, projectId: true }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const fileData = await db.query.file.findFirst({
|
const fileData = await db.query.file.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
eq(file.projectId, input.projectId),
|
eq(file.projectId, input.projectId),
|
||||||
eq(file.id, input.id),
|
eq(file.id, input.id),
|
||||||
isNull(file.deletedAt)
|
isNull(file.deletedAt)
|
||||||
),
|
),
|
||||||
|
with: { project: { columns: { visibility: true, userId: true } } },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!fileData) {
|
if (!fileData) {
|
||||||
throw new TRPCError({ code: "NOT_FOUND" });
|
throw new TRPCError({ code: "NOT_FOUND" });
|
||||||
}
|
}
|
||||||
|
if (!hasPermission(ctx, fileData.project, "w")) {
|
||||||
|
throw new TRPCError({ code: "FORBIDDEN" });
|
||||||
|
}
|
||||||
|
|
||||||
return db.transaction(async (tx) => {
|
return db.transaction(async (tx) => {
|
||||||
const data = { ...input };
|
const data = { ...input };
|
||||||
@ -109,13 +138,17 @@ const fileRouter = router({
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: procedure.input(z.number()).mutation(async ({ input }) => {
|
delete: procedure.input(z.number()).mutation(async ({ ctx, input }) => {
|
||||||
const fileData = await db.query.file.findFirst({
|
const fileData = await db.query.file.findFirst({
|
||||||
where: and(eq(file.id, input), isNull(file.deletedAt)),
|
where: and(eq(file.id, input), isNull(file.deletedAt)),
|
||||||
|
with: { project: { columns: { visibility: true, userId: true } } },
|
||||||
});
|
});
|
||||||
if (!fileData) {
|
if (!fileData) {
|
||||||
throw new TRPCError({ code: "NOT_FOUND" });
|
throw new TRPCError({ code: "NOT_FOUND" });
|
||||||
}
|
}
|
||||||
|
if (!hasPermission(ctx, fileData.project, "w")) {
|
||||||
|
throw new TRPCError({ code: "FORBIDDEN" });
|
||||||
|
}
|
||||||
|
|
||||||
return db.transaction(async (tx) => {
|
return db.transaction(async (tx) => {
|
||||||
const [result] = await tx
|
const [result] = await tx
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
project,
|
project,
|
||||||
insertProjectSchema,
|
insertProjectSchema,
|
||||||
selectProjectSchema,
|
selectProjectSchema,
|
||||||
|
ProjectSchema,
|
||||||
} from "../db/schema/project";
|
} from "../db/schema/project";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
@ -15,6 +16,7 @@ import { ucwords } from "~/lib/utils";
|
|||||||
import { hashPassword } from "../lib/crypto";
|
import { hashPassword } from "../lib/crypto";
|
||||||
import { createToken } from "../lib/jwt";
|
import { createToken } from "../lib/jwt";
|
||||||
import { file } from "../db/schema/file";
|
import { file } from "../db/schema/file";
|
||||||
|
import { Context } from "../api/trpc/context";
|
||||||
|
|
||||||
const projectRouter = router({
|
const projectRouter = router({
|
||||||
getAll: procedure
|
getAll: procedure
|
||||||
@ -45,7 +47,7 @@ const projectRouter = router({
|
|||||||
|
|
||||||
getById: procedure
|
getById: procedure
|
||||||
.input(z.number().or(z.string()))
|
.input(z.number().or(z.string()))
|
||||||
.query(async ({ input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const where = and(
|
const where = and(
|
||||||
typeof input === "string"
|
typeof input === "string"
|
||||||
? eq(project.slug, input)
|
? eq(project.slug, input)
|
||||||
@ -53,12 +55,20 @@ const projectRouter = router({
|
|||||||
isNull(project.deletedAt)
|
isNull(project.deletedAt)
|
||||||
);
|
);
|
||||||
|
|
||||||
return db.query.project.findFirst({
|
const result = await db.query.project.findFirst({
|
||||||
where,
|
where,
|
||||||
with: {
|
with: {
|
||||||
user: { columns: { password: false } },
|
user: { columns: { password: false } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!hasPermission(ctx, result, "r")) {
|
||||||
|
throw new TRPCError({ code: "FORBIDDEN" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMutable = hasPermission(ctx, result, "w");
|
||||||
|
|
||||||
|
return { ...result, isMutable };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
create: procedure
|
create: procedure
|
||||||
@ -127,6 +137,10 @@ const projectRouter = router({
|
|||||||
throw new Error("Fork Project not found!");
|
throw new Error("Fork Project not found!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasPermission(ctx, forkProject, "r")) {
|
||||||
|
throw new TRPCError({ code: "FORBIDDEN" });
|
||||||
|
}
|
||||||
|
|
||||||
const forkFiles = await tx.query.file.findMany({
|
const forkFiles = await tx.query.file.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
eq(file.projectId, input.forkFromId),
|
eq(file.projectId, input.forkFromId),
|
||||||
@ -146,7 +160,7 @@ const projectRouter = router({
|
|||||||
|
|
||||||
await tx
|
await tx
|
||||||
.update(project)
|
.update(project)
|
||||||
.set({ settings: forkProject.settings })
|
.set({ settings: forkProject.settings, forkId: forkProject.id })
|
||||||
.where(eq(project.id, projectData.id));
|
.where(eq(project.id, projectData.id));
|
||||||
} else {
|
} else {
|
||||||
await tx.insert(file).values([
|
await tx.insert(file).values([
|
||||||
@ -171,7 +185,7 @@ const projectRouter = router({
|
|||||||
.omit({ slug: true, userId: true })
|
.omit({ slug: true, userId: true })
|
||||||
.required({ id: true })
|
.required({ id: true })
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const data = { ...input };
|
const data = { ...input };
|
||||||
|
|
||||||
const projectData = await db.query.project.findFirst({
|
const projectData = await db.query.project.findFirst({
|
||||||
@ -181,6 +195,10 @@ const projectRouter = router({
|
|||||||
throw new TRPCError({ code: "NOT_FOUND" });
|
throw new TRPCError({ code: "NOT_FOUND" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasPermission(ctx, projectData, "w")) {
|
||||||
|
throw new TRPCError({ code: "FORBIDDEN" });
|
||||||
|
}
|
||||||
|
|
||||||
if (data.settings) {
|
if (data.settings) {
|
||||||
data.settings = Object.assign(
|
data.settings = Object.assign(
|
||||||
projectData.settings || {},
|
projectData.settings || {},
|
||||||
@ -197,7 +215,7 @@ const projectRouter = router({
|
|||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: procedure.input(z.number()).mutation(async ({ input }) => {
|
delete: procedure.input(z.number()).mutation(async ({ ctx, input }) => {
|
||||||
const projectData = await db.query.project.findFirst({
|
const projectData = await db.query.project.findFirst({
|
||||||
where: and(eq(project.id, input), isNull(project.deletedAt)),
|
where: and(eq(project.id, input), isNull(project.deletedAt)),
|
||||||
});
|
});
|
||||||
@ -205,6 +223,10 @@ const projectRouter = router({
|
|||||||
throw new TRPCError({ code: "NOT_FOUND" });
|
throw new TRPCError({ code: "NOT_FOUND" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasPermission(ctx, projectData, "r")) {
|
||||||
|
throw new TRPCError({ code: "FORBIDDEN" });
|
||||||
|
}
|
||||||
|
|
||||||
const [result] = await db
|
const [result] = await db
|
||||||
.update(project)
|
.update(project)
|
||||||
.set({ deletedAt: sql`CURRENT_TIMESTAMP` })
|
.set({ deletedAt: sql`CURRENT_TIMESTAMP` })
|
||||||
@ -232,4 +254,38 @@ const projectRouter = router({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function hasPermission(
|
||||||
|
ctx: Context,
|
||||||
|
project: Pick<ProjectSchema, "userId" | "visibility"> | undefined,
|
||||||
|
permission: "r" | "w"
|
||||||
|
) {
|
||||||
|
if (!project) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let read = false,
|
||||||
|
write = false;
|
||||||
|
|
||||||
|
if (ctx.user?.id === project.userId) {
|
||||||
|
read = true;
|
||||||
|
write = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (["public", "unlisted"].includes(project.visibility as never)) {
|
||||||
|
read = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return permission === "r" ? read : write;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProjectById(id: number) {
|
||||||
|
const projectData = await db.query.project.findFirst({
|
||||||
|
where: and(eq(project.id, id), isNull(project.deletedAt)),
|
||||||
|
});
|
||||||
|
if (!projectData) {
|
||||||
|
throw new TRPCError({ code: "NOT_FOUND" });
|
||||||
|
}
|
||||||
|
return projectData;
|
||||||
|
}
|
||||||
|
|
||||||
export default projectRouter;
|
export default projectRouter;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user