mirror of
https://gitea.smigz.com/smiggiddy/odin-codeprojects.git
synced 2025-06-27 20:45:35 -04:00
feat: added file upload + dir creation
This commit is contained in:
parent
b1c295d2ac
commit
eedab606f8
25 changed files with 965 additions and 103 deletions
262
file-uploader/package-lock.json
generated
262
file-uploader/package-lock.json
generated
|
@ -10,12 +10,15 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.5.0",
|
"@prisma/client": "^6.5.0",
|
||||||
|
"@quixo3/prisma-session-store": "^3.1.13",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"express-validator": "^7.2.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": {
|
"devDependencies": {
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
|
@ -521,6 +524,27 @@
|
||||||
"node-pre-gyp": "bin/node-pre-gyp"
|
"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": {
|
"node_modules/@prisma/client": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.5.0.tgz",
|
||||||
|
@ -597,6 +621,24 @@
|
||||||
"@prisma/debug": "6.5.0"
|
"@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": {
|
"node_modules/@types/eslint": {
|
||||||
"version": "9.6.1",
|
"version": "9.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
"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"
|
"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": {
|
"node_modules/aproba": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
|
||||||
|
@ -1146,9 +1194,19 @@
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
"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": {
|
"node_modules/console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
|
"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": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="
|
"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": {
|
"node_modules/isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
|
@ -2754,6 +2869,15 @@
|
||||||
"node": "*"
|
"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": {
|
"node_modules/minipass": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
"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": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
@ -3057,6 +3254,17 @@
|
||||||
"url": "https://github.com/sponsors/jaredhanson"
|
"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": {
|
"node_modules/passport-strategy": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
|
"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": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
@ -3791,6 +4005,14 @@
|
||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
@ -3975,6 +4197,15 @@
|
||||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
@ -3982,6 +4213,18 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "0BSD"
|
"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": {
|
"node_modules/type-is": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||||
|
@ -3995,6 +4238,12 @@
|
||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/uid-safe": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
"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": {
|
"node_modules/yallist": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
|
|
@ -11,12 +11,15 @@
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.5.0",
|
"@prisma/client": "^6.5.0",
|
||||||
|
"@quixo3/prisma-session-store": "^3.1.13",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"express-validator": "^7.2.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": {
|
"devDependencies": {
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
|
|
|
@ -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");
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Folder" ALTER COLUMN "modification_date" SET DEFAULT CURRENT_TIMESTAMP;
|
|
@ -20,12 +20,15 @@ model User {
|
||||||
email String @unique
|
email String @unique
|
||||||
password String
|
password String
|
||||||
files File[]
|
files File[]
|
||||||
|
Folder Folder[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model File {
|
model File {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String @db.VarChar(255)
|
name String @db.VarChar(255)
|
||||||
url String
|
url String
|
||||||
|
size Int
|
||||||
|
mimetype String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
owner User? @relation(fields: [ownerId], references: [id])
|
owner User? @relation(fields: [ownerId], references: [id])
|
||||||
ownerId Int?
|
ownerId Int?
|
||||||
|
@ -36,5 +39,19 @@ model File {
|
||||||
model Folder {
|
model Folder {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String @db.VarChar(255)
|
name String @db.VarChar(255)
|
||||||
|
creation_date DateTime @default(now())
|
||||||
|
modification_date DateTime @default(now())
|
||||||
File File[]
|
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
|
||||||
}
|
}
|
||||||
|
|
142
file-uploader/public/css/style.css
Normal file
142
file-uploader/public/css/style.css
Normal 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; */
|
||||||
|
/* } */
|
|
@ -1,14 +1,57 @@
|
||||||
console.log('form stuff');
|
|
||||||
|
|
||||||
const usernameCheck = async (username) => {
|
const usernameCheck = async (username) => {
|
||||||
|
if (username.length > 0) {
|
||||||
let res = await fetch(`/auth/username?username=${username}`);
|
let res = await fetch(`/auth/username?username=${username}`);
|
||||||
return await res.json();
|
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) => {
|
const isDataValid = async (e, callback) => {
|
||||||
let input = e.target.value;
|
let test = await callback(e.target.value);
|
||||||
let test = await usernameCheck(input);
|
test
|
||||||
test.results ? console.log('user exists') : console.log('user doesnt');
|
? (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();
|
||||||
|
|
|
@ -1,21 +1,30 @@
|
||||||
const { Auth } = require('../models/auth');
|
const Db = require('../models/db');
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
|
|
||||||
const db = new Auth();
|
const db = new Db();
|
||||||
|
|
||||||
const loginGet = (req, res, next) => {
|
const loginGet = (req, res, next) => {
|
||||||
res.send('Login Route');
|
res.send('Login Route');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const logout = (req, res, next) => {
|
||||||
|
req.logout((err) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
res.redirect('/');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const registerGet = (req, res) => {
|
const registerGet = (req, res) => {
|
||||||
res.render('register');
|
res.render('register', { pageTitle: 'Register' });
|
||||||
};
|
};
|
||||||
const registerPost = async (req, res, next) => {
|
const registerPost = async (req, res, next) => {
|
||||||
const { username, email, password } = req.body;
|
const { username, email, password } = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const hashedPassword = bcrypt.hash(password);
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
const result = await db.createUser({
|
const result = await db.auth.createUser({
|
||||||
username: username,
|
username: username,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
email: email,
|
email: email,
|
||||||
|
@ -38,7 +47,7 @@ const findByUsername = async (req, res, next) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (username && username.length > 0) {
|
if (username && username.length > 0) {
|
||||||
const exists = (await db.getUserByUsername(username))
|
const exists = (await db.auth.getUserByUsername(username))
|
||||||
? true
|
? true
|
||||||
: false;
|
: 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,
|
||||||
|
};
|
||||||
|
|
65
file-uploader/src/controllers/fileController.js
Normal file
65
file-uploader/src/controllers/fileController.js
Normal 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 };
|
|
@ -1,9 +1,17 @@
|
||||||
// const db = require("../models/auth");
|
const Db = require('../models/db');
|
||||||
|
|
||||||
|
const db = new Db();
|
||||||
const indexGet = async (req, res) => {
|
const indexGet = async (req, res) => {
|
||||||
// const data = await db.allUsers()
|
// const data = await db.allUsers()
|
||||||
// console.dir(data)
|
// console.dir(data)
|
||||||
res.render("main");
|
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 };
|
module.exports = { indexGet };
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const session = require('express-session');
|
const expressSession = require('express-session');
|
||||||
const passport = require('passport');
|
const { PrismaSessionStore } = require('@quixo3/prisma-session-store');
|
||||||
|
const { PrismaClient } = require('@prisma/client');
|
||||||
|
const { passport } = require('./middlewares/auth');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
|
|
||||||
const port = process.env.APP_PORT || 3000;
|
const port = process.env.APP_PORT || 3000;
|
||||||
|
@ -9,17 +11,40 @@ const app = express();
|
||||||
const assetsPath = path.join(path.dirname(__dirname), 'public');
|
const assetsPath = path.join(path.dirname(__dirname), 'public');
|
||||||
app.use(express.static(assetsPath));
|
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 authRouter = require('./routes/authRouter');
|
||||||
const indexRouter = require('./routes/indexRouter');
|
const indexRouter = require('./routes/indexRouter');
|
||||||
|
const fileRouter = require('./routes/fileRouter');
|
||||||
|
|
||||||
app.set('views', path.join(__dirname, 'views'));
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
app.set('view engine', 'ejs');
|
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('/', indexRouter);
|
||||||
app.use('/auth', authRouter);
|
app.use('/auth', authRouter);
|
||||||
|
app.use('/file', fileRouter);
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`App listening on ${port}`);
|
console.log(`App listening on ${port}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
51
file-uploader/src/middlewares/auth.js
Normal file
51
file-uploader/src/middlewares/auth.js
Normal 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 };
|
|
@ -1,37 +1,40 @@
|
||||||
const { PrismaClient, Prisma } = require("@prisma/client");
|
const { PrismaClient, Prisma } = require('@prisma/client');
|
||||||
// const { Prisma } = require("prisma");
|
// const { Prisma } = require("prisma");
|
||||||
|
|
||||||
class Auth {
|
class Auth {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.primsa = new PrismaClient();
|
this.prisma = new PrismaClient();
|
||||||
}
|
}
|
||||||
async createUser(user) {
|
async createUser(user) {
|
||||||
try {
|
try {
|
||||||
await this.primsa.user.create({
|
await this.prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user?.email,
|
email: user?.email,
|
||||||
password: user.password,
|
password: user.password,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return { message: "user created" };
|
return { message: 'user created' };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.log(`PRISMA error ${e}`);
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
if (e.code === "P2002") {
|
if (e.code === 'P2002') {
|
||||||
console.log("User must use a unique username");
|
console.log('There is a unique constraint violation');
|
||||||
return { error: "must use unique username" };
|
return {
|
||||||
|
error: 'must use unique username and a unique email.',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async allUsers() {
|
async allUsers() {
|
||||||
return await this.primsa.user.findMany();
|
return await this.prisma.user.findMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserByUsername(username) {
|
async getUserByUsername(username) {
|
||||||
try {
|
try {
|
||||||
return await this.primsa.user.findUnique({
|
return await this.prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
username: username,
|
username: username,
|
||||||
},
|
},
|
||||||
|
@ -40,6 +43,30 @@ class Auth {
|
||||||
return { error: 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() {
|
// async function main() {
|
||||||
|
@ -58,4 +85,3 @@ class Auth {
|
||||||
// })
|
// })
|
||||||
|
|
||||||
module.exports = { Auth };
|
module.exports = { Auth };
|
||||||
|
|
||||||
|
|
11
file-uploader/src/models/db.js
Normal file
11
file-uploader/src/models/db.js
Normal 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;
|
77
file-uploader/src/models/file.js
Normal file
77
file-uploader/src/models/file.js
Normal 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 };
|
|
@ -1,12 +1,21 @@
|
||||||
const { Router } = require('express');
|
const { Router } = require('express');
|
||||||
const authController = require('../controllers/authController');
|
const authController = require('../controllers/authController');
|
||||||
|
const { passport } = require('../middlewares/auth');
|
||||||
|
|
||||||
const authRouter = Router();
|
const authRouter = Router();
|
||||||
|
|
||||||
authRouter.get('/login', authController.loginGet);
|
authRouter.get('/login', authController.loginGet);
|
||||||
|
authRouter.post(
|
||||||
|
'/login',
|
||||||
|
passport.authenticate('local', {
|
||||||
|
successRedirect: '/',
|
||||||
|
failureRedirect: '/',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
authRouter.get('/logout', authController.logout);
|
||||||
authRouter.get('/register', authController.registerGet);
|
authRouter.get('/register', authController.registerGet);
|
||||||
authRouter.post('/register', authController.registerPost);
|
authRouter.post('/register', authController.registerPost);
|
||||||
authRouter.get('/username', authController.findByUsername);
|
authRouter.get('/username', authController.findByUsername);
|
||||||
|
authRouter.get('/email', authController.findByEmail);
|
||||||
|
|
||||||
module.exports = authRouter;
|
module.exports = authRouter;
|
||||||
|
|
||||||
|
|
13
file-uploader/src/routes/fileRouter.js
Normal file
13
file-uploader/src/routes/fileRouter.js
Normal 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;
|
|
@ -1,13 +1,10 @@
|
||||||
<!DOCTYPE html>
|
<%- include('partials/header') %>
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Document</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
<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">
|
<form action="/auth/login" method="post">
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
<input type="text" name="username">
|
<input type="text" name="username">
|
||||||
|
@ -16,6 +13,6 @@
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
<a href="/auth/register">Click here to register</a>
|
<a href="/auth/register">Click here to register</a>
|
||||||
</form>
|
</form>
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
<% } %>
|
||||||
|
<%- include('partials/footer') %>
|
||||||
|
|
17
file-uploader/src/views/partials/fileUpload.ejs
Normal file
17
file-uploader/src/views/partials/fileUpload.ejs
Normal 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>
|
3
file-uploader/src/views/partials/footer.ejs
Normal file
3
file-uploader/src/views/partials/footer.ejs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
11
file-uploader/src/views/partials/header.ejs
Normal file
11
file-uploader/src/views/partials/header.ejs
Normal 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>
|
|
@ -1,25 +1,27 @@
|
||||||
<!DOCTYPE html>
|
<%- include('partials/header') %>
|
||||||
<html lang="en">
|
<form action="/auth/register" method="post" class="register-form">
|
||||||
|
<div class="form-input">
|
||||||
<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>
|
<label for="username">Username</label>
|
||||||
<input type="text" name="username">
|
<input type="text" name="username">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="form-input">
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
<input type="password" name="password" id="password">
|
<input type="password" name="password" id="password" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-input">
|
||||||
<label for="password-confirmation">Password Confirmation</label>
|
<label for="password-confirmation">Password Confirmation</label>
|
||||||
<input type="password-confirmation" name="password-confirmation" id="password-confirmation">
|
<input type="password" name="password-confirmation" id="password-confirmation" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-input">
|
||||||
<label for="email">Email</label>
|
<label for="email">Email</label>
|
||||||
<input type="email" name="email" id="email">
|
<input type="email" name="email" id="email" required>
|
||||||
|
</div>
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
<script src="/js/form.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
<script src="/js/form.js"></script>
|
||||||
|
|
||||||
|
<%- include('partials/footer') %>
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue