1
0
mirror of https://github.com/stonith404/pingvin-share.git synced 2024-06-27 13:20:48 +02:00

feat: remove appwrite and add nextjs backend

This commit is contained in:
Elias Schneider 2022-10-09 22:30:32 +02:00
parent 7728351158
commit 4bab33ad8a
153 changed files with 13400 additions and 2811 deletions

View File

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

View File

@ -1,9 +1,15 @@
# Appwrite
APPWRITE_FUNCTION_API_KEY=your-api-key
PUBLIC_APPWRITE_HOST=http://localhost/v1
# DATABASE
DB_URL="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}/pingvin-share?schema=public"
DB_USER=postgres
DB_PASSWORD=postgres
DB_HOST=db:5432
# Frontend
PUBLIC_MAX_FILE_SIZE=300000000 # Note: Should be the same as in the _APP_STORAGE_LIMIT in the Appwrite .env file
PUBLIC_DISABLE_REGISTRATION=true # Note: In the Appwrite console you have to change your user limit to 0 if false and else to 1
PUBLIC_DISABLE_HOME_PAGE=false
PUBLIC_MAIL_SHARE_ENABLED=false # Note: If set to true you have to add your SMTP server. See "Additional configurations" in the README file
# GENERAL
APP_URL=http://localhost:3000
BACKEND_URL=http://backend:8080
SHOW_HOME_PAGE=true
ALLOW_REGISTRATION=true
MAX_FILE_SIZE=1000000000
# SECURITY
JWT_SECRET=long-random-string

29
.gitignore vendored
View File

@ -1,19 +1,15 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
/.pnp
.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
/frontend/.next/
/frontend/out/
# production
/build
# build
build/
dist/
# misc
.DS_Store
@ -25,16 +21,19 @@ yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# env file
.env
# vercel
.vercel
# typescript
*.tsbuildinfo
.env
# PWA
/public/workbox-*
/public/sw.*
/frontend/public/workbox-*
/frontend/public/sw.*
# project specific
/backend/uploads/
/uploads/

View File

@ -1,104 +0,0 @@
export default [
{
$id: "shares",
$read: [],
$write: [],
name: "Shares",
enabled: true,
permission: "document",
attributes: [
{
key: "securityID",
type: "string",
required: false,
array: false,
size: 255,
default: null,
},
{
key: "users",
type: "string",
required: false,
array: true,
size: 255,
default: null,
},
{
key: "createdAt",
type: "integer",
required: true,
array: false,
min: 0,
max: 9007199254740991,
default: null,
},
{
key: "expiresAt",
type: "integer",
required: true,
array: false,
min: 0,
max: 9007199254740991,
default: null,
},
{
key: "visitorCount",
type: "integer",
required: false,
array: false,
min: 0,
max: 9007199254740991,
default: 0,
},
{
key: "enabled",
type: "boolean",
required: false,
array: false,
default: false,
},
],
indexes: [
{
key: "expiresAt",
type: "key",
attributes: ["expiresAt"],
orders: ["ASC"],
},
{
key: "enabled",
type: "key",
attributes: ["enabled"],
orders: ["ASC"],
},
],
},
{
$id: "shareSecurity",
$read: [],
$write: [],
name: "ShareSecurity",
enabled: true,
permission: "collection",
attributes: [
{
key: "password",
type: "string",
required: false,
array: false,
size: 128,
default: null,
},
{
key: "maxVisitors",
type: "integer",
required: false,
array: false,
min: 0,
max: 9007199254740991,
default: null,
},
],
indexes: [],
},
];

View File

@ -1,53 +0,0 @@
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"],
SMTP_HOST: "-",
SMTP_PORT: "-",
SMTP_USER: "-",
SMTP_PASSWORD: "-",
SMTP_FROM: "-",
FRONTEND_URL: "-",
},
events: [],
schedule: "",
timeout: 15,
},
{
$id: "finishShare",
execute: ["role:all"],
name: "Finish 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: "cleanShares",
execute: [],
name: "Clean Shares",
runtime: "node-16.0",
vars: {
APPWRITE_FUNCTION_ENDPOINT: host,
APPWRITE_FUNCTION_API_KEY: process.env["APPWRITE_FUNCTION_API_KEY"],
},
events: [],
schedule: "00 * * * *",
timeout: 60,
},
];
};

View File

@ -1,60 +0,0 @@
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",
}
);
process.env["APPWRITE_HOST"] = process.env["APPWRITE_HOST"].replace(
"localhost",
"host.docker.internal"
);
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 host...");
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
View File

@ -1,676 +0,0 @@
{
"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
}
}
}

View File

@ -1,24 +0,0 @@
{
"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

@ -1,11 +0,0 @@
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

@ -1,45 +0,0 @@
import api from "./api.service";
import rl from "readline-sync";
import cookie from "cookie";
const getToken = async () => {
console.info("Please enter your Appwrite credentials");
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

@ -1,18 +0,0 @@
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

@ -1,145 +0,0 @@
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
);
}
}
// Wait until the indexes are created
for (const index of indexes) {
const getStatus = async () =>
(
await aw().database.getAttribute(collection.$id, index.key)
).status.toString();
while ((await getStatus()) == "processing") {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
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",
"users.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,
};

View File

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

View File

@ -1,17 +0,0 @@
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;

View File

@ -1,34 +0,0 @@
# 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
FROM node:16-alpine AS script-builder
WORKDIR /opt/app
COPY .setup .setup
WORKDIR /opt/app/.setup
RUN npm install
RUN npm i -g @vercel/ncc
RUN ncc build index.ts
# Production image, copy all the files and run next
FROM node:16-alpine AS runner
WORKDIR /opt/app
ENV NODE_ENV=production
COPY ./functions ./functions
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 --from=script-builder /opt/app/.setup/dist/index.js ./scripts/setup.js
EXPOSE 3000
CMD ["node_modules/.bin/next", "start"]

View File

@ -1,8 +1,6 @@
# <div align="center"><img src="https://user-images.githubusercontent.com/58886915/166198400-c2134044-1198-4647-a8b6-da9c4a204c68.svg" width="40"/> </br>Pingvin Share</div>
> ⚠️ This project is no longer maintained and only compatible with Apprite version 0.14 and below.
Pingvin Share is a self-hosted file sharing platform made for the [Appwrite Hackathon](https://dev.to/devteam/announcing-the-appwrite-hackathon-on-dev-1oc0).
Pingvin Share is self-hosted file sharing platform and an alternative for WeTransfer.
## 🎪 Showcase
@ -13,59 +11,48 @@ Demo: https://pingvin-share.dev.eliasschneider.com
## ✨ Features
- Create a simple share with a link
- Secure your share with a visitor limit and a password
- Share your files with specific emails and send an invitation email
- No file size limit, only your disk will be your limit
- Optionally secure your share with a visitor limit and a password
- Dark mode
## ⌨️ Setup
At the moment, the setup is a bit time-consuming. I will improve the setup in the future.
1. Download the `docker-compose.yml` and `.env.example` file.
2. Rename the `.env.example` file to `.env` and change the environment variables so that they fit to your environment. If you need help with the environment variables take a look [here](#environment-variables)
3. Run `docker-compose up -d`
### 1. Appwrite
The website is now listening available on `http://localhost:8080`, have fun with Pingvin Share 🐧!
Pingvin Share uses Appwrite as backend. You have to install and setup Appwrite first
### Environment variables
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. Frontend
First of all you have to start the Docker container.
1. Clone the `docker-compose.yml` file and the `.env.example` file from this repository
2. Rename the `.env.example` file to `.env`
3. Start the container with `docker-compose up -d`
The container is now running. Now you have to setup the Appwrite structure, but no worries there is a simple setup script.
To run the script run `docker-compose exec pingvin-share node scripts/setup.js`.
You're almost done, now you have to change your environment variables that they fit to your setup.
1. Go to your Appwrite console, visit "API Keys" and copy the "Functions API Key" secret to your clipboard.
2. Paste the key to the `APPWRITE_FUNCTION_API_KEY` variable in the `.env` file
3. Change `PUBLIC_APPWRITE_HOST` in the `.env` file to the host where your Appwrite instance runs
4. Change the rest of the environment variables in the `.env` that they fit to your preferences
5. To save the environment variables run `docker-compose up -d`
6. Well done! Get a coffee and enjoy Pingvin Share 🎉
## ⚙️ Additional configurations
### SMTP
1. Enable `PUBLIC_MAIL_SHARE_ENABLE` in the `.env` file.
2. Visit your Appwrite console, click on functions and select the `Create Share` function.
3. At the settings tab change the empty variables to your SMTP setup.
## 💁‍♂️ Known issues / Limitations
Pingvin Share is currently in beta and there are issues and limitations that should be fixed in the future.
- `DownloadAll` generates the zip file on the client side. This takes alot of time. Because of that I temporarily limited this function to maximal 150 MB.
- If a user knows the share id, he can list and download the files directly from the Appwrite API even if the share is secured by a password or a visitor limit.
| Variable | Description | Possible values |
| -------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------- |
| `APP_URL` | On which URL Pingvin Share is available. E.g http://localhost or https://pingvin-share.com. | URL |
| `BACKEND_URL` | Where the backend is listening on your local machine. If you use the default installation, use `http://backend:8080`. | URL |
| `SHOW_HOME_PAGE` | Wether the Pingvin Share home page should be shown. | true/false |
| `ALLOW_REGISTRATION` | Wether a new user can create a new account. | true/false |
| `MAX_FILE_SIZE` | Maximum allowed size per file in bytes. | Number |
| `JWT_SECRET` | Random string to sign the JWT's. | Random string |
## 🖤 Contribute
You're very welcome to contribute to Pingvin Share!
Contact me, create an issue or directly create a pull request.
### Development setup
#### Database & Backend
1. Open the `backend`
2. Duplicate the `.env.example` file, rename the duplicate to `.env` and change the environment variables if needed
3. Install the dependencies with `npm install`
4. Start the database by running `docker-compose up -d`
5. Push the database schema to the database by running `npx prisma db push`
6. Start the backend with `npm run dev`
#### Frontend
1. Open the `frontend` folder
2. Duplicate the `.env.example` file, rename the duplicate to `.env` and change the environment variables if needed
3. Install the dependencies with `npm install`
4. Start the frontend with `npm run dev`
You're all set!

View File

@ -1,39 +0,0 @@
{
"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
}
]
}

3
backend/.dockerignore Normal file
View File

@ -0,0 +1,3 @@
node_modules/
dist/
.git/

12
backend/.env.example Normal file
View File

@ -0,0 +1,12 @@
# Environment variables declared in this file are automatically made available to Prisma.
DB_URL="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}/pingvin-share?schema=public"
DB_USER=postgres
DB_PASSWORD=postgres
DB_HOST=localhost:5432
APP_URL=http://localhost:3000
ALLOW_REGISTRATION=true
MAX_FILE_SIZE=5000000000
JWT_SECRET=csdkdfmfdfdkslfjskl3987rfkhjgdfnkjdf

18
backend/.eslintrc.json Normal file
View File

@ -0,0 +1,18 @@
{
"env": {
"browser": true,
"es2021": true
},
"overrides": [
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
}
}

22
backend/Dockerfile Normal file
View File

@ -0,0 +1,22 @@
FROM node:18 AS deps
WORKDIR /opt/app
COPY package.json package-lock.json ./
COPY prisma ./prisma
RUN npm ci
RUN npx prisma generate
FROM node:18 As build
WORKDIR /opt/app
COPY . .
COPY --from=deps /opt/app/node_modules ./node_modules
RUN npm run build
FROM node:18 As runner
WORKDIR /opt/app
COPY --from=build /opt/app/node_modules ./node_modules
COPY --from=build /opt/app/dist ./dist
COPY --from=build /opt/app/prisma ./prisma
COPY --from=deps /opt/app/package.json ./
CMD npm run prod

View File

@ -0,0 +1,15 @@
version: '3.8'
services:
db:
image: postgres:14.1-alpine
restart: always
environment:
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=pingvin-share
ports:
- '5432:5432'
volumes:
- pingvin-share-dev-db:/var/lib/postgresql/data
volumes:
pingvin-share-dev-db:

5
backend/nest-cli.json Normal file
View File

@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src"
}

11069
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

60
backend/package.json Normal file
View File

@ -0,0 +1,60 @@
{
"name": "pingvin-share-backend",
"version": "0.0.1",
"scripts": {
"build": "nest build",
"dev": "dotenv -- nest start --watch",
"prod": "npx prisma migrate deploy && dotenv node dist/main",
"lint": "eslint 'src/**/*.ts'",
"format": "prettier --write 'src/**/*.ts'"
},
"dependencies": {
"@nestjs/common": "^9.1.2",
"@nestjs/config": "^2.2.0",
"@nestjs/core": "^9.1.2",
"@nestjs/jwt": "^9.0.0",
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.1.2",
"@nestjs/schedule": "^2.1.0",
"@nestjs/swagger": "^6.1.2",
"archiver": "^5.3.1",
"argon2": "^0.29.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"mime-types": "^2.1.35",
"moment": "^2.29.4",
"multer": "^1.4.5-lts.1",
"passport": "^0.6.0",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"rimraf": "^3.0.2"
},
"devDependencies": {
"@nestjs/cli": "^9.1.4",
"@nestjs/schematics": "^9.0.3",
"@nestjs/testing": "^9.1.2",
"@prisma/client": "^4.4.0",
"@types/archiver": "^5.3.1",
"@types/cron": "^2.0.0",
"@types/express": "^4.17.14",
"@types/mime-types": "^2.1.1",
"@types/multer": "^1.4.7",
"@types/node": "^18.7.23",
"@types/passport-jwt": "^3.0.7",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^5.39.0",
"@typescript-eslint/parser": "^5.39.0",
"cross-env": "^7.0.3",
"dotenv-cli": "^6.0.0",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"prettier": "^2.7.1",
"prisma": "^4.4.0",
"source-map-support": "^0.5.21",
"ts-loader": "^9.4.1",
"ts-node": "^10.9.1",
"tsconfig-paths": "4.1.0",
"typescript": "^4.3.5"
}
}

View File

@ -0,0 +1,75 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"firstName" TEXT,
"lastName" TEXT,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RefreshToken" (
"token" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expiresAt" TIMESTAMP(3) NOT NULL DEFAULT NOW() + interval '3 months',
"userId" TEXT NOT NULL,
CONSTRAINT "RefreshToken_pkey" PRIMARY KEY ("token")
);
-- CreateTable
CREATE TABLE "Share" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"uploadLocked" BOOLEAN NOT NULL DEFAULT false,
"isZipReady" BOOLEAN NOT NULL DEFAULT false,
"views" INTEGER NOT NULL DEFAULT 0,
"expiration" TIMESTAMP(3) NOT NULL,
"creatorId" TEXT NOT NULL,
CONSTRAINT "Share_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "File" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"name" TEXT NOT NULL,
"size" TEXT NOT NULL,
"shareId" TEXT NOT NULL,
CONSTRAINT "File_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ShareSecurity" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"password" TEXT,
"maxViews" INTEGER,
"shareId" TEXT,
CONSTRAINT "ShareSecurity_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "ShareSecurity_shareId_key" ON "ShareSecurity"("shareId");
-- AddForeignKey
ALTER TABLE "RefreshToken" ADD CONSTRAINT "RefreshToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Share" ADD CONSTRAINT "Share_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "File" ADD CONSTRAINT "File_shareId_fkey" FOREIGN KEY ("shareId") REFERENCES "Share"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ShareSecurity" ADD CONSTRAINT "ShareSecurity_shareId_fkey" FOREIGN KEY ("shareId") REFERENCES "Share"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@ -0,0 +1,69 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DB_URL")
}
model User {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
email String @unique
password String
firstName String?
lastName String?
shares Share[]
refreshTokens RefreshToken[]
}
model RefreshToken {
token String @id @default(uuid())
createdAt DateTime @default(now())
expiresAt DateTime @default(dbgenerated("NOW() + interval '3 months'"))
userId String
user User @relation(fields: [userId], references: [id])
}
model Share {
id String @id @default(uuid())
createdAt DateTime @default(now())
uploadLocked Boolean @default(false)
isZipReady Boolean @default(false)
views Int @default(0)
expiration DateTime
creatorId String
creator User @relation(fields: [creatorId], references: [id])
security ShareSecurity?
files File[]
}
model File {
id String @id @default(uuid())
createdAt DateTime @default(now())
name String
size String
shareId String
share Share @relation(fields: [shareId], references: [id], onDelete: Cascade)
}
model ShareSecurity {
id String @id @default(uuid())
createdAt DateTime @default(now())
password String?
maxViews Int?
shareId String? @unique
share Share? @relation(fields: [shareId], references: [id], onDelete: Cascade)
}

27
backend/src/app.module.ts Normal file
View File

@ -0,0 +1,27 @@
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { ScheduleModule } from "@nestjs/schedule";
import { AuthModule } from "./auth/auth.module";
import { JobsService } from "./auth/jobs/jobs.service";
import { FileController } from "./file/file.controller";
import { FileModule } from "./file/file.module";
import { PrismaModule } from "./prisma/prisma.module";
import { PrismaService } from "./prisma/prisma.service";
import { ShareController } from "./share/share.controller";
import { ShareModule } from "./share/share.module";
import { UserController } from "./user/user.controller";
@Module({
imports: [
AuthModule,
ShareModule,
FileModule,
PrismaModule,
ConfigModule.forRoot({ isGlobal: true }),
ScheduleModule.forRoot(),
],
providers: [PrismaService, JobsService],
controllers: [UserController, ShareController, FileController],
})
export class AppModule {}

View File

@ -0,0 +1,42 @@
import {
Body,
Controller,
ForbiddenException,
HttpCode,
Post,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { AuthService } from "./auth.service";
import { AuthDTO } from "./dto/auth.dto";
import { AuthRegisterDTO } from "./dto/authRegister.dto";
import { RefreshAccessTokenDTO } from "./dto/refreshAccessToken.dto";
@Controller("auth")
export class AuthController {
constructor(
private authService: AuthService,
private config: ConfigService
) {}
@Post("signUp")
signUp(@Body() dto: AuthRegisterDTO) {
if (!this.config.get("ALLOW_REGISTRATION"))
throw new ForbiddenException("Registration is not allowed");
return this.authService.signUp(dto);
}
@Post("signIn")
signIn(@Body() dto: AuthDTO) {
return this.authService.signIn(dto);
}
@Post("token")
@HttpCode(200)
async refreshAccessToken(@Body() body: RefreshAccessTokenDTO) {
const accessToken = await this.authService.refreshAccessToken(
body.refreshToken
);
return { accessToken };
}
}

View File

@ -0,0 +1,13 @@
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { JwtStrategy } from "./strategy/jwt.strategy";
@Module({
imports: [JwtModule.register({})],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}

View File

@ -0,0 +1,95 @@
import {
BadRequestException,
Injectable,
UnauthorizedException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { JwtService } from "@nestjs/jwt";
import { User } from "@prisma/client";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import * as argon from "argon2";
import { PrismaService } from "src/prisma/prisma.service";
import { AuthDTO } from "./dto/auth.dto";
import { AuthRegisterDTO } from "./dto/authRegister.dto";
@Injectable()
export class AuthService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
private config: ConfigService
) {}
async signUp(dto: AuthRegisterDTO) {
const hash = await argon.hash(dto.password);
try {
const user = await this.prisma.user.create({
data: {
email: dto.email,
password: hash,
},
});
const accessToken = await this.createAccessToken(user);
const refreshToken = await this.createRefreshToken(user.id);
return { accessToken, refreshToken };
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e.code == "P2002") {
throw new BadRequestException("Credentials taken");
}
}
}
}
async signIn(dto: AuthDTO) {
const user = await this.prisma.user.findUnique({
where: {
email: dto.email,
},
});
if (!user || !(await argon.verify(user.password, dto.password)))
throw new UnauthorizedException("Wrong email or password");
const accessToken = await this.createAccessToken(user);
const refreshToken = await this.createRefreshToken(user.id);
return { accessToken, refreshToken };
}
async createAccessToken(user: User) {
return this.jwtService.sign(
{
sub: user.id,
email: user.email,
},
{
expiresIn: "15min",
secret: this.config.get("JWT_SECRET"),
}
);
}
async refreshAccessToken(refreshToken: string) {
const refreshTokenMetaData = await this.prisma.refreshToken.findUnique({
where: { token: refreshToken },
include: { user: true },
});
if (!refreshTokenMetaData || refreshTokenMetaData.expiresAt < new Date())
throw new UnauthorizedException();
return this.createAccessToken(refreshTokenMetaData.user);
}
async createRefreshToken(userId: string) {
const refreshToken = (
await this.prisma.refreshToken.create({ data: { userId } })
).token;
return refreshToken;
}
}

View File

@ -0,0 +1,9 @@
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
export const GetUser = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
}
);

View File

@ -0,0 +1,26 @@
import { Expose, plainToClass } from "class-transformer";
import { IsEmail, IsNotEmpty, IsString } from "class-validator";
export class AuthDTO {
@Expose()
id: string;
@Expose()
firstName: string;
@Expose()
lastName: string;
@Expose()
@IsNotEmpty()
@IsEmail()
email: string;
@IsNotEmpty()
@IsString()
password: string;
constructor(partial: Partial<AuthDTO>) {
return plainToClass(AuthDTO, partial, { excludeExtraneousValues: true });
}
}

View File

@ -0,0 +1,4 @@
import { PickType } from "@nestjs/swagger";
import { AuthDTO } from "./auth.dto";
export class AuthRegisterDTO extends AuthDTO {}

View File

@ -0,0 +1,7 @@
import { PickType } from "@nestjs/swagger";
import { AuthDTO } from "./auth.dto";
export class AuthSignInDTO extends PickType(AuthDTO, [
"email",
"password",
] as const) {}

View File

@ -0,0 +1,6 @@
import { IsNotEmpty, IsString } from "class-validator";
export class RefreshAccessTokenDTO {
@IsNotEmpty()
refreshToken: string;
}

View File

@ -0,0 +1,7 @@
import { AuthGuard } from "@nestjs/passport";
export class JwtGuard extends AuthGuard("jwt") {
constructor() {
super();
}
}

View File

@ -0,0 +1,37 @@
import { Injectable } from "@nestjs/common";
import { Cron } from "@nestjs/schedule";
import { FileService } from "src/file/file.service";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class JobsService {
constructor(
private prisma: PrismaService,
private fileService: FileService
) {}
@Cron("0 * * * *")
async deleteExpiredShares() {
const expiredShares = await this.prisma.share.findMany({
where: { expiration: { lt: new Date() } },
});
for (const expiredShare of expiredShares) {
await this.prisma.share.delete({
where: { id: expiredShare.id },
});
await this.fileService.deleteAllFiles(expiredShare.id);
}
console.log(`job: deleted ${expiredShares.length} expired shares`);
}
@Cron("0 * * * *")
async deleteExpiredRefreshTokens() {
const expiredShares = await this.prisma.refreshToken.deleteMany({
where: { expiresAt: { lt: new Date() } },
});
console.log(`job: deleted ${expiredShares.count} expired refresh tokens`);
}
}

View File

@ -0,0 +1,24 @@
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PassportStrategy } from "@nestjs/passport";
import { User } from "@prisma/client";
import { ExtractJwt, Strategy } from "passport-jwt";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: ConfigService, private prisma: PrismaService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.get("JWT_SECRET"),
});
}
async validate(payload: any) {
const user: User = await this.prisma.user.findUnique({
where: { id: payload.sub },
});
return user;
}
}

View File

@ -0,0 +1,22 @@
import { Expose, plainToClass } from "class-transformer";
import { ShareDTO } from "src/share/dto/share.dto";
export class FileDTO {
@Expose()
id: string;
@Expose()
name: string;
@Expose()
size: string;
@Expose()
url: boolean;
share: ShareDTO;
constructor(partial: Partial<FileDTO>) {
return plainToClass(FileDTO, partial, { excludeExtraneousValues: true });
}
}

View File

@ -0,0 +1,107 @@
import {
Controller,
Get,
Param,
ParseFilePipeBuilder,
Post,
Res,
StreamableFile,
UploadedFile,
UseGuards,
UseInterceptors,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { FileInterceptor } from "@nestjs/platform-express";
import { Response } from "express";
import { JwtGuard } from "src/auth/guard/jwt.guard";
import { FileDownloadGuard } from "src/file/guard/fileDownload.guard";
import { ShareSecurityGuard } from "src/share/guard/shareSecurity.guard";
import { FileService } from "./file.service";
@Controller("shares/:shareId/files")
export class FileController {
constructor(
private fileService: FileService,
private config: ConfigService
) {}
@Post()
@UseGuards(JwtGuard)
@UseInterceptors(
FileInterceptor("file", {
dest: "./uploads/_temp/",
})
)
async create(
@UploadedFile(
new ParseFilePipeBuilder()
.addMaxSizeValidator({
maxSize: parseInt(process.env.MAX_FILE_SIZE),
})
.build()
)
file: Express.Multer.File,
@Param("shareId") shareId: string
) {
return await this.fileService.create(file, shareId);
}
@Get(":fileId/download")
@UseGuards(ShareSecurityGuard)
async getFileDownloadUrl(
@Res({ passthrough: true }) res: Response,
@Param("shareId") shareId: string,
@Param("fileId") fileId: string
) {
const url = this.fileService.getFileDownloadUrl(shareId, fileId);
return { url };
}
@Get("zip/download")
@UseGuards(ShareSecurityGuard)
async getZipArchiveDownloadURL(
@Res({ passthrough: true }) res: Response,
@Param("shareId") shareId: string,
@Param("fileId") fileId: string
) {
const url = this.fileService.getFileDownloadUrl(shareId, fileId);
res.set({
"Content-Type": "application/zip",
});
return { url };
}
@Get("zip")
@UseGuards(FileDownloadGuard)
async getZip(
@Res({ passthrough: true }) res: Response,
@Param("shareId") shareId: string
) {
const zip = this.fileService.getZip(shareId);
res.set({
"Content-Type": "application/zip",
"Content-Disposition": `attachment ; filename="pingvin-share-${shareId}"`,
});
return new StreamableFile(zip);
}
@Get(":fileId")
@UseGuards(FileDownloadGuard)
async getFile(
@Res({ passthrough: true }) res: Response,
@Param("shareId") shareId: string,
@Param("fileId") fileId: string
) {
const file = await this.fileService.get(shareId, fileId);
res.set({
"Content-Type": file.metaData.mimeType,
"Content-Length": file.metaData.size,
"Content-Disposition": `attachment ; filename="${file.metaData.name}"`,
});
return new StreamableFile(file.file);
}
}

View File

@ -0,0 +1,14 @@
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { ShareModule } from "src/share/share.module";
import { ShareService } from "src/share/share.service";
import { FileController } from "./file.controller";
import { FileService } from "./file.service";
@Module({
imports: [JwtModule.register({}), ShareModule],
controllers: [FileController],
providers: [FileService],
exports: [FileService],
})
export class FileModule {}

View File

@ -0,0 +1,112 @@
import {
BadRequestException,
Injectable,
NotFoundException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { JwtService } from "@nestjs/jwt";
import { randomUUID } from "crypto";
import * as fs from "fs";
import * as mime from "mime-types";
import { join } from "path";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class FileService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
private config: ConfigService
) {}
async create(file: Express.Multer.File, shareId: string) {
const share = await this.prisma.share.findUnique({
where: { id: shareId },
});
if (share.uploadLocked)
throw new BadRequestException("Share is already completed");
const fileId = randomUUID();
await fs.promises.mkdir(`./uploads/shares/${shareId}`, { recursive: true });
fs.promises.rename(
`./uploads/_temp/${file.filename}`,
`./uploads/shares/${shareId}/${fileId}`
);
return await this.prisma.file.create({
data: {
id: fileId,
name: file.originalname,
size: file.size.toString(),
share: { connect: { id: shareId } },
},
});
}
async get(shareId: string, fileId: string) {
const fileMetaData = await this.prisma.file.findUnique({
where: { id: fileId },
});
if (!fileMetaData) throw new NotFoundException("File not found");
const file = fs.createReadStream(
join(process.cwd(), `uploads/shares/${shareId}/${fileId}`)
);
return {
metaData: {
mimeType: mime.contentType(fileMetaData.name.split(".").pop()),
...fileMetaData,
size: fileMetaData.size,
},
file,
};
}
async deleteAllFiles(shareId: string) {
await fs.promises.rm(`./uploads/shares/${shareId}`, {
recursive: true,
force: true,
});
}
getZip(shareId: string) {
return fs.createReadStream(`./uploads/shares/${shareId}/archive.zip`);
}
getFileDownloadUrl(shareId: string, fileId: string) {
const downloadToken = this.generateFileDownloadToken(shareId, fileId);
return `${this.config.get(
"APP_URL"
)}/api/shares/${shareId}/files/${fileId}?token=${downloadToken}`;
}
generateFileDownloadToken(shareId: string, fileId: string) {
if (fileId == "zip") fileId = undefined;
return this.jwtService.sign(
{
shareId,
fileId,
},
{
expiresIn: "10min",
secret: this.config.get("JWT_SECRET"),
}
);
}
verifyFileDownloadToken(shareId: string, fileId: string, token: string) {
try {
const claims = this.jwtService.verify(token, {
secret: this.config.get("JWT_SECRET"),
});
return claims.shareId == shareId && claims.fileId == fileId;
} catch {
return false;
}
}
}

View File

@ -0,0 +1,23 @@
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Request } from "express";
import { FileService } from "src/file/file.service";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class FileDownloadGuard implements CanActivate {
constructor(
private reflector: Reflector,
private fileService: FileService,
private prisma: PrismaService
) {}
async canActivate(context: ExecutionContext) {
const request: Request = context.switchToHttp().getRequest();
const token = request.query.token as string;
const { shareId, fileId } = request.params;
return this.fileService.verifyFileDownloadToken(shareId, fileId, token);
}
}

15
backend/src/main.ts Normal file
View File

@ -0,0 +1,15 @@
import { ClassSerializerInterceptor, ValidationPipe } from "@nestjs/common";
import { NestFactory, Reflector } from "@nestjs/core";
import * as fs from "fs";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
await fs.promises.mkdir("./uploads/_temp", { recursive: true });
app.setGlobalPrefix("api");
await app.listen(8080);
}
bootstrap();

View File

@ -0,0 +1,6 @@
import { Global, Module } from "@nestjs/common";
import { PrismaService } from "./prisma.service";
@Global()
@Module({ providers: [PrismaService], exports: [PrismaService] })
export class PrismaModule {}

View File

@ -0,0 +1,18 @@
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PrismaClient } from "@prisma/client";
@Injectable()
export class PrismaService extends PrismaClient {
constructor(config: ConfigService) {
super({
datasources: {
db: {
url: config.get("DB_URL"),
},
},
});
console.log(config.get("DB_URL"));
super.$connect().then(() => console.info("Connected to the database"));
}
}

View File

@ -0,0 +1,18 @@
import { Type } from "class-transformer";
import { IsString, Matches, ValidateNested } from "class-validator";
import { ShareSecurityDTO } from "./shareSecurity.dto";
export class CreateShareDTO {
@IsString()
@Matches("^[a-zA-Z0-9_-]*$", undefined, {
message: "ID only can contain letters, numbers, underscores and hyphens",
})
id: string;
@IsString()
expiration: string;
@ValidateNested()
@Type(() => ShareSecurityDTO)
security: ShareSecurityDTO;
}

View File

@ -0,0 +1,20 @@
import { Expose, plainToClass } from "class-transformer";
import { ShareDTO } from "./share.dto";
export class MyShareDTO extends ShareDTO {
@Expose()
views: number;
@Expose()
createdAt: Date;
from(partial: Partial<MyShareDTO>) {
return plainToClass(MyShareDTO, partial, { excludeExtraneousValues: true });
}
fromList(partial: Partial<MyShareDTO>[]) {
return partial.map((part) =>
plainToClass(MyShareDTO, part, { excludeExtraneousValues: true })
);
}
}

View File

@ -0,0 +1,29 @@
import { Expose, plainToClass, Type } from "class-transformer";
import { AuthDTO } from "src/auth/dto/auth.dto";
import { FileDTO } from "src/file/dto/file.dto";
export class ShareDTO {
@Expose()
id: string;
@Expose()
expiration: Date;
@Expose()
@Type(() => FileDTO)
files: FileDTO[];
@Expose()
@Type(() => AuthDTO)
creator: AuthDTO;
from(partial: Partial<ShareDTO>) {
return plainToClass(ShareDTO, partial, { excludeExtraneousValues: true });
}
fromList(partial: Partial<ShareDTO>[]) {
return partial.map((part) =>
plainToClass(ShareDTO, part, { excludeExtraneousValues: true })
);
}
}

View File

@ -0,0 +1,15 @@
import { Expose, plainToClass } from "class-transformer";
export class ShareMetaDataDTO {
@Expose()
id: string;
@Expose()
isZipReady: boolean;
from(partial: Partial<ShareMetaDataDTO>) {
return plainToClass(ShareMetaDataDTO, partial, {
excludeExtraneousValues: true,
});
}
}

View File

@ -0,0 +1,6 @@
import { IsNotEmpty } from "class-validator";
export class SharePasswordDto {
@IsNotEmpty()
password: string;
}

View File

@ -0,0 +1,12 @@
import { IsNumber, IsOptional, IsString, Length } from "class-validator";
export class ShareSecurityDTO {
@IsString()
@IsOptional()
@Length(3, 30)
password: string;
@IsNumber()
@IsOptional()
maxViews: number;
}

View File

@ -0,0 +1,57 @@
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
NotFoundException,
} from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Request } from "express";
import * as moment from "moment";
import { PrismaService } from "src/prisma/prisma.service";
import { ShareService } from "src/share/share.service";
@Injectable()
export class ShareSecurityGuard implements CanActivate {
constructor(
private reflector: Reflector,
private shareService: ShareService,
private prisma: PrismaService
) {}
async canActivate(context: ExecutionContext) {
const request: Request = context.switchToHttp().getRequest();
const shareId = Object.prototype.hasOwnProperty.call(
request.params,
"shareId"
)
? request.params.shareId
: request.params.id;
const share = await this.prisma.share.findUnique({
where: { id: shareId },
include: { security: true },
});
if (!share || moment().isAfter(share.expiration))
throw new NotFoundException("Share not found");
if (!share.security) return true;
if (share.security.maxViews && share.security.maxViews <= share.views)
throw new ForbiddenException(
"Maximum views exceeded",
"share_max_views_exceeded"
);
if (
!this.shareService.verifyShareToken(shareId, request.get("X-Share-Token"))
)
throw new ForbiddenException(
"This share is password protected",
"share_token_required"
);
return true;
}
}

View File

@ -0,0 +1,77 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Post,
UseGuards,
} from "@nestjs/common";
import { User } from "@prisma/client";
import { GetUser } from "src/auth/decorator/getUser.decorator";
import { JwtGuard } from "src/auth/guard/jwt.guard";
import { CreateShareDTO } from "./dto/createShare.dto";
import { MyShareDTO } from "./dto/myShare.dto";
import { ShareDTO } from "./dto/share.dto";
import { ShareMetaDataDTO } from "./dto/shareMetaData.dto";
import { SharePasswordDto } from "./dto/sharePassword.dto";
import { ShareSecurityGuard } from "./guard/shareSecurity.guard";
import { ShareService } from "./share.service";
@Controller("shares")
export class ShareController {
constructor(private shareService: ShareService) {}
@Get()
@UseGuards(JwtGuard)
async getMyShares(@GetUser() user: User) {
return new MyShareDTO().fromList(
await this.shareService.getSharesByUser(user.id)
);
}
@Get(":id")
@UseGuards(ShareSecurityGuard)
async get(@Param("id") id: string) {
return new ShareDTO().from(await this.shareService.get(id));
}
@Get(":id/metaData")
@UseGuards(ShareSecurityGuard)
async getMetaData(@Param("id") id: string) {
return new ShareMetaDataDTO().from(await this.shareService.getMetaData(id));
}
@Post()
@UseGuards(JwtGuard)
async create(@Body() body: CreateShareDTO, @GetUser() user: User) {
return new ShareDTO().from(await this.shareService.create(body, user));
}
@Delete(":id")
@UseGuards(JwtGuard)
async remove(@Param("id") id: string, @GetUser() user: User) {
await this.shareService.remove(id, user.id);
}
@Post(":id/complete")
@HttpCode(202)
@UseGuards(JwtGuard)
async complete(@Param("id") id: string) {
return new ShareDTO().from(await this.shareService.complete(id));
}
@Get("isShareIdAvailable/:id")
async isShareIdAvailable(@Param("id") id: string) {
return this.shareService.isShareIdAvailable(id);
}
@Post(":id/password")
async exchangeSharePasswordWithToken(
@Param("id") id: string,
@Body() body: SharePasswordDto
) {
return this.shareService.exchangeSharePasswordWithToken(id, body.password);
}
}

View File

@ -0,0 +1,14 @@
import { Module } from "@nestjs/common";
import { JwtModule, JwtService } from "@nestjs/jwt";
import { AuthModule } from "src/auth/auth.module";
import { FileModule } from "src/file/file.module";
import { ShareController } from "./share.controller";
import { ShareService } from "./share.service";
@Module({
imports: [JwtModule.register({})],
controllers: [ShareController],
providers: [ShareService],
exports: [ShareService],
})
export class ShareModule {}

View File

@ -0,0 +1,200 @@
import {
BadRequestException,
ForbiddenException,
Injectable,
NotFoundException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { JwtService } from "@nestjs/jwt";
import { Share, User } from "@prisma/client";
import * as archiver from "archiver";
import * as argon from "argon2";
import * as fs from "fs";
import * as moment from "moment";
import { PrismaService } from "src/prisma/prisma.service";
import { CreateShareDTO } from "./dto/createShare.dto";
@Injectable()
export class ShareService {
constructor(
private prisma: PrismaService,
private config: ConfigService,
private jwtService: JwtService
) {}
async create(share: CreateShareDTO, user: User) {
if (!(await this.isShareIdAvailable(share.id)).isAvailable)
throw new BadRequestException("Share id already in use");
if (!share.security || Object.keys(share.security).length == 0)
share.security = undefined;
if (share.security?.password) {
share.security.password = await argon.hash(share.security.password);
}
const expirationDate = moment()
.add(
share.expiration.split("-")[0],
share.expiration.split("-")[1] as moment.unitOfTime.DurationConstructor
)
.toDate();
// Throw error if expiration date is now
if (expirationDate.setMilliseconds(0) == new Date().setMilliseconds(0))
throw new BadRequestException("Invalid expiration date");
return await this.prisma.share.create({
data: {
...share,
expiration: expirationDate,
creator: { connect: { id: user.id } },
security: { create: share.security },
},
});
}
async createZip(shareId: string) {
const path = `./uploads/shares/${shareId}`;
const files = await this.prisma.file.findMany({ where: { shareId } });
const archive = archiver("zip", {
zlib: { level: 9 },
});
const writeStream = fs.createWriteStream(`${path}/archive.zip`);
for (const file of files) {
archive.append(fs.createReadStream(`${path}/${file.id}`), {
name: file.name,
});
}
archive.pipe(writeStream);
await archive.finalize();
}
async complete(id: string) {
const moreThanOneFileInShare =
(await this.prisma.file.findMany({ where: { shareId: id } })).length != 0;
if (!moreThanOneFileInShare)
throw new BadRequestException(
"You need at least on file in your share to complete it."
);
this.createZip(id).then(() =>
this.prisma.share.update({ where: { id }, data: { isZipReady: true } })
);
return await this.prisma.share.update({
where: { id },
data: { uploadLocked: true },
});
}
async getSharesByUser(userId: string) {
return await this.prisma.share.findMany({
where: { creator: { id: userId }, expiration: { gt: new Date() } },
});
}
async get(id: string) {
let share: any = await this.prisma.share.findUnique({
where: { id },
include: {
files: true,
creator: true,
},
});
if (!share || !share.uploadLocked)
throw new NotFoundException("Share not found");
share.files = share.files.map((file) => {
file["url"] = `http://localhost:8080/file/${file.id}`;
return file;
});
await this.increaseViewCount(share);
return share;
}
async getMetaData(id: string) {
const share = await this.prisma.share.findUnique({
where: { id },
});
if (!share || !share.uploadLocked)
throw new NotFoundException("Share not found");
return share;
}
async remove(shareId: string, userId: string) {
const share = await this.prisma.share.findUnique({
where: { id: shareId },
});
if (!share) throw new NotFoundException("Share not found");
if (share.creatorId != userId) throw new ForbiddenException();
await this.prisma.share.delete({ where: { id: shareId } });
}
async isShareCompleted(id: string) {
return (await this.prisma.share.findUnique({ where: { id } })).uploadLocked;
}
async isShareIdAvailable(id: string) {
const share = await this.prisma.share.findUnique({ where: { id } });
return { isAvailable: !share };
}
async increaseViewCount(share: Share) {
await this.prisma.share.update({
where: { id: share.id },
data: { views: share.views + 1 },
});
}
async exchangeSharePasswordWithToken(shareId: string, password: string) {
const sharePassword = (
await this.prisma.shareSecurity.findFirst({
where: { share: { id: shareId } },
})
).password;
if (!(await argon.verify(sharePassword, password)))
throw new ForbiddenException("Wrong password");
const token = this.generateShareToken(shareId);
return { token };
}
generateShareToken(shareId: string) {
return this.jwtService.sign(
{
shareId,
},
{
expiresIn: "1h",
secret: this.config.get("JWT_SECRET"),
}
);
}
verifyShareToken(shareId: string, token: string) {
try {
const claims = this.jwtService.verify(token, {
secret: this.config.get("JWT_SECRET"),
});
return claims.shareId == shareId;
} catch {
return false;
}
}
}

View File

@ -0,0 +1,13 @@
import { Controller, Get, UseGuards } from "@nestjs/common";
import { User } from "@prisma/client";
import { GetUser } from "src/auth/decorator/getUser.decorator";
import { JwtGuard } from "src/auth/guard/jwt.guard";
@Controller("users")
export class UserController {
@Get("me")
@UseGuards(JwtGuard)
async getCurrentUser(@GetUser() user: User) {
return user;
}
}

View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

21
backend/tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}

View File

@ -1,14 +1,45 @@
version: '3.3'
version: '3.8'
services:
pingvin-share:
restart: "unless-stopped"
ports:
- '3000:3000'
image: stonith404/pingvin-share
environment:
- APPWRITE_FUNCTION_API_KEY=${APPWRITE_FUNCTION_API_KEY}
- PUBLIC_APPWRITE_HOST=${PUBLIC_APPWRITE_HOST}
- PUBLIC_MAX_FILE_SIZE=${PUBLIC_MAX_FILE_SIZE}
- PUBLIC_DISABLE_REGISTRATION=${PUBLIC_DISABLE_REGISTRATION}
- PUBLIC_DISABLE_HOME_PAGE=${PUBLIC_DISABLE_HOME_PAGE}
- PUBLIC_MAIL_SHARE_ENABLED=${PUBLIC_MAIL_SHARE_ENABLED}
db:
image: postgres:14.1-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=pingvin-share
volumes:
- pingvin-share-db:/var/lib/postgresql/data
backend:
image: pingvin-share/backend
restart: unless-stopped
environment:
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- DB_HOST=${DB_HOST}
- DB_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}/pingvin-share?schema=public
- APP_URL=${APP_URL}
- ALLOW_REGISTRATION=${ALLOW_REGISTRATION}
- MAX_FILE_SIZE=${MAX_FILE_SIZE}
- JWT_SECRET=${JWT_SECRET}
ports:
- '8080:8080'
depends_on:
- db
volumes:
- "./uploads:/usr/src/app/uploads"
frontend:
restart: unless-stopped
ports:
- '3000:3000'
image: pingvin-share/frontend
environment:
- SHOW_HOME_PAGE=${SHOW_HOME_PAGE}
- ALLOW_REGISTRATION=${ALLOW_REGISTRATION}
- MAX_FILE_SIZE=${MAX_FILE_SIZE}
- BACKEND_URL=${BACKEND_URL}
depends_on:
- backend
volumes:
pingvin-share-db:

4
frontend/.dockerignore Normal file
View File

@ -0,0 +1,4 @@
node_modules/
dist/
.next/
.git/

4
frontend/.env.example Normal file
View File

@ -0,0 +1,4 @@
BACKEND_URL=http://localhost:8080
SHOW_HOME_PAGE=true
ALLOW_REGISTRATION=true
MAX_FILE_SIZE=1000000000

View File

@ -1,5 +1,5 @@
{
"extends": ["eslint-config-next", "eslint:recommended"],
"extends": ["eslint-config-next", "eslint:recommended", "prettier"],
"plugins": ["react"],
"rules": {
"quotes": ["warn", "double", { "allowTemplateLiterals": true }],

26
frontend/Dockerfile Normal file
View File

@ -0,0 +1,26 @@
FROM node:18-alpine AS deps
WORKDIR /opt/app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:18-alpine AS builder
ENV NODE_ENV=production
WORKDIR /opt/app
COPY . .
COPY --from=deps /opt/app/node_modules ./node_modules
RUN npm run build
FROM node:18-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
RUN npm i -g dotenv-cli
EXPOSE 3000
CMD dotenv -e .env.development node_modules/.bin/next start

View File

@ -0,0 +1,14 @@
version: '3.3'
services:
pingvin-share:
restart: "unless-stopped"
ports:
- '3000:3000'
image: stonith404/pingvin-share
environment:
- APPWRITE_FUNCTION_API_KEY=${APPWRITE_FUNCTION_API_KEY}
- PUBLIC_APPWRITE_HOST=${PUBLIC_APPWRITE_HOST}
- PUBLIC_MAX_FILE_SIZE=${PUBLIC_MAX_FILE_SIZE}
- PUBLIC_DISABLE_REGISTRATION=${PUBLIC_DISABLE_REGISTRATION}
- PUBLIC_DISABLE_HOME_PAGE=${PUBLIC_DISABLE_HOME_PAGE}
- PUBLIC_MAIL_SHARE_ENABLED=${PUBLIC_MAIL_SHARE_ENABLED}

View File

@ -8,6 +8,12 @@ const nextConfig = withPWA({
dest: "public",
disable: process.env.NODE_ENV == "development"
},
publicRuntimeConfig: {
ALLOW_REGISTRATION: process.env.ALLOW_REGISTRATION,
SHOW_HOME_PAGE: process.env.SHOW_HOME_PAGE,
MAX_FILE_SIZE: process.env.MAX_FILE_SIZE,
BACKEND_URL: process.env.BACKEND_URL
}
})
module.exports = nextConfig

File diff suppressed because it is too large Load Diff

45
frontend/package.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "pingvin-share",
"version": "0.0.1",
"scripts": {
"dev": "dotenv next dev",
"build": "next build",
"start": "dotenv next start",
"lint": "next lint",
"format": "prettier --write \"src/**/*.ts\""
},
"dependencies": {
"@mantine/core": "^4.2.0",
"@mantine/dropzone": "^4.2.0",
"@mantine/form": "^4.2.0",
"@mantine/hooks": "^4.2.0",
"@mantine/modals": "^4.2.0",
"@mantine/next": "^4.2.0",
"@mantine/notifications": "^4.2.0",
"axios": "^0.26.1",
"cookies-next": "^2.0.4",
"file-saver": "^2.0.5",
"jose": "^4.8.1",
"moment": "^2.29.3",
"next": "12.1.5",
"next-http-proxy-middleware": "^1.2.4",
"next-pwa": "^5.5.2",
"react": "18.0.0",
"react-dom": "18.0.0",
"tabler-icons-react": "^1.44.0",
"yup": "^0.32.11"
},
"devDependencies": {
"@types/node": "17.0.23",
"@types/react": "18.0.4",
"@types/react-dom": "18.0.0",
"axios": "^0.26.1",
"dotenv-cli": "^6.0.0",
"eslint": "8.13.0",
"eslint-config-next": "12.1.5",
"eslint-config-prettier": "^8.5.0",
"prettier": "^2.7.1",
"tar": "^6.1.11",
"typescript": "^4.6.3"
}
}

View File

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 944 B

After

Width:  |  Height:  |  Size: 944 B

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1018 B

After

Width:  |  Height:  |  Size: 1018 B

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -9,13 +9,14 @@ import {
Title,
} from "@mantine/core";
import { useForm, yupResolver } from "@mantine/form";
import getConfig from "next/config";
import * as yup from "yup";
import aw from "../../utils/appwrite.util";
import { useConfig } from "../../utils/config.util";
import authService from "../../services/auth.service";
import toast from "../../utils/toast.util";
const { publicRuntimeConfig } = getConfig();
const AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => {
const config = useConfig();
const validationSchema = yup.object().shape({
email: yup.string().email().required(),
password: yup.string().min(8).required(),
@ -30,17 +31,18 @@ const AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => {
});
const signIn = (email: string, password: string) => {
aw.account
.createSession(email, password)
authService
.signIn(email, password)
.then(() => window.location.replace("/upload"))
.catch((e) => toast.error(e.message));
.catch((e) => toast.error(e.response.data.message));
};
const signUp = (email: string, password: string) => {
aw.account
.create("unique()", email, password)
authService
.signUp(email, password)
.then(() => signIn(email, password))
.catch((e) => toast.error(e.message));
.catch((e) => toast.error(e.response.data.message));
};
return (
<Container size={420} my={40}>
<Title
@ -52,7 +54,7 @@ const AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => {
>
{mode == "signUp" ? "Sign up" : "Welcome back"}
</Title>
{!config.DISABLE_REGISTRATION && (
{publicRuntimeConfig.ALLOW_REGISTRATION == "true" && (
<Text color="dimmed" size="sm" align="center" mt={5}>
{mode == "signUp"
? "You have an account already?"

View File

@ -1,7 +1,7 @@
import { ActionIcon, Avatar, Menu } from "@mantine/core";
import { NextLink } from "@mantine/next";
import { DoorExit, Link, Moon } from "tabler-icons-react";
import aw from "../../utils/appwrite.util";
import authService from "../../services/auth.service";
import ToggleThemeButton from "./ToggleThemeButton";
const ActionAvatar = () => {
@ -30,8 +30,7 @@ const ActionAvatar = () => {
</Menu.Item> */}
<Menu.Item
onClick={async () => {
await aw.account.deleteSession("current");
window.location.reload();
await authService.signOut();
}}
icon={<DoorExit size={14} />}
>

View File

@ -9,13 +9,15 @@ import {
} from "@mantine/core";
import { useBooleanToggle } from "@mantine/hooks";
import { NextLink } from "@mantine/next";
import getConfig from "next/config";
import Image from "next/image";
import React, { ReactNode, useContext, useEffect, useState } from "react";
import { ReactNode, useEffect, useState } from "react";
import useUser from "../../hooks/user.hook";
import headerStyle from "../../styles/header.style";
import { IsSignedInContext } from "../../utils/auth.util";
import { useConfig } from "../../utils/config.util";
import ActionAvatar from "./ActionAvatar";
const { publicRuntimeConfig } = getConfig();
type Link = {
link?: string;
label?: string;
@ -26,8 +28,8 @@ type Link = {
const Header = () => {
const [opened, toggleOpened] = useBooleanToggle(false);
const [active, setActive] = useState<string>();
const isSignedIn = useContext(IsSignedInContext);
const config = useConfig();
const user = useUser();
const { classes, cx } = headerStyle();
const authenticatedLinks: Link[] = [
@ -47,28 +49,26 @@ const Header = () => {
},
];
if (!config.DISABLE_HOME_PAGE)
if (publicRuntimeConfig.SHOW_HOME_PAGE == "true")
unauthenticatedLinks.unshift({
link: "/",
label: "Home",
});
if (!config.DISABLE_REGISTRATION)
if (publicRuntimeConfig.ALLOW_REGISTRATION == "true")
unauthenticatedLinks.push({
link: "/auth/signUp",
label: "Sign up",
});
const links = isSignedIn ? authenticatedLinks : unauthenticatedLinks;
const links = user ? authenticatedLinks : unauthenticatedLinks;
const items = links.map((link) => {
const items = links.map((link, i) => {
if (link.component) {
return (
<>
<Container pl={5} py={15}>
{link.component}
</Container>
</>
<Container key={i} pl={5} py={15}>
{link.component}
</Container>
);
}
if (link) {
@ -77,7 +77,7 @@ const Header = () => {
if (window.location.pathname == link.link) {
setActive(link.link);
}
});
}, []);
return (
<NextLink
key={link.label}

View File

@ -4,7 +4,6 @@ import {
Col,
Grid,
Group,
MultiSelect,
NumberInput,
PasswordInput,
Select,
@ -15,37 +14,37 @@ import { useForm, yupResolver } from "@mantine/form";
import { useModals } from "@mantine/modals";
import * as yup from "yup";
import shareService from "../../services/share.service";
import toast from "../../utils/toast.util";
import { ShareSecurity } from "../../types/share.type";
const CreateUploadModalBody = ({
mode,
uploadCallback,
}: {
mode: "standard" | "email";
uploadCallback: (
id: string,
expiration: number,
security: { password?: string; maxVisitors?: number },
emails?: string[]
expiration: string,
security: ShareSecurity
) => void;
}) => {
const modals = useModals();
const validationSchema = yup.object().shape({
link: yup.string().required().min(2).max(50),
emails:
mode == "email"
? yup.array().of(yup.string().email()).min(1)
: yup.array(),
password: yup.string().min(3).max(100),
maxVisitors: yup.number().min(1),
link: yup
.string()
.required()
.min(3)
.max(100)
.matches(new RegExp("^[a-zA-Z0-9_-]*$"), {
message: "Can only contain letters, numbers, underscores and hyphens",
}),
password: yup.string().min(3).max(30),
maxViews: yup.number().min(1),
});
const form = useForm({
initialValues: {
link: "",
emails: [] as string[],
password: undefined,
maxVisitors: undefined,
expiration: "1440",
maxViews: undefined,
expiration: "1-day",
},
schema: yupResolver(validationSchema),
});
@ -53,19 +52,14 @@ const CreateUploadModalBody = ({
return (
<form
onSubmit={form.onSubmit(async (values) => {
if (await shareService.isIdAlreadyInUse(values.link)) {
form.setFieldError("link", "Link already in use.");
if (!(await shareService.isShareIdAvailable(values.link))) {
form.setFieldError("link", "This link is already in use");
} else {
uploadCallback(values.link, values.expiration, {
password: values.password,
maxViews: values.maxViews,
});
modals.closeAll();
uploadCallback(
values.link,
parseInt(values.expiration),
{
password: values.password,
maxVisitors: values.maxVisitors,
},
values.emails
);
}
})}
>
@ -105,35 +99,18 @@ const CreateUploadModalBody = ({
{window.location.origin}/share/
{form.values.link == "" ? "myAwesomeShare" : form.values.link}
</Text>
{mode == "email" && (
<MultiSelect
label="Email adresses"
data={form.values.emails}
placeholder="Email adresses"
searchable
creatable
rightSection={<></>}
getCreateLabel={(email) => `${email}`}
onCreate={async (email) => {
if (!(await shareService.doesUserExist(email))) {
form.setFieldValue("emails", form.values.emails);
toast.error(
`${email} doesn't have an account at Pingvin Share.`
);
}
}}
{...form.getInputProps("emails")}
/>
)}
<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" },
{
value: "10-minutes",
label: "10 Minutes",
},
{ value: "1-hour", label: "1 Hour" },
{ value: "1-day", label: "1 Day" },
{ value: "1-week".toString(), label: "1 Week" },
{ value: "1-month", label: "1 Month" },
]}
/>
<Accordion>
@ -142,20 +119,20 @@ const CreateUploadModalBody = ({
sx={{ borderBottom: "none" }}
>
<Group direction="column" grow>
{mode == "standard" && (
<PasswordInput
variant="filled"
placeholder="No password"
label="Password protection"
{...form.getInputProps("password")}
/>
)}
<PasswordInput
variant="filled"
placeholder="No password"
label="Password protection"
{...form.getInputProps("password")}
/>
<NumberInput
min={1}
type="number"
variant="filled"
placeholder="No limit"
label="Maximal views"
{...form.getInputProps("maxVisitors")}
{...form.getInputProps("maxViews")}
/>
</Group>
</Accordion.Item>

View File

@ -0,0 +1,53 @@
import { Button, Tooltip } from "@mantine/core";
import { useEffect, useState } from "react";
import shareService from "../../services/share.service";
const DownloadAllButton = ({ shareId }: { shareId: string }) => {
const [isZipReady, setIsZipReady] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const downloadAll = async () => {
setIsLoading(true);
await shareService
.downloadFile(shareId, "zip")
.then(() => setIsLoading(false));
};
useEffect(() => {
shareService
.getMetaData(shareId)
.then((share) => setIsZipReady(share.isZipReady))
.catch(() => {});
const timer = setInterval(() => {
shareService.getMetaData(shareId).then((share) => {
setIsZipReady(share.isZipReady);
if (share.isZipReady) clearInterval(timer);
}).catch(() => {});
}, 5000);
return () => {
clearInterval(timer);
};
}, []);
if (!isZipReady)
return (
<Tooltip
wrapLines
position="bottom"
width={220}
withArrow
label="The share is preparing. This can take a few minutes."
>
<Button variant="outline" onClick={downloadAll} disabled>
Download all
</Button>
</Tooltip>
);
return (
<Button variant="outline" loading={isLoading} onClick={downloadAll}>
Download all
</Button>
);
};
export default DownloadAllButton;

View File

@ -1,17 +1,16 @@
import { ActionIcon, Loader, Skeleton, Table } from "@mantine/core";
import Image from "next/image";
import { useRouter } from "next/router";
import { CircleCheck, Download } from "tabler-icons-react";
import { AppwriteFileWithPreview } from "../../types/File.type";
import aw from "../../utils/appwrite.util";
import { bytesToSize } from "../../utils/math/byteToSize.util";
import shareService from "../../services/share.service";
import { byteStringToHumanSizeString } from "../../utils/math/byteStringToHumanSizeString.util";
const FileList = ({
files,
shareId,
isLoading,
}: {
files: AppwriteFileWithPreview[];
files: any[];
shareId: string;
isLoading: boolean;
}) => {
@ -37,7 +36,7 @@ const FileList = ({
const rows = files.map((file) => (
<tr key={file.name}>
<td>
<Image
{/* <Image
width={30}
height={30}
alt={file.name}
@ -46,10 +45,10 @@ const FileList = ({
src={`data:image/png;base64,${new Buffer(file.preview).toString(
"base64"
)}`}
></Image>
></Image> */}
</td>
<td>{file.name}</td>
<td>{bytesToSize(file.sizeOriginal)}</td>
<td>{byteStringToHumanSizeString(file.size)}</td>
<td>
{file.uploadingState ? (
file.uploadingState != "finished" ? (
@ -60,9 +59,9 @@ const FileList = ({
) : (
<ActionIcon
size={25}
onClick={() =>
router.push(aw.storage.getFileDownload(shareId, file.$id))
}
onClick={async () => {
await shareService.downloadFile(shareId, file.id);
}}
>
<Download />
</ActionIcon>

View File

@ -42,7 +42,7 @@ const Body = ({ submitCallback }: { submitCallback: any }) => {
.then((res: any) => res)
.catch((e: any) => {
const error = e.response.data.message;
if (error == "wrong_password") {
if (error == "Wrong password") {
setPasswordWrong(true);
}
})

View File

@ -8,11 +8,13 @@ import {
useMantineTheme,
} from "@mantine/core";
import { Dropzone as MantineDropzone, DropzoneStatus } from "@mantine/dropzone";
import getConfig from "next/config";
import React, { Dispatch, ForwardedRef, SetStateAction, useRef } from "react";
import { CloudUpload, Upload } from "tabler-icons-react";
import { useConfig } from "../../utils/config.util";
import toast from "../../utils/toast.util";
const { publicRuntimeConfig } = getConfig()
const useStyles = createStyles((theme) => ({
wrapper: {
position: "relative",
@ -53,13 +55,12 @@ const Dropzone = ({
setFiles: Dispatch<SetStateAction<File[]>>;
}) => {
const theme = useMantineTheme();
const config = useConfig();
const { classes } = useStyles();
const openRef = useRef<() => void>();
return (
<div className={classes.wrapper}>
<MantineDropzone
maxSize={config.MAX_FILE_SIZE}
maxSize={parseInt(publicRuntimeConfig.MAX_FILE_SIZE!)}
onReject={(e) => {
toast.error(e[0].errors[0].message);
}}

Some files were not shown because too many files have changed in this diff Show More