initial commit
This commit is contained in:
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
README.md
|
||||||
|
dist
|
||||||
|
.next
|
||||||
|
.git
|
||||||
4
.env.example
Normal file
4
.env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
APPWRITE_FUNCTION_API_KEY=
|
||||||
|
# IMPORTANT If you're running the website inside docker and your Appwrite instance runs on localhost host,
|
||||||
|
# use host.docker.internal instead of localhost
|
||||||
|
APPWRITE_HOST=http://appwrite/v1
|
||||||
11
.eslintrc.json
Normal file
11
.eslintrc.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": ["eslint-config-next", "eslint:recommended"],
|
||||||
|
"plugins": ["react"],
|
||||||
|
"rules": {
|
||||||
|
"quotes": ["warn", "double", { "allowTemplateLiterals": true }],
|
||||||
|
"react-hooks/exhaustive-deps": ["off"],
|
||||||
|
"import/no-anonymous-default-export": ["off"],
|
||||||
|
"no-unused-vars": ["warn"],
|
||||||
|
"react/no-unescaped-entities": ["off"]
|
||||||
|
}
|
||||||
|
}
|
||||||
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
.env
|
||||||
90
.setup/data/collections.ts
Normal file
90
.setup/data/collections.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
export default [
|
||||||
|
{
|
||||||
|
$id: "shares",
|
||||||
|
$read: [],
|
||||||
|
$write: [],
|
||||||
|
name: "Shares",
|
||||||
|
enabled: true,
|
||||||
|
permission: "document",
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
key: "securityID",
|
||||||
|
type: "string",
|
||||||
|
status: "available",
|
||||||
|
required: false,
|
||||||
|
array: false,
|
||||||
|
size: 255,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "createdAt",
|
||||||
|
type: "integer",
|
||||||
|
status: "available",
|
||||||
|
required: true,
|
||||||
|
array: false,
|
||||||
|
min: 0,
|
||||||
|
max: 9007199254740991,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "expiresAt",
|
||||||
|
type: "integer",
|
||||||
|
status: "available",
|
||||||
|
required: true,
|
||||||
|
array: false,
|
||||||
|
min: 0,
|
||||||
|
max: 9007199254740991,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "visitorCount",
|
||||||
|
type: "integer",
|
||||||
|
status: "available",
|
||||||
|
required: false,
|
||||||
|
array: false,
|
||||||
|
min: 0,
|
||||||
|
max: 9007199254740991,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "enabled",
|
||||||
|
type: "boolean",
|
||||||
|
status: "available",
|
||||||
|
required: false,
|
||||||
|
array: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$id: "shareSecurity",
|
||||||
|
$read: [],
|
||||||
|
$write: [],
|
||||||
|
name: "ShareSecurity",
|
||||||
|
enabled: true,
|
||||||
|
permission: "document",
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
key: "password",
|
||||||
|
type: "string",
|
||||||
|
status: "available",
|
||||||
|
required: false,
|
||||||
|
array: false,
|
||||||
|
size: 128,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "maxVisitors",
|
||||||
|
type: "integer",
|
||||||
|
status: "available",
|
||||||
|
required: false,
|
||||||
|
array: false,
|
||||||
|
min: 0,
|
||||||
|
max: 9007199254740991,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
46
.setup/data/functions.ts
Normal file
46
.setup/data/functions.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
export default () => {
|
||||||
|
const host = process.env["APPWRITE_HOST"].replace(
|
||||||
|
"localhost",
|
||||||
|
"host.docker.internal"
|
||||||
|
);
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
$id: "createShare",
|
||||||
|
execute: ["role:all"],
|
||||||
|
name: "Create Share",
|
||||||
|
runtime: "node-16.0",
|
||||||
|
vars: {
|
||||||
|
APPWRITE_FUNCTION_ENDPOINT: host,
|
||||||
|
APPWRITE_FUNCTION_API_KEY: process.env["APPWRITE_FUNCTION_API_KEY"],
|
||||||
|
},
|
||||||
|
events: [],
|
||||||
|
schedule: "",
|
||||||
|
timeout: 15,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$id: "finishShare",
|
||||||
|
execute: ["role:all"],
|
||||||
|
name: "Finish Share",
|
||||||
|
runtime: "node-16.0",
|
||||||
|
deployment: "625db8ded97874b96590",
|
||||||
|
vars: {
|
||||||
|
APPWRITE_FUNCTION_ENDPOINT: host,
|
||||||
|
APPWRITE_FUNCTION_API_KEY: process.env["APPWRITE_FUNCTION_API_KEY"],
|
||||||
|
},
|
||||||
|
events: [],
|
||||||
|
schedule: "",
|
||||||
|
timeout: 15,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$id: "cleanShares",
|
||||||
|
name: "Clean Shares",
|
||||||
|
runtime: "node-16.0",
|
||||||
|
path: "functions/cleanShares",
|
||||||
|
entrypoint: "src/index.js",
|
||||||
|
execute: ["role:all"],
|
||||||
|
events: [],
|
||||||
|
schedule: "30,59 * * * *",
|
||||||
|
timeout: 60,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
55
.setup/index.ts
Normal file
55
.setup/index.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import authService from "./services/auth.service";
|
||||||
|
import setupService from "./services/setup.service";
|
||||||
|
import rl from "readline-sync";
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
console.info("\nWelcome to the Pingvin Share Appwrite setup 👋");
|
||||||
|
console.info(
|
||||||
|
"Please follow the questions and be sure that you ENTER THE CORRECT informations. Because the error handling isn't good.\n"
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
process.env["APPWRITE_HOST"] = rl.question(
|
||||||
|
"Appwrite host (http://localhost/v1): ",
|
||||||
|
{
|
||||||
|
defaultInput: "http://localhost/v1",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.info("Authenticate...");
|
||||||
|
process.env["APPWRITE_USER_TOKEN"] = await authService.getToken();
|
||||||
|
|
||||||
|
console.info("Creating project...");
|
||||||
|
await setupService.createProject();
|
||||||
|
|
||||||
|
console.info("Generating API key for setup...");
|
||||||
|
process.env["APPWRITE_API_KEY"] = await authService.generateApiKey();
|
||||||
|
|
||||||
|
console.info("Generating API key for functions...");
|
||||||
|
process.env["APPWRITE_FUNCTION_API_KEY"] =
|
||||||
|
await setupService.generateFunctionsApiKey();
|
||||||
|
|
||||||
|
console.info("Creating collections...");
|
||||||
|
await setupService.createCollections();
|
||||||
|
|
||||||
|
console.info("Creating functions...");
|
||||||
|
await setupService.createFunctions();
|
||||||
|
|
||||||
|
console.info("Creating function deployments...");
|
||||||
|
await setupService.createFunctionDeployments();
|
||||||
|
|
||||||
|
console.info("Adding frontend url...");
|
||||||
|
await setupService.addPlatform(
|
||||||
|
rl.question("Frontend host of Pingvin Share (localhost): ", {
|
||||||
|
defaultInput: "localhost",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
console.error("\n\n ❌ Error: " + e.message);
|
||||||
|
console.info(
|
||||||
|
"\nSorry, an error occured while the setup. The full logs can be found above."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.info("\n✅ Done");
|
||||||
|
})();
|
||||||
|
export {};
|
||||||
676
.setup/package-lock.json
generated
Normal file
676
.setup/package-lock.json
generated
Normal file
@@ -0,0 +1,676 @@
|
|||||||
|
{
|
||||||
|
"name": "setup",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "setup",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.26.1",
|
||||||
|
"cookie": "^0.5.0",
|
||||||
|
"node-appwrite": "^5.1.0",
|
||||||
|
"readline-sync": "^1.4.10",
|
||||||
|
"tar": "^6.1.11",
|
||||||
|
"typescript": "^4.6.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/readline-sync": "^1.4.4",
|
||||||
|
"@types/tar": "^6.1.1",
|
||||||
|
"ts-node": "^10.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@cspotcode/source-map-consumer": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
|
"version": "0.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
|
||||||
|
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@cspotcode/source-map-consumer": "0.8.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node10": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node12": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node14": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node16": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/minipass": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/minipass/-/minipass-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-foLGjgrJkUjLG/o2t2ymlZGEoBNBa/TfoUZ7oCTkOjP1T43UGBJspovJou/l3ZuHvye2ewR5cZNtp2zyWgILMA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "17.0.25",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.25.tgz",
|
||||||
|
"integrity": "sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/readline-sync": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/readline-sync/-/readline-sync-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-cFjVIoiamX7U6zkO2VPvXyTxbFDdiRo902IarJuPVxBhpDnXhwSaVE86ip+SCuyWBbEioKCkT4C88RNTxBM1Dw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/tar": {
|
||||||
|
"version": "6.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.1.tgz",
|
||||||
|
"integrity": "sha512-8mto3YZfVpqB1CHMaYz1TUYIQfZFbh/QbEq5Hsn6D0ilCfqRVCdalmc89B7vi3jhl9UYIk+dWDABShNfOkv5HA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/minipass": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/acorn": {
|
||||||
|
"version": "8.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
||||||
|
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"acorn": "bin/acorn"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/acorn-walk": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
|
||||||
|
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/arg": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||||
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "0.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
||||||
|
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.14.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chownr": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/create-require": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/diff": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.14.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
||||||
|
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fs-minipass": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||||
|
"dependencies": {
|
||||||
|
"minipass": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/make-error": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minipass": {
|
||||||
|
"version": "3.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz",
|
||||||
|
"integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minizlib": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"minipass": "^3.0.0",
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-appwrite": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-CuSa4z7mh0VgR+VkjKWVuwpwiDU2pHNkSFpSEEo/gYJXgPpaNWguJfdJJKFTbUgC1CfIRUHYBLQIdHTX/LgsIg==",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.26.1",
|
||||||
|
"form-data": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/readline-sync": {
|
||||||
|
"version": "1.4.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz",
|
||||||
|
"integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar": {
|
||||||
|
"version": "6.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",
|
||||||
|
"integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==",
|
||||||
|
"dependencies": {
|
||||||
|
"chownr": "^2.0.0",
|
||||||
|
"fs-minipass": "^2.0.0",
|
||||||
|
"minipass": "^3.0.0",
|
||||||
|
"minizlib": "^2.1.1",
|
||||||
|
"mkdirp": "^1.0.3",
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar/node_modules/mkdirp": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||||
|
"bin": {
|
||||||
|
"mkdirp": "bin/cmd.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-node": {
|
||||||
|
"version": "10.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz",
|
||||||
|
"integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@cspotcode/source-map-support": "0.7.0",
|
||||||
|
"@tsconfig/node10": "^1.0.7",
|
||||||
|
"@tsconfig/node12": "^1.0.7",
|
||||||
|
"@tsconfig/node14": "^1.0.0",
|
||||||
|
"@tsconfig/node16": "^1.0.2",
|
||||||
|
"acorn": "^8.4.1",
|
||||||
|
"acorn-walk": "^8.1.1",
|
||||||
|
"arg": "^4.1.0",
|
||||||
|
"create-require": "^1.1.0",
|
||||||
|
"diff": "^4.0.1",
|
||||||
|
"make-error": "^1.1.1",
|
||||||
|
"v8-compile-cache-lib": "^3.0.0",
|
||||||
|
"yn": "3.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ts-node": "dist/bin.js",
|
||||||
|
"ts-node-cwd": "dist/bin-cwd.js",
|
||||||
|
"ts-node-esm": "dist/bin-esm.js",
|
||||||
|
"ts-node-script": "dist/bin-script.js",
|
||||||
|
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||||
|
"ts-script": "dist/bin-script-deprecated.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@swc/core": ">=1.2.50",
|
||||||
|
"@swc/wasm": ">=1.2.50",
|
||||||
|
"@types/node": "*",
|
||||||
|
"typescript": ">=2.7"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@swc/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/wasm": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "4.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
|
||||||
|
"integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/v8-compile-cache-lib": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/yallist": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||||
|
},
|
||||||
|
"node_modules/yn": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@cspotcode/source-map-consumer": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@cspotcode/source-map-support": {
|
||||||
|
"version": "0.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
|
||||||
|
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@cspotcode/source-map-consumer": "0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tsconfig/node10": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@tsconfig/node12": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@tsconfig/node14": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@tsconfig/node16": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/minipass": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/minipass/-/minipass-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-foLGjgrJkUjLG/o2t2ymlZGEoBNBa/TfoUZ7oCTkOjP1T43UGBJspovJou/l3ZuHvye2ewR5cZNtp2zyWgILMA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"version": "17.0.25",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.25.tgz",
|
||||||
|
"integrity": "sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/readline-sync": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/readline-sync/-/readline-sync-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-cFjVIoiamX7U6zkO2VPvXyTxbFDdiRo902IarJuPVxBhpDnXhwSaVE86ip+SCuyWBbEioKCkT4C88RNTxBM1Dw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/tar": {
|
||||||
|
"version": "6.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.1.tgz",
|
||||||
|
"integrity": "sha512-8mto3YZfVpqB1CHMaYz1TUYIQfZFbh/QbEq5Hsn6D0ilCfqRVCdalmc89B7vi3jhl9UYIk+dWDABShNfOkv5HA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/minipass": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"acorn": {
|
||||||
|
"version": "8.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
||||||
|
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"acorn-walk": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
|
||||||
|
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"arg": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||||
|
},
|
||||||
|
"axios": {
|
||||||
|
"version": "0.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
||||||
|
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
||||||
|
"requires": {
|
||||||
|
"follow-redirects": "^1.14.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chownr": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="
|
||||||
|
},
|
||||||
|
"combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"requires": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cookie": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
|
||||||
|
},
|
||||||
|
"create-require": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
||||||
|
},
|
||||||
|
"diff": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"follow-redirects": {
|
||||||
|
"version": "1.14.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
||||||
|
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
|
||||||
|
},
|
||||||
|
"form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"requires": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fs-minipass": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||||
|
"requires": {
|
||||||
|
"minipass": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"make-error": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"requires": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minipass": {
|
||||||
|
"version": "3.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz",
|
||||||
|
"integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==",
|
||||||
|
"requires": {
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minizlib": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||||
|
"requires": {
|
||||||
|
"minipass": "^3.0.0",
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node-appwrite": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-CuSa4z7mh0VgR+VkjKWVuwpwiDU2pHNkSFpSEEo/gYJXgPpaNWguJfdJJKFTbUgC1CfIRUHYBLQIdHTX/LgsIg==",
|
||||||
|
"requires": {
|
||||||
|
"axios": "^0.26.1",
|
||||||
|
"form-data": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"readline-sync": {
|
||||||
|
"version": "1.4.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz",
|
||||||
|
"integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw=="
|
||||||
|
},
|
||||||
|
"tar": {
|
||||||
|
"version": "6.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",
|
||||||
|
"integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==",
|
||||||
|
"requires": {
|
||||||
|
"chownr": "^2.0.0",
|
||||||
|
"fs-minipass": "^2.0.0",
|
||||||
|
"minipass": "^3.0.0",
|
||||||
|
"minizlib": "^2.1.1",
|
||||||
|
"mkdirp": "^1.0.3",
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"mkdirp": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ts-node": {
|
||||||
|
"version": "10.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz",
|
||||||
|
"integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@cspotcode/source-map-support": "0.7.0",
|
||||||
|
"@tsconfig/node10": "^1.0.7",
|
||||||
|
"@tsconfig/node12": "^1.0.7",
|
||||||
|
"@tsconfig/node14": "^1.0.0",
|
||||||
|
"@tsconfig/node16": "^1.0.2",
|
||||||
|
"acorn": "^8.4.1",
|
||||||
|
"acorn-walk": "^8.1.1",
|
||||||
|
"arg": "^4.1.0",
|
||||||
|
"create-require": "^1.1.0",
|
||||||
|
"diff": "^4.0.1",
|
||||||
|
"make-error": "^1.1.1",
|
||||||
|
"v8-compile-cache-lib": "^3.0.0",
|
||||||
|
"yn": "3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"version": "4.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
|
||||||
|
"integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw=="
|
||||||
|
},
|
||||||
|
"v8-compile-cache-lib": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"yallist": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||||
|
},
|
||||||
|
"yn": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
.setup/package.json
Normal file
24
.setup/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "setup",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"typescript": "^4.6.3",
|
||||||
|
"axios": "^0.26.1",
|
||||||
|
"cookie": "^0.5.0",
|
||||||
|
"node-appwrite": "^5.1.0",
|
||||||
|
"readline-sync": "^1.4.10",
|
||||||
|
"tar": "^6.1.11"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/readline-sync": "^1.4.4",
|
||||||
|
"@types/tar": "^6.1.1",
|
||||||
|
"ts-node": "^10.7.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
||||||
11
.setup/services/api.service.ts
Normal file
11
.setup/services/api.service.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const api = () =>
|
||||||
|
axios.create({
|
||||||
|
baseURL: process.env["APPWRITE_HOST"],
|
||||||
|
headers: {
|
||||||
|
cookie: `a_session_console=${process.env["APPWRITE_USER_TOKEN"]}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default api;
|
||||||
44
.setup/services/auth.service.ts
Normal file
44
.setup/services/auth.service.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import api from "./api.service";
|
||||||
|
import rl from "readline-sync";
|
||||||
|
import cookie from "cookie";
|
||||||
|
|
||||||
|
const getToken = async () => {
|
||||||
|
var email = rl.question("Email: ");
|
||||||
|
var password = rl.question("Password: ", {
|
||||||
|
hideEchoBack: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const credentials = await api().post("/account/sessions", {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
return cookie.parse(credentials.headers["set-cookie"].toString())
|
||||||
|
.a_session_console_legacy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateApiKey = async () => {
|
||||||
|
const res = await api().post("/projects/pingvin-share/keys", {
|
||||||
|
name: "Setup key",
|
||||||
|
scopes: [
|
||||||
|
"collections.read",
|
||||||
|
"collections.write",
|
||||||
|
"attributes.read",
|
||||||
|
"attributes.write",
|
||||||
|
"indexes.read",
|
||||||
|
"indexes.write",
|
||||||
|
"documents.read",
|
||||||
|
"documents.write",
|
||||||
|
"functions.read",
|
||||||
|
"functions.write",
|
||||||
|
"execution.read",
|
||||||
|
"execution.write",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return res.data.secret;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getToken,
|
||||||
|
generateApiKey,
|
||||||
|
};
|
||||||
18
.setup/services/aw.service.ts
Normal file
18
.setup/services/aw.service.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import sdk from "node-appwrite";
|
||||||
|
const aw = () => {
|
||||||
|
let client = new sdk.Client();
|
||||||
|
|
||||||
|
client
|
||||||
|
.setEndpoint(process.env["APPWRITE_HOST"])
|
||||||
|
.setProject("pingvin-share")
|
||||||
|
.setKey(process.env["APPWRITE_API_KEY"])
|
||||||
|
.setSelfSigned();
|
||||||
|
|
||||||
|
const database = new sdk.Database(client);
|
||||||
|
const storage = new sdk.Database(client);
|
||||||
|
const functions = new sdk.Functions(client);
|
||||||
|
|
||||||
|
return { database, storage, functions };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default aw;
|
||||||
137
.setup/services/setup.service.ts
Normal file
137
.setup/services/setup.service.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import api from "./api.service";
|
||||||
|
import aw from "./aw.service";
|
||||||
|
import collections from "../data/collections";
|
||||||
|
import functions from "../data/functions";
|
||||||
|
import zipDirectory from "../utils/compress.util";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
const createProject = async () => {
|
||||||
|
const teamId = (
|
||||||
|
await api().post("/teams", {
|
||||||
|
teamId: "unique()",
|
||||||
|
name: "Pingvin Share",
|
||||||
|
})
|
||||||
|
).data.$id;
|
||||||
|
return await api().post("/projects", {
|
||||||
|
projectId: "pingvin-share",
|
||||||
|
name: "Pingvin Share",
|
||||||
|
teamId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addPlatform = async (hostname : string) => {
|
||||||
|
await api().post("/projects/pingvin-share/platforms", {
|
||||||
|
type: "web",
|
||||||
|
name: "Pingvin Share Web Frontend",
|
||||||
|
hostname: hostname,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createCollections = async () => {
|
||||||
|
for (const collection of collections) {
|
||||||
|
const { attributes } = collection;
|
||||||
|
const { indexes } = collection;
|
||||||
|
|
||||||
|
await aw().database.createCollection(
|
||||||
|
collection.$id,
|
||||||
|
collection.name,
|
||||||
|
collection.permission,
|
||||||
|
collection.$read,
|
||||||
|
collection.$write
|
||||||
|
);
|
||||||
|
for (const attribute of attributes) {
|
||||||
|
if (attribute.type == "string") {
|
||||||
|
await aw().database.createStringAttribute(
|
||||||
|
collection.$id,
|
||||||
|
attribute.key,
|
||||||
|
attribute.size,
|
||||||
|
attribute.required,
|
||||||
|
attribute.default,
|
||||||
|
attribute.array
|
||||||
|
);
|
||||||
|
} else if (attribute.type == "integer") {
|
||||||
|
await aw().database.createIntegerAttribute(
|
||||||
|
collection.$id,
|
||||||
|
attribute.key,
|
||||||
|
attribute.required,
|
||||||
|
attribute.min,
|
||||||
|
attribute.max,
|
||||||
|
attribute.default,
|
||||||
|
attribute.array
|
||||||
|
);
|
||||||
|
} else if (attribute.type == "boolean") {
|
||||||
|
await aw().database.createBooleanAttribute(
|
||||||
|
collection.$id,
|
||||||
|
attribute.key,
|
||||||
|
attribute.required,
|
||||||
|
attribute.default,
|
||||||
|
attribute.array
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const index of indexes) {
|
||||||
|
aw().database.createIndex(
|
||||||
|
collection.$id,
|
||||||
|
index.key,
|
||||||
|
index.type,
|
||||||
|
index.attributes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateFunctionsApiKey = async () => {
|
||||||
|
const res = await api().post("/projects/pingvin-share/keys", {
|
||||||
|
name: "Functions API Key",
|
||||||
|
scopes: [
|
||||||
|
"documents.read",
|
||||||
|
"documents.write",
|
||||||
|
"buckets.read",
|
||||||
|
"buckets.write",
|
||||||
|
"files.read",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return res.data.secret;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFunctions = async () => {
|
||||||
|
for (const fcn of functions()) {
|
||||||
|
await aw().functions.create(
|
||||||
|
fcn.$id,
|
||||||
|
fcn.name,
|
||||||
|
fcn.execute,
|
||||||
|
fcn.runtime,
|
||||||
|
fcn.vars,
|
||||||
|
fcn.events,
|
||||||
|
fcn.schedule,
|
||||||
|
fcn.timeout
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFunctionDeployments = async () => {
|
||||||
|
let path: string;
|
||||||
|
for (const fcn of functions()) {
|
||||||
|
(path = (await zipDirectory(fcn.$id)) as string),
|
||||||
|
await aw().functions.createDeployment(
|
||||||
|
fcn.$id,
|
||||||
|
"src/index.js",
|
||||||
|
path,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Delete zip
|
||||||
|
fs.unlinkSync(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
createProject,
|
||||||
|
createCollections,
|
||||||
|
createFunctions,
|
||||||
|
createFunctionDeployments,
|
||||||
|
generateFunctionsApiKey,
|
||||||
|
addPlatform,
|
||||||
|
};
|
||||||
|
function token(token: any) {
|
||||||
|
throw new Error("Function not implemented.");
|
||||||
|
}
|
||||||
11
.setup/tsconfig.json
Normal file
11
.setup/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"target": "es6",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"lib": ["es2015"]
|
||||||
|
}
|
||||||
17
.setup/utils/compress.util.ts
Normal file
17
.setup/utils/compress.util.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import tar from "tar";
|
||||||
|
|
||||||
|
const zipDirectory = (functionName: string) => {
|
||||||
|
tar.create(
|
||||||
|
{
|
||||||
|
gzip: true,
|
||||||
|
sync: true,
|
||||||
|
cwd: `./../functions/${functionName}`,
|
||||||
|
file: "code.tar.gz",
|
||||||
|
},
|
||||||
|
["./"]
|
||||||
|
);
|
||||||
|
return fs.realpathSync("code.tar.gz");
|
||||||
|
};
|
||||||
|
|
||||||
|
export default zipDirectory;
|
||||||
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Install dependencies only when needed
|
||||||
|
FROM node:16-alpine AS deps
|
||||||
|
WORKDIR /opt/app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
FROM node:16-alpine AS builder
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
WORKDIR /opt/app
|
||||||
|
COPY . .
|
||||||
|
COPY --from=deps /opt/app/node_modules ./node_modules
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production image, copy all the files and run next
|
||||||
|
FROM node:16-alpine AS runner
|
||||||
|
WORKDIR /opt/app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
COPY --from=builder /opt/app/next.config.js ./
|
||||||
|
COPY --from=builder /opt/app/public ./public
|
||||||
|
COPY --from=builder /opt/app/.next ./.next
|
||||||
|
COPY --from=builder /opt/app/node_modules ./node_modules
|
||||||
|
COPY ./.env ./
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node_modules/.bin/next", "start"]
|
||||||
55
README.md
Normal file
55
README.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# <div align="center"><img src="./public/logo.svg" width="40"/> </br>Pingvin Share</div>
|
||||||
|
|
||||||
|
Pingvin Share is a selfhosted file sharing plattform made for the [Appwrite Hackathon](https://dev.to/devteam/announcing-the-appwrite-hackathon-on-dev-1oc0).
|
||||||
|
|
||||||
|
## Showcase
|
||||||
|
|
||||||
|
https://pingvin-share.demo.eliasschneider.com
|
||||||
|
|
||||||
|
<img src="assets/screenshots/home.png" width="700"/>
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
At the moment, the setup is a bit time-consuming. I will improve the setup in the future.
|
||||||
|
|
||||||
|
### 1. Appwrite
|
||||||
|
|
||||||
|
Pingvin Share uses Appwrite as backend. You have to install and setup Appwrite first
|
||||||
|
|
||||||
|
1. [Install Appwrite](https://appwrite.io/docs/installation)
|
||||||
|
2. Create an Account on your Appwrite instance
|
||||||
|
3. Change the `_APP_STORAGE_LIMIT` variable in the `.env` file of Appwrite to your prefered max size limit per share
|
||||||
|
|
||||||
|
### 2. Setup script
|
||||||
|
|
||||||
|
To setup the backend structure of Pingvin Share you have to run the setup script.
|
||||||
|
|
||||||
|
1. [Install Node](https://nodejs.org/en/download/)
|
||||||
|
2. Clone the repository with `git clone https://github.com/stonith404/pingvin-share`
|
||||||
|
3. Visit the repository directory with `cd pingvin-share`
|
||||||
|
4. Run `npm run init:appwrite`
|
||||||
|
|
||||||
|
### 3. Frontend
|
||||||
|
|
||||||
|
To set up the frontend of Pingvin Share follow these steps.
|
||||||
|
|
||||||
|
1. Go to your Appwrite console, visit "API Keys" and copy the "Functions API Key" secret to your clipboard.
|
||||||
|
2. Rename the `.env.example` file to `.env`
|
||||||
|
3. Paste the key in the `.env` file
|
||||||
|
4. Change `APPWRITE_HOST` in the `.env` file to the host where your Appwrite instance runs
|
||||||
|
|
||||||
|
Start the frontend:
|
||||||
|
|
||||||
|
With docker:
|
||||||
|
|
||||||
|
1. Run `docker-compose up -d --build`
|
||||||
|
|
||||||
|
Without docker:
|
||||||
|
|
||||||
|
1. Run `npm install`
|
||||||
|
2. Run `npm run build && npm run start`
|
||||||
|
|
||||||
|
## Contribute
|
||||||
|
|
||||||
|
You're very welcome to contribute to Pingvin Share!
|
||||||
|
Contact me, create an issue or directly create a pull request.
|
||||||
39
appwrite.json
Normal file
39
appwrite.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"projectId": "pingvin-share",
|
||||||
|
"projectName": "Pingvin Share",
|
||||||
|
"functions": [
|
||||||
|
{
|
||||||
|
"$id": "createShare",
|
||||||
|
"name": "Create Share",
|
||||||
|
"runtime": "node-16.0",
|
||||||
|
"path": "functions/createShare",
|
||||||
|
"entrypoint": "src/index.js",
|
||||||
|
"execute": ["role:all"],
|
||||||
|
"events": [],
|
||||||
|
"schedule": "",
|
||||||
|
"timeout": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$id": "finishShare",
|
||||||
|
"name": "Finish Share",
|
||||||
|
"runtime": "node-16.0",
|
||||||
|
"path": "functions/finishShare",
|
||||||
|
"entrypoint": "src/index.js",
|
||||||
|
"execute": [],
|
||||||
|
"events": [],
|
||||||
|
"schedule": "",
|
||||||
|
"timeout": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$id": "cleanShares",
|
||||||
|
"name": "Clean Shares",
|
||||||
|
"runtime": "node-16.0",
|
||||||
|
"path": "functions/cleanShares",
|
||||||
|
"entrypoint": "src/index.js",
|
||||||
|
"execute": ["role:all"],
|
||||||
|
"events": [],
|
||||||
|
"schedule": "30,59 * * * *",
|
||||||
|
"timeout": 60
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
assets/screenshots/home-dark.png
Normal file
BIN
assets/screenshots/home-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 551 KiB |
BIN
assets/screenshots/home-share.png
Normal file
BIN
assets/screenshots/home-share.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 729 KiB |
BIN
assets/screenshots/home.png
Normal file
BIN
assets/screenshots/home.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 915 KiB |
BIN
assets/screenshots/share.png
Normal file
BIN
assets/screenshots/share.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 925 KiB |
8
docker-compose.yml
Normal file
8
docker-compose.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
version: '3.3'
|
||||||
|
services:
|
||||||
|
pingvin-share:
|
||||||
|
ports:
|
||||||
|
- '3000:3000'
|
||||||
|
image: stonith404/pingvin-share
|
||||||
|
build:
|
||||||
|
context: ./
|
||||||
12
functions/cleanShares/package.json
Normal file
12
functions/cleanShares/package.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "appwrite-function",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"node-appwrite": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
39
functions/cleanShares/src/index.js
Normal file
39
functions/cleanShares/src/index.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
const sdk = require("node-appwrite")
|
||||||
|
|
||||||
|
module.exports = async function (req, res) {
|
||||||
|
const client = new sdk.Client();
|
||||||
|
|
||||||
|
let database = new sdk.Database(client);
|
||||||
|
|
||||||
|
let storage = new sdk.Storage(client);
|
||||||
|
|
||||||
|
client
|
||||||
|
.setEndpoint(req.env["APPWRITE_FUNCTION_ENDPOINT"])
|
||||||
|
.setProject(req.env["APPWRITE_FUNCTION_PROJECT_ID"])
|
||||||
|
.setKey(req.env["APPWRITE_FUNCTION_API_KEY"])
|
||||||
|
.setSelfSigned(true);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const deletedShares = (await database.listDocuments("shares", [sdk.Query.lesser("expiresAt",Date.now())],
|
||||||
|
100)).documents;
|
||||||
|
console.log(deletedShares)
|
||||||
|
for (const share of deletedShares) {
|
||||||
|
database.deleteDocument("shares", share.$id)
|
||||||
|
if (share.securityID != null) {
|
||||||
|
database.deleteDocument("shareSecurity", share.securityID)
|
||||||
|
}
|
||||||
|
storage.deleteBucket(share.$id)
|
||||||
|
console.log("deleted" + share.$id)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
status: "done"
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
12
functions/createShare/package.json
Normal file
12
functions/createShare/package.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "appwrite-function",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"node-appwrite": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
58
functions/createShare/src/index.js
Normal file
58
functions/createShare/src/index.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const sdk = require("node-appwrite")
|
||||||
|
const util = require("./util")
|
||||||
|
|
||||||
|
module.exports = async function (req, res) {
|
||||||
|
const client = new sdk.Client();
|
||||||
|
|
||||||
|
// You can remove services you don't use
|
||||||
|
let database = new sdk.Database(client);
|
||||||
|
let storage = new sdk.Storage(client);
|
||||||
|
|
||||||
|
client
|
||||||
|
.setEndpoint(req.env["APPWRITE_FUNCTION_ENDPOINT"])
|
||||||
|
.setProject(req.env["APPWRITE_FUNCTION_PROJECT_ID"])
|
||||||
|
.setKey(req.env["APPWRITE_FUNCTION_API_KEY"])
|
||||||
|
.setSelfSigned(true);
|
||||||
|
|
||||||
|
// Payload (HTTP body) that was sent
|
||||||
|
const payload = JSON.parse(req.payload);
|
||||||
|
|
||||||
|
// User Id from the user which created a share
|
||||||
|
const userId = req.env["APPWRITE_FUNCTION_USER_ID"];
|
||||||
|
|
||||||
|
let securityDocumentId;
|
||||||
|
|
||||||
|
// If a security property was given create a document in the Share Security collection
|
||||||
|
if (Object.getOwnPropertyNames(payload.security).length != 0) {
|
||||||
|
securityDocumentId = (
|
||||||
|
await database.createDocument(
|
||||||
|
"shareSecurity",
|
||||||
|
"unique()",
|
||||||
|
{ maxVisitors: payload.security.maxVisitors, password: payload.security.password ? util.hashPassword(payload.security.password, payload.id) : undefined, },
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
).$id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the storage bucket
|
||||||
|
await storage.createBucket(
|
||||||
|
payload.id,
|
||||||
|
`Share-${payload.id}`,
|
||||||
|
"bucket",
|
||||||
|
["role:all"],
|
||||||
|
[`user:${userId}`],
|
||||||
|
)
|
||||||
|
|
||||||
|
const expiration = Date.now() + (payload.expiration * 60 * 1000)
|
||||||
|
|
||||||
|
// Create document in Shares collection
|
||||||
|
await database.createDocument("shares", payload.id, {
|
||||||
|
securityID: securityDocumentId,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
expiresAt: expiration,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
id: payload.id,
|
||||||
|
});
|
||||||
|
};
|
||||||
9
functions/createShare/src/util.js
Normal file
9
functions/createShare/src/util.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const { scryptSync } = require("crypto");
|
||||||
|
|
||||||
|
const hashPassword = (password, salt) => {
|
||||||
|
return scryptSync(password, salt, 64).toString("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
hashPassword,
|
||||||
|
}
|
||||||
12
functions/finishShare/package.json
Normal file
12
functions/finishShare/package.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "appwrite-function",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"node-appwrite": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
functions/finishShare/src/index.js
Normal file
22
functions/finishShare/src/index.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const sdk = require("node-appwrite")
|
||||||
|
|
||||||
|
module.exports = async function (req, res) {
|
||||||
|
const client = new sdk.Client();
|
||||||
|
|
||||||
|
let database = new sdk.Database(client);
|
||||||
|
|
||||||
|
client
|
||||||
|
.setEndpoint(req.env["APPWRITE_FUNCTION_ENDPOINT"])
|
||||||
|
.setProject(req.env["APPWRITE_FUNCTION_PROJECT_ID"])
|
||||||
|
.setKey(req.env["APPWRITE_FUNCTION_API_KEY"])
|
||||||
|
.setSelfSigned(true);
|
||||||
|
|
||||||
|
|
||||||
|
const payload = JSON.parse(req.payload);
|
||||||
|
database.updateDocument("shares", payload.id, {
|
||||||
|
enabled: true
|
||||||
|
})
|
||||||
|
res.json({
|
||||||
|
id: payload.id,
|
||||||
|
});
|
||||||
|
};
|
||||||
5
next-env.d.ts
vendored
Normal file
5
next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
6
next.config.js
Normal file
6
next.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
||||||
8268
package-lock.json
generated
Normal file
8268
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
package.json
Normal file
49
package.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "pingvin-share",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"init:appwrite": "cd .setup && npm install && npx ts-node index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mantine/core": "^4.1.3",
|
||||||
|
"@mantine/dropzone": "^4.1.3",
|
||||||
|
"@mantine/form": "^4.1.3",
|
||||||
|
"@mantine/hooks": "^4.1.3",
|
||||||
|
"@mantine/modals": "^4.1.3",
|
||||||
|
"@mantine/next": "^4.1.3",
|
||||||
|
"@mantine/notifications": "^4.1.3",
|
||||||
|
"appwrite": "^7.0.0",
|
||||||
|
"axios": "^0.26.1",
|
||||||
|
"cookie": "^0.5.0",
|
||||||
|
"cookies-next": "^2.0.4",
|
||||||
|
"js-file-download": "^0.4.12",
|
||||||
|
"next": "12.1.5",
|
||||||
|
"node-appwrite": "^5.1.0",
|
||||||
|
"react": "18.0.0",
|
||||||
|
"react-dom": "18.0.0",
|
||||||
|
"tabler-icons-react": "^1.44.0",
|
||||||
|
"yup": "^0.32.11"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/cookie": "^0.5.0",
|
||||||
|
"@types/node": "17.0.23",
|
||||||
|
"@types/react": "18.0.4",
|
||||||
|
"@types/react-dom": "18.0.0",
|
||||||
|
"@types/uuid": "^8.3.4",
|
||||||
|
"@types/readline-sync": "^1.4.4",
|
||||||
|
"@types/tar": "^6.1.1",
|
||||||
|
"eslint": "8.13.0",
|
||||||
|
"eslint-config-next": "12.1.5",
|
||||||
|
"typescript": "^4.6.3",
|
||||||
|
"axios": "^0.26.1",
|
||||||
|
"cookie": "^0.5.0",
|
||||||
|
"node-appwrite": "^5.1.0",
|
||||||
|
"readline-sync": "^1.4.10",
|
||||||
|
"tar": "^6.1.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
1
public/logo.svg
Normal file
1
public/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 943.11 911.62"><ellipse cx="471.56" cy="454.28" rx="471.56" ry="454.28" fill="#46509e"/><ellipse cx="471.56" cy="390.28" rx="233.66" ry="207" fill="#37474f"/><path d="M705.22,849c-36.69,21.14-123.09,64.32-240.64,62.57A469.81,469.81,0,0,1,237.89,849V394.76H705.22Z" fill="#37474f"/><path d="M658.81,397.7V873.49a478.12,478.12,0,0,1-374.19,0V397.7c0-95.55,83.78-173,187.1-173S658.81,302.15,658.81,397.7Z" fill="#fff"/><polygon points="565.02 431.68 471.56 514.49 378.09 431.68 565.02 431.68" fill="#46509e"/><ellipse cx="378.09" cy="369.58" rx="23.37" ry="20.7" fill="#37474f"/><ellipse cx="565.02" cy="369.58" rx="23.37" ry="20.7" fill="#37474f"/><path d="M658.49,400.63c0-40-36.6-72.45-81.79-72.45s-81.78,32.41-81.78,72.45a64.79,64.79,0,0,0,7.9,31.05H440.29a64.79,64.79,0,0,0,7.9-31.05c0-40-36.59-72.45-81.78-72.45s-81.79,32.41-81.79,72.45l-46.73-10.35c0-114.31,104.64-207,233.67-207s233.66,92.69,233.66,207Z" fill="#37474f"/></svg>
|
||||||
|
After Width: | Height: | Size: 1018 B |
91
src/components/auth/AuthForm.tsx
Normal file
91
src/components/auth/AuthForm.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import {
|
||||||
|
Anchor,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Paper,
|
||||||
|
PasswordInput,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
|
import * as yup from "yup";
|
||||||
|
import aw from "../../utils/appwrite.util";
|
||||||
|
import toast from "../../utils/toast.util";
|
||||||
|
|
||||||
|
const AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => {
|
||||||
|
const validationSchema = yup.object().shape({
|
||||||
|
email: yup.string().email().required(),
|
||||||
|
password: yup.string().min(8).required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
schema: yupResolver(validationSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const signIn = (email: string, password: string) => {
|
||||||
|
aw.account
|
||||||
|
.createSession(email, password)
|
||||||
|
.then(() => window.location.replace("/upload"))
|
||||||
|
.catch((e) => toast.error(e.message));
|
||||||
|
};
|
||||||
|
const signUp = (email: string, password: string) => {
|
||||||
|
aw.account
|
||||||
|
.create("unique()", email, password)
|
||||||
|
.then(() => signIn(email, password))
|
||||||
|
.catch((e) => toast.error(e.message));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size={420} my={40}>
|
||||||
|
<Title
|
||||||
|
align="center"
|
||||||
|
sx={(theme) => ({
|
||||||
|
fontFamily: `Greycliff CF, ${theme.fontFamily}`,
|
||||||
|
fontWeight: 900,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{mode == "signUp" ? "Sign up" : "Welcome back"}
|
||||||
|
</Title>
|
||||||
|
<Text color="dimmed" size="sm" align="center" mt={5}>
|
||||||
|
{mode == "signUp"
|
||||||
|
? "You have an account already?"
|
||||||
|
: "You don't have an account yet?"}{" "}
|
||||||
|
<Anchor href={mode == "signUp" ? "signIn" : "signUp"} size="sm">
|
||||||
|
{mode == "signUp" ? "Sign in" : "Sign up"}
|
||||||
|
</Anchor>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit((values) =>
|
||||||
|
mode == "signIn"
|
||||||
|
? signIn(values.email, values.password)
|
||||||
|
: signUp(values.email, values.password)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
label="Email"
|
||||||
|
placeholder="you@email.com"
|
||||||
|
{...form.getInputProps("email")}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
label="Password"
|
||||||
|
placeholder="Your password"
|
||||||
|
mt="md"
|
||||||
|
{...form.getInputProps("password")}
|
||||||
|
/>
|
||||||
|
<Button fullWidth mt="xl" type="submit">
|
||||||
|
{mode == "signUp" ? "Let's get started" : "Sign in"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthForm;
|
||||||
41
src/components/mantine/ThemeProvider.tsx
Normal file
41
src/components/mantine/ThemeProvider.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
ColorScheme,
|
||||||
|
ColorSchemeProvider,
|
||||||
|
MantineProvider,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { ModalsProvider } from "@mantine/modals";
|
||||||
|
import { setCookies } from "cookies-next";
|
||||||
|
import { Dispatch, ReactNode, SetStateAction } from "react";
|
||||||
|
import mantineTheme from "../../styles/global.style";
|
||||||
|
|
||||||
|
const ThemeProvider = ({
|
||||||
|
children,
|
||||||
|
colorScheme,
|
||||||
|
setColorScheme,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
colorScheme: ColorScheme;
|
||||||
|
setColorScheme: Dispatch<SetStateAction<ColorScheme>>;
|
||||||
|
}) => {
|
||||||
|
const toggleColorScheme = (value?: ColorScheme) => {
|
||||||
|
const nextColorScheme =
|
||||||
|
value || (colorScheme === "dark" ? "light" : "dark");
|
||||||
|
setColorScheme(nextColorScheme);
|
||||||
|
setCookies("mantine-color-scheme", nextColorScheme, {
|
||||||
|
maxAge: 60 * 60 * 24 * 30,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MantineProvider theme={{ colorScheme, ...mantineTheme }} withGlobalStyles>
|
||||||
|
<ColorSchemeProvider
|
||||||
|
colorScheme={colorScheme}
|
||||||
|
toggleColorScheme={toggleColorScheme}
|
||||||
|
>
|
||||||
|
<ModalsProvider>{children}</ModalsProvider>
|
||||||
|
</ColorSchemeProvider>
|
||||||
|
</MantineProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeProvider;
|
||||||
122
src/components/navBar/NavBar.tsx
Normal file
122
src/components/navBar/NavBar.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import {
|
||||||
|
Burger,
|
||||||
|
Container,
|
||||||
|
Group,
|
||||||
|
Header as MantineHeader,
|
||||||
|
Paper,
|
||||||
|
Space,
|
||||||
|
Text,
|
||||||
|
Transition,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useBooleanToggle } from "@mantine/hooks";
|
||||||
|
import { NextLink } from "@mantine/next";
|
||||||
|
import Image from "next/image";
|
||||||
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
|
import headerStyle from "../../styles/header.style";
|
||||||
|
import aw from "../../utils/appwrite.util";
|
||||||
|
import { IsSignedInContext } from "../../utils/auth.util";
|
||||||
|
import ToggleThemeButton from "./ToggleThemeButton";
|
||||||
|
|
||||||
|
type Link = {
|
||||||
|
link?: string;
|
||||||
|
label: string;
|
||||||
|
action?: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const authenticatedLinks: Link[] = [
|
||||||
|
{
|
||||||
|
link: "/upload",
|
||||||
|
label: "Upload",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Sign out",
|
||||||
|
action: async () => {
|
||||||
|
await aw.account.deleteSession("current");
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const unauthenticatedLinks: Link[] = [
|
||||||
|
{
|
||||||
|
link: "/",
|
||||||
|
label: "Home",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: "/auth/signUp",
|
||||||
|
label: "Sign up",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: "/auth/signIn",
|
||||||
|
label: "Sign in",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
const [opened, toggleOpened] = useBooleanToggle(false);
|
||||||
|
const [active, setActive] = useState<string>();
|
||||||
|
const isSignedIn = useContext(IsSignedInContext);
|
||||||
|
const { classes, cx } = headerStyle();
|
||||||
|
|
||||||
|
const links = isSignedIn ? authenticatedLinks : unauthenticatedLinks;
|
||||||
|
|
||||||
|
const items = links.map((link) => {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
useEffect(() => {
|
||||||
|
if (window.location.pathname == link.link) {
|
||||||
|
setActive(link.link);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<NextLink
|
||||||
|
key={link.label}
|
||||||
|
href={link.link ?? ""}
|
||||||
|
onClick={link.action}
|
||||||
|
className={cx(classes.link, {
|
||||||
|
[classes.linkActive]: link.link && active === link.link,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</NextLink>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MantineHeader height={60} mb={20} className={classes.root}>
|
||||||
|
<Container className={classes.header}>
|
||||||
|
<NextLink href="/">
|
||||||
|
<Group>
|
||||||
|
<Image
|
||||||
|
src="/logo.svg"
|
||||||
|
alt="Pinvgin Share Logo"
|
||||||
|
height={40}
|
||||||
|
width={40}
|
||||||
|
/>
|
||||||
|
<Text weight={600}>Pingvin Share</Text>
|
||||||
|
</Group>
|
||||||
|
</NextLink>
|
||||||
|
<Group spacing={5} className={classes.links}>
|
||||||
|
{items}
|
||||||
|
<Space w={5} />
|
||||||
|
<ToggleThemeButton />
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={() => toggleOpened()}
|
||||||
|
className={classes.burger}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Transition transition="pop-top-right" duration={200} mounted={opened}>
|
||||||
|
{(styles) => (
|
||||||
|
<Paper className={classes.dropdown} withBorder style={styles}>
|
||||||
|
{items}
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
</Container>
|
||||||
|
</MantineHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Header;
|
||||||
26
src/components/navBar/ToggleThemeButton.tsx
Normal file
26
src/components/navBar/ToggleThemeButton.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { ActionIcon, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { Sun, MoonStars } from "tabler-icons-react";
|
||||||
|
|
||||||
|
const ToggleThemeButton = () => {
|
||||||
|
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionIcon
|
||||||
|
onClick={() => toggleColorScheme()}
|
||||||
|
sx={(theme) => ({
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme === "dark"
|
||||||
|
? theme.colors.dark[6]
|
||||||
|
: theme.colors.gray[0],
|
||||||
|
color:
|
||||||
|
theme.colorScheme === "dark"
|
||||||
|
? theme.colors.yellow[4]
|
||||||
|
: theme.colors.violet,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{colorScheme === "dark" ? <Sun size={18} /> : <MoonStars size={18} />}
|
||||||
|
</ActionIcon>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToggleThemeButton;
|
||||||
80
src/components/share/FileList.tsx
Normal file
80
src/components/share/FileList.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { ActionIcon, Skeleton, Table } from "@mantine/core";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { Download } from "tabler-icons-react";
|
||||||
|
import { AppwriteFileWithPreview } from "../../types/File.type";
|
||||||
|
import aw from "../../utils/appwrite.util";
|
||||||
|
import { bytesToSize } from "../../utils/math/byteToSize.util";
|
||||||
|
|
||||||
|
const FileList = ({
|
||||||
|
files,
|
||||||
|
shareId,
|
||||||
|
isLoading,
|
||||||
|
}: {
|
||||||
|
files: AppwriteFileWithPreview[];
|
||||||
|
shareId: string;
|
||||||
|
isLoading: boolean;
|
||||||
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const skeletonRows = [...Array(5)].map((c, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>
|
||||||
|
<Skeleton height={30} width={30} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Skeleton height={14} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Skeleton height={14} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Skeleton height={25} width={25} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
|
||||||
|
const rows = files.map((file) => (
|
||||||
|
<tr key={file.name}>
|
||||||
|
<td>
|
||||||
|
<Image
|
||||||
|
width={30}
|
||||||
|
height={30}
|
||||||
|
alt={file.name}
|
||||||
|
objectFit="cover"
|
||||||
|
src={`data:image/png;base64,${new Buffer(file.preview).toString(
|
||||||
|
"base64"
|
||||||
|
)}`}
|
||||||
|
></Image>
|
||||||
|
</td>
|
||||||
|
<td>{file.name}</td>
|
||||||
|
<td>{bytesToSize(file.sizeOriginal)}</td>
|
||||||
|
<td>
|
||||||
|
<ActionIcon
|
||||||
|
size={25}
|
||||||
|
onClick={() =>
|
||||||
|
router.push(aw.storage.getFileDownload(shareId, file.$id))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Download />
|
||||||
|
</ActionIcon>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{isLoading ? skeletonRows : rows}</tbody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileList;
|
||||||
58
src/components/share/showEnterPasswordModal.tsx
Normal file
58
src/components/share/showEnterPasswordModal.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Button, Group, PasswordInput, Text, Title } from "@mantine/core";
|
||||||
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const showEnterPasswordModal = (
|
||||||
|
modals: ModalsContextProps,
|
||||||
|
submitCallback: any
|
||||||
|
) => {
|
||||||
|
return modals.openModal({
|
||||||
|
closeOnClickOutside: false,
|
||||||
|
withCloseButton: false,
|
||||||
|
closeOnEscape: false,
|
||||||
|
title: (
|
||||||
|
<>
|
||||||
|
<Title order={4}>Password required</Title>
|
||||||
|
<Text size="sm">
|
||||||
|
This access this share please enter the password for the share.
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
children: <Body submitCallback={submitCallback} />,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const Body = ({ submitCallback }: { submitCallback: any }) => {
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [passwordWrong, setPasswordWrong] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Group grow direction="column">
|
||||||
|
<PasswordInput
|
||||||
|
variant="filled"
|
||||||
|
placeholder="Password"
|
||||||
|
error={passwordWrong && "Wrong password"}
|
||||||
|
onFocus={() => setPasswordWrong(false)}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
value={password}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
submitCallback(password)
|
||||||
|
.then((res: any) => res)
|
||||||
|
.catch((e: any) => {
|
||||||
|
const error = e.response.data.message;
|
||||||
|
if (error == "wrong_password") {
|
||||||
|
setPasswordWrong(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default showEnterPasswordModal;
|
||||||
39
src/components/share/showShareNotFoundModal.tsx
Normal file
39
src/components/share/showShareNotFoundModal.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Button, Group, Text, Title } from "@mantine/core";
|
||||||
|
import { useModals } from "@mantine/modals";
|
||||||
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
const showShareNotFoundModal = (modals: ModalsContextProps) => {
|
||||||
|
return modals.openModal({
|
||||||
|
closeOnClickOutside: false,
|
||||||
|
withCloseButton: false,
|
||||||
|
closeOnEscape: false,
|
||||||
|
title: <Title order={4}>Not found</Title>,
|
||||||
|
|
||||||
|
children: <Body />,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const Body = () => {
|
||||||
|
const modals = useModals();
|
||||||
|
const router = useRouter();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Group grow direction="column">
|
||||||
|
<Text size="sm">
|
||||||
|
This share can't be found. Please check your link.
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
modals.closeAll();
|
||||||
|
router.back();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default showShareNotFoundModal;
|
||||||
39
src/components/share/showVisitorLimitExceededModal.tsx
Normal file
39
src/components/share/showVisitorLimitExceededModal.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Button, Group, Text, Title } from "@mantine/core";
|
||||||
|
import { useModals } from "@mantine/modals";
|
||||||
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
const showVisitorLimitExceededModal = (modals: ModalsContextProps) => {
|
||||||
|
return modals.openModal({
|
||||||
|
closeOnClickOutside: false,
|
||||||
|
withCloseButton: false,
|
||||||
|
closeOnEscape: false,
|
||||||
|
title: <Title order={4}>Visitor limit exceeded</Title>,
|
||||||
|
|
||||||
|
children: <Body />,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const Body = () => {
|
||||||
|
const modals = useModals();
|
||||||
|
const router = useRouter();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Group grow direction="column">
|
||||||
|
<Text size="sm">
|
||||||
|
The visitor count limit from this share has been exceeded.
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
modals.closeAll();
|
||||||
|
router.back();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default showVisitorLimitExceededModal;
|
||||||
104
src/components/upload/Dropzone.tsx
Normal file
104
src/components/upload/Dropzone.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
createStyles,
|
||||||
|
Group,
|
||||||
|
MantineTheme,
|
||||||
|
Text,
|
||||||
|
useMantineTheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { Dropzone as MantineDropzone, DropzoneStatus } from "@mantine/dropzone";
|
||||||
|
import React, { Dispatch, ForwardedRef, SetStateAction, useRef } from "react";
|
||||||
|
import { CloudUpload, Upload } from "tabler-icons-react";
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
wrapper: {
|
||||||
|
position: "relative",
|
||||||
|
marginBottom: 30,
|
||||||
|
},
|
||||||
|
|
||||||
|
dropzone: {
|
||||||
|
borderWidth: 1,
|
||||||
|
paddingBottom: 50,
|
||||||
|
},
|
||||||
|
|
||||||
|
icon: {
|
||||||
|
color:
|
||||||
|
theme.colorScheme === "dark"
|
||||||
|
? theme.colors.dark[3]
|
||||||
|
: theme.colors.gray[4],
|
||||||
|
},
|
||||||
|
|
||||||
|
control: {
|
||||||
|
position: "absolute",
|
||||||
|
bottom: -20,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function getActiveColor(status: DropzoneStatus, theme: MantineTheme) {
|
||||||
|
return status.accepted
|
||||||
|
? theme.colors[theme.primaryColor][6]
|
||||||
|
: theme.colorScheme === "dark"
|
||||||
|
? theme.colors.dark[2]
|
||||||
|
: theme.black;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Dropzone = ({
|
||||||
|
isUploading,
|
||||||
|
setFiles,
|
||||||
|
}: {
|
||||||
|
isUploading: boolean;
|
||||||
|
setFiles: Dispatch<SetStateAction<File[]>>;
|
||||||
|
}) => {
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
const { classes } = useStyles();
|
||||||
|
const openRef = useRef<() => void>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.wrapper}>
|
||||||
|
<MantineDropzone
|
||||||
|
disabled={isUploading}
|
||||||
|
openRef={openRef as ForwardedRef<() => void>}
|
||||||
|
onDrop={(files) => {
|
||||||
|
setFiles(files);
|
||||||
|
}}
|
||||||
|
className={classes.dropzone}
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
{(status) => (
|
||||||
|
<div style={{ pointerEvents: "none" }}>
|
||||||
|
<Group position="center">
|
||||||
|
<CloudUpload size={50} color={getActiveColor(status, theme)} />
|
||||||
|
</Group>
|
||||||
|
<Text
|
||||||
|
align="center"
|
||||||
|
weight={700}
|
||||||
|
size="lg"
|
||||||
|
mt="xl"
|
||||||
|
sx={{ color: getActiveColor(status, theme) }}
|
||||||
|
>
|
||||||
|
{status.accepted ? "Drop files here" : "Upload files"}
|
||||||
|
</Text>
|
||||||
|
<Text align="center" size="sm" mt="xs" color="dimmed">
|
||||||
|
Drag and drop your files or use the upload button to start your
|
||||||
|
share.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</MantineDropzone>
|
||||||
|
<Center>
|
||||||
|
<Button
|
||||||
|
className={classes.control}
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
radius="xl"
|
||||||
|
disabled={isUploading}
|
||||||
|
onClick={() => openRef.current && openRef.current()}
|
||||||
|
>
|
||||||
|
{<Upload />}
|
||||||
|
</Button>
|
||||||
|
</Center>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Dropzone;
|
||||||
60
src/components/upload/FileList.tsx
Normal file
60
src/components/upload/FileList.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { ActionIcon, Loader, Table } from "@mantine/core";
|
||||||
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
import { CircleCheck, Trash } from "tabler-icons-react";
|
||||||
|
import { FileUpload } from "../../types/File.type";
|
||||||
|
import { bytesToSize } from "../../utils/math/byteToSize.util";
|
||||||
|
|
||||||
|
const FileList = ({
|
||||||
|
files,
|
||||||
|
setFiles,
|
||||||
|
}: {
|
||||||
|
files: FileUpload[];
|
||||||
|
setFiles: Dispatch<SetStateAction<FileUpload[]>>;
|
||||||
|
}) => {
|
||||||
|
const remove = (index: number) => {
|
||||||
|
files.splice(index, 1);
|
||||||
|
setFiles([...files]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rows = files.map((file, i) => (
|
||||||
|
<tr key={file.name}>
|
||||||
|
<td>{file.name}</td>
|
||||||
|
<td>{file.type}</td>
|
||||||
|
<td>{bytesToSize(file.size)}</td>
|
||||||
|
<td>
|
||||||
|
{file.uploadingState ? (
|
||||||
|
file.uploadingState != "finished" ? (
|
||||||
|
<Loader size={22} />
|
||||||
|
) : (
|
||||||
|
<CircleCheck color="green" size={22} />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<ActionIcon
|
||||||
|
color="red"
|
||||||
|
variant="light"
|
||||||
|
size={25}
|
||||||
|
onClick={() => remove(i)}
|
||||||
|
>
|
||||||
|
<Trash />
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{rows}</tbody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileList;
|
||||||
65
src/components/upload/showCompletedUploadModal.tsx
Normal file
65
src/components/upload/showCompletedUploadModal.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useClipboard } from "@mantine/hooks";
|
||||||
|
import { useModals } from "@mantine/modals";
|
||||||
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { Copy } from "tabler-icons-react";
|
||||||
|
|
||||||
|
const showCompletedUploadModal = (
|
||||||
|
modals: ModalsContextProps,
|
||||||
|
link: string,
|
||||||
|
expiresAt: string
|
||||||
|
) => {
|
||||||
|
return modals.openModal({
|
||||||
|
closeOnClickOutside: false,
|
||||||
|
withCloseButton: false,
|
||||||
|
closeOnEscape: false,
|
||||||
|
title: <Title order={4}>Share ready</Title>,
|
||||||
|
children: <Body link={link} expiresAt={expiresAt} />,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const Body = ({ link, expiresAt }: { link: string; expiresAt: string }) => {
|
||||||
|
const clipboard = useClipboard({ timeout: 500 });
|
||||||
|
const modals = useModals();
|
||||||
|
const router = useRouter();
|
||||||
|
return (
|
||||||
|
<Group grow direction="column">
|
||||||
|
<TextInput
|
||||||
|
variant="filled"
|
||||||
|
value={link}
|
||||||
|
rightSection={
|
||||||
|
<ActionIcon onClick={() => clipboard.copy(link)}>
|
||||||
|
<Copy />
|
||||||
|
</ActionIcon>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
sx={(theme) => ({
|
||||||
|
color: theme.colors.gray[6],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Your share expires at {expiresAt}{" "}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
modals.closeAll();
|
||||||
|
router.push("/upload");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default showCompletedUploadModal;
|
||||||
140
src/components/upload/showCreateUploadModal.tsx
Normal file
140
src/components/upload/showCreateUploadModal.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
NumberInput,
|
||||||
|
PasswordInput,
|
||||||
|
Select,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
|
import { useModals } from "@mantine/modals";
|
||||||
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
import * as yup from "yup";
|
||||||
|
|
||||||
|
const showCreateUploadModal = (
|
||||||
|
modals: ModalsContextProps,
|
||||||
|
uploadCallback: (
|
||||||
|
id: string,
|
||||||
|
expiration: number,
|
||||||
|
security: { password?: string; maxVisitors?: number }
|
||||||
|
) => void
|
||||||
|
) => {
|
||||||
|
return modals.openModal({
|
||||||
|
title: <Title order={4}>Share</Title>,
|
||||||
|
children: <Body uploadCallback={uploadCallback} />,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const Body = ({
|
||||||
|
uploadCallback,
|
||||||
|
}: {
|
||||||
|
uploadCallback: (
|
||||||
|
id: string,
|
||||||
|
expiration: number,
|
||||||
|
security: { password?: string; maxVisitors?: number }
|
||||||
|
) => void;
|
||||||
|
}) => {
|
||||||
|
const modals = useModals();
|
||||||
|
const validationSchema = yup.object().shape({
|
||||||
|
link: yup.string().required().min(2).max(50),
|
||||||
|
password: yup.string().min(3).max(100),
|
||||||
|
maxVisitors: yup.number().min(1),
|
||||||
|
});
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
link: "",
|
||||||
|
password: undefined,
|
||||||
|
maxVisitors: undefined,
|
||||||
|
expiration: "1440",
|
||||||
|
},
|
||||||
|
schema: yupResolver(validationSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit((values) => {
|
||||||
|
modals.closeAll();
|
||||||
|
uploadCallback(values.link, parseInt(values.expiration), {
|
||||||
|
password: values.password,
|
||||||
|
maxVisitors: values.maxVisitors,
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Group direction="column" grow>
|
||||||
|
<Grid align="flex-end">
|
||||||
|
<Col xs={9}>
|
||||||
|
<TextInput
|
||||||
|
variant="filled"
|
||||||
|
label="Link"
|
||||||
|
placeholder="myAwesomeShare"
|
||||||
|
{...form.getInputProps("link")}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={3}>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
form.setFieldValue(
|
||||||
|
"link",
|
||||||
|
Buffer.from(Math.random().toString(), "utf8")
|
||||||
|
.toString("base64")
|
||||||
|
.substr(10, 7)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Generate
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
sx={(theme) => ({
|
||||||
|
color: theme.colors.gray[6],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{window.location.origin}/share/
|
||||||
|
{form.values.link == "" ? "myAwesomeShare" : form.values.link}
|
||||||
|
</Text>
|
||||||
|
<Select
|
||||||
|
label="Expiration"
|
||||||
|
{...form.getInputProps("expiration")}
|
||||||
|
data={[
|
||||||
|
{ value: "10", label: "10 Minutes" },
|
||||||
|
{ value: "60", label: "1 Hour" },
|
||||||
|
{ value: "1440", label: "1 Day" },
|
||||||
|
{ value: "1080", label: "1 Week" },
|
||||||
|
{ value: "43000", label: "1 Month" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Accordion>
|
||||||
|
<Accordion.Item label="Security" sx={{ borderBottom: "none" }}>
|
||||||
|
<Group direction="column" grow>
|
||||||
|
<PasswordInput
|
||||||
|
variant="filled"
|
||||||
|
placeholder="No password"
|
||||||
|
label="Password protection"
|
||||||
|
{...form.getInputProps("password")}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
type="number"
|
||||||
|
variant="filled"
|
||||||
|
placeholder="No limit"
|
||||||
|
label="Maximal views"
|
||||||
|
{...form.getInputProps("maxVisitors")}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
<Button type="submit">Share</Button>
|
||||||
|
</Group>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default showCreateUploadModal;
|
||||||
67
src/pages/_app.tsx
Normal file
67
src/pages/_app.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
ColorScheme,
|
||||||
|
Container,
|
||||||
|
LoadingOverlay,
|
||||||
|
MantineProvider,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { ModalsProvider } from "@mantine/modals";
|
||||||
|
import { NotificationsProvider } from "@mantine/notifications";
|
||||||
|
import { getCookie } from "cookies-next";
|
||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
import type { AppProps } from "next/app";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import "../../styles/globals.css";
|
||||||
|
import ThemeProvider from "../components/mantine/ThemeProvider";
|
||||||
|
import Header from "../components/navBar/NavBar";
|
||||||
|
import globalStyle from "../styles/global.style";
|
||||||
|
import authUtil, { IsSignedInContext } from "../utils/auth.util";
|
||||||
|
import { GlobalLoadingContext } from "../utils/loading.util";
|
||||||
|
|
||||||
|
function App(props: AppProps & { colorScheme: ColorScheme }) {
|
||||||
|
const { Component, pageProps } = props;
|
||||||
|
|
||||||
|
const [colorScheme, setColorScheme] = useState<ColorScheme>(
|
||||||
|
props.colorScheme
|
||||||
|
);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isSignedIn, setIsSignedIn] = useState(false);
|
||||||
|
|
||||||
|
const checkIfSignedIn = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setIsSignedIn(await authUtil.isSignedIn());
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
checkIfSignedIn();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MantineProvider withGlobalStyles withNormalizeCSS theme={globalStyle}>
|
||||||
|
<ThemeProvider colorScheme={colorScheme} setColorScheme={setColorScheme}>
|
||||||
|
<NotificationsProvider>
|
||||||
|
<ModalsProvider>
|
||||||
|
<GlobalLoadingContext.Provider value={{ isLoading, setIsLoading }}>
|
||||||
|
{isLoading ? (
|
||||||
|
<LoadingOverlay visible overlayOpacity={1} />
|
||||||
|
) : (
|
||||||
|
<IsSignedInContext.Provider value={isSignedIn}>
|
||||||
|
<LoadingOverlay visible={isLoading} overlayOpacity={1} />
|
||||||
|
<Header />
|
||||||
|
<Container>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</Container>
|
||||||
|
</IsSignedInContext.Provider>
|
||||||
|
)}
|
||||||
|
</GlobalLoadingContext.Provider>
|
||||||
|
</ModalsProvider>
|
||||||
|
</NotificationsProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</MantineProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
||||||
|
App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => ({
|
||||||
|
colorScheme: getCookie("mantine-color-scheme", ctx) || "light",
|
||||||
|
});
|
||||||
8
src/pages/_document.tsx
Normal file
8
src/pages/_document.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import Document from "next/document";
|
||||||
|
import { createGetInitialProps } from "@mantine/next";
|
||||||
|
|
||||||
|
const getInitialProps = createGetInitialProps();
|
||||||
|
|
||||||
|
export default class _Document extends Document {
|
||||||
|
static getInitialProps = getInitialProps;
|
||||||
|
}
|
||||||
44
src/pages/api/share/[shareId]/enterPassword.ts
Normal file
44
src/pages/api/share/[shareId]/enterPassword.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import {
|
||||||
|
SecurityDocument,
|
||||||
|
ShareDocument,
|
||||||
|
} from "../../../../types/Appwrite.type";
|
||||||
|
import awServer from "../../../../utils/appwriteServer.util";
|
||||||
|
import { hashPassword } from "../../../../utils/shares/security.util";
|
||||||
|
|
||||||
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
const shareId = req.query.shareId as string;
|
||||||
|
let hashedPassword;
|
||||||
|
try {
|
||||||
|
hashedPassword = await checkPassword(shareId, req.body.password);
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(403).json({ message: e });
|
||||||
|
}
|
||||||
|
if (hashedPassword)
|
||||||
|
res.setHeader(
|
||||||
|
"Set-Cookie",
|
||||||
|
`${shareId}-password=${hashedPassword}; Path=/api/share/${shareId}; max-age=3600; HttpOnly`
|
||||||
|
);
|
||||||
|
res.send(200);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkPassword = async (shareId: string, password?: string) => {
|
||||||
|
let hashedPassword;
|
||||||
|
const shareDocument = await awServer.database.getDocument<ShareDocument>(
|
||||||
|
"shares",
|
||||||
|
shareId
|
||||||
|
);
|
||||||
|
await awServer.database
|
||||||
|
.getDocument<SecurityDocument>("shareSecurity", shareDocument.securityID)
|
||||||
|
.then((securityDocument) => {
|
||||||
|
if (securityDocument.password) {
|
||||||
|
hashedPassword = hashPassword(password as string, shareId);
|
||||||
|
if (hashedPassword !== securityDocument.password) {
|
||||||
|
throw "wrong_password";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return hashedPassword;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handler;
|
||||||
64
src/pages/api/share/[shareId]/index.ts
Normal file
64
src/pages/api/share/[shareId]/index.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { ShareDocument } from "../../../../types/Appwrite.type";
|
||||||
|
import { AppwriteFileWithPreview } from "../../../../types/File.type";
|
||||||
|
import awServer from "../../../../utils/appwriteServer.util";
|
||||||
|
import { checkSecurity } from "../../../../utils/shares/security.util";
|
||||||
|
|
||||||
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
const shareId = req.query.shareId as string;
|
||||||
|
const fileList: AppwriteFileWithPreview[] = [];
|
||||||
|
const hashedPassword = req.cookies[`${shareId}-password`];
|
||||||
|
|
||||||
|
if (!(await shareExists(shareId)))
|
||||||
|
return res.status(404).json({ message: "not_found" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await checkSecurity(shareId, hashedPassword);
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(403).json({ message: e });
|
||||||
|
}
|
||||||
|
|
||||||
|
addVisitorCount(shareId);
|
||||||
|
|
||||||
|
const fileListWithoutPreview = (await awServer.storage.listFiles(shareId))
|
||||||
|
.files;
|
||||||
|
|
||||||
|
for (const file of fileListWithoutPreview) {
|
||||||
|
const filePreview = await awServer.storage.getFilePreview(
|
||||||
|
shareId,
|
||||||
|
file.$id
|
||||||
|
);
|
||||||
|
fileList.push({ ...file, preview: filePreview });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashedPassword)
|
||||||
|
res.setHeader(
|
||||||
|
"Set-Cookie",
|
||||||
|
`${shareId}-password=${hashedPassword}; Path=/share/${shareId}; max-age=3600; HttpOnly`
|
||||||
|
);
|
||||||
|
res.status(200).json(fileList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const shareExists = async (shareId: string) => {
|
||||||
|
try {
|
||||||
|
const shareDocument = await awServer.database.getDocument<ShareDocument>(
|
||||||
|
"shares",
|
||||||
|
shareId
|
||||||
|
);
|
||||||
|
return shareDocument.enabled && shareDocument.expiresAt > Date.now();
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addVisitorCount = async (shareId: string) => {
|
||||||
|
const currentDocument = await awServer.database.getDocument<ShareDocument>(
|
||||||
|
"shares",
|
||||||
|
shareId
|
||||||
|
);
|
||||||
|
currentDocument.visitorCount++;
|
||||||
|
|
||||||
|
awServer.database.updateDocument("shares", shareId, currentDocument);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handler;
|
||||||
15
src/pages/auth/signIn.tsx
Normal file
15
src/pages/auth/signIn.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import React, { useContext } from "react";
|
||||||
|
import AuthForm from "../../components/auth/AuthForm";
|
||||||
|
import { IsSignedInContext } from "../../utils/auth.util";
|
||||||
|
|
||||||
|
const SignIn = () => {
|
||||||
|
const isSignedIn = useContext(IsSignedInContext);
|
||||||
|
const router = useRouter();
|
||||||
|
if (isSignedIn) {
|
||||||
|
router.replace("/");
|
||||||
|
} else {
|
||||||
|
return <AuthForm mode="signIn" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default SignIn;
|
||||||
15
src/pages/auth/signUp.tsx
Normal file
15
src/pages/auth/signUp.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import React, { useContext } from "react";
|
||||||
|
import AuthForm from "../../components/auth/AuthForm";
|
||||||
|
import { IsSignedInContext } from "../../utils/auth.util";
|
||||||
|
|
||||||
|
const SignUp = () => {
|
||||||
|
const isSignedIn = useContext(IsSignedInContext);
|
||||||
|
const router = useRouter();
|
||||||
|
if (isSignedIn) {
|
||||||
|
router.replace("/");
|
||||||
|
} else {
|
||||||
|
return <AuthForm mode="signUp" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default SignUp;
|
||||||
151
src/pages/index.tsx
Normal file
151
src/pages/index.tsx
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
createStyles,
|
||||||
|
Group,
|
||||||
|
List,
|
||||||
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { NextLink } from "@mantine/next";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import React, { useContext } from "react";
|
||||||
|
import { Check } from "tabler-icons-react";
|
||||||
|
import { IsSignedInContext } from "../utils/auth.util";
|
||||||
|
import Image from "next/image";
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
inner: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
paddingTop: theme.spacing.xl * 4,
|
||||||
|
paddingBottom: theme.spacing.xl * 4,
|
||||||
|
},
|
||||||
|
|
||||||
|
content: {
|
||||||
|
maxWidth: 480,
|
||||||
|
marginRight: theme.spacing.xl * 3,
|
||||||
|
|
||||||
|
[theme.fn.smallerThan("md")]: {
|
||||||
|
maxWidth: "100%",
|
||||||
|
marginRight: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
title: {
|
||||||
|
color: theme.colorScheme === "dark" ? theme.white : theme.black,
|
||||||
|
fontFamily: `Greycliff CF, ${theme.fontFamily}`,
|
||||||
|
fontSize: 44,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
fontWeight: 900,
|
||||||
|
|
||||||
|
[theme.fn.smallerThan("xs")]: {
|
||||||
|
fontSize: 28,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
control: {
|
||||||
|
[theme.fn.smallerThan("xs")]: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
image: {
|
||||||
|
flex: 1,
|
||||||
|
|
||||||
|
[theme.fn.smallerThan("md")]: {
|
||||||
|
display: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
highlight: {
|
||||||
|
position: "relative",
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme === "dark"
|
||||||
|
? theme.fn.rgba(theme.colors[theme.primaryColor][6], 0.55)
|
||||||
|
: theme.colors[theme.primaryColor][0],
|
||||||
|
borderRadius: theme.radius.sm,
|
||||||
|
padding: "4px 12px",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const isSignedIn = useContext(IsSignedInContext);
|
||||||
|
const { classes } = useStyles();
|
||||||
|
const router = useRouter();
|
||||||
|
if (isSignedIn) {
|
||||||
|
router.replace("/upload");
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Container>
|
||||||
|
<div className={classes.inner}>
|
||||||
|
<div className={classes.content}>
|
||||||
|
<Title className={classes.title}>
|
||||||
|
A <span className={classes.highlight}>self-hosted</span> <br />{" "}
|
||||||
|
file sharing platform.
|
||||||
|
</Title>
|
||||||
|
<Text color="dimmed" mt="md">
|
||||||
|
Do you really want to give your personal files in the hand of
|
||||||
|
third parties like WeTransfer?
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<List
|
||||||
|
mt={30}
|
||||||
|
spacing="sm"
|
||||||
|
size="sm"
|
||||||
|
icon={
|
||||||
|
<ThemeIcon size={20} radius="xl">
|
||||||
|
<Check size={12} />
|
||||||
|
</ThemeIcon>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<List.Item>
|
||||||
|
<b>Self-Hosted</b> - Host Pingvin Share on your own machine.
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
<b>Privacy</b> - Your files are your files and should never
|
||||||
|
get into the hands of third parties.
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
<b>No annoying file size limit</b> - Upload as big files as
|
||||||
|
you want. Only your hard drive will be your limit.
|
||||||
|
</List.Item>
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<Group mt={30}>
|
||||||
|
<Button
|
||||||
|
component={NextLink}
|
||||||
|
href="/auth/signUp"
|
||||||
|
radius="xl"
|
||||||
|
size="md"
|
||||||
|
className={classes.control}
|
||||||
|
>
|
||||||
|
Get started
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
component={NextLink}
|
||||||
|
href="https://github.com/stonith404/pingvin-share"
|
||||||
|
target="_blank"
|
||||||
|
variant="default"
|
||||||
|
radius="xl"
|
||||||
|
size="md"
|
||||||
|
className={classes.control}
|
||||||
|
>
|
||||||
|
Source code
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
<Image
|
||||||
|
src="/logo.svg"
|
||||||
|
alt="Pingvin Share Logo"
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
className={classes.image}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/pages/share/[shareId].tsx
Normal file
54
src/pages/share/[shareId].tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { useModals } from "@mantine/modals";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import FileList from "../../components/share/FileList";
|
||||||
|
import showEnterPasswordModal from "../../components/share/showEnterPasswordModal";
|
||||||
|
import showShareNotFoundModal from "../../components/share/showShareNotFoundModal";
|
||||||
|
import showVisitorLimitExceededModal from "../../components/share/showVisitorLimitExceededModal";
|
||||||
|
import shareService from "../../services/share.service";
|
||||||
|
import { AppwriteFileWithPreview } from "../../types/File.type";
|
||||||
|
|
||||||
|
const Share = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const modals = useModals();
|
||||||
|
const shareId = router.query.shareId as string;
|
||||||
|
const [shareList, setShareList] = useState<AppwriteFileWithPreview[]>([]);
|
||||||
|
|
||||||
|
const submitPassword = async (password: string) => {
|
||||||
|
await shareService.authenticateWithPassword(shareId, password).then(() => {
|
||||||
|
modals.closeAll();
|
||||||
|
getFiles();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFiles = (password?: string) =>
|
||||||
|
shareService
|
||||||
|
.get(shareId, password)
|
||||||
|
.then((files) => setShareList(files))
|
||||||
|
.catch((e) => {
|
||||||
|
const error = e.response.data.message;
|
||||||
|
if (e.response.status == 404) {
|
||||||
|
showShareNotFoundModal(modals);
|
||||||
|
} else if (error == "password_required") {
|
||||||
|
showEnterPasswordModal(modals, submitPassword);
|
||||||
|
} else if (error == "visitor_limit_exceeded") {
|
||||||
|
showVisitorLimitExceededModal(modals);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getFiles();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FileList
|
||||||
|
files={shareList}
|
||||||
|
shareId={shareId}
|
||||||
|
isLoading={shareList.length == 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Share;
|
||||||
103
src/pages/upload.tsx
Normal file
103
src/pages/upload.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { Button, Group, Menu } from "@mantine/core";
|
||||||
|
import { useModals } from "@mantine/modals";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useContext, useState } from "react";
|
||||||
|
import { Link, Mail } from "tabler-icons-react";
|
||||||
|
import Dropzone from "../components/upload/Dropzone";
|
||||||
|
import FileList from "../components/upload/FileList";
|
||||||
|
import showCompletedUploadModal from "../components/upload/showCompletedUploadModal";
|
||||||
|
import showCreateUploadModal from "../components/upload/showCreateUploadModal";
|
||||||
|
import { FileUpload } from "../types/File.type";
|
||||||
|
import aw from "../utils/appwrite.util";
|
||||||
|
import { IsSignedInContext } from "../utils/auth.util";
|
||||||
|
import toast from "../utils/toast.util";
|
||||||
|
|
||||||
|
const Upload = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const modals = useModals();
|
||||||
|
const isSignedIn = useContext(IsSignedInContext);
|
||||||
|
const [files, setFiles] = useState<FileUpload[]>([]);
|
||||||
|
const [isUploading, setisUploading] = useState(false);
|
||||||
|
|
||||||
|
const uploadFiles = async (
|
||||||
|
id: string,
|
||||||
|
expiration: number,
|
||||||
|
security: { password?: string; maxVisitors?: number }
|
||||||
|
) => {
|
||||||
|
setisUploading(true);
|
||||||
|
|
||||||
|
const bucketId = JSON.parse(
|
||||||
|
(
|
||||||
|
await aw.functions.createExecution(
|
||||||
|
"createShare",
|
||||||
|
JSON.stringify({ id, security, expiration }),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
).stdout
|
||||||
|
).id;
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
files[i].uploadingState = "inProgress";
|
||||||
|
setFiles([...files]);
|
||||||
|
aw.storage.createFile(bucketId, "unique()", files[i]).then(
|
||||||
|
async () => {
|
||||||
|
files[i].uploadingState = "finished";
|
||||||
|
setFiles([...files]);
|
||||||
|
if (!files.some((f) => f.uploadingState == "inProgress")) {
|
||||||
|
await aw.functions.createExecution(
|
||||||
|
"finishShare",
|
||||||
|
JSON.stringify({ id }),
|
||||||
|
false
|
||||||
|
),
|
||||||
|
setisUploading(false);
|
||||||
|
showCompletedUploadModal(
|
||||||
|
modals,
|
||||||
|
`${window.location.origin}/share/${bucketId}`,
|
||||||
|
new Date(Date.now()).toLocaleString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
files[i].uploadingState = undefined;
|
||||||
|
toast.error(error.message);
|
||||||
|
setisUploading(false);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isSignedIn) {
|
||||||
|
router.replace("/");
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Group position="right" mb={20}>
|
||||||
|
<div>
|
||||||
|
<Menu
|
||||||
|
control={
|
||||||
|
<Button loading={isUploading} disabled={files.length <= 0}>
|
||||||
|
Share
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
transition="pop-top-right"
|
||||||
|
placement="end"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<Menu.Item
|
||||||
|
icon={<Link size={16} />}
|
||||||
|
onClick={() => showCreateUploadModal(modals, uploadFiles)}
|
||||||
|
>
|
||||||
|
Share with link
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item disabled icon={<Mail size={16} />}>
|
||||||
|
Share with email
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
<Dropzone setFiles={setFiles} isUploading={isUploading} />
|
||||||
|
{files.length > 0 && <FileList files={files} setFiles={setFiles} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default Upload;
|
||||||
5
src/pages/user/account.tsx
Normal file
5
src/pages/user/account.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const Account = () => {
|
||||||
|
return <div></div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Account;
|
||||||
19
src/services/share.service.ts
Normal file
19
src/services/share.service.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { AppwriteFileWithPreview } from "../types/File.type";
|
||||||
|
|
||||||
|
const get = async (shareId: string, password?: string) => {
|
||||||
|
return (
|
||||||
|
await axios.post(`http://localhost:3000/api/share/${shareId}`, { password })
|
||||||
|
).data as AppwriteFileWithPreview[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const authenticateWithPassword = async (shareId: string, password?: string) => {
|
||||||
|
return (
|
||||||
|
await axios.post(
|
||||||
|
`http://localhost:3000/api/share/${shareId}/enterPassword`,
|
||||||
|
{ password }
|
||||||
|
)
|
||||||
|
).data as AppwriteFileWithPreview[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default { get, authenticateWithPassword };
|
||||||
19
src/styles/global.style.ts
Normal file
19
src/styles/global.style.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { MantineThemeOverride } from "@mantine/core";
|
||||||
|
|
||||||
|
export default <MantineThemeOverride>{
|
||||||
|
colors: {
|
||||||
|
victoria: [
|
||||||
|
"#E2E1F1",
|
||||||
|
"#C2C0E7",
|
||||||
|
"#A19DE4",
|
||||||
|
"#7D76E8",
|
||||||
|
"#544AF4",
|
||||||
|
"#4940DE",
|
||||||
|
"#4239C8",
|
||||||
|
"#463FA8",
|
||||||
|
"#47428E",
|
||||||
|
"#464379",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
primaryColor: "victoria",
|
||||||
|
};
|
||||||
80
src/styles/header.style.ts
Normal file
80
src/styles/header.style.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { createStyles } from "@mantine/core";
|
||||||
|
|
||||||
|
export default createStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
position: "relative",
|
||||||
|
zIndex: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
dropdown: {
|
||||||
|
position: "absolute",
|
||||||
|
top: 60,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
zIndex: 0,
|
||||||
|
borderTopRightRadius: 0,
|
||||||
|
borderTopLeftRadius: 0,
|
||||||
|
borderTopWidth: 0,
|
||||||
|
overflow: "hidden",
|
||||||
|
|
||||||
|
[theme.fn.largerThan("sm")]: {
|
||||||
|
display: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
header: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "100%",
|
||||||
|
},
|
||||||
|
|
||||||
|
links: {
|
||||||
|
[theme.fn.smallerThan("sm")]: {
|
||||||
|
display: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
burger: {
|
||||||
|
[theme.fn.largerThan("sm")]: {
|
||||||
|
display: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
link: {
|
||||||
|
display: "block",
|
||||||
|
lineHeight: 1,
|
||||||
|
padding: "8px 12px",
|
||||||
|
borderRadius: theme.radius.sm,
|
||||||
|
textDecoration: "none",
|
||||||
|
color:
|
||||||
|
theme.colorScheme === "dark"
|
||||||
|
? theme.colors.dark[0]
|
||||||
|
: theme.colors.gray[7],
|
||||||
|
fontSize: theme.fontSizes.sm,
|
||||||
|
fontWeight: 500,
|
||||||
|
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme === "dark"
|
||||||
|
? theme.colors.dark[6]
|
||||||
|
: theme.colors.gray[0],
|
||||||
|
},
|
||||||
|
|
||||||
|
[theme.fn.smallerThan("sm")]: {
|
||||||
|
borderRadius: 0,
|
||||||
|
padding: theme.spacing.md,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
linkActive: {
|
||||||
|
"&, &:hover": {
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme === "dark"
|
||||||
|
? theme.fn.rgba(theme.colors[theme.primaryColor][9], 0.25)
|
||||||
|
: theme.colors[theme.primaryColor][0],
|
||||||
|
color:
|
||||||
|
theme.colors[theme.primaryColor][theme.colorScheme === "dark" ? 3 : 7],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
15
src/types/Appwrite.type.ts
Normal file
15
src/types/Appwrite.type.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Models } from "node-appwrite";
|
||||||
|
|
||||||
|
export type ShareDocument = {
|
||||||
|
securityID: string;
|
||||||
|
createdAt: number;
|
||||||
|
expiresAt: number;
|
||||||
|
visitorCount: number;
|
||||||
|
enabled: boolean;
|
||||||
|
} & Models.Document;
|
||||||
|
|
||||||
|
|
||||||
|
export type SecurityDocument = {
|
||||||
|
password: string;
|
||||||
|
maxVisitors: number;
|
||||||
|
} & Models.Document;
|
||||||
7
src/types/File.type.ts
Normal file
7
src/types/File.type.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Models } from "appwrite";
|
||||||
|
|
||||||
|
export type FileUpload = File & { uploadingState?: UploadState };
|
||||||
|
export type UploadState = "finished" | "inProgress" | undefined;
|
||||||
|
|
||||||
|
export type AppwriteFileWithPreview = Models.File & { preview: Buffer };
|
||||||
|
|
||||||
9
src/utils/appwrite.util.ts
Normal file
9
src/utils/appwrite.util.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Appwrite } from "appwrite";
|
||||||
|
|
||||||
|
// SDK for client side (browser)
|
||||||
|
const aw = new Appwrite();
|
||||||
|
|
||||||
|
aw.setEndpoint("http://localhost:86/v1")
|
||||||
|
.setProject("pingvin-share");
|
||||||
|
|
||||||
|
export default aw;
|
||||||
17
src/utils/appwriteServer.util.ts
Normal file
17
src/utils/appwriteServer.util.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import sdk from "node-appwrite";
|
||||||
|
|
||||||
|
// SDK for server side (api)
|
||||||
|
const client = new sdk.Client();
|
||||||
|
|
||||||
|
client
|
||||||
|
.setEndpoint(process.env["APPWRITE_HOST"] as string)
|
||||||
|
.setProject("pingvin-share")
|
||||||
|
.setKey(process.env["APPWRITE_FUNCTION_API_KEY"] as string);
|
||||||
|
|
||||||
|
const awServer = {
|
||||||
|
user: new sdk.Users(client),
|
||||||
|
storage: new sdk.Storage(client),
|
||||||
|
database: new sdk.Database(client),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default awServer;
|
||||||
17
src/utils/auth.util.ts
Normal file
17
src/utils/auth.util.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { createContext } from "react";
|
||||||
|
import aw from "./appwrite.util";
|
||||||
|
|
||||||
|
const isSignedIn = async() => {
|
||||||
|
try {
|
||||||
|
await aw.account.get();
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IsSignedInContext = createContext(false);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
isSignedIn,
|
||||||
|
};
|
||||||
6
src/utils/loading.util.ts
Normal file
6
src/utils/loading.util.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createContext, Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
|
export const GlobalLoadingContext = createContext<{
|
||||||
|
isLoading: boolean;
|
||||||
|
setIsLoading: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}>({ isLoading: false, setIsLoading: () => {} });
|
||||||
6
src/utils/math/byteToSize.util.ts
Normal file
6
src/utils/math/byteToSize.util.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export function bytesToSize(bytes: number) {
|
||||||
|
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||||
|
if (bytes == 0) return "0 Byte";
|
||||||
|
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString());
|
||||||
|
return (bytes / Math.pow(1024, i)).toFixed(1).toString() + " " + sizes[i];
|
||||||
|
}
|
||||||
35
src/utils/shares/security.util.ts
Normal file
35
src/utils/shares/security.util.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { scryptSync } from "crypto";
|
||||||
|
import { SecurityDocument, ShareDocument } from "../../types/Appwrite.type";
|
||||||
|
import awServer from "../appwriteServer.util";
|
||||||
|
|
||||||
|
export const hashPassword = (password: string, salt: string) => {
|
||||||
|
return scryptSync(password, salt, 64).toString("hex");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkSecurity = async (
|
||||||
|
shareId: string,
|
||||||
|
hashedPassword?: string
|
||||||
|
) => {
|
||||||
|
const shareDocument = await awServer.database.getDocument<ShareDocument>(
|
||||||
|
"shares",
|
||||||
|
shareId
|
||||||
|
);
|
||||||
|
if (!shareDocument.securityID) return;
|
||||||
|
await awServer.database
|
||||||
|
.getDocument<SecurityDocument>("shareSecurity", shareDocument.securityID)
|
||||||
|
.then((securityDocument) => {
|
||||||
|
if (securityDocument.maxVisitors) {
|
||||||
|
if (shareDocument.visitorCount > securityDocument.maxVisitors) {
|
||||||
|
throw "visitor_limit_exceeded";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (securityDocument.password) {
|
||||||
|
if (!hashedPassword) throw "password_required";
|
||||||
|
|
||||||
|
if (hashedPassword !== securityDocument.password) {
|
||||||
|
throw "wrong_password";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { hashedPassword };
|
||||||
|
};
|
||||||
26
src/utils/toast.util.tsx
Normal file
26
src/utils/toast.util.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { showNotification } from "@mantine/notifications";
|
||||||
|
import { Check, X } from "tabler-icons-react";
|
||||||
|
|
||||||
|
const error = (message: string) =>
|
||||||
|
showNotification({
|
||||||
|
icon: <X />,
|
||||||
|
color: "red",
|
||||||
|
radius: "md",
|
||||||
|
title: "Error",
|
||||||
|
message: message,
|
||||||
|
});
|
||||||
|
|
||||||
|
const success = (message: string) =>
|
||||||
|
showNotification({
|
||||||
|
icon: <Check />,
|
||||||
|
color: "green",
|
||||||
|
radius: "md",
|
||||||
|
title: "Success",
|
||||||
|
message: message,
|
||||||
|
});
|
||||||
|
|
||||||
|
const toast = {
|
||||||
|
error,
|
||||||
|
success,
|
||||||
|
};
|
||||||
|
export default toast;
|
||||||
16
styles/globals.css
Normal file
16
styles/globals.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
html,
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||||
|
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
32
tsconfig.json
Normal file
32
tsconfig.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"functions/finishShare/src/index.js",
|
||||||
|
"functions/createShare/src/index.js"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user