1
0
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:
Elias Schneider 2022-04-25 15:15:17 +02:00
commit 61be87b72a
No known key found for this signature in database
GPG Key ID: D5EC1C72D93244FD
72 changed files with 11502 additions and 0 deletions

8
.dockerignore Normal file
View File

@ -0,0 +1,8 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
dist
.next
.git

4
.env.example Normal file
View 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
View 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
View 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

View 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
View 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
View 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
View 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
View 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"
}

View 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;

View 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,
};

View 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;

View 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
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "es6",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist"
},
"lib": ["es2015"]
}

View 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
View 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
View 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
View 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
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 KiB

BIN
assets/screenshots/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 KiB

8
docker-compose.yml Normal file
View File

@ -0,0 +1,8 @@
version: '3.3'
services:
pingvin-share:
ports:
- '3000:3000'
image: stonith404/pingvin-share
build:
context: ./

View 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"
}
}

View 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"
});
};

View 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"
}
}

View 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,
});
};

View File

@ -0,0 +1,9 @@
const { scryptSync } = require("crypto");
const hashPassword = (password, salt) => {
return scryptSync(password, salt, 64).toString("hex");
}
module.exports = {
hashPassword,
}

View 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"
}
}

View 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
View 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
View File

@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig

8268
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

49
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

1
public/logo.svg Normal file
View 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

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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
View 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
View 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;
}

View 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;

View 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
View 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
View 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
View 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>
);
}
}

View 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
View 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;

View File

@ -0,0 +1,5 @@
const Account = () => {
return <div></div>;
};
export default Account;

View 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 };

View 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",
};

View 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],
},
},
}));

View 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
View 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 };

View 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;

View 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
View 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,
};

View File

@ -0,0 +1,6 @@
import { createContext, Dispatch, SetStateAction } from "react";
export const GlobalLoadingContext = createContext<{
isLoading: boolean;
setIsLoading: Dispatch<SetStateAction<boolean>>;
}>({ isLoading: false, setIsLoading: () => {} });

View 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];
}

View 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
View 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
View 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
View 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"
]
}