1
0
mirror of https://github.com/stonith404/pingvin-share.git synced 2024-11-05 15:30:14 +01:00

Merge branch 'feat/refactor-project-nestjs' into main

This commit is contained in:
Elias Schneider 2022-10-10 22:34:37 +02:00
commit 7164ed36ab
157 changed files with 14696 additions and 3967 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 # DATABASE
APPWRITE_FUNCTION_API_KEY=your-api-key DB_URL="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}/pingvin-share?schema=public"
PUBLIC_APPWRITE_HOST=http://localhost/v1 DB_USER=postgres
DB_PASSWORD=postgres
DB_HOST=db:5432
# Frontend # GENERAL
PUBLIC_MAX_FILE_SIZE=300000000 # Note: Should be the same as in the _APP_STORAGE_LIMIT in the Appwrite .env file APP_URL=http://localhost:3000
PUBLIC_DISABLE_REGISTRATION=true # Note: In the Appwrite console you have to change your user limit to 0 if false and else to 1 BACKEND_URL=http://backend:8080
PUBLIC_DISABLE_HOME_PAGE=false SHOW_HOME_PAGE=true
PUBLIC_MAIL_SHARE_ENABLED=false # Note: If set to true you have to add your SMTP server. See "Additional configurations" in the README file ALLOW_REGISTRATION=true
MAX_FILE_SIZE=1000000000
# SECURITY
JWT_SECRET=long-random-string

View File

@ -1,9 +1,10 @@
name: Docker Image CI name: Create Docker Image for Backend
on: on:
workflow_dispatch:
push: push:
branches: main branches: main
paths:
- 'backend/**'
jobs: jobs:
build: build:
@ -11,15 +12,14 @@ jobs:
steps: steps:
- name: checkout code - name: checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: install buildx - name: Set up QEMU
id: buildx uses: docker/setup-qemu-action@v1
uses: crazy-max/ghaction-docker-buildx@v1 - name: Set up Docker Buildx
with: uses: docker/setup-buildx-action@v1
version: latest
- name: login to docker registry - name: login to docker registry
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: build the image - name: build the image
run: | run: |
docker buildx build --push \ docker buildx build --push \
--tag stonith404/pingvin-share:latest \ --tag stonith404/pingvin-share-backend:latest \
--platform linux/amd64,linux/arm64 . --platform linux/amd64,linux/arm64 .

View File

@ -0,0 +1,25 @@
name: Create Docker Image for Frontend
on:
push:
branches: main
paths:
- 'frontend/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: checkout code
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: login to docker registry
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: build the image
run: |
docker buildx build --push \
--tag stonith404/pingvin-share-frontend:latest \
--platform linux/amd64,linux/arm64 .

29
.gitignore vendored
View File

@ -1,19 +1,15 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies # dependencies
node_modules node_modules
/.pnp .pnp
.pnp.js .pnp.js
# testing
/coverage
# next.js # next.js
/.next/ /frontend/.next/
/out/ /frontend/out/
# production # build
/build build/
dist/
# misc # misc
.DS_Store .DS_Store
@ -25,16 +21,19 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
.pnpm-debug.log* .pnpm-debug.log*
# local env files # env file
.env*.local .env
# vercel # vercel
.vercel .vercel
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
.env
# PWA # PWA
/public/workbox-* /frontend/public/workbox-*
/public/sw.* /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> # <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 self-hosted file sharing platform and an alternative for WeTransfer.
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).
## 🎪 Showcase ## 🎪 Showcase
@ -13,59 +11,50 @@ Demo: https://pingvin-share.dev.eliasschneider.com
## ✨ Features ## ✨ Features
- Create a simple share with a link - Create a simple share with a link
- Secure your share with a visitor limit and a password - No file size limit, only your disk will be your limit
- Share your files with specific emails and send an invitation email - Optionally secure your share with a visitor limit and a password
- Dark mode - Dark mode
## ⌨️ Setup ## ⌨️ 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) | Variable | Description | Possible values |
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 | `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 |
### 2. Frontend | `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 |
First of all you have to start the Docker container. | `MAX_FILE_SIZE` | Maximum allowed size per file in bytes. | Number |
| `JWT_SECRET` | Random string to sign the JWT's. | Random string |
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.
## 🖤 Contribute ## 🖤 Contribute
You're very welcome to contribute to Pingvin Share! You're very welcome to contribute to Pingvin Share!
Contact me, create an issue or directly create a pull request. 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,43 @@
import {
Body,
Controller,
ForbiddenException,
HttpCode,
Post,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { UserDTO } from "src/user/dto/user.dto";
import { AuthService } from "./auth.service";
import { AuthRegisterDTO } from "./dto/authRegister.dto";
import { AuthSignInDTO } from "./dto/authSignIn.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: AuthSignInDTO) {
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 { AuthRegisterDTO } from "./dto/authRegister.dto";
import { AuthSignInDTO } from "./dto/authSignIn.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: AuthSignInDTO) {
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,4 @@
import { PickType } from "@nestjs/swagger";
import { UserDTO } from "src/user/dto/user.dto";
export class AuthRegisterDTO extends UserDTO {}

View File

@ -0,0 +1,7 @@
import { PickType } from "@nestjs/swagger";
import { UserDTO } from "src/user/dto/user.dto";
export class AuthSignInDTO extends PickType(UserDTO, [
"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;
from(partial: Partial<FileDTO>) {
return plainToClass(FileDTO, partial, { excludeExtraneousValues: true });
}
}

View File

@ -0,0 +1,108 @@
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 { ShareDTO } from "src/share/dto/share.dto";
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 new ShareDTO().from( 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 { AuthSignInDTO } from "src/auth/dto/authSignIn.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(() => AuthSignInDTO)
creator: AuthSignInDTO;
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,26 @@
import { Expose, plainToClass } from "class-transformer";
import { IsEmail, IsNotEmpty, IsString } from "class-validator";
export class UserDTO {
@Expose()
id: string;
@Expose()
firstName: string;
@Expose()
lastName: string;
@Expose()
@IsNotEmpty()
@IsEmail()
email: string;
@IsNotEmpty()
@IsString()
password: string;
from(partial: Partial<UserDTO>) {
return plainToClass(UserDTO, partial, { excludeExtraneousValues: true });
}
}

View File

@ -0,0 +1,14 @@
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";
import { UserDTO } from "./dto/user.dto";
@Controller("users")
export class UserController {
@Get("me")
@UseGuards(JwtGuard)
async getCurrentUser(@GetUser() user: User) {
return new UserDTO().from(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,43 @@
version: '3.3' version: '3.8'
services: services:
pingvin-share: db:
restart: "unless-stopped" 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: stonith404/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}
depends_on:
- db
volumes:
- "./uploads:/usr/src/app/uploads"
frontend:
restart: unless-stopped
ports: ports:
- '3000:3000' - '3000:3000'
image: stonith404/pingvin-share image: stonith404/pingvin-share-frontend
environment: environment:
- APPWRITE_FUNCTION_API_KEY=${APPWRITE_FUNCTION_API_KEY} - SHOW_HOME_PAGE=${SHOW_HOME_PAGE}
- PUBLIC_APPWRITE_HOST=${PUBLIC_APPWRITE_HOST} - ALLOW_REGISTRATION=${ALLOW_REGISTRATION}
- PUBLIC_MAX_FILE_SIZE=${PUBLIC_MAX_FILE_SIZE} - MAX_FILE_SIZE=${MAX_FILE_SIZE}
- PUBLIC_DISABLE_REGISTRATION=${PUBLIC_DISABLE_REGISTRATION} - BACKEND_URL=${BACKEND_URL}
- PUBLIC_DISABLE_HOME_PAGE=${PUBLIC_DISABLE_HOME_PAGE} depends_on:
- PUBLIC_MAIL_SHARE_ENABLED=${PUBLIC_MAIL_SHARE_ENABLED} - 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"], "plugins": ["react"],
"rules": { "rules": {
"quotes": ["warn", "double", { "allowTemplateLiterals": true }], "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

@ -3,11 +3,16 @@
const withPWA = require("next-pwa"); const withPWA = require("next-pwa");
const nextConfig = withPWA({ const nextConfig = withPWA({
reactStrictMode: true,
pwa: { pwa: {
dest: "public", dest: "public",
disable: process.env.NODE_ENV == "development" 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 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": "^5.5.2",
"@mantine/dropzone": "^5.5.2",
"@mantine/form": "^5.5.2",
"@mantine/hooks": "^5.5.2",
"@mantine/modals": "^5.5.2",
"@mantine/next": "^5.5.2",
"@mantine/notifications": "^5.5.2",
"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,15 @@ import {
Title, Title,
} from "@mantine/core"; } from "@mantine/core";
import { useForm, yupResolver } from "@mantine/form"; import { useForm, yupResolver } from "@mantine/form";
import { NextLink } from "@mantine/next";
import getConfig from "next/config";
import * as yup from "yup"; import * as yup from "yup";
import aw from "../../utils/appwrite.util"; import authService from "../../services/auth.service";
import { useConfig } from "../../utils/config.util";
import toast from "../../utils/toast.util"; import toast from "../../utils/toast.util";
const { publicRuntimeConfig } = getConfig();
const AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => { const AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => {
const config = useConfig();
const validationSchema = yup.object().shape({ const validationSchema = yup.object().shape({
email: yup.string().email().required(), email: yup.string().email().required(),
password: yup.string().min(8).required(), password: yup.string().min(8).required(),
@ -26,21 +28,22 @@ const AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => {
email: "", email: "",
password: "", password: "",
}, },
schema: yupResolver(validationSchema), validate: yupResolver(validationSchema),
}); });
const signIn = (email: string, password: string) => { const signIn = (email: string, password: string) => {
aw.account authService
.createSession(email, password) .signIn(email, password)
.then(() => window.location.replace("/upload")) .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) => { const signUp = (email: string, password: string) => {
aw.account authService
.create("unique()", email, password) .signUp(email, password)
.then(() => signIn(email, password)) .then(() => signIn(email, password))
.catch((e) => toast.error(e.message)); .catch((e) => toast.error(e.response.data.message));
}; };
return ( return (
<Container size={420} my={40}> <Container size={420} my={40}>
<Title <Title
@ -52,12 +55,16 @@ const AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => {
> >
{mode == "signUp" ? "Sign up" : "Welcome back"} {mode == "signUp" ? "Sign up" : "Welcome back"}
</Title> </Title>
{!config.DISABLE_REGISTRATION && ( {publicRuntimeConfig.ALLOW_REGISTRATION == "true" && (
<Text color="dimmed" size="sm" align="center" mt={5}> <Text color="dimmed" size="sm" align="center" mt={5}>
{mode == "signUp" {mode == "signUp"
? "You have an account already?" ? "You have an account already?"
: "You don't have an account yet?"}{" "} : "You don't have an account yet?"}{" "}
<Anchor href={mode == "signUp" ? "signIn" : "signUp"} size="sm"> <Anchor
component={NextLink}
href={mode == "signUp" ? "signIn" : "signUp"}
size="sm"
>
{mode == "signUp" ? "Sign in" : "Sign up"} {mode == "signUp" ? "Sign in" : "Sign up"}
</Anchor> </Anchor>
</Text> </Text>

View File

@ -0,0 +1,35 @@
import { ActionIcon, Avatar, Menu } from "@mantine/core";
import { NextLink } from "@mantine/next";
import { DoorExit, Link } from "tabler-icons-react";
import authService from "../../services/auth.service";
const ActionAvatar = () => {
return (
<Menu>
<Menu.Target>
<ActionIcon>
<Avatar size={28} radius="xl" />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
component={NextLink}
href="/account/shares"
icon={<Link size={14} />}
>
My shares
</Menu.Item>
<Menu.Item
onClick={async () => {
authService.signOut();
}}
icon={<DoorExit size={14} />}
>
Sign out
</Menu.Item>
</Menu.Dropdown>
</Menu>
);
};
export default ActionAvatar;

View File

@ -0,0 +1,215 @@
import {
Box,
Burger,
Container,
createStyles,
Group,
Header,
Image,
Paper,
Stack,
Text,
Transition,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { NextLink } from "@mantine/next";
import getConfig from "next/config";
import { ReactNode, useEffect, useState } from "react";
import useUser from "../../hooks/user.hook";
import ActionAvatar from "./ActionAvatar";
const { publicRuntimeConfig } = getConfig();
const HEADER_HEIGHT = 60;
type Link = {
link?: string;
label?: string;
component?: ReactNode;
action?: () => Promise<void>;
};
const useStyles = createStyles((theme) => ({
root: {
position: "relative",
zIndex: 1,
},
dropdown: {
position: "absolute",
top: HEADER_HEIGHT,
left: 0,
right: 0,
zIndex: 0,
borderTopRightRadius: 0,
borderTopLeftRadius: 0,
borderTopWidth: 0,
overflow: "hidden",
[theme.fn.largerThan("sm")]: {
display: "none",
},
},
header: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
height: "100%",
},
links: {
[theme.fn.smallerThan("sm")]: {
display: "none",
},
},
burger: {
[theme.fn.largerThan("sm")]: {
display: "none",
},
},
link: {
display: "block",
lineHeight: 1,
padding: "8px 12px",
borderRadius: theme.radius.sm,
textDecoration: "none",
color:
theme.colorScheme === "dark"
? theme.colors.dark[0]
: theme.colors.gray[7],
fontSize: theme.fontSizes.sm,
fontWeight: 500,
"&:hover": {
backgroundColor:
theme.colorScheme === "dark"
? theme.colors.dark[6]
: theme.colors.gray[0],
},
[theme.fn.smallerThan("sm")]: {
borderRadius: 0,
padding: theme.spacing.md,
},
},
linkActive: {
"&, &:hover": {
backgroundColor:
theme.colorScheme === "dark"
? theme.fn.rgba(theme.colors[theme.primaryColor][9], 0.25)
: theme.colors[theme.primaryColor][0],
color:
theme.colors[theme.primaryColor][theme.colorScheme === "dark" ? 3 : 7],
},
},
}));
const NavBar = () => {
const user = useUser();
const [opened, toggleOpened] = useDisclosure(false);
const authenticatedLinks = [
{
link: "/upload",
label: "Upload",
},
{
component: <ActionAvatar />,
},
];
const [unauthenticatedLinks, setUnauthenticatedLinks] = useState<Link[]>([
{
link: "/auth/signIn",
label: "Sign in",
},
]);
useEffect(() => {
if (publicRuntimeConfig.SHOW_HOME_PAGE == "true")
setUnauthenticatedLinks((array) => [
{
link: "/",
label: "Home",
},
...array,
]);
if (publicRuntimeConfig.ALLOW_REGISTRATION == "true")
setUnauthenticatedLinks((array) => [
...array,
{
link: "/auth/signUp",
label: "Sign up",
},
]);
}, []);
const { classes, cx } = useStyles();
const items = (
<>
{(user ? authenticatedLinks : unauthenticatedLinks).map((link) => {
if (link.component) {
return (
<>
<Box pl={5} py={15}>
{link.component}
</Box>
</>
);
}
return (
<NextLink
key={link.label}
href={link.link ?? ""}
onClick={() => toggleOpened.toggle()}
className={cx(classes.link, {
[classes.linkActive]: window.location.pathname == link.link,
})}
>
{link.label}
</NextLink>
);
})}
</>
);
return (
<Header height={HEADER_HEIGHT} mb={40} className={classes.root}>
<Container className={classes.header}>
<NextLink href="/">
<Group>
<Image
src="/img/logo.svg"
alt="Pinvgin Share Logo"
height={35}
width={35}
/>
<Text weight={600}>Pingvin Share</Text>
</Group>
</NextLink>
<Group spacing={5} className={classes.links}>
<Group>{items} </Group>
</Group>
<Burger
opened={opened}
onClick={() => toggleOpened.toggle()}
className={classes.burger}
size="sm"
/>
<Transition transition="pop-top-right" duration={200} mounted={opened}>
{(styles) => (
<Paper className={classes.dropdown} withBorder style={styles}>
<Stack spacing={0}> {items}</Stack>
</Paper>
)}
</Transition>
</Container>
</Header>
);
};
export default NavBar;

View File

@ -0,0 +1,146 @@
import {
Accordion,
Button,
Col,
Grid,
NumberInput,
PasswordInput,
Select,
Stack,
Text,
TextInput,
} from "@mantine/core";
import { useForm, yupResolver } from "@mantine/form";
import { useModals } from "@mantine/modals";
import * as yup from "yup";
import shareService from "../../services/share.service";
import { ShareSecurity } from "../../types/share.type";
const CreateUploadModalBody = ({
uploadCallback,
}: {
uploadCallback: (
id: string,
expiration: string,
security: ShareSecurity
) => void;
}) => {
const modals = useModals();
const validationSchema = yup.object().shape({
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: "",
password: undefined,
maxViews: undefined,
expiration: "1-day",
},
validate: yupResolver(validationSchema),
});
return (
<form
onSubmit={form.onSubmit(async (values) => {
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();
}
})}
>
<Stack align="stretch">
<Grid align={form.errors.link ? "center" : "flex-end"}>
<Col xs={9}>
<TextInput
variant="filled"
label="Link"
placeholder="myAwesomeShare"
{...form.getInputProps("link")}
/>
</Col>
<Col xs={3}>
<Button
variant="outline"
onClick={() =>
form.setFieldValue(
"link",
Buffer.from(Math.random().toString(), "utf8")
.toString("base64")
.substr(10, 7)
)
}
>
Generate
</Button>
</Col>
</Grid>
<Text
size="xs"
sx={(theme) => ({
color: theme.colors.gray[6],
})}
>
{window.location.origin}/share/
{form.values.link == "" ? "myAwesomeShare" : form.values.link}
</Text>
<Select
label="Expiration"
{...form.getInputProps("expiration")}
data={[
{
value: "10-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>
<Accordion.Item
value="Security options"
sx={{ borderBottom: "none" }}
>
<Stack align="stretch">
<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("maxViews")}
/>
</Stack>
</Accordion.Item>
</Accordion>
<Button type="submit">Share</Button>
</Stack>
</form>
);
};
export default CreateUploadModalBody;

View File

@ -0,0 +1,55 @@
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
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,22 +1,18 @@
import { ActionIcon, Loader, Skeleton, Table } from "@mantine/core"; 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 { CircleCheck, Download } from "tabler-icons-react";
import { AppwriteFileWithPreview } from "../../types/File.type"; import shareService from "../../services/share.service";
import aw from "../../utils/appwrite.util";
import { bytesToSize } from "../../utils/math/byteToSize.util"; import { byteStringToHumanSizeString } from "../../utils/math/byteStringToHumanSizeString.util";
const FileList = ({ const FileList = ({
files, files,
shareId, shareId,
isLoading, isLoading,
}: { }: {
files: AppwriteFileWithPreview[]; files: any[];
shareId: string; shareId: string;
isLoading: boolean; isLoading: boolean;
}) => { }) => {
const router = useRouter();
const skeletonRows = [...Array(5)].map((c, i) => ( const skeletonRows = [...Array(5)].map((c, i) => (
<tr key={i}> <tr key={i}>
<td> <td>
@ -36,20 +32,8 @@ const FileList = ({
const rows = files.map((file) => ( const rows = files.map((file) => (
<tr key={file.name}> <tr key={file.name}>
<td>
<Image
width={30}
height={30}
alt={file.name}
objectFit="cover"
style={{ borderRadius: 3 }}
src={`data:image/png;base64,${new Buffer(file.preview).toString(
"base64"
)}`}
></Image>
</td>
<td>{file.name}</td> <td>{file.name}</td>
<td>{bytesToSize(file.sizeOriginal)}</td> <td>{byteStringToHumanSizeString(file.size)}</td>
<td> <td>
{file.uploadingState ? ( {file.uploadingState ? (
file.uploadingState != "finished" ? ( file.uploadingState != "finished" ? (
@ -60,9 +44,9 @@ const FileList = ({
) : ( ) : (
<ActionIcon <ActionIcon
size={25} size={25}
onClick={() => onClick={async () => {
router.push(aw.storage.getFileDownload(shareId, file.$id)) await shareService.downloadFile(shareId, file.id);
} }}
> >
<Download /> <Download />
</ActionIcon> </ActionIcon>
@ -75,7 +59,6 @@ const FileList = ({
<Table> <Table>
<thead> <thead>
<tr> <tr>
<th></th>
<th>Name</th> <th>Name</th>
<th>Size</th> <th>Size</th>
<th></th> <th></th>

View File

@ -1,4 +1,4 @@
import { Button, Group, PasswordInput, Text, Title } from "@mantine/core"; import { Button, Group, PasswordInput, Stack, Text, Title } from "@mantine/core";
import { ModalsContextProps } from "@mantine/modals/lib/context"; import { ModalsContextProps } from "@mantine/modals/lib/context";
import { useState } from "react"; import { useState } from "react";
@ -27,7 +27,7 @@ const Body = ({ submitCallback }: { submitCallback: any }) => {
const [passwordWrong, setPasswordWrong] = useState(false); const [passwordWrong, setPasswordWrong] = useState(false);
return ( return (
<> <>
<Group grow direction="column"> <Stack align="stretch">
<PasswordInput <PasswordInput
variant="filled" variant="filled"
placeholder="Password" placeholder="Password"
@ -42,7 +42,7 @@ const Body = ({ submitCallback }: { submitCallback: any }) => {
.then((res: any) => res) .then((res: any) => res)
.catch((e: any) => { .catch((e: any) => {
const error = e.response.data.message; const error = e.response.data.message;
if (error == "wrong_password") { if (error == "Wrong password") {
setPasswordWrong(true); setPasswordWrong(true);
} }
}) })
@ -50,7 +50,7 @@ const Body = ({ submitCallback }: { submitCallback: any }) => {
> >
Submit Submit
</Button> </Button>
</Group> </Stack>
</> </>
); );
}; };

View File

@ -1,4 +1,4 @@
import { Button, Group, Text, Title } from "@mantine/core"; import { Button, Group, Stack, Text, Title } from "@mantine/core";
import { useModals } from "@mantine/modals"; import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context"; import { ModalsContextProps } from "@mantine/modals/lib/context";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@ -23,7 +23,7 @@ const Body = ({ text }: { text: string }) => {
const router = useRouter(); const router = useRouter();
return ( return (
<> <>
<Group grow direction="column"> <Stack align="stretch">
<Text size="sm">{text}</Text> <Text size="sm">{text}</Text>
<Button <Button
onClick={() => { onClick={() => {
@ -33,7 +33,7 @@ const Body = ({ text }: { text: string }) => {
> >
Go back Go back
</Button> </Button>
</Group> </Stack>
</> </>
); );
}; };

View File

@ -3,16 +3,18 @@ import {
Center, Center,
createStyles, createStyles,
Group, Group,
MantineTheme,
Text, Text,
useMantineTheme, useMantineTheme,
} from "@mantine/core"; } from "@mantine/core";
import { Dropzone as MantineDropzone, DropzoneStatus } from "@mantine/dropzone"; import { Dropzone as MantineDropzone } from "@mantine/dropzone";
import React, { Dispatch, ForwardedRef, SetStateAction, useRef } from "react"; import getConfig from "next/config";
import { Dispatch, ForwardedRef, SetStateAction, useRef } from "react";
import { CloudUpload, Upload } from "tabler-icons-react"; import { CloudUpload, Upload } from "tabler-icons-react";
import { useConfig } from "../../utils/config.util"; import { byteStringToHumanSizeString } from "../../utils/math/byteStringToHumanSizeString.util";
import toast from "../../utils/toast.util"; import toast from "../../utils/toast.util";
const { publicRuntimeConfig } = getConfig();
const useStyles = createStyles((theme) => ({ const useStyles = createStyles((theme) => ({
wrapper: { wrapper: {
position: "relative", position: "relative",
@ -37,14 +39,6 @@ const useStyles = createStyles((theme) => ({
}, },
})); }));
function getActiveColor(status: DropzoneStatus, theme: MantineTheme) {
return status.accepted
? theme.colors[theme.primaryColor][6]
: theme.colorScheme === "dark"
? theme.colors.dark[2]
: theme.black;
}
const Dropzone = ({ const Dropzone = ({
isUploading, isUploading,
setFiles, setFiles,
@ -53,13 +47,12 @@ const Dropzone = ({
setFiles: Dispatch<SetStateAction<File[]>>; setFiles: Dispatch<SetStateAction<File[]>>;
}) => { }) => {
const theme = useMantineTheme(); const theme = useMantineTheme();
const config = useConfig();
const { classes } = useStyles(); const { classes } = useStyles();
const openRef = useRef<() => void>(); const openRef = useRef<() => void>();
return ( return (
<div className={classes.wrapper}> <div className={classes.wrapper}>
<MantineDropzone <MantineDropzone
maxSize={config.MAX_FILE_SIZE} maxSize={parseInt(publicRuntimeConfig.MAX_FILE_SIZE!)}
onReject={(e) => { onReject={(e) => {
toast.error(e[0].errors[0].message); toast.error(e[0].errors[0].message);
}} }}
@ -75,26 +68,20 @@ const Dropzone = ({
className={classes.dropzone} className={classes.dropzone}
radius="md" radius="md"
> >
{(status) => (
<div style={{ pointerEvents: "none" }}> <div style={{ pointerEvents: "none" }}>
<Group position="center"> <Group position="center">
<CloudUpload size={50} color={getActiveColor(status, theme)} /> <CloudUpload size={50} />
</Group> </Group>
<Text <Text align="center" weight={700} size="lg" mt="xl">
align="center" Upload files
weight={700}
size="lg"
mt="xl"
sx={{ color: getActiveColor(status, theme) }}
>
{status.accepted ? "Drop files here" : "Upload files"}
</Text> </Text>
<Text align="center" size="sm" mt="xs" color="dimmed"> <Text align="center" size="sm" mt="xs" color="dimmed">
Drag and drop your files or use the upload button to start your Drag&apos;n&apos;drop files here to start your share. We can accept
share. only files that are less than{" "}
{byteStringToHumanSizeString(publicRuntimeConfig.MAX_FILE_SIZE)} in
size.
</Text> </Text>
</div> </div>
)}
</MantineDropzone> </MantineDropzone>
<Center> <Center>
<Button <Button

View File

@ -2,7 +2,7 @@ import { ActionIcon, Loader, Table } from "@mantine/core";
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction } from "react";
import { CircleCheck, Trash } from "tabler-icons-react"; import { CircleCheck, Trash } from "tabler-icons-react";
import { FileUpload } from "../../types/File.type"; import { FileUpload } from "../../types/File.type";
import { bytesToSize } from "../../utils/math/byteToSize.util"; import { byteStringToHumanSizeString } from "../../utils/math/byteStringToHumanSizeString.util";
const FileList = ({ const FileList = ({
files, files,
@ -19,8 +19,7 @@ const FileList = ({
const rows = files.map((file, i) => ( const rows = files.map((file, i) => (
<tr key={i}> <tr key={i}>
<td>{file.name}</td> <td>{file.name}</td>
<td>{file.type}</td> <td>{byteStringToHumanSizeString(file.size.toString())}</td>
<td>{bytesToSize(file.size)}</td>
<td> <td>
{file.uploadingState ? ( {file.uploadingState ? (
file.uploadingState != "finished" ? ( file.uploadingState != "finished" ? (
@ -47,7 +46,6 @@ const FileList = ({
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Type</th>
<th>Size</th> <th>Size</th>
<th></th> <th></th>
</tr> </tr>

View File

@ -2,45 +2,44 @@ import {
ActionIcon, ActionIcon,
Button, Button,
Group, Group,
Stack,
Text, Text,
TextInput, TextInput,
Title, Title
} from "@mantine/core"; } from "@mantine/core";
import { useClipboard } from "@mantine/hooks"; import { useClipboard } from "@mantine/hooks";
import { useModals } from "@mantine/modals"; import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context"; import { ModalsContextProps } from "@mantine/modals/lib/context";
import moment from "moment";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Copy } from "tabler-icons-react"; import { Copy } from "tabler-icons-react";
import { Share } from "../../types/share.type";
import toast from "../../utils/toast.util"; import toast from "../../utils/toast.util";
const showCompletedUploadModal = ( const showCompletedUploadModal = (
modals: ModalsContextProps, modals: ModalsContextProps,
link: string, share: Share,
expiresAt: string,
mode: "email" | "standard"
) => { ) => {
return modals.openModal({ return modals.openModal({
closeOnClickOutside: false, closeOnClickOutside: false,
withCloseButton: false, withCloseButton: false,
closeOnEscape: false, closeOnEscape: false,
title: ( title: (
<Group grow direction="column" spacing={0}> <Stack align="stretch" spacing={0}>
<Title order={4}>Share ready</Title> <Title order={4}>Share ready</Title>
{mode == "email" && ( </Stack>
<Text size="sm"> Emails were sent to the to invited users.</Text>
)}
</Group>
), ),
children: <Body link={link} expiresAt={expiresAt} />, children: <Body share={share} />,
}); });
}; };
const Body = ({ link, expiresAt }: { link: string; expiresAt: string }) => { const Body = ({ share }: { share: Share }) => {
const clipboard = useClipboard({ timeout: 500 }); const clipboard = useClipboard({ timeout: 500 });
const modals = useModals(); const modals = useModals();
const router = useRouter(); const router = useRouter();
const link = `${window.location.origin}/share/${share.id}`;
return ( return (
<Group grow direction="column"> <Stack align="stretch">
<TextInput <TextInput
variant="filled" variant="filled"
value={link} value={link}
@ -61,7 +60,7 @@ const Body = ({ link, expiresAt }: { link: string; expiresAt: string }) => {
color: theme.colors.gray[6], color: theme.colors.gray[6],
})} })}
> >
Your share expires at {expiresAt}{" "} Your share expires at {moment(share.expiration).format("LLL")}
</Text> </Text>
<Button <Button
@ -72,7 +71,7 @@ const Body = ({ link, expiresAt }: { link: string; expiresAt: string }) => {
> >
Done Done
</Button> </Button>
</Group> </Stack>
); );
}; };

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