mirror of
https://github.com/stonith404/pingvin-share.git
synced 2024-11-04 23:10:13 +01:00
initial commit
This commit is contained in:
commit
61be87b72a
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"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user