Merge branch 'feat/refactor-project-nestjs' into main
@ -1,8 +0,0 @@
|
|||||||
Dockerfile
|
|
||||||
.dockerignore
|
|
||||||
node_modules
|
|
||||||
npm-debug.log
|
|
||||||
README.md
|
|
||||||
dist
|
|
||||||
.next
|
|
||||||
.git
|
|
22
.env.example
@ -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
|
||||||
|
@ -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 .
|
25
.github/workflows/docker-image-frontend.yml
vendored
Normal 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
@ -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/
|
@ -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: [],
|
|
||||||
},
|
|
||||||
];
|
|
@ -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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
@ -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
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"
|
|
||||||
}
|
|
@ -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;
|
|
@ -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,
|
|
||||||
};
|
|
@ -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;
|
|
@ -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,
|
|
||||||
};
|
|
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"module": "commonjs",
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"target": "es6",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"sourceMap": true,
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
|
||||||
"lib": ["es2015"]
|
|
||||||
}
|
|
@ -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;
|
|
34
Dockerfile
@ -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"]
|
|
83
README.md
@ -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!
|
||||||
|
@ -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
@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.git/
|
12
backend/.env.example
Normal 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
@ -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
@ -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
|
15
backend/docker-compose.yml
Normal 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
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src"
|
||||||
|
}
|
11069
backend/package-lock.json
generated
Normal file
60
backend/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
3
backend/prisma/migrations/migration_lock.toml
Normal 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"
|
69
backend/prisma/schema.prisma
Normal 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
@ -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 {}
|
43
backend/src/auth/auth.controller.ts
Normal 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 };
|
||||||
|
}
|
||||||
|
}
|
13
backend/src/auth/auth.module.ts
Normal 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 {}
|
95
backend/src/auth/auth.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
9
backend/src/auth/decorator/getUser.decorator.ts
Normal 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;
|
||||||
|
}
|
||||||
|
);
|
4
backend/src/auth/dto/authRegister.dto.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { PickType } from "@nestjs/swagger";
|
||||||
|
import { UserDTO } from "src/user/dto/user.dto";
|
||||||
|
|
||||||
|
export class AuthRegisterDTO extends UserDTO {}
|
7
backend/src/auth/dto/authSignIn.dto.ts
Normal 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) {}
|
6
backend/src/auth/dto/refreshAccessToken.dto.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { IsNotEmpty, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class RefreshAccessTokenDTO {
|
||||||
|
@IsNotEmpty()
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
7
backend/src/auth/guard/jwt.guard.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
|
||||||
|
export class JwtGuard extends AuthGuard("jwt") {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
37
backend/src/auth/jobs/jobs.service.ts
Normal 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`);
|
||||||
|
}
|
||||||
|
}
|
24
backend/src/auth/strategy/jwt.strategy.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
22
backend/src/file/dto/file.dto.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
108
backend/src/file/file.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
14
backend/src/file/file.module.ts
Normal 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 {}
|
112
backend/src/file/file.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
backend/src/file/guard/fileDownload.guard.ts
Normal 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
@ -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();
|
6
backend/src/prisma/prisma.module.ts
Normal 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 {}
|
18
backend/src/prisma/prisma.service.ts
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
18
backend/src/share/dto/createShare.dto.ts
Normal 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;
|
||||||
|
}
|
20
backend/src/share/dto/myShare.dto.ts
Normal 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 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
29
backend/src/share/dto/share.dto.ts
Normal 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 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
15
backend/src/share/dto/shareMetaData.dto.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
6
backend/src/share/dto/sharePassword.dto.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { IsNotEmpty } from "class-validator";
|
||||||
|
|
||||||
|
export class SharePasswordDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
password: string;
|
||||||
|
}
|
12
backend/src/share/dto/shareSecurity.dto.ts
Normal 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;
|
||||||
|
}
|
57
backend/src/share/guard/shareSecurity.guard.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
77
backend/src/share/share.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
14
backend/src/share/share.module.ts
Normal 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 {}
|
200
backend/src/share/share.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
backend/src/user/dto/user.dto.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
14
backend/src/user/user.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
4
backend/tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
21
backend/tsconfig.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,43 @@
|
|||||||
version: '3.3'
|
version: '3.8'
|
||||||
services:
|
services:
|
||||||
pingvin-share:
|
db:
|
||||||
restart: "unless-stopped"
|
image: postgres:14.1-alpine
|
||||||
ports:
|
restart: unless-stopped
|
||||||
- '3000:3000'
|
environment:
|
||||||
image: stonith404/pingvin-share
|
- POSTGRES_USER=${DB_USER}
|
||||||
environment:
|
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||||
- APPWRITE_FUNCTION_API_KEY=${APPWRITE_FUNCTION_API_KEY}
|
- POSTGRES_DB=pingvin-share
|
||||||
- PUBLIC_APPWRITE_HOST=${PUBLIC_APPWRITE_HOST}
|
volumes:
|
||||||
- PUBLIC_MAX_FILE_SIZE=${PUBLIC_MAX_FILE_SIZE}
|
- pingvin-share-db:/var/lib/postgresql/data
|
||||||
- PUBLIC_DISABLE_REGISTRATION=${PUBLIC_DISABLE_REGISTRATION}
|
backend:
|
||||||
- PUBLIC_DISABLE_HOME_PAGE=${PUBLIC_DISABLE_HOME_PAGE}
|
image: stonith404/pingvin-share-backend
|
||||||
- PUBLIC_MAIL_SHARE_ENABLED=${PUBLIC_MAIL_SHARE_ENABLED}
|
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:
|
||||||
|
- '3000:3000'
|
||||||
|
image: stonith404/pingvin-share-frontend
|
||||||
|
environment:
|
||||||
|
- SHOW_HOME_PAGE=${SHOW_HOME_PAGE}
|
||||||
|
- ALLOW_REGISTRATION=${ALLOW_REGISTRATION}
|
||||||
|
- MAX_FILE_SIZE=${MAX_FILE_SIZE}
|
||||||
|
- BACKEND_URL=${BACKEND_URL}
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pingvin-share-db:
|
4
frontend/.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.next/
|
||||||
|
.git/
|
4
frontend/.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
BACKEND_URL=http://localhost:8080
|
||||||
|
SHOW_HOME_PAGE=true
|
||||||
|
ALLOW_REGISTRATION=true
|
||||||
|
MAX_FILE_SIZE=1000000000
|
@ -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
@ -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
|
0
next-env.d.ts → frontend/next-env.d.ts
vendored
@ -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
|
2204
package-lock.json → frontend/package-lock.json
generated
45
frontend/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 944 B After Width: | Height: | Size: 944 B |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 1018 B After Width: | Height: | Size: 1018 B |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@ -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>
|
35
frontend/src/components/navBar/ActionAvatar.tsx
Normal 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;
|
215
frontend/src/components/navBar/NavBar.tsx
Normal 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;
|
146
frontend/src/components/share/CreateUploadModalBody.tsx
Normal 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;
|
55
frontend/src/components/share/DownloadAllButton.tsx
Normal 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;
|
@ -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>
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -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} />
|
||||||
<CloudUpload size={50} color={getActiveColor(status, theme)} />
|
</Group>
|
||||||
</Group>
|
<Text align="center" weight={700} size="lg" mt="xl">
|
||||||
<Text
|
Upload files
|
||||||
align="center"
|
</Text>
|
||||||
weight={700}
|
<Text align="center" size="sm" mt="xs" color="dimmed">
|
||||||
size="lg"
|
Drag'n'drop files here to start your share. We can accept
|
||||||
mt="xl"
|
only files that are less than{" "}
|
||||||
sx={{ color: getActiveColor(status, theme) }}
|
{byteStringToHumanSizeString(publicRuntimeConfig.MAX_FILE_SIZE)} in
|
||||||
>
|
size.
|
||||||
{status.accepted ? "Drop files here" : "Upload files"}
|
</Text>
|
||||||
</Text>
|
</div>
|
||||||
<Text align="center" size="sm" mt="xs" color="dimmed">
|
|
||||||
Drag and drop your files or use the upload button to start your
|
|
||||||
share.
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</MantineDropzone>
|
</MantineDropzone>
|
||||||
<Center>
|
<Center>
|
||||||
<Button
|
<Button
|
@ -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>
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|