feat: added file upload + dir creation

This commit is contained in:
Smigz 2025-05-01 20:59:01 -04:00
parent b1c295d2ac
commit eedab606f8
25 changed files with 965 additions and 103 deletions

View file

@ -10,12 +10,15 @@
"license": "ISC",
"dependencies": {
"@prisma/client": "^6.5.0",
"@quixo3/prisma-session-store": "^3.1.13",
"bcrypt": "^5.1.1",
"ejs": "^3.1.10",
"express": "^5.1.0",
"express-session": "^1.18.1",
"express-validator": "^7.2.1",
"passport": "^0.7.0"
"multer": "^1.4.5-lts.2",
"passport": "^0.7.0",
"passport-local": "^1.0.0"
},
"devDependencies": {
"css-loader": "^7.1.2",
@ -521,6 +524,27 @@
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@noble/hashes": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz",
"integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@paralleldrive/cuid2": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
"integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.1.5"
}
},
"node_modules/@prisma/client": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.5.0.tgz",
@ -597,6 +621,24 @@
"@prisma/debug": "6.5.0"
}
},
"node_modules/@quixo3/prisma-session-store": {
"version": "3.1.13",
"resolved": "https://registry.npmjs.org/@quixo3/prisma-session-store/-/prisma-session-store-3.1.13.tgz",
"integrity": "sha512-EAuOvYAaAsQ0OqxkdJG/Qs3cxlT4VV8SFHjtsA3G01uB1b6r7xftX3oeg7mcG0HN/DI1qOqwvy3YFoJ38ls0iA==",
"license": "MIT",
"dependencies": {
"@paralleldrive/cuid2": "^2.2.0",
"ts-dedent": "^2.2.0",
"type-fest": "^2.5.2"
},
"engines": {
"node": ">=12.0"
},
"peerDependencies": {
"@prisma/client": ">=2.16.1",
"express-session": ">=1.17.1"
}
},
"node_modules/@types/eslint": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
@ -1009,6 +1051,12 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
@ -1146,9 +1194,19 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -1322,6 +1380,51 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"engines": [
"node >= 0.8"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"typedarray": "^0.0.6"
}
},
"node_modules/concat-stream/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/concat-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/concat-stream/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@ -1360,6 +1463,12 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -2539,6 +2648,12 @@
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -2754,6 +2869,15 @@
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
@ -2805,6 +2929,79 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/multer": {
"version": "1.4.5-lts.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
"integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.0.0",
"concat-stream": "^1.5.2",
"mkdirp": "^0.5.4",
"object-assign": "^4.1.1",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/multer/node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/multer/node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@ -3057,6 +3254,17 @@
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-local": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
"integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==",
"dependencies": {
"passport-strategy": "1.x.x"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
@ -3286,6 +3494,12 @@
}
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -3791,6 +4005,14 @@
"node": ">= 0.8"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@ -3975,6 +4197,15 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/ts-dedent": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
"integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
"license": "MIT",
"engines": {
"node": ">=6.10"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@ -3982,6 +4213,18 @@
"dev": true,
"license": "0BSD"
},
"node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/type-is": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
@ -3995,6 +4238,12 @@
"node": ">= 0.6"
}
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
@ -4304,6 +4553,15 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",

View file

@ -11,12 +11,15 @@
"description": "",
"dependencies": {
"@prisma/client": "^6.5.0",
"@quixo3/prisma-session-store": "^3.1.13",
"bcrypt": "^5.1.1",
"ejs": "^3.1.10",
"express": "^5.1.0",
"express-session": "^1.18.1",
"express-validator": "^7.2.1",
"passport": "^0.7.0"
"multer": "^1.4.5-lts.2",
"passport": "^0.7.0",
"passport-local": "^1.0.0"
},
"devDependencies": {
"css-loader": "^7.1.2",

View file

@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"sid" TEXT NOT NULL,
"data" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Session_sid_key" ON "Session"("sid");

View file

@ -0,0 +1,10 @@
/*
Warnings:
- Added the required column `mimetype` to the `File` table without a default value. This is not possible if the table is not empty.
- Added the required column `size` to the `File` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "File" ADD COLUMN "mimetype" TEXT NOT NULL,
ADD COLUMN "size" INTEGER NOT NULL;

View file

@ -0,0 +1,17 @@
/*
Warnings:
- Added the required column `full_path` to the `Folder` table without a default value. This is not possible if the table is not empty.
- Added the required column `modification_date` to the `Folder` table without a default value. This is not possible if the table is not empty.
- Added the required column `parent_folder_id` to the `Folder` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Folder" ADD COLUMN "creation_date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "full_path" TEXT NOT NULL,
ADD COLUMN "modification_date" TIMESTAMP(3) NOT NULL,
ADD COLUMN "owner_user_id" INTEGER,
ADD COLUMN "parent_folder_id" INTEGER NOT NULL;
-- AddForeignKey
ALTER TABLE "Folder" ADD CONSTRAINT "Folder_owner_user_id_fkey" FOREIGN KEY ("owner_user_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -0,0 +1,14 @@
/*
Warnings:
- You are about to drop the column `full_path` on the `Folder` table. All the data in the column will be lost.
- You are about to drop the column `parent_folder_id` on the `Folder` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Folder" DROP COLUMN "full_path",
DROP COLUMN "parent_folder_id",
ADD COLUMN "parentId" INTEGER;
-- AddForeignKey
ALTER TABLE "Folder" ADD CONSTRAINT "Folder_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Folder" ALTER COLUMN "modification_date" SET DEFAULT CURRENT_TIMESTAMP;

View file

@ -15,17 +15,20 @@ datasource db {
}
model User {
id Int @id @default(autoincrement())
username String @unique @db.VarChar(50)
email String @unique
id Int @id @default(autoincrement())
username String @unique @db.VarChar(50)
email String @unique
password String
files File[]
Folder Folder[]
}
model File {
id Int @id @default(autoincrement())
name String @db.VarChar(255)
url String
size Int
mimetype String
createdAt DateTime @default(now())
owner User? @relation(fields: [ownerId], references: [id])
ownerId Int?
@ -34,7 +37,21 @@ model File {
}
model Folder {
id Int @id @default(autoincrement())
name String @db.VarChar(255)
File File[]
id Int @id @default(autoincrement())
name String @db.VarChar(255)
creation_date DateTime @default(now())
modification_date DateTime @default(now())
File File[]
owner User? @relation(fields: [owner_user_id], references: [id])
owner_user_id Int?
parentId Int?
parent Folder? @relation("ParentDirectory", fields: [parentId], references: [id])
Directories Folder[] @relation("ParentDirectory")
}
model Session {
id String @id
sid String @unique
data String
expiresAt DateTime
}

View file

@ -0,0 +1,142 @@
/* 1. Use a more-intuitive box-sizing model */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* 2. Remove default margin */
* {
margin: 0;
}
/* 3. Enable keyword animations */
@media (prefers-reduced-motion: no-preference) {
html {
interpolate-size: allow-keywords;
}
}
body {
/* 4. Add accessible line-height */
line-height: 1.5;
/* 5. Improve text rendering */
-webkit-font-smoothing: antialiased;
}
/* 6. Improve media defaults */
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
/* 7. Inherit fonts for form controls */
input,
button,
textarea,
select {
font: inherit;
}
/* 8. Avoid text overflows */
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
/* 9. Improve line wrapping */
p {
text-wrap: pretty;
}
h1,
h2,
h3,
h4,
h5,
h6 {
text-wrap: balance;
}
/*
10. Create a root stacking context
*/
#root,
#__next {
isolation: isolate;
}
.register-form {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.form-input {
display: flex;
flex-direction: column;
gap: 0.5em;
}
input[type='file'] {
outline: none;
padding: 4px;
margin: -4px;
}
input[type='file']::file-selector-button {
cursor: pointer;
border-radius: 4px;
padding: 0 1em;
height: 2em;
background-color: white;
border: 1px solid rgba(0, 0, 0, 0.16);
box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.05);
margin-right: 16px;
transition: background-color 200ms;
}
/* file upload button hover state */
input[type='file']::file-selector-button:hover {
background-color: #f3f4f6;
}
/* file upload button active state */
input[type='file']::file-selector-button:active {
background-color: #e5e7eb;
}
input[type='file'] {
position: relative;
outline: none;
padding: 4px;
margin: -4px;
}
input[type='file']:focus-within::file-selector-button,
input[type='file']:focus::file-selector-button {
outline: 2px solid #0964b0;
outline-offset: 2px;
}
@supports (-moz-appearance: none) {
input[type='file']::file-selector-button {
color: #0964b0;
}
}
/* .file { */
/* opacity: 0; */
/* width: 0.1px; */
/* height: 0.1px; */
/* position: absolute; */
/* } */

View file

@ -1,14 +1,57 @@
console.log('form stuff');
const usernameCheck = async (username) => {
let res = await fetch(`/auth/username?username=${username}`);
return await res.json();
if (username.length > 0) {
let res = await fetch(`/auth/username?username=${username}`);
const data = await res.json();
// return true if username is available
return data.results === false;
}
};
const input = document.querySelector("input[name='username']");
const emailCheck = async (email) => {
if (email.length > 0) {
let res = await fetch(`/auth/email?email=${email}`);
const data = await res.json();
return data.results === false;
}
};
input.addEventListener('input', async (e) => {
let input = e.target.value;
let test = await usernameCheck(input);
test.results ? console.log('user exists') : console.log('user doesnt');
});
const isDataValid = async (e, callback) => {
let test = await callback(e.target.value);
test
? (e.target.style.border = '3px green solid')
: (e.target.style.border = 'red solid 3px');
};
const passwordValid = () => {
const password = document.querySelector('#password');
const passwordConfirmation = document.querySelector(
'#password-confirmation',
);
const passwordConfirmationCheck = (value) => {
return password.value === value;
};
const passwordValidityCheck = (password) => {
if (!/\d/.test(password)) return false;
if (!/[\!@#%\^&\*\(\)\_\+\-=\[\]{}\|;':",\.\/\<\>\~\`]/.test(password))
return false;
if (password.length < 8) return false;
return true;
};
password.addEventListener('input', (e) =>
isDataValid(e, passwordValidityCheck),
);
passwordConfirmation.addEventListener('input', (e) =>
isDataValid(e, passwordConfirmationCheck),
);
};
const usernameInput = document.querySelector("input[name='username']");
usernameInput.addEventListener('input', (e) => isDataValid(e, usernameCheck));
const emailInput = document.querySelector("input[name='email']");
emailInput.addEventListener('input', (e) => isDataValid(e, emailCheck));
passwordValid();

View file

@ -1,21 +1,30 @@
const { Auth } = require('../models/auth');
const Db = require('../models/db');
const bcrypt = require('bcrypt');
const db = new Auth();
const db = new Db();
const loginGet = (req, res, next) => {
res.send('Login Route');
};
const logout = (req, res, next) => {
req.logout((err) => {
if (err) {
return next(err);
}
res.redirect('/');
});
};
const registerGet = (req, res) => {
res.render('register');
res.render('register', { pageTitle: 'Register' });
};
const registerPost = async (req, res, next) => {
const { username, email, password } = req.body;
try {
const hashedPassword = bcrypt.hash(password);
const result = await db.createUser({
const hashedPassword = await bcrypt.hash(password, 10);
const result = await db.auth.createUser({
username: username,
password: hashedPassword,
email: email,
@ -38,7 +47,7 @@ const findByUsername = async (req, res, next) => {
try {
if (username && username.length > 0) {
const exists = (await db.getUserByUsername(username))
const exists = (await db.auth.getUserByUsername(username))
? true
: false;
@ -50,4 +59,24 @@ const findByUsername = async (req, res, next) => {
}
};
module.exports = { findByUsername, loginGet, registerPost, registerGet };
const findByEmail = async (req, res, next) => {
const { email } = req.query;
try {
if (email && email.length > 0) {
const exists = (await db.auth.getByEmail(email)) ? true : false;
const data = { results: exists };
res.json(data);
}
} catch (e) {
res.json({ error: e });
}
};
module.exports = {
findByUsername,
findByEmail,
loginGet,
logout,
registerPost,
registerGet,
};

View file

@ -0,0 +1,65 @@
const multer = require('multer');
const Db = require('../models/db');
const upload = multer({ dest: '/tmp/testing/' });
const db = new Db();
//
// const createUserRootDir = async () => {
// const user = await db.auth.getUserByUsername(username);
// console.log(`USERID: ${user.id}`);
// const dir = await db.file.createDirectory(user, 'root');
// if (dir.error) {
// console.log(dir.error);
// } else {
// console.log(dir.message);
// }
// };
const createDirectory = async (req, res) => {
const { parentId, 'directory-name': directoryName } = req.body;
const result = await db.file.createDirectory(
req.user,
directoryName,
Number(parentId),
);
res.redirect('/');
};
const fileUpload = async (req, res) => {
console.log(req.file);
const { folderId } = req.body;
const file = {
name: req.file.originalname,
path: req.file.path,
filename: req.file.filename,
folderId: Number(folderId),
mimetype: req.file.mimetype,
size: req.file.size,
};
console.log(file, req.user);
try {
const result = await db.file.createFile(req.user.id, file);
console.log(result);
} catch (e) {
console.log(e);
}
res.redirect('/');
};
const userDirs = (req, res) => {
if (req.user) {
// logic to grab the current dir based on path maybe
// logic to grab all user dirs?
}
};
const getFiles = (req, res) => {
if (req.user) {
// logic to grab the files
}
};
module.exports = { createDirectory, upload, fileUpload };

View file

@ -1,9 +1,17 @@
// const db = require("../models/auth");
const Db = require('../models/db');
const db = new Db();
const indexGet = async (req, res) => {
// const data = await db.allUsers()
// console.dir(data)
res.render("main");
// const data = await db.allUsers()
// console.dir(data)
if (req.user) {
const id = await db.file.getUserRootDirectoryId(req.user.id);
if (id === null) console.log('id is null');
console.log(id);
res.render('main', { pageTitle: 'FileUpload', folder: { id: id } });
} else {
res.render('main', { pageTitle: 'FileUpload', folder: {} });
}
};
module.exports = { indexGet };

View file

@ -1,6 +1,8 @@
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const expressSession = require('express-session');
const { PrismaSessionStore } = require('@quixo3/prisma-session-store');
const { PrismaClient } = require('@prisma/client');
const { passport } = require('./middlewares/auth');
const path = require('node:path');
const port = process.env.APP_PORT || 3000;
@ -9,17 +11,40 @@ const app = express();
const assetsPath = path.join(path.dirname(__dirname), 'public');
app.use(express.static(assetsPath));
app.use(
expressSession({
cookie: {
maxAge: 7 * 24 * 60 * 60 * 1000,
},
secret: 'deez',
resave: false,
saveUninitialized: false,
store: new PrismaSessionStore(new PrismaClient(), {
checkPeriod: 2 * 60 * 1000,
dbRecordIdFunction: undefined,
dbRecordIdIsSessionId: true,
}),
}),
);
app.use(passport.session());
app.use(express.urlencoded({ extended: false }));
const authRouter = require('./routes/authRouter');
const indexRouter = require('./routes/indexRouter');
const fileRouter = require('./routes/fileRouter');
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(express.urlencoded({ extended: false }));
app.use((req, res, next) => {
res.locals.currentUser = req.user;
next();
});
app.use('/', indexRouter);
app.use('/auth', authRouter);
app.use('/file', fileRouter);
app.listen(port, () => {
console.log(`App listening on ${port}`);
});

View file

@ -0,0 +1,51 @@
const bcrypt = require('bcrypt');
const passport = require('passport');
const Db = require('../models/db');
const LocalStrategy = require('passport-local').Strategy;
const db = new Db();
passport.use(
new LocalStrategy(async (username, password, done) => {
try {
const user = await db.auth.getUserByUsername(username);
if (!user) {
return done(null, false, {
message: 'Incorrect username or passowrd',
});
}
const passwordMatch = await bcrypt.compare(password, user.password);
if (!passwordMatch) {
return done(null, false, {
message: 'inccorect username or password',
});
}
// ensure users root directory has been created
const rootDirId = await db.file.getUserRootDirectoryId(user.id);
console.log(rootDirId);
if (rootDirId === null)
await db.file.createDirectory(user, 'root', null);
return done(null, user);
} catch (err) {
return done(err);
}
}),
);
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id, done) => {
try {
const user = await db.auth.getUserById(id);
done(null, user);
} catch (err) {
done(err);
}
});
module.exports = { passport };

View file

@ -1,45 +1,72 @@
const { PrismaClient, Prisma } = require("@prisma/client");
const { PrismaClient, Prisma } = require('@prisma/client');
// const { Prisma } = require("prisma");
class Auth {
constructor() {
this.primsa = new PrismaClient();
}
async createUser(user) {
try {
await this.primsa.user.create({
data: {
username: user.username,
email: user?.email,
password: user.password,
},
});
return { message: "user created" };
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === "P2002") {
console.log("User must use a unique username");
return { error: "must use unique username" };
constructor() {
this.prisma = new PrismaClient();
}
async createUser(user) {
try {
await this.prisma.user.create({
data: {
username: user.username,
email: user?.email,
password: user.password,
},
});
return { message: 'user created' };
} catch (e) {
console.log(`PRISMA error ${e}`);
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === 'P2002') {
console.log('There is a unique constraint violation');
return {
error: 'must use unique username and a unique email.',
};
}
}
}
}
}
}
async allUsers() {
return await this.primsa.user.findMany();
}
async getUserByUsername(username) {
try {
return await this.primsa.user.findUnique({
where: {
username: username,
},
});
} catch (e) {
return { error: e };
async allUsers() {
return await this.prisma.user.findMany();
}
async getUserByUsername(username) {
try {
return await this.prisma.user.findUnique({
where: {
username: username,
},
});
} catch (e) {
return { error: e };
}
}
async getUserById(id) {
try {
return await this.prisma.user.findUnique({
where: {
id: id,
},
});
} catch (e) {
return { error: e };
}
}
async getByEmail(email) {
try {
return await this.prisma.user.findUnique({
where: {
email: email,
},
});
} catch (e) {
return { error: e };
}
}
}
}
// async function main() {
@ -58,4 +85,3 @@ class Auth {
// })
module.exports = { Auth };

View file

@ -0,0 +1,11 @@
const { Auth } = require('./auth');
const { File } = require('./file');
class Db {
constructor() {
this.auth = new Auth();
this.file = new File();
}
}
module.exports = Db;

View file

@ -0,0 +1,77 @@
const { PrismaClient } = require('@prisma/client');
class File {
constructor() {
this.prisma = new PrismaClient();
}
async createDirectory(user, dirName, parentId) {
try {
await this.prisma.folder.create({
data: {
name: dirName,
owner_user_id: user.id,
parentId: parentId,
},
});
return { message: 'dir created' };
} catch (e) {
console.error(e);
return { error: 'issue creating dir' };
}
}
async getDirectoryIdByName(dirId, userId) {
try {
await this.prisma.folder.findUnique({
where: {
id: dirId,
},
});
} catch (e) {
console.error(e);
}
}
async getUserRootDirectoryId(userId) {
try {
const dir = await this.prisma.folder.findFirst({
where: {
owner_user_id: userId,
name: 'root',
parentId: null,
},
});
if (dir !== null) {
return dir.id;
} else {
return null;
}
} catch (e) {
console.error(e);
return null;
}
}
async getDirectoriesByUser() {}
async createFile(userId, file) {
try {
await this.prisma.file.create({
data: {
name: file.name,
size: file.size,
mimetype: file.mimetype,
url: file.path,
folderId: file.folderId,
ownerId: userId,
},
});
} catch (e) {
console.error(e);
}
}
}
module.exports = { File };

View file

@ -1,12 +1,21 @@
const { Router } = require('express');
const authController = require('../controllers/authController');
const { passport } = require('../middlewares/auth');
const authRouter = Router();
authRouter.get('/login', authController.loginGet);
authRouter.post(
'/login',
passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/',
}),
);
authRouter.get('/logout', authController.logout);
authRouter.get('/register', authController.registerGet);
authRouter.post('/register', authController.registerPost);
authRouter.get('/username', authController.findByUsername);
authRouter.get('/email', authController.findByEmail);
module.exports = authRouter;

View file

@ -0,0 +1,13 @@
const { Router } = require('express');
const fileController = require('../controllers/fileController');
const fileRouter = Router();
fileRouter.post(
'/',
fileController.upload.single('fileUpload'),
fileController.fileUpload,
);
fileRouter.post('/directory', fileController.createDirectory);
module.exports = fileRouter;

View file

@ -1,13 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<%- include('partials/header') %>
<body>
<% if(currentUser) { %>
<p> Hello <%= currentUser.username %> </p>
<a href="/auth/logout">sign out</a>
<%- include('partials/fileUpload') %>
<% } else { %>
<form action="/auth/login" method="post">
<label for="username">Username</label>
<input type="text" name="username">
@ -16,6 +13,6 @@
<button type="submit">Submit</button>
<a href="/auth/register">Click here to register</a>
</form>
</body>
</html>
<% } %>
<%- include('partials/footer') %>

View file

@ -0,0 +1,17 @@
<form action="/file" method="post" enctype="multipart/form-data">
<div class="container file-upload">
<input type="file" name="fileUpload" id="fileUpload" class="file">
<input type="hidden" name="folderId" value=<%= folder.id %>>
<button type="submit">Upload</button>
</div>
</form>
<form action="/file/directory" method="post">
<div class="container">
<label for="directory-name">Directory Name:</label>
<input type="text" name="directory-name" placeholder="">
<input type="hidden" name="parentId" value=<%= folder.id %>>
<button>Create Directory</button>
</div>
</form>

View file

@ -0,0 +1,3 @@
</body>
</html>

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/style.css">
<title><%= pageTitle %></title>
</head>
<body>

View file

@ -1,25 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<%- include('partials/header') %>
<form action="/auth/register" method="post" class="register-form">
<div class="form-input">
<label for="username">Username</label>
<input type="text" name="username">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<form action="/auth/register" method="post">
<label for="username">Username</label>
<input type="text" name="username">
</div>
<div class="form-input">
<label for="password">Password</label>
<input type="password" name="password" id="password">
<label for="password-confirmation">Password Confirmation</label>
<input type="password-confirmation" name="password-confirmation" id="password-confirmation">
<label for="email">Email</label>
<input type="email" name="email" id="email">
<input type="password" name="password" id="password" required>
</div>
<div class="form-input">
<label for="password-confirmation">Password Confirmation</label>
<input type="password" name="password-confirmation" id="password-confirmation" required>
</div>
<div class="form-input">
<label for="email">Email</label>
<input type="email" name="email" id="email" required>
</div>
<button type="submit">Submit</button>
</form>
<script src="/js/form.js"></script>
</body>
</html>
<script src="/js/form.js"></script>
<%- include('partials/footer') %>