mirror of
https://github.com/stonith404/pingvin-share.git
synced 2024-07-02 07:20:38 +02:00
Merge pull request #27 from stonith404/feat/administrator-page
Feat/administrator page
This commit is contained in:
commit
50887b000d
18
.env.example
18
.env.example
|
@ -1,18 +0,0 @@
|
||||||
# Read what every environment variable does: https://github.com/stonith404/pingvin-share#environment-variables
|
|
||||||
|
|
||||||
# General
|
|
||||||
APP_URL=http://localhost:3000
|
|
||||||
SHOW_HOME_PAGE=true
|
|
||||||
ALLOW_REGISTRATION=true
|
|
||||||
ALLOW_UNAUTHENTICATED_SHARES=false
|
|
||||||
MAX_FILE_SIZE=1000000000
|
|
||||||
|
|
||||||
# Security
|
|
||||||
JWT_SECRET=long-random-string
|
|
||||||
|
|
||||||
# Email
|
|
||||||
EMAIL_RECIPIENTS_ENABLED=false
|
|
||||||
SMTP_HOST=smtp.example.com
|
|
||||||
SMTP_PORT=587
|
|
||||||
SMTP_EMAIL=pingvin-share@example.com
|
|
||||||
SMTP_PASSWORD=example
|
|
|
@ -3,10 +3,11 @@
|
||||||
We would ❤️ for you to contribute to Pingvin Share and help make it better! All contributions are welcome, including issues, suggestions, pull requests and more.
|
We would ❤️ for you to contribute to Pingvin Share and help make it better! All contributions are welcome, including issues, suggestions, pull requests and more.
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
You've found a bug, have suggestion or something else, just create an issue on GitHub and we can get in touch 😊.
|
You've found a bug, have suggestion or something else, just create an issue on GitHub and we can get in touch 😊.
|
||||||
|
|
||||||
|
## Submit a Pull Request
|
||||||
|
|
||||||
## Submit a Pull Request
|
|
||||||
Once you created a issue and you want to create a pull request, follow this guide.
|
Once you created a issue and you want to create a pull request, follow this guide.
|
||||||
|
|
||||||
Branch naming convention is as following
|
Branch naming convention is as following
|
||||||
|
@ -74,20 +75,21 @@ The backend is built with [Nest.js](https://nestjs.com) and uses Typescript.
|
||||||
#### Setup
|
#### Setup
|
||||||
|
|
||||||
1. Open the `backend` folder
|
1. Open the `backend` folder
|
||||||
2. Duplicate the `.env.example` file, rename the duplicate to `.env` and change the environment variables if needed
|
2. Install the dependencies with `npm install`
|
||||||
3. Install the dependencies with `npm install`
|
3. Push the database schema to the database by running `npx prisma db push`
|
||||||
4. Push the database schema to the database by running `npx prisma db push`
|
4. Seed the database with `npx prisma db seed`
|
||||||
5. Start the backend with `npm run dev`
|
5. Start the backend with `npm run dev`
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
|
|
||||||
The frontend is built with [Next.js](https://nextjs.org) and uses Typescript.
|
The frontend is built with [Next.js](https://nextjs.org) and uses Typescript.
|
||||||
|
|
||||||
#### Setup
|
#### Setup
|
||||||
|
|
||||||
1. Start the backend first
|
1. Start the backend first
|
||||||
2. Open the `frontend` folder
|
2. Open the `frontend` folder
|
||||||
3. 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. Install the dependencies with `npm install`
|
4. Start the frontend with `npm run dev`
|
||||||
5. Start the frontend with `npm run dev`
|
|
||||||
|
|
||||||
You're all set!
|
You're all set!
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,5 @@ COPY --from=backend-builder /opt/app/prisma ./prisma
|
||||||
COPY --from=backend-builder /opt/app/package.json ./
|
COPY --from=backend-builder /opt/app/package.json ./
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
|
|
||||||
RUN npm i -g dotenv-cli
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD cd frontend && dotenv node_modules/.bin/next start & cd backend && npm run prod
|
CMD cd frontend && node_modules/.bin/next start & cd backend && npm run prod
|
24
README.md
24
README.md
|
@ -10,6 +10,7 @@ Demo: https://pingvin-share.dev.eliasschneider.com
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
|
- Spin up your instance within 2 minutes
|
||||||
- Create a share with files that you can access with a link
|
- Create a share with files that you can access with a link
|
||||||
- No file size limit, only your disk will be your limit
|
- No file size limit, only your disk will be your limit
|
||||||
- Set a share expiration
|
- Set a share expiration
|
||||||
|
@ -21,31 +22,14 @@ Demo: https://pingvin-share.dev.eliasschneider.com
|
||||||
|
|
||||||
> Pleas note that Pingvin Share is in early stage and could include some bugs
|
> Pleas note that Pingvin Share is in early stage and could include some bugs
|
||||||
|
|
||||||
1. Download the `docker-compose.yml` and `.env.example` file.
|
1. Download the `docker-compose.yml` 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)
|
2. Run `docker-compose up -d`
|
||||||
3. Run `docker-compose up -d`
|
|
||||||
|
|
||||||
The website is now listening available on `http://localhost:3000`, have fun with Pingvin Share 🐧!
|
The website is now listening available on `http://localhost:3000`, have fun with Pingvin Share 🐧!
|
||||||
|
|
||||||
### Environment variables
|
|
||||||
|
|
||||||
| Variable | Description | Possible values |
|
|
||||||
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | --------------- |
|
|
||||||
| `APP_URL` | On which URL Pingvin Share is available. E.g http://localhost or https://pingvin-share.com. | URL |
|
|
||||||
| `SHOW_HOME_PAGE` | Whether the Pingvin Share home page should be shown. | true/false |
|
|
||||||
| `ALLOW_REGISTRATION` | Whether a new user can create a new account. | true/false |
|
|
||||||
| `ALLOW_UNAUTHENTICATED_SHARES` | Whether a user can create a share without being signed in. | true/false |
|
|
||||||
| `MAX_FILE_SIZE` | Maximum allowed size per file in bytes. | Number |
|
|
||||||
| `JWT_SECRET` | Long random string to sign the JWT's. | Random string |
|
|
||||||
| `EMAIL_RECIPIENTS_ENABLED` | Whether email reciepients are enabled. Only set this to true if you entered the host, port, email and password of your SMTP server. | true/false |
|
|
||||||
| `SMTP_HOST`, `SMTP_PORT`, `SMTP_EMAIL`, `SMTP_PASSWORD` | Credentials for your SMTP server. | - |
|
|
||||||
|
|
||||||
### Upgrade to a new version
|
### Upgrade to a new version
|
||||||
|
|
||||||
1. Check if your local `docker-compose.yml` and `.env` files are up to date with the files in the repository
|
Run `docker compose pull && docker compose up -d` to update your docker container
|
||||||
2. Run `docker compose pull && docker compose up -d` to update your docker container
|
|
||||||
|
|
||||||
> Note: If you installed Pingvin Share before it used Sqlite, you unfortunately have to set up the project from scratch again, sorry for that.
|
|
||||||
|
|
||||||
## 🖤 Contribute
|
## 🖤 Contribute
|
||||||
|
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
# General
|
|
||||||
APP_URL=http://localhost:3000
|
|
||||||
ALLOW_REGISTRATION=true
|
|
||||||
MAX_FILE_SIZE=5000000000
|
|
||||||
ALLOW_UNAUTHENTICATED_SHARES=false
|
|
||||||
|
|
||||||
# Security
|
|
||||||
JWT_SECRET=random-string
|
|
||||||
|
|
||||||
# Email
|
|
||||||
EMAIL_RECIPIENTS_ENABLED=false
|
|
||||||
SMTP_HOST=smtp.example.com
|
|
||||||
SMTP_PORT=587
|
|
||||||
SMTP_EMAIL=pingvin-share@example.com
|
|
||||||
SMTP_PASSWORD=example
|
|
753
backend/package-lock.json
generated
753
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -3,24 +3,27 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"dev": "dotenv -- nest start --watch",
|
"dev": "nest start --watch",
|
||||||
"prod": "npx prisma migrate deploy && dotenv node dist/main",
|
"prod": "prisma migrate deploy && prisma db seed && node dist/src/main",
|
||||||
"lint": "eslint 'src/**/*.ts'",
|
"lint": "eslint 'src/**/*.ts'",
|
||||||
"format": "prettier --write 'src/**/*.ts'",
|
"format": "prettier --write 'src/**/*.ts'",
|
||||||
"test:system": "npx prisma migrate reset -f && nest start & sleep 10 && newman run ./test/system/newman-system-tests.json"
|
"test:system": "prisma migrate reset -f && nest start & sleep 10 && newman run ./test/system/newman-system-tests.json"
|
||||||
|
},
|
||||||
|
"prisma": {
|
||||||
|
"seed": "ts-node prisma/seed/config.seed.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^9.1.2",
|
"@nestjs/common": "^9.2.1",
|
||||||
"@nestjs/config": "^2.2.0",
|
"@nestjs/config": "^2.2.0",
|
||||||
"@nestjs/core": "^9.1.2",
|
"@nestjs/core": "^9.2.1",
|
||||||
"@nestjs/jwt": "^9.0.0",
|
"@nestjs/jwt": "^9.0.0",
|
||||||
|
"@nestjs/mapped-types": "^1.2.0",
|
||||||
"@nestjs/passport": "^9.0.0",
|
"@nestjs/passport": "^9.0.0",
|
||||||
"@nestjs/platform-express": "^9.1.2",
|
"@nestjs/platform-express": "^9.2.1",
|
||||||
"@nestjs/schedule": "^2.1.0",
|
"@nestjs/schedule": "^2.1.0",
|
||||||
"@nestjs/swagger": "^6.1.2",
|
|
||||||
"@nestjs/throttler": "^3.1.0",
|
"@nestjs/throttler": "^3.1.0",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
"argon2": "^0.29.1",
|
"argon2": "^0.30.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.13.2",
|
"class-validator": "^0.13.2",
|
||||||
"content-disposition": "^0.5.4",
|
"content-disposition": "^0.5.4",
|
||||||
|
@ -33,36 +36,35 @@
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs": "^7.5.7"
|
"rxjs": "^7.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^9.1.4",
|
"@nestjs/cli": "^9.1.5",
|
||||||
"@nestjs/schematics": "^9.0.3",
|
"@nestjs/schematics": "^9.0.3",
|
||||||
"@nestjs/testing": "^9.1.2",
|
"@nestjs/testing": "^9.2.1",
|
||||||
"@prisma/client": "^4.4.0",
|
"@prisma/client": "^4.7.1",
|
||||||
"@types/archiver": "^5.3.1",
|
"@types/archiver": "^5.3.1",
|
||||||
"@types/cron": "^2.0.0",
|
"@types/cron": "^2.0.0",
|
||||||
"@types/express": "^4.17.14",
|
"@types/express": "^4.17.14",
|
||||||
"@types/mime-types": "^2.1.1",
|
"@types/mime-types": "^2.1.1",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^18.7.23",
|
"@types/node": "^18.11.10",
|
||||||
"@types/nodemailer": "^6.4.6",
|
"@types/nodemailer": "^6.4.6",
|
||||||
"@types/passport-jwt": "^3.0.7",
|
"@types/passport-jwt": "^3.0.7",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
"@typescript-eslint/parser": "^5.40.0",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv-cli": "^6.0.0",
|
"eslint": "^8.29.0",
|
||||||
"eslint": "^8.25.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
|
||||||
"newman": "^5.3.2",
|
"newman": "^5.3.2",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.8.0",
|
||||||
"prisma": "^4.4.0",
|
"prisma": "^4.7.1",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"ts-loader": "^9.4.1",
|
"ts-loader": "^9.4.2",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsconfig-paths": "4.1.0",
|
"tsconfig-paths": "4.1.1",
|
||||||
"typescript": "^4.8.4"
|
"typescript": "^4.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `firstName` on the `User` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `lastName` on the `User` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `username` to the `User` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Config" (
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"key" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"value" TEXT NOT NULL,
|
||||||
|
"description" TEXT NOT NULL,
|
||||||
|
"secret" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"locked" BOOLEAN NOT NULL DEFAULT false
|
||||||
|
);
|
||||||
|
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_User" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"username" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"password" TEXT NOT NULL,
|
||||||
|
"isAdmin" BOOLEAN NOT NULL DEFAULT false
|
||||||
|
);
|
||||||
|
INSERT INTO "new_User" ("createdAt", "email", "id", "password", "updatedAt", "username") SELECT "createdAt", "email", "id", "password", "updatedAt", 'user-' || User.id as "username" FROM "User";
|
||||||
|
DROP TABLE "User";
|
||||||
|
ALTER TABLE "new_User" RENAME TO "User";
|
||||||
|
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
|
@ -12,10 +12,10 @@ model User {
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
email String @unique
|
username String @unique
|
||||||
password String
|
email String @unique
|
||||||
firstName String?
|
password String
|
||||||
lastName String?
|
isAdmin Boolean @default(false)
|
||||||
|
|
||||||
shares Share[]
|
shares Share[]
|
||||||
refreshTokens RefreshToken[]
|
refreshTokens RefreshToken[]
|
||||||
|
@ -76,3 +76,14 @@ model ShareSecurity {
|
||||||
shareId String? @unique
|
shareId String? @unique
|
||||||
share Share? @relation(fields: [shareId], references: [id], onDelete: Cascade)
|
share Share? @relation(fields: [shareId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Config {
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
key String @id
|
||||||
|
type String
|
||||||
|
value String
|
||||||
|
description String
|
||||||
|
secret Boolean @default(true)
|
||||||
|
locked Boolean @default(false)
|
||||||
|
}
|
||||||
|
|
124
backend/prisma/seed/config.seed.ts
Normal file
124
backend/prisma/seed/config.seed.ts
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import * as crypto from "crypto";
|
||||||
|
|
||||||
|
const configVariables = [
|
||||||
|
{
|
||||||
|
key: "SETUP_FINISHED",
|
||||||
|
description: "Whether the setup has been finished",
|
||||||
|
type: "boolean",
|
||||||
|
value: "false",
|
||||||
|
secret: false,
|
||||||
|
locked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "APP_URL",
|
||||||
|
description: "On which URL Pingvin Share is available",
|
||||||
|
type: "string",
|
||||||
|
value: "http://localhost:3000",
|
||||||
|
secret: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "SHOW_HOME_PAGE",
|
||||||
|
description: "Whether to show the home page",
|
||||||
|
type: "boolean",
|
||||||
|
value: "true",
|
||||||
|
secret: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ALLOW_REGISTRATION",
|
||||||
|
description: "Whether registration is allowed",
|
||||||
|
type: "boolean",
|
||||||
|
value: "true",
|
||||||
|
secret: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ALLOW_UNAUTHENTICATED_SHARES",
|
||||||
|
description: "Whether unauthorized users can create shares",
|
||||||
|
type: "boolean",
|
||||||
|
value: "false",
|
||||||
|
secret: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "MAX_FILE_SIZE",
|
||||||
|
description: "Maximum file size in bytes",
|
||||||
|
type: "number",
|
||||||
|
value: "1000000000",
|
||||||
|
secret: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "JWT_SECRET",
|
||||||
|
description: "Long random string used to sign JWT tokens",
|
||||||
|
type: "string",
|
||||||
|
value: crypto.randomBytes(256).toString("base64"),
|
||||||
|
locked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ENABLE_EMAIL_RECIPIENTS",
|
||||||
|
description:
|
||||||
|
"Whether to send emails to recipients. Only set this to true if you entered the host, port, email and password of your SMTP server.",
|
||||||
|
type: "boolean",
|
||||||
|
value: "false",
|
||||||
|
secret: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "SMTP_HOST",
|
||||||
|
description: "Host of the SMTP server",
|
||||||
|
type: "string",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "SMTP_PORT",
|
||||||
|
description: "Port of the SMTP server",
|
||||||
|
type: "number",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "SMTP_EMAIL",
|
||||||
|
description: "Email address of the SMTP server",
|
||||||
|
type: "string",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "SMTP_PASSWORD",
|
||||||
|
description: "Password of the SMTP server",
|
||||||
|
type: "string",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
for (const variable of configVariables) {
|
||||||
|
const existingConfigVariable = await prisma.config.findUnique({
|
||||||
|
where: { key: variable.key },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a new config variable if it doesn't exist
|
||||||
|
if (!existingConfigVariable) {
|
||||||
|
await prisma.config.create({
|
||||||
|
data: variable,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the config variable if it doesn't exist anymore
|
||||||
|
const configVariablesFromDatabase = await prisma.config.findMany();
|
||||||
|
|
||||||
|
for (const configVariableFromDatabase of configVariablesFromDatabase) {
|
||||||
|
if (!configVariables.find((v) => v.key == configVariableFromDatabase.key)) {
|
||||||
|
await prisma.config.delete({
|
||||||
|
where: { key: configVariableFromDatabase.key },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
main()
|
||||||
|
.then(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
})
|
||||||
|
.catch(async (e) => {
|
||||||
|
console.error(e);
|
||||||
|
await prisma.$disconnect();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
|
@ -1,19 +1,18 @@
|
||||||
import { Module } from "@nestjs/common";
|
import { HttpException, HttpStatus, Module } from "@nestjs/common";
|
||||||
import { ConfigModule } from "@nestjs/config";
|
|
||||||
import { ScheduleModule } from "@nestjs/schedule";
|
import { ScheduleModule } from "@nestjs/schedule";
|
||||||
import { AuthModule } from "./auth/auth.module";
|
import { AuthModule } from "./auth/auth.module";
|
||||||
import { JobsService } from "./jobs/jobs.service";
|
|
||||||
|
|
||||||
import { APP_GUARD } from "@nestjs/core";
|
import { MulterModule } from "@nestjs/platform-express";
|
||||||
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
|
import { ThrottlerModule } from "@nestjs/throttler";
|
||||||
import { FileController } from "./file/file.controller";
|
import { Request } from "express";
|
||||||
|
import { ConfigModule } from "./config/config.module";
|
||||||
|
import { ConfigService } from "./config/config.service";
|
||||||
|
import { EmailModule } from "./email/email.module";
|
||||||
import { FileModule } from "./file/file.module";
|
import { FileModule } from "./file/file.module";
|
||||||
import { PrismaModule } from "./prisma/prisma.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 { ShareModule } from "./share/share.module";
|
||||||
import { UserController } from "./user/user.controller";
|
import { UserModule } from "./user/user.module";
|
||||||
import { EmailModule } from "./email/email.module";
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -22,21 +21,31 @@ import { EmailModule } from "./email/email.module";
|
||||||
FileModule,
|
FileModule,
|
||||||
EmailModule,
|
EmailModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule,
|
||||||
|
UserModule,
|
||||||
|
MulterModule.registerAsync({
|
||||||
|
useFactory: (config: ConfigService) => ({
|
||||||
|
fileFilter: (req: Request, file, cb) => {
|
||||||
|
const MAX_FILE_SIZE = config.get("MAX_FILE_SIZE");
|
||||||
|
const requestFileSize = parseInt(req.headers["content-length"]);
|
||||||
|
const isValidFileSize = requestFileSize <= MAX_FILE_SIZE;
|
||||||
|
cb(
|
||||||
|
!isValidFileSize &&
|
||||||
|
new HttpException(
|
||||||
|
`File must be smaller than ${MAX_FILE_SIZE} bytes`,
|
||||||
|
HttpStatus.PAYLOAD_TOO_LARGE
|
||||||
|
),
|
||||||
|
isValidFileSize
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
ThrottlerModule.forRoot({
|
ThrottlerModule.forRoot({
|
||||||
ttl: 60,
|
ttl: 60,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
}),
|
}),
|
||||||
ScheduleModule.forRoot(),
|
ScheduleModule.forRoot(),
|
||||||
],
|
],
|
||||||
providers: [
|
|
||||||
PrismaService,
|
|
||||||
JobsService,
|
|
||||||
{
|
|
||||||
provide: APP_GUARD,
|
|
||||||
useClass: ThrottlerGuard,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
controllers: [UserController, ShareController, FileController],
|
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|
|
@ -3,14 +3,20 @@ import {
|
||||||
Controller,
|
Controller,
|
||||||
ForbiddenException,
|
ForbiddenException,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
|
Patch,
|
||||||
Post,
|
Post,
|
||||||
|
UseGuards,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { Throttle } from "@nestjs/throttler";
|
import { Throttle } from "@nestjs/throttler";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
import { ConfigService } from "src/config/config.service";
|
||||||
import { AuthService } from "./auth.service";
|
import { AuthService } from "./auth.service";
|
||||||
|
import { GetUser } from "./decorator/getUser.decorator";
|
||||||
import { AuthRegisterDTO } from "./dto/authRegister.dto";
|
import { AuthRegisterDTO } from "./dto/authRegister.dto";
|
||||||
import { AuthSignInDTO } from "./dto/authSignIn.dto";
|
import { AuthSignInDTO } from "./dto/authSignIn.dto";
|
||||||
import { RefreshAccessTokenDTO } from "./dto/refreshAccessToken.dto";
|
import { RefreshAccessTokenDTO } from "./dto/refreshAccessToken.dto";
|
||||||
|
import { UpdatePasswordDTO } from "./dto/updatePassword.dto";
|
||||||
|
import { JwtGuard } from "./guard/jwt.guard";
|
||||||
|
|
||||||
@Controller("auth")
|
@Controller("auth")
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
|
@ -21,8 +27,8 @@ export class AuthController {
|
||||||
|
|
||||||
@Throttle(10, 5 * 60)
|
@Throttle(10, 5 * 60)
|
||||||
@Post("signUp")
|
@Post("signUp")
|
||||||
signUp(@Body() dto: AuthRegisterDTO) {
|
async signUp(@Body() dto: AuthRegisterDTO) {
|
||||||
if (this.config.get("ALLOW_REGISTRATION") == "false")
|
if (!this.config.get("ALLOW_REGISTRATION"))
|
||||||
throw new ForbiddenException("Registration is not allowed");
|
throw new ForbiddenException("Registration is not allowed");
|
||||||
return this.authService.signUp(dto);
|
return this.authService.signUp(dto);
|
||||||
}
|
}
|
||||||
|
@ -34,6 +40,12 @@ export class AuthController {
|
||||||
return this.authService.signIn(dto);
|
return this.authService.signIn(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch("password")
|
||||||
|
@UseGuards(JwtGuard)
|
||||||
|
async updatePassword(@GetUser() user: User, @Body() dto: UpdatePasswordDTO) {
|
||||||
|
await this.authService.updatePassword(user, dto.oldPassword, dto.password);
|
||||||
|
}
|
||||||
|
|
||||||
@Post("token")
|
@Post("token")
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
async refreshAccessToken(@Body() body: RefreshAccessTokenDTO) {
|
async refreshAccessToken(@Body() body: RefreshAccessTokenDTO) {
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
|
ForbiddenException,
|
||||||
Injectable,
|
Injectable,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { JwtService } from "@nestjs/jwt";
|
import { JwtService } from "@nestjs/jwt";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||||
import * as argon from "argon2";
|
import * as argon from "argon2";
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
|
import { ConfigService } from "src/config/config.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { AuthRegisterDTO } from "./dto/authRegister.dto";
|
import { AuthRegisterDTO } from "./dto/authRegister.dto";
|
||||||
import { AuthSignInDTO } from "./dto/authSignIn.dto";
|
import { AuthSignInDTO } from "./dto/authSignIn.dto";
|
||||||
|
@ -27,7 +28,9 @@ export class AuthService {
|
||||||
const user = await this.prisma.user.create({
|
const user = await this.prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
email: dto.email,
|
email: dto.email,
|
||||||
|
username: dto.username,
|
||||||
password: hash,
|
password: hash,
|
||||||
|
isAdmin: !this.config.get("SETUP_FINISHED"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -38,16 +41,22 @@ export class AuthService {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof PrismaClientKnownRequestError) {
|
if (e instanceof PrismaClientKnownRequestError) {
|
||||||
if (e.code == "P2002") {
|
if (e.code == "P2002") {
|
||||||
throw new BadRequestException("Credentials taken");
|
const duplicatedField: string = e.meta.target[0];
|
||||||
|
throw new BadRequestException(
|
||||||
|
`A user with this ${duplicatedField} already exists`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async signIn(dto: AuthSignInDTO) {
|
async signIn(dto: AuthSignInDTO) {
|
||||||
const user = await this.prisma.user.findUnique({
|
if (!dto.email && !dto.username)
|
||||||
|
throw new BadRequestException("Email or username is required");
|
||||||
|
|
||||||
|
const user = await this.prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
email: dto.email,
|
OR: [{ email: dto.email }, { username: dto.username }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -60,6 +69,18 @@ export class AuthService {
|
||||||
return { accessToken, refreshToken };
|
return { accessToken, refreshToken };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updatePassword(user: User, oldPassword: string, newPassword: string) {
|
||||||
|
if (argon.verify(user.password, oldPassword))
|
||||||
|
throw new ForbiddenException("Invalid password");
|
||||||
|
|
||||||
|
const hash = await argon.hash(newPassword);
|
||||||
|
|
||||||
|
this.prisma.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: { password: hash },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async createAccessToken(user: User) {
|
async createAccessToken(user: User) {
|
||||||
return this.jwtService.sign(
|
return this.jwtService.sign(
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
import { PickType } from "@nestjs/mapped-types";
|
||||||
import { UserDTO } from "src/user/dto/user.dto";
|
import { UserDTO } from "src/user/dto/user.dto";
|
||||||
|
|
||||||
export class AuthRegisterDTO extends UserDTO {}
|
export class AuthRegisterDTO extends PickType(UserDTO, [
|
||||||
|
"email",
|
||||||
|
"username",
|
||||||
|
"password",
|
||||||
|
] as const) {}
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import { PickType } from "@nestjs/swagger";
|
import { PickType } from "@nestjs/mapped-types";
|
||||||
|
import { IsEmail, IsOptional, IsString } from "class-validator";
|
||||||
import { UserDTO } from "src/user/dto/user.dto";
|
import { UserDTO } from "src/user/dto/user.dto";
|
||||||
|
|
||||||
export class AuthSignInDTO extends PickType(UserDTO, [
|
export class AuthSignInDTO extends PickType(UserDTO, ["password"] as const) {
|
||||||
"email",
|
@IsEmail()
|
||||||
"password",
|
@IsOptional()
|
||||||
] as const) {}
|
email: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
8
backend/src/auth/dto/updatePassword.dto.ts
Normal file
8
backend/src/auth/dto/updatePassword.dto.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { PickType } from "@nestjs/mapped-types";
|
||||||
|
import { IsString } from "class-validator";
|
||||||
|
import { UserDTO } from "src/user/dto/user.dto";
|
||||||
|
|
||||||
|
export class UpdatePasswordDTO extends PickType(UserDTO, ["password"]) {
|
||||||
|
@IsString()
|
||||||
|
oldPassword: string;
|
||||||
|
}
|
13
backend/src/auth/guard/isAdmin.guard.ts
Normal file
13
backend/src/auth/guard/isAdmin.guard.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdministratorGuard implements CanActivate {
|
||||||
|
canActivate(context: ExecutionContext) {
|
||||||
|
const { user }: { user: User } = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
if (!user) return false;
|
||||||
|
|
||||||
|
return user.isAdmin;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,17 @@
|
||||||
import { ExecutionContext } from "@nestjs/common";
|
import { ExecutionContext, Injectable } from "@nestjs/common";
|
||||||
import { AuthGuard } from "@nestjs/passport";
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
import { ConfigService } from "src/config/config.service";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
export class JwtGuard extends AuthGuard("jwt") {
|
export class JwtGuard extends AuthGuard("jwt") {
|
||||||
constructor() {
|
constructor(private config: ConfigService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
return (await super.canActivate(context)) as boolean;
|
return (await super.canActivate(context)) as boolean;
|
||||||
} catch {
|
} catch {
|
||||||
return process.env.ALLOW_UNAUTHENTICATED_SHARES == "true";
|
return this.config.get("ALLOW_UNAUTHENTICATED_SHARES");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { PassportStrategy } from "@nestjs/passport";
|
import { PassportStrategy } from "@nestjs/passport";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import { ExtractJwt, Strategy } from "passport-jwt";
|
import { ExtractJwt, Strategy } from "passport-jwt";
|
||||||
|
import { ConfigService } from "src/config/config.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
constructor(config: ConfigService, private prisma: PrismaService) {
|
constructor(config: ConfigService, private prisma: PrismaService) {
|
||||||
|
config.get("JWT_SECRET");
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
secretOrKey: config.get("JWT_SECRET"),
|
secretOrKey: config.get("JWT_SECRET"),
|
||||||
|
@ -18,7 +19,6 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
const user: User = await this.prisma.user.findUnique({
|
const user: User = await this.prisma.user.findUnique({
|
||||||
where: { id: payload.sub },
|
where: { id: payload.sub },
|
||||||
});
|
});
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
47
backend/src/config/config.controller.ts
Normal file
47
backend/src/config/config.controller.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { AdministratorGuard } from "src/auth/guard/isAdmin.guard";
|
||||||
|
import { JwtGuard } from "src/auth/guard/jwt.guard";
|
||||||
|
import { ConfigService } from "./config.service";
|
||||||
|
import { AdminConfigDTO } from "./dto/adminConfig.dto";
|
||||||
|
import { ConfigDTO } from "./dto/config.dto";
|
||||||
|
import UpdateConfigDTO from "./dto/updateConfig.dto";
|
||||||
|
|
||||||
|
@Controller("configs")
|
||||||
|
export class ConfigController {
|
||||||
|
constructor(private configService: ConfigService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async list() {
|
||||||
|
return new ConfigDTO().fromList(await this.configService.list());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("admin")
|
||||||
|
@UseGuards(JwtGuard, AdministratorGuard)
|
||||||
|
async listForAdmin() {
|
||||||
|
return new AdminConfigDTO().fromList(
|
||||||
|
await this.configService.listForAdmin()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch("admin/:key")
|
||||||
|
@UseGuards(JwtGuard, AdministratorGuard)
|
||||||
|
async update(@Param("key") key: string, @Body() data: UpdateConfigDTO) {
|
||||||
|
return new AdminConfigDTO().from(
|
||||||
|
await this.configService.update(key, data.value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("admin/finishSetup")
|
||||||
|
@UseGuards(JwtGuard, AdministratorGuard)
|
||||||
|
async finishSetup() {
|
||||||
|
return await this.configService.finishSetup();
|
||||||
|
}
|
||||||
|
}
|
21
backend/src/config/config.module.ts
Normal file
21
backend/src/config/config.module.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { Global, Module } from "@nestjs/common";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { ConfigController } from "./config.controller";
|
||||||
|
import { ConfigService } from "./config.service";
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: "CONFIG_VARIABLES",
|
||||||
|
useFactory: async (prisma: PrismaService) => {
|
||||||
|
return await prisma.config.findMany();
|
||||||
|
},
|
||||||
|
inject: [PrismaService],
|
||||||
|
},
|
||||||
|
ConfigService,
|
||||||
|
],
|
||||||
|
controllers: [ConfigController],
|
||||||
|
exports: [ConfigService],
|
||||||
|
})
|
||||||
|
export class ConfigModule {}
|
70
backend/src/config/config.service.ts
Normal file
70
backend/src/config/config.service.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { Config } from "@prisma/client";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ConfigService {
|
||||||
|
constructor(
|
||||||
|
@Inject("CONFIG_VARIABLES") private configVariables: Config[],
|
||||||
|
private prisma: PrismaService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get(key: string): any {
|
||||||
|
const configVariable = this.configVariables.filter(
|
||||||
|
(variable) => variable.key == key
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
if (!configVariable) throw new Error(`Config variable ${key} not found`);
|
||||||
|
|
||||||
|
if (configVariable.type == "number") return parseInt(configVariable.value);
|
||||||
|
if (configVariable.type == "boolean") return configVariable.value == "true";
|
||||||
|
if (configVariable.type == "string") return configVariable.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listForAdmin() {
|
||||||
|
return await this.prisma.config.findMany({
|
||||||
|
where: { locked: { equals: false } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async list() {
|
||||||
|
return await this.prisma.config.findMany({
|
||||||
|
where: { secret: { equals: false } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(key: string, value: string | number | boolean) {
|
||||||
|
const configVariable = await this.prisma.config.findUnique({
|
||||||
|
where: { key },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!configVariable || configVariable.locked)
|
||||||
|
throw new NotFoundException("Config variable not found");
|
||||||
|
|
||||||
|
if (typeof value != configVariable.type)
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Config variable must be of type ${configVariable.type}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedVariable = await this.prisma.config.update({
|
||||||
|
where: { key },
|
||||||
|
data: { value: value.toString() },
|
||||||
|
});
|
||||||
|
|
||||||
|
this.configVariables = await this.prisma.config.findMany();
|
||||||
|
|
||||||
|
return updatedVariable;
|
||||||
|
}
|
||||||
|
|
||||||
|
async finishSetup() {
|
||||||
|
return await this.prisma.config.update({
|
||||||
|
where: { key: "SETUP_FINISHED" },
|
||||||
|
data: { value: "true" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
25
backend/src/config/dto/adminConfig.dto.ts
Normal file
25
backend/src/config/dto/adminConfig.dto.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { Expose, plainToClass } from "class-transformer";
|
||||||
|
import { ConfigDTO } from "./config.dto";
|
||||||
|
|
||||||
|
export class AdminConfigDTO extends ConfigDTO {
|
||||||
|
@Expose()
|
||||||
|
secret: boolean;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
updatedAt: Date;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
from(partial: Partial<AdminConfigDTO>) {
|
||||||
|
return plainToClass(AdminConfigDTO, partial, {
|
||||||
|
excludeExtraneousValues: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fromList(partial: Partial<AdminConfigDTO>[]) {
|
||||||
|
return partial.map((part) =>
|
||||||
|
plainToClass(AdminConfigDTO, part, { excludeExtraneousValues: true })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
18
backend/src/config/dto/config.dto.ts
Normal file
18
backend/src/config/dto/config.dto.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { Expose, plainToClass } from "class-transformer";
|
||||||
|
|
||||||
|
export class ConfigDTO {
|
||||||
|
@Expose()
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
fromList(partial: Partial<ConfigDTO>[]) {
|
||||||
|
return partial.map((part) =>
|
||||||
|
plainToClass(ConfigDTO, part, { excludeExtraneousValues: true })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
8
backend/src/config/dto/updateConfig.dto.ts
Normal file
8
backend/src/config/dto/updateConfig.dto.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { IsNotEmpty } from "class-validator";
|
||||||
|
|
||||||
|
class UpdateConfigDTO {
|
||||||
|
@IsNotEmpty()
|
||||||
|
value: string | number | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpdateConfigDTO;
|
|
@ -1,38 +1,34 @@
|
||||||
import { Injectable, InternalServerErrorException } from "@nestjs/common";
|
import { Injectable, InternalServerErrorException } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import * as nodemailer from "nodemailer";
|
import * as nodemailer from "nodemailer";
|
||||||
|
import { ConfigService } from "src/config/config.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EmailService {
|
export class EmailService {
|
||||||
constructor(private config: ConfigService) {}
|
constructor(private config: ConfigService) {}
|
||||||
|
|
||||||
// create reusable transporter object using the default SMTP transport
|
|
||||||
transporter = nodemailer.createTransport({
|
|
||||||
host: this.config.get("SMTP_HOST"),
|
|
||||||
port: parseInt(this.config.get("SMTP_PORT")),
|
|
||||||
secure: parseInt(this.config.get("SMTP_PORT")) == 465,
|
|
||||||
auth: {
|
|
||||||
user: this.config.get("SMTP_EMAIL"),
|
|
||||||
pass: this.config.get("SMTP_PASSWORD"),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
async sendMail(recipientEmail: string, shareId: string, creator: User) {
|
async sendMail(recipientEmail: string, shareId: string, creator: User) {
|
||||||
if (this.config.get("EMAIL_RECIPIENTS_ENABLED") == "false")
|
// create reusable transporter object using the default SMTP transport
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: this.config.get("SMTP_HOST"),
|
||||||
|
port: parseInt(this.config.get("SMTP_PORT")),
|
||||||
|
secure: parseInt(this.config.get("SMTP_PORT")) == 465,
|
||||||
|
auth: {
|
||||||
|
user: this.config.get("SMTP_EMAIL"),
|
||||||
|
pass: this.config.get("SMTP_PASSWORD"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.config.get("ENABLE_EMAIL_RECIPIENTS"))
|
||||||
throw new InternalServerErrorException("Email service disabled");
|
throw new InternalServerErrorException("Email service disabled");
|
||||||
|
|
||||||
const shareUrl = `${this.config.get("APP_URL")}/share/${shareId}`;
|
const shareUrl = `${this.config.get("APP_URL")}/share/${shareId}`;
|
||||||
const creatorIdentifier = creator ?
|
|
||||||
creator.firstName && creator.lastName
|
|
||||||
? `${creator.firstName} ${creator.lastName}`
|
|
||||||
: creator.email : "A Pingvin Share user";
|
|
||||||
|
|
||||||
await this.transporter.sendMail({
|
await transporter.sendMail({
|
||||||
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
|
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
|
||||||
to: recipientEmail,
|
to: recipientEmail,
|
||||||
subject: "Files shared with you",
|
subject: "Files shared with you",
|
||||||
text: `Hey!\n${creatorIdentifier} shared some files with you. View or dowload the files with this link: ${shareUrl}.\n Shared securely with Pingvin Share 🐧`,
|
text: `Hey!\n${creator.username} shared some files with you. View or dowload the files with this link: ${shareUrl}.\n Shared securely with Pingvin Share 🐧`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
Param,
|
Param,
|
||||||
ParseFilePipeBuilder,
|
|
||||||
Post,
|
Post,
|
||||||
Res,
|
Res,
|
||||||
StreamableFile,
|
StreamableFile,
|
||||||
|
@ -32,13 +31,7 @@ export class FileController {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
async create(
|
async create(
|
||||||
@UploadedFile(
|
@UploadedFile()
|
||||||
new ParseFilePipeBuilder()
|
|
||||||
.addMaxSizeValidator({
|
|
||||||
maxSize: parseInt(process.env.MAX_FILE_SIZE),
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
file: Express.Multer.File,
|
file: Express.Multer.File,
|
||||||
@Param("shareId") shareId: string
|
@Param("shareId") shareId: string
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -3,11 +3,12 @@ import { JwtModule } from "@nestjs/jwt";
|
||||||
import { ShareModule } from "src/share/share.module";
|
import { ShareModule } from "src/share/share.module";
|
||||||
import { FileController } from "./file.controller";
|
import { FileController } from "./file.controller";
|
||||||
import { FileService } from "./file.service";
|
import { FileService } from "./file.service";
|
||||||
|
import { FileValidationPipe } from "./pipe/fileValidation.pipe";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [JwtModule.register({}), ShareModule],
|
imports: [JwtModule.register({}), ShareModule],
|
||||||
controllers: [FileController],
|
controllers: [FileController],
|
||||||
providers: [FileService],
|
providers: [FileService, FileValidationPipe],
|
||||||
exports: [FileService],
|
exports: [FileService],
|
||||||
})
|
})
|
||||||
export class FileModule {}
|
export class FileModule {}
|
||||||
|
|
|
@ -3,11 +3,11 @@ import {
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { JwtService } from "@nestjs/jwt";
|
import { JwtService } from "@nestjs/jwt";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as mime from "mime-types";
|
import * as mime from "mime-types";
|
||||||
|
import { ConfigService } from "src/config/config.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -80,6 +80,7 @@ export class FileService {
|
||||||
|
|
||||||
getFileDownloadUrl(shareId: string, fileId: string) {
|
getFileDownloadUrl(shareId: string, fileId: string) {
|
||||||
const downloadToken = this.generateFileDownloadToken(shareId, fileId);
|
const downloadToken = this.generateFileDownloadToken(shareId, fileId);
|
||||||
|
|
||||||
return `${this.config.get(
|
return `${this.config.get(
|
||||||
"APP_URL"
|
"APP_URL"
|
||||||
)}/api/shares/${shareId}/files/${fileId}?token=${downloadToken}`;
|
)}/api/shares/${shareId}/files/${fileId}?token=${downloadToken}`;
|
||||||
|
|
17
backend/src/file/pipe/fileValidation.pipe.ts
Normal file
17
backend/src/file/pipe/fileValidation.pipe.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import {
|
||||||
|
ArgumentMetadata,
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
PipeTransform,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ConfigService } from "src/config/config.service";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FileValidationPipe implements PipeTransform {
|
||||||
|
constructor(private config: ConfigService) {}
|
||||||
|
async transform(value: any, metadata: ArgumentMetadata) {
|
||||||
|
if (value.size > this.config.get("MAX_FILE_SIZE"))
|
||||||
|
throw new BadRequestException("File is ");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Cron } from "@nestjs/schedule";
|
import { Cron } from "@nestjs/schedule";
|
||||||
|
import * as fs from "fs";
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
import { FileService } from "src/file/file.service";
|
import { FileService } from "src/file/file.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
@ -35,6 +36,22 @@ export class JobsService {
|
||||||
console.log(`job: deleted ${expiredShares.length} expired shares`);
|
console.log(`job: deleted ${expiredShares.length} expired shares`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Cron("0 0 * * *")
|
||||||
|
deleteTemporaryFiles() {
|
||||||
|
const files = fs.readdirSync("./data/uploads/_temp");
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const stats = fs.statSync(`./data/uploads/_temp/${file}`);
|
||||||
|
const isOlderThanOneDay = moment(stats.mtime)
|
||||||
|
.add(1, "day")
|
||||||
|
.isBefore(moment());
|
||||||
|
|
||||||
|
if (isOlderThanOneDay) fs.rmSync(`./data/uploads/_temp/${file}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`job: deleted ${files.length} temporary files`);
|
||||||
|
}
|
||||||
|
|
||||||
@Cron("0 * * * *")
|
@Cron("0 * * * *")
|
||||||
async deleteExpiredRefreshTokens() {
|
async deleteExpiredRefreshTokens() {
|
||||||
const expiredRefreshTokens = await this.prisma.refreshToken.deleteMany({
|
const expiredRefreshTokens = await this.prisma.refreshToken.deleteMany({
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { AppModule } from "./app.module";
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||||
app.useGlobalPipes(new ValidationPipe());
|
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
|
||||||
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
|
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
|
||||||
|
|
||||||
app.set("trust proxy", true);
|
app.set("trust proxy", true);
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PrismaService extends PrismaClient {
|
export class PrismaService extends PrismaClient {
|
||||||
constructor(config: ConfigService) {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
datasources: {
|
datasources: {
|
||||||
db: {
|
db: {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { ShareSecurityDTO } from "./shareSecurity.dto";
|
||||||
export class CreateShareDTO {
|
export class CreateShareDTO {
|
||||||
@IsString()
|
@IsString()
|
||||||
@Matches("^[a-zA-Z0-9_-]*$", undefined, {
|
@Matches("^[a-zA-Z0-9_-]*$", undefined, {
|
||||||
message: "ID only can contain letters, numbers, underscores and hyphens",
|
message: "ID can only contain letters, numbers, underscores and hyphens",
|
||||||
})
|
})
|
||||||
@Length(3, 50)
|
@Length(3, 50)
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { IsString } from "class-validator";
|
||||||
|
|
||||||
export class SharePasswordDto {
|
export class SharePasswordDto {
|
||||||
|
@IsString()
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,13 @@ import {
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { JwtService } from "@nestjs/jwt";
|
import { JwtService } from "@nestjs/jwt";
|
||||||
import { Share, User } from "@prisma/client";
|
import { Share, User } from "@prisma/client";
|
||||||
import * as archiver from "archiver";
|
import * as archiver from "archiver";
|
||||||
import * as argon from "argon2";
|
import * as argon from "argon2";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
|
import { ConfigService } from "src/config/config.service";
|
||||||
import { EmailService } from "src/email/email.service";
|
import { EmailService } from "src/email/email.service";
|
||||||
import { FileService } from "src/file/file.service";
|
import { FileService } from "src/file/file.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
15
backend/src/user/dto/createUser.dto.ts
Normal file
15
backend/src/user/dto/createUser.dto.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { Expose, plainToClass } from "class-transformer";
|
||||||
|
import { Allow } from "class-validator";
|
||||||
|
import { UserDTO } from "./user.dto";
|
||||||
|
|
||||||
|
export class CreateUserDTO extends UserDTO {
|
||||||
|
@Expose()
|
||||||
|
@Allow()
|
||||||
|
isAdmin: boolean;
|
||||||
|
|
||||||
|
from(partial: Partial<CreateUserDTO>) {
|
||||||
|
return plainToClass(CreateUserDTO, partial, {
|
||||||
|
excludeExtraneousValues: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { PickType } from "@nestjs/swagger";
|
import { PickType } from "@nestjs/mapped-types";
|
||||||
import { UserDTO } from "./user.dto";
|
import { UserDTO } from "./user.dto";
|
||||||
|
|
||||||
export class PublicUserDTO extends PickType(UserDTO, ["email"] as const) {}
|
export class PublicUserDTO extends PickType(UserDTO, ["email"] as const) {}
|
||||||
|
|
6
backend/src/user/dto/updateOwnUser.dto.ts
Normal file
6
backend/src/user/dto/updateOwnUser.dto.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { OmitType, PartialType } from "@nestjs/mapped-types";
|
||||||
|
import { UserDTO } from "./user.dto";
|
||||||
|
|
||||||
|
export class UpdateOwnUserDTO extends PartialType(
|
||||||
|
OmitType(UserDTO, ["isAdmin", "password"] as const)
|
||||||
|
) {}
|
4
backend/src/user/dto/updateUser.dto.ts
Normal file
4
backend/src/user/dto/updateUser.dto.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { PartialType } from "@nestjs/mapped-types";
|
||||||
|
import { CreateUserDTO } from "./createUser.dto";
|
||||||
|
|
||||||
|
export class UpdateUserDto extends PartialType(CreateUserDTO) {}
|
|
@ -1,26 +1,34 @@
|
||||||
import { Expose, plainToClass } from "class-transformer";
|
import { Expose, plainToClass } from "class-transformer";
|
||||||
import { IsEmail, IsNotEmpty, IsString } from "class-validator";
|
import { IsEmail, Length, Matches, MinLength } from "class-validator";
|
||||||
|
|
||||||
export class UserDTO {
|
export class UserDTO {
|
||||||
@Expose()
|
@Expose()
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@Expose()
|
@Expose()
|
||||||
firstName: string;
|
@Matches("^[a-zA-Z0-9_.]*$", undefined, {
|
||||||
|
message: "Username can only contain letters, numbers, dots and underscores",
|
||||||
|
})
|
||||||
|
@Length(3, 32)
|
||||||
|
username: string;
|
||||||
|
|
||||||
@Expose()
|
@Expose()
|
||||||
lastName: string;
|
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
@IsNotEmpty()
|
@MinLength(8)
|
||||||
@IsString()
|
|
||||||
password: string;
|
password: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
isAdmin: boolean;
|
||||||
|
|
||||||
from(partial: Partial<UserDTO>) {
|
from(partial: Partial<UserDTO>) {
|
||||||
return plainToClass(UserDTO, partial, { excludeExtraneousValues: true });
|
return plainToClass(UserDTO, partial, { excludeExtraneousValues: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fromList(partial: Partial<UserDTO>[]) {
|
||||||
|
return partial.map((part) =>
|
||||||
|
plainToClass(UserDTO, part, { excludeExtraneousValues: true })
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,71 @@
|
||||||
import { Controller, Get, UseGuards } from "@nestjs/common";
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import { GetUser } from "src/auth/decorator/getUser.decorator";
|
import { GetUser } from "src/auth/decorator/getUser.decorator";
|
||||||
|
import { AdministratorGuard } from "src/auth/guard/isAdmin.guard";
|
||||||
import { JwtGuard } from "src/auth/guard/jwt.guard";
|
import { JwtGuard } from "src/auth/guard/jwt.guard";
|
||||||
|
import { CreateUserDTO } from "./dto/createUser.dto";
|
||||||
|
import { UpdateOwnUserDTO } from "./dto/updateOwnUser.dto";
|
||||||
|
import { UpdateUserDto } from "./dto/updateUser.dto";
|
||||||
import { UserDTO } from "./dto/user.dto";
|
import { UserDTO } from "./dto/user.dto";
|
||||||
|
import { UserSevice } from "./user.service";
|
||||||
|
|
||||||
@Controller("users")
|
@Controller("users")
|
||||||
export class UserController {
|
export class UserController {
|
||||||
|
constructor(private userService: UserSevice) {}
|
||||||
|
|
||||||
|
// Own user operations
|
||||||
@Get("me")
|
@Get("me")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
async getCurrentUser(@GetUser() user: User) {
|
async getCurrentUser(@GetUser() user: User) {
|
||||||
return new UserDTO().from(user);
|
return new UserDTO().from(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch("me")
|
||||||
|
@UseGuards(JwtGuard)
|
||||||
|
async updateCurrentUser(
|
||||||
|
@GetUser() user: User,
|
||||||
|
@Body() data: UpdateOwnUserDTO
|
||||||
|
) {
|
||||||
|
return new UserDTO().from(await this.userService.update(user.id, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete("me")
|
||||||
|
@UseGuards(JwtGuard)
|
||||||
|
async deleteCurrentUser(@GetUser() user: User) {
|
||||||
|
return new UserDTO().from(await this.userService.delete(user.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global user operations
|
||||||
|
@Get()
|
||||||
|
@UseGuards(JwtGuard, AdministratorGuard)
|
||||||
|
async list() {
|
||||||
|
return new UserDTO().fromList(await this.userService.list());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@UseGuards(JwtGuard, AdministratorGuard)
|
||||||
|
async create(@Body() user: CreateUserDTO) {
|
||||||
|
return new UserDTO().from(await this.userService.create(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(":id")
|
||||||
|
@UseGuards(JwtGuard, AdministratorGuard)
|
||||||
|
async update(@Param("id") id: string, @Body() user: UpdateUserDto) {
|
||||||
|
return new UserDTO().from(await this.userService.update(id, user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(":id")
|
||||||
|
@UseGuards(JwtGuard, AdministratorGuard)
|
||||||
|
async delete(@Param("id") id: string) {
|
||||||
|
return new UserDTO().from(await this.userService.delete(id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
9
backend/src/user/user.module.ts
Normal file
9
backend/src/user/user.module.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { UserController } from "./user.controller";
|
||||||
|
import { UserSevice } from "./user.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [UserSevice],
|
||||||
|
controllers: [UserController],
|
||||||
|
})
|
||||||
|
export class UserModule {}
|
65
backend/src/user/user.service.ts
Normal file
65
backend/src/user/user.service.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||||
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||||
|
import * as argon from "argon2";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { CreateUserDTO } from "./dto/createUser.dto";
|
||||||
|
import { UpdateUserDto } from "./dto/updateUser.dto";
|
||||||
|
import { UserDTO } from "./dto/user.dto";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserSevice {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async list() {
|
||||||
|
return await this.prisma.user.findMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id: string) {
|
||||||
|
return await this.prisma.user.findUnique({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(dto: CreateUserDTO) {
|
||||||
|
const hash = await argon.hash(dto.password);
|
||||||
|
try {
|
||||||
|
return await this.prisma.user.create({
|
||||||
|
data: {
|
||||||
|
...dto,
|
||||||
|
password: hash,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof PrismaClientKnownRequestError) {
|
||||||
|
if (e.code == "P2002") {
|
||||||
|
const duplicatedField: string = e.meta.target[0];
|
||||||
|
throw new BadRequestException(
|
||||||
|
`A user with this ${duplicatedField} already exists`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, user: UpdateUserDto) {
|
||||||
|
try {
|
||||||
|
const hash = user.password && (await argon.hash(user.password));
|
||||||
|
|
||||||
|
return await this.prisma.user.update({
|
||||||
|
where: { id },
|
||||||
|
data: { ...user, password: hash },
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof PrismaClientKnownRequestError) {
|
||||||
|
if (e.code == "P2002") {
|
||||||
|
const duplicatedField: string = e.meta.target[0];
|
||||||
|
throw new BadRequestException(
|
||||||
|
`A user with this ${duplicatedField} already exists`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string) {
|
||||||
|
return await this.prisma.user.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,7 +36,7 @@
|
||||||
"header": [],
|
"header": [],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
"raw": "{\n \"firstName\" : \"System\",\n \"lastName\" : \"Test\",\n \"email\": \"system@test.org\",\n \"password\": \"J2y8unpJUcJDRv\"\n}",
|
"raw": "{\n \"email\": \"system@test.org\",\n \"username\": \"system.test\",\n \"password\": \"J2y8unpJUcJDRv\"\n}",
|
||||||
"options": {
|
"options": {
|
||||||
"raw": {
|
"raw": {
|
||||||
"language": "json"
|
"language": "json"
|
||||||
|
@ -97,7 +97,7 @@
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
"raw": "{\n \"firstName\" : \"System\",\n \"lastName\" : \"Test2\",\n \"email\": \"system2@test.org\",\n \"password\": \"N44HcHgeuAvfCT\"\n}",
|
"raw": "{\n \"email\": \"system2@test.org\",\n \"username\": \"system.test2\",\n \"password\": \"N44HcHgeuAvfCT\"\n}",
|
||||||
"options": {
|
"options": {
|
||||||
"raw": {
|
"raw": {
|
||||||
"language": "json"
|
"language": "json"
|
||||||
|
|
|
@ -5,17 +5,5 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
environment:
|
|
||||||
- APP_URL=${APP_URL}
|
|
||||||
- SHOW_HOME_PAGE=${SHOW_HOME_PAGE}
|
|
||||||
- ALLOW_REGISTRATION=${ALLOW_REGISTRATION}
|
|
||||||
- ALLOW_UNAUTHENTICATED_SHARES=${ALLOW_UNAUTHENTICATED_SHARES}
|
|
||||||
- MAX_FILE_SIZE=${MAX_FILE_SIZE}
|
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
|
||||||
- EMAIL_RECIPIENTS_ENABLED=${EMAIL_RECIPIENTS_ENABLED}
|
|
||||||
- SMTP_HOST=${SMTP_HOST}
|
|
||||||
- SMTP_PORT=${SMTP_PORT}
|
|
||||||
- SMTP_EMAIL=${SMTP_EMAIL}
|
|
||||||
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
|
||||||
volumes:
|
volumes:
|
||||||
- "${PWD}/data:/opt/app/backend/data"
|
- "${PWD}/data:/opt/app/backend/data"
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
SHOW_HOME_PAGE=true
|
|
||||||
ALLOW_REGISTRATION=true
|
|
||||||
MAX_FILE_SIZE=1000000000
|
|
||||||
ALLOW_UNAUTHENTICATED_SHARES=false
|
|
||||||
EMAIL_RECIPIENTS_ENABLED=false
|
|
|
@ -1,14 +1,5 @@
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
|
|
||||||
const nextConfig = {
|
|
||||||
publicRuntimeConfig: {
|
|
||||||
ALLOW_REGISTRATION: process.env.ALLOW_REGISTRATION,
|
|
||||||
SHOW_HOME_PAGE: process.env.SHOW_HOME_PAGE,
|
|
||||||
MAX_FILE_SIZE: process.env.MAX_FILE_SIZE,
|
|
||||||
ALLOW_UNAUTHENTICATED_SHARES: process.env.ALLOW_UNAUTHENTICATED_SHARES,
|
|
||||||
EMAIL_RECIPIENTS_ENABLED: process.env.EMAIL_RECIPIENTS_ENABLED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const withPWA = require("next-pwa")({
|
const withPWA = require("next-pwa")({
|
||||||
dest: "public",
|
dest: "public",
|
||||||
|
@ -16,4 +7,4 @@ const withPWA = require("next-pwa")({
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
module.exports = withPWA(nextConfig);
|
module.exports = withPWA();
|
||||||
|
|
1886
frontend/package-lock.json
generated
1886
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -2,47 +2,46 @@
|
||||||
"name": "pingvin-share",
|
"name": "pingvin-share",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "dotenv next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "dotenv next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"format": "prettier --write \"src/**/*.ts*\""
|
"format": "prettier --write \"src/**/*.ts*\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.10.4",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/server": "^11.10.0",
|
"@emotion/server": "^11.10.0",
|
||||||
"@mantine/core": "^5.5.2",
|
"@mantine/core": "^5.9.1",
|
||||||
"@mantine/dropzone": "^5.5.2",
|
"@mantine/dropzone": "^5.9.1",
|
||||||
"@mantine/form": "^5.5.2",
|
"@mantine/form": "^5.9.1",
|
||||||
"@mantine/hooks": "^5.5.2",
|
"@mantine/hooks": "^5.9.1",
|
||||||
"@mantine/modals": "^5.5.2",
|
"@mantine/modals": "^5.9.1",
|
||||||
"@mantine/next": "^5.5.2",
|
"@mantine/next": "^5.9.1",
|
||||||
"@mantine/notifications": "^5.5.2",
|
"@mantine/notifications": "^5.9.1",
|
||||||
"axios": "^0.26.1",
|
"axios": "^1.2.0",
|
||||||
"cookies-next": "^2.0.4",
|
"cookies-next": "^2.1.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"jose": "^4.8.1",
|
"jose": "^4.11.1",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"next": "^13.0.0",
|
"next": "^13.0.6",
|
||||||
"next-cookies": "^2.0.3",
|
"next-cookies": "^2.0.3",
|
||||||
"next-http-proxy-middleware": "^1.2.4",
|
"next-http-proxy-middleware": "^1.2.5",
|
||||||
"next-pwa": "^5.6.0",
|
"next-pwa": "^5.6.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-icons": "^4.4.0",
|
"react-icons": "^4.7.1",
|
||||||
"yup": "^0.32.11"
|
"yup": "^0.32.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "17.0.23",
|
"@types/node": "18.11.10",
|
||||||
"@types/react": "18.0.4",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-dom": "18.0.0",
|
"@types/react-dom": "18.0.9",
|
||||||
"axios": "^0.26.1",
|
"axios": "^1.2.0",
|
||||||
"dotenv-cli": "^6.0.0",
|
"eslint": "8.29.0",
|
||||||
"eslint": "8.13.0",
|
"eslint-config-next": "^13.0.6",
|
||||||
"eslint-config-next": "^13.0.0",
|
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.8.0",
|
||||||
"tar": "^6.1.11",
|
"tar": "^6.1.12",
|
||||||
"typescript": "^4.6.3"
|
"typescript": "^4.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
93
frontend/src/components/admin/AdminConfigTable.tsx
Normal file
93
frontend/src/components/admin/AdminConfigTable.tsx
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import { ActionIcon, Code, Group, Skeleton, Table, Text } from "@mantine/core";
|
||||||
|
import { useModals } from "@mantine/modals";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { TbEdit, TbLock } from "react-icons/tb";
|
||||||
|
import configService from "../../services/config.service";
|
||||||
|
import { AdminConfig as AdminConfigType } from "../../types/config.type";
|
||||||
|
import showUpdateConfigVariableModal from "./showUpdateConfigVariableModal";
|
||||||
|
|
||||||
|
const AdminConfigTable = () => {
|
||||||
|
const modals = useModals();
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const [configVariables, setConfigVariables] = useState<AdminConfigType[]>([]);
|
||||||
|
|
||||||
|
const getConfigVariables = async () => {
|
||||||
|
await configService.listForAdmin().then((configVariables) => {
|
||||||
|
setConfigVariables(configVariables);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
getConfigVariables().then(() => setIsLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const skeletonRows = [...Array(9)].map((c, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>
|
||||||
|
<Skeleton height={18} width={80} mb="sm" />
|
||||||
|
<Skeleton height={30} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Skeleton height={18} />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<Group position="right">
|
||||||
|
<Skeleton height={25} width={25} />
|
||||||
|
</Group>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table verticalSpacing="sm" horizontalSpacing="xl" withBorder>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Key</th>
|
||||||
|
<th>Value</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{isLoading
|
||||||
|
? skeletonRows
|
||||||
|
: configVariables.map((element) => (
|
||||||
|
<tr key={element.key}>
|
||||||
|
<td style={{ maxWidth: "200px" }}>
|
||||||
|
<Code>{element.key}</Code> {element.secret && <TbLock />}{" "}
|
||||||
|
<br />
|
||||||
|
<Text size="xs" color="dimmed">
|
||||||
|
{element.description}
|
||||||
|
</Text>
|
||||||
|
</td>
|
||||||
|
<td>{element.value}</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<Group position="right">
|
||||||
|
<ActionIcon
|
||||||
|
color="primary"
|
||||||
|
variant="light"
|
||||||
|
size={25}
|
||||||
|
onClick={() =>
|
||||||
|
showUpdateConfigVariableModal(
|
||||||
|
modals,
|
||||||
|
element,
|
||||||
|
getConfigVariables
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TbEdit />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminConfigTable;
|
86
frontend/src/components/admin/ManageUserTable.tsx
Normal file
86
frontend/src/components/admin/ManageUserTable.tsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import { ActionIcon, Box, Group, Skeleton, Table } from "@mantine/core";
|
||||||
|
import { useModals } from "@mantine/modals";
|
||||||
|
import { TbCheck, TbEdit, TbTrash } from "react-icons/tb";
|
||||||
|
import User from "../../types/user.type";
|
||||||
|
import showUpdateUserModal from "./showUpdateUserModal";
|
||||||
|
|
||||||
|
const ManageUserTable = ({
|
||||||
|
users,
|
||||||
|
getUsers,
|
||||||
|
deleteUser,
|
||||||
|
isLoading,
|
||||||
|
}: {
|
||||||
|
users: User[];
|
||||||
|
getUsers: () => void;
|
||||||
|
deleteUser: (user: User) => void;
|
||||||
|
isLoading: boolean;
|
||||||
|
}) => {
|
||||||
|
const modals = useModals();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: "block", overflowX: "auto", whiteSpace: "nowrap" }}>
|
||||||
|
<Table verticalSpacing="sm" highlightOnHover>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Admin</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{isLoading
|
||||||
|
? skeletonRows
|
||||||
|
: users.map((user) => (
|
||||||
|
<tr key={user.id}>
|
||||||
|
<td>{user.username}</td>
|
||||||
|
<td>{user.email}</td>
|
||||||
|
<td>{user.isAdmin && <TbCheck />}</td>
|
||||||
|
<td>
|
||||||
|
<Group position="right">
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
showUpdateUserModal(modals, user, getUsers)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TbEdit />
|
||||||
|
</ActionIcon>
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => deleteUser(user)}
|
||||||
|
>
|
||||||
|
<TbTrash />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const skeletonRows = [...Array(10)].map((v, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>
|
||||||
|
<Skeleton key={i} height={20} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Skeleton key={i} height={20} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Skeleton key={i} height={20} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Skeleton key={i} height={20} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
|
||||||
|
export default ManageUserTable;
|
88
frontend/src/components/admin/showCreateUserModal.tsx
Normal file
88
frontend/src/components/admin/showCreateUserModal.tsx
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
PasswordInput,
|
||||||
|
Stack,
|
||||||
|
Switch,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
import * as yup from "yup";
|
||||||
|
import userService from "../../services/user.service";
|
||||||
|
import toast from "../../utils/toast.util";
|
||||||
|
|
||||||
|
const showCreateUserModal = (
|
||||||
|
modals: ModalsContextProps,
|
||||||
|
getUsers: () => void
|
||||||
|
) => {
|
||||||
|
return modals.openModal({
|
||||||
|
title: <Title order={5}>Create user</Title>,
|
||||||
|
children: <Body modals={modals} getUsers={getUsers} />,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const Body = ({
|
||||||
|
modals,
|
||||||
|
getUsers,
|
||||||
|
}: {
|
||||||
|
modals: ModalsContextProps;
|
||||||
|
getUsers: () => void;
|
||||||
|
}) => {
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
username: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
isAdmin: false,
|
||||||
|
},
|
||||||
|
validate: yupResolver(
|
||||||
|
yup.object().shape({
|
||||||
|
email: yup.string().email(),
|
||||||
|
username: yup.string().min(3),
|
||||||
|
password: yup.string().min(8),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit(async (values) => {
|
||||||
|
console.log(values);
|
||||||
|
userService
|
||||||
|
.create(values)
|
||||||
|
.then(() => {
|
||||||
|
getUsers();
|
||||||
|
modals.closeAll();
|
||||||
|
})
|
||||||
|
.catch(toast.axiosError);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<TextInput label="Username" {...form.getInputProps("username")} />
|
||||||
|
<TextInput
|
||||||
|
label="Email"
|
||||||
|
{...form.getInputProps("email")}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
label="New password"
|
||||||
|
{...form.getInputProps("password")}
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
mt="xs"
|
||||||
|
labelPosition="left"
|
||||||
|
label="Admin privileges"
|
||||||
|
{...form.getInputProps("isAdmin", { type: "checkbox" })}
|
||||||
|
/>
|
||||||
|
<Group position="right">
|
||||||
|
<Button type="submit">Create</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default showCreateUserModal;
|
|
@ -0,0 +1,96 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Code,
|
||||||
|
NumberInput,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { useModals } from "@mantine/modals";
|
||||||
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
import configService from "../../services/config.service";
|
||||||
|
import { AdminConfig } from "../../types/config.type";
|
||||||
|
import toast from "../../utils/toast.util";
|
||||||
|
|
||||||
|
const showUpdateConfigVariableModal = (
|
||||||
|
modals: ModalsContextProps,
|
||||||
|
configVariable: AdminConfig,
|
||||||
|
getConfigVariables: () => void
|
||||||
|
) => {
|
||||||
|
return modals.openModal({
|
||||||
|
title: <Title order={5}>Update configuration variable</Title>,
|
||||||
|
children: (
|
||||||
|
<Body
|
||||||
|
configVariable={configVariable}
|
||||||
|
getConfigVariables={getConfigVariables}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const Body = ({
|
||||||
|
configVariable,
|
||||||
|
getConfigVariables,
|
||||||
|
}: {
|
||||||
|
configVariable: AdminConfig;
|
||||||
|
getConfigVariables: () => void;
|
||||||
|
}) => {
|
||||||
|
const modals = useModals();
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
stringValue: configVariable.value,
|
||||||
|
numberValue: parseInt(configVariable.value),
|
||||||
|
booleanValue: configVariable.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Stack align="stretch">
|
||||||
|
<Text>
|
||||||
|
Set <Code>{configVariable.key}</Code> to
|
||||||
|
</Text>
|
||||||
|
{configVariable.type == "string" && (
|
||||||
|
<TextInput label="Value" {...form.getInputProps("stringValue")} />
|
||||||
|
)}
|
||||||
|
{configVariable.type == "number" && (
|
||||||
|
<NumberInput label="Value" {...form.getInputProps("numberValue")} />
|
||||||
|
)}
|
||||||
|
{configVariable.type == "boolean" && (
|
||||||
|
<Select
|
||||||
|
data={[
|
||||||
|
{ value: "true", label: "True" },
|
||||||
|
{ value: "false", label: "False" },
|
||||||
|
]}
|
||||||
|
{...form.getInputProps("booleanValue")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Space />
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
const value =
|
||||||
|
configVariable.type == "string"
|
||||||
|
? form.values.stringValue
|
||||||
|
: configVariable.type == "number"
|
||||||
|
? form.values.numberValue
|
||||||
|
: form.values.booleanValue == "true";
|
||||||
|
|
||||||
|
await configService
|
||||||
|
.update(configVariable.key, value)
|
||||||
|
.then(() => {
|
||||||
|
getConfigVariables();
|
||||||
|
modals.closeAll();
|
||||||
|
})
|
||||||
|
.catch(toast.axiosError);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default showUpdateConfigVariableModal;
|
130
frontend/src/components/admin/showUpdateUserModal.tsx
Normal file
130
frontend/src/components/admin/showUpdateUserModal.tsx
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
PasswordInput,
|
||||||
|
Stack,
|
||||||
|
Switch,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
import * as yup from "yup";
|
||||||
|
import userService from "../../services/user.service";
|
||||||
|
import User from "../../types/user.type";
|
||||||
|
import toast from "../../utils/toast.util";
|
||||||
|
|
||||||
|
const showUpdateUserModal = (
|
||||||
|
modals: ModalsContextProps,
|
||||||
|
user: User,
|
||||||
|
getUsers: () => void
|
||||||
|
) => {
|
||||||
|
return modals.openModal({
|
||||||
|
title: <Title order={5}>Update {user.username}</Title>,
|
||||||
|
children: <Body user={user} modals={modals} getUsers={getUsers} />,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const Body = ({
|
||||||
|
user,
|
||||||
|
modals,
|
||||||
|
getUsers,
|
||||||
|
}: {
|
||||||
|
modals: ModalsContextProps;
|
||||||
|
user: User;
|
||||||
|
getUsers: () => void;
|
||||||
|
}) => {
|
||||||
|
const accountForm = useForm({
|
||||||
|
initialValues: {
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
|
},
|
||||||
|
validate: yupResolver(
|
||||||
|
yup.object().shape({
|
||||||
|
email: yup.string().email(),
|
||||||
|
username: yup.string().min(3),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const passwordForm = useForm({
|
||||||
|
initialValues: {
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
validate: yupResolver(
|
||||||
|
yup.object().shape({
|
||||||
|
password: yup.string().min(8),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<form
|
||||||
|
id="accountForm"
|
||||||
|
onSubmit={accountForm.onSubmit(async (values) => {
|
||||||
|
userService
|
||||||
|
.update(user.id, values)
|
||||||
|
.then(() => {
|
||||||
|
getUsers();
|
||||||
|
modals.closeAll();
|
||||||
|
})
|
||||||
|
.catch(toast.axiosError);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<TextInput
|
||||||
|
label="Username"
|
||||||
|
{...accountForm.getInputProps("username")}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Email"
|
||||||
|
{...accountForm.getInputProps("email")}
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
mt="xs"
|
||||||
|
labelPosition="left"
|
||||||
|
label="Admin privileges"
|
||||||
|
{...accountForm.getInputProps("isAdmin", { type: "checkbox" })}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
<Accordion>
|
||||||
|
<Accordion.Item sx={{ borderBottom: "none" }} value="changePassword">
|
||||||
|
<Accordion.Control>Change password</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<form
|
||||||
|
onSubmit={passwordForm.onSubmit(async (values) => {
|
||||||
|
userService
|
||||||
|
.update(user.id, {
|
||||||
|
password: values.password,
|
||||||
|
})
|
||||||
|
.then(() => toast.success("Password changed successfully"))
|
||||||
|
.catch(toast.axiosError);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<PasswordInput
|
||||||
|
label="New password"
|
||||||
|
{...passwordForm.getInputProps("password")}
|
||||||
|
/>
|
||||||
|
<Button variant="light" type="submit">
|
||||||
|
Save new password
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
<Group position="right">
|
||||||
|
<Button type="submit" form="accountForm">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default showUpdateUserModal;
|
86
frontend/src/components/auth/SignInForm.tsx
Normal file
86
frontend/src/components/auth/SignInForm.tsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import {
|
||||||
|
Anchor,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Paper,
|
||||||
|
PasswordInput,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
|
import Link from "next/link";
|
||||||
|
import * as yup from "yup";
|
||||||
|
import useConfig from "../../hooks/config.hook";
|
||||||
|
import authService from "../../services/auth.service";
|
||||||
|
import toast from "../../utils/toast.util";
|
||||||
|
|
||||||
|
const SignInForm = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
|
||||||
|
const validationSchema = yup.object().shape({
|
||||||
|
emailOrUsername: yup.string().required(),
|
||||||
|
password: yup.string().min(8).required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
emailOrUsername: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
validate: yupResolver(validationSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const signIn = (email: string, password: string) => {
|
||||||
|
authService
|
||||||
|
.signIn(email, password)
|
||||||
|
.then(() => window.location.replace("/"))
|
||||||
|
.catch(toast.axiosError);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size={420} my={40}>
|
||||||
|
<Title
|
||||||
|
align="center"
|
||||||
|
sx={(theme) => ({
|
||||||
|
fontFamily: `Greycliff CF, ${theme.fontFamily}`,
|
||||||
|
fontWeight: 900,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Welcome back
|
||||||
|
</Title>
|
||||||
|
{config.get("ALLOW_REGISTRATION") && (
|
||||||
|
<Text color="dimmed" size="sm" align="center" mt={5}>
|
||||||
|
You don't have an account yet?{" "}
|
||||||
|
<Anchor component={Link} href={"signUp"} size="sm">
|
||||||
|
{"Sign up"}
|
||||||
|
</Anchor>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit((values) =>
|
||||||
|
signIn(values.emailOrUsername, values.password)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
label="Email or username"
|
||||||
|
placeholder="you@email.com"
|
||||||
|
{...form.getInputProps("emailOrUsername")}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
label="Password"
|
||||||
|
placeholder="Your password"
|
||||||
|
mt="md"
|
||||||
|
{...form.getInputProps("password")}
|
||||||
|
/>
|
||||||
|
<Button fullWidth mt="xl" type="submit">
|
||||||
|
Sign in
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignInForm;
|
|
@ -9,23 +9,25 @@ import {
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useForm, yupResolver } from "@mantine/form";
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
import getConfig from "next/config";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
import useConfig from "../../hooks/config.hook";
|
||||||
import authService from "../../services/auth.service";
|
import authService from "../../services/auth.service";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
|
|
||||||
const { publicRuntimeConfig } = getConfig();
|
const SignUpForm = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
|
||||||
const AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => {
|
|
||||||
const validationSchema = yup.object().shape({
|
const validationSchema = yup.object().shape({
|
||||||
email: yup.string().email().required(),
|
email: yup.string().email().required(),
|
||||||
|
username: yup.string().min(3).required(),
|
||||||
password: yup.string().min(8).required(),
|
password: yup.string().min(8).required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
email: "",
|
email: "",
|
||||||
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
},
|
},
|
||||||
validate: yupResolver(validationSchema),
|
validate: yupResolver(validationSchema),
|
||||||
|
@ -34,14 +36,14 @@ const AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => {
|
||||||
const signIn = (email: string, password: string) => {
|
const signIn = (email: string, password: string) => {
|
||||||
authService
|
authService
|
||||||
.signIn(email, password)
|
.signIn(email, password)
|
||||||
.then(() => window.location.replace("/upload"))
|
.then(() => window.location.replace("/"))
|
||||||
.catch((e) => toast.error(e.response.data.message));
|
.catch(toast.axiosError);
|
||||||
};
|
};
|
||||||
const signUp = (email: string, password: string) => {
|
const signUp = (email: string, username: string, password: string) => {
|
||||||
authService
|
authService
|
||||||
.signUp(email, password)
|
.signUp(email, username, password)
|
||||||
.then(() => signIn(email, password))
|
.then(() => signIn(email, password))
|
||||||
.catch((e) => toast.error(e.response.data.message));
|
.catch(toast.axiosError);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -53,33 +55,31 @@ const AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => {
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{mode == "signUp" ? "Sign up" : "Welcome back"}
|
Sign up
|
||||||
</Title>
|
</Title>
|
||||||
{publicRuntimeConfig.ALLOW_REGISTRATION == "true" && (
|
{config.get("ALLOW_REGISTRATION") && (
|
||||||
<Text color="dimmed" size="sm" align="center" mt={5}>
|
<Text color="dimmed" size="sm" align="center" mt={5}>
|
||||||
{mode == "signUp"
|
You have an account already?{" "}
|
||||||
? "You have an account already?"
|
<Anchor component={Link} href={"signIn"} size="sm">
|
||||||
: "You don't have an account yet?"}{" "}
|
Sign in
|
||||||
<Anchor
|
|
||||||
component={Link}
|
|
||||||
href={mode == "signUp" ? "signIn" : "signUp"}
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
{mode == "signUp" ? "Sign in" : "Sign up"}
|
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
||||||
<form
|
<form
|
||||||
onSubmit={form.onSubmit((values) =>
|
onSubmit={form.onSubmit((values) =>
|
||||||
mode == "signIn"
|
signUp(values.email, values.username, values.password)
|
||||||
? signIn(values.email, values.password)
|
|
||||||
: signUp(values.email, values.password)
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<TextInput
|
||||||
|
label="Username"
|
||||||
|
placeholder="john.doe"
|
||||||
|
{...form.getInputProps("username")}
|
||||||
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Email"
|
label="Email"
|
||||||
placeholder="you@email.com"
|
placeholder="you@email.com"
|
||||||
|
mt="md"
|
||||||
{...form.getInputProps("email")}
|
{...form.getInputProps("email")}
|
||||||
/>
|
/>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
|
@ -89,7 +89,7 @@ const AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => {
|
||||||
{...form.getInputProps("password")}
|
{...form.getInputProps("password")}
|
||||||
/>
|
/>
|
||||||
<Button fullWidth mt="xl" type="submit">
|
<Button fullWidth mt="xl" type="submit">
|
||||||
{mode == "signUp" ? "Let's get started" : "Sign in"}
|
Let's get started
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
@ -97,4 +97,4 @@ const AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AuthForm;
|
export default SignUpForm;
|
|
@ -1,9 +1,12 @@
|
||||||
import { ActionIcon, Avatar, Menu } from "@mantine/core";
|
import { ActionIcon, Avatar, Menu } from "@mantine/core";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { TbDoorExit, TbLink } from "react-icons/tb";
|
import { TbDoorExit, TbLink, TbSettings, TbUser } from "react-icons/tb";
|
||||||
|
import useUser from "../../hooks/user.hook";
|
||||||
import authService from "../../services/auth.service";
|
import authService from "../../services/auth.service";
|
||||||
|
|
||||||
const ActionAvatar = () => {
|
const ActionAvatar = () => {
|
||||||
|
const user = useUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu position="bottom-start" withinPortal>
|
<Menu position="bottom-start" withinPortal>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
|
@ -19,6 +22,19 @@ const ActionAvatar = () => {
|
||||||
>
|
>
|
||||||
My shares
|
My shares
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item component={Link} href="/account" icon={<TbUser size={14} />}>
|
||||||
|
My account
|
||||||
|
</Menu.Item>
|
||||||
|
{user!.isAdmin && (
|
||||||
|
<Menu.Item
|
||||||
|
component={Link}
|
||||||
|
href="/admin"
|
||||||
|
icon={<TbSettings size={14} />}
|
||||||
|
>
|
||||||
|
Administration
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
authService.signOut();
|
authService.signOut();
|
||||||
|
|
|
@ -11,15 +11,13 @@ import {
|
||||||
Transition,
|
Transition,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import getConfig from "next/config";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ReactNode, useEffect, useState } from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
|
import useConfig from "../../hooks/config.hook";
|
||||||
import useUser from "../../hooks/user.hook";
|
import useUser from "../../hooks/user.hook";
|
||||||
import Logo from "../Logo";
|
import Logo from "../Logo";
|
||||||
import ActionAvatar from "./ActionAvatar";
|
import ActionAvatar from "./ActionAvatar";
|
||||||
|
|
||||||
const { publicRuntimeConfig } = getConfig();
|
|
||||||
|
|
||||||
const HEADER_HEIGHT = 60;
|
const HEADER_HEIGHT = 60;
|
||||||
|
|
||||||
type NavLink = {
|
type NavLink = {
|
||||||
|
@ -110,6 +108,8 @@ const useStyles = createStyles((theme) => ({
|
||||||
|
|
||||||
const NavBar = () => {
|
const NavBar = () => {
|
||||||
const user = useUser();
|
const user = useUser();
|
||||||
|
const config = useConfig();
|
||||||
|
|
||||||
const [opened, toggleOpened] = useDisclosure(false);
|
const [opened, toggleOpened] = useDisclosure(false);
|
||||||
|
|
||||||
const authenticatedLinks = [
|
const authenticatedLinks = [
|
||||||
|
@ -130,7 +130,7 @@ const NavBar = () => {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (publicRuntimeConfig.SHOW_HOME_PAGE == "true")
|
if (config.get("SHOW_HOME_PAGE"))
|
||||||
setUnauthenticatedLinks((array) => [
|
setUnauthenticatedLinks((array) => [
|
||||||
{
|
{
|
||||||
link: "/",
|
link: "/",
|
||||||
|
@ -139,7 +139,7 @@ const NavBar = () => {
|
||||||
...array,
|
...array,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (publicRuntimeConfig.ALLOW_REGISTRATION == "true")
|
if (config.get("ALLOW_REGISTRATION"))
|
||||||
setUnauthenticatedLinks((array) => [
|
setUnauthenticatedLinks((array) => [
|
||||||
...array,
|
...array,
|
||||||
{
|
{
|
||||||
|
|
|
@ -13,23 +13,6 @@ const FileList = ({
|
||||||
shareId: string;
|
shareId: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const skeletonRows = [...Array(5)].map((c, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>
|
|
||||||
<Skeleton height={30} width={30} />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Skeleton height={14} />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Skeleton height={14} />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Skeleton height={25} width={25} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
));
|
|
||||||
|
|
||||||
const rows = files.map((file) => (
|
const rows = files.map((file) => (
|
||||||
<tr key={file.name}>
|
<tr key={file.name}>
|
||||||
<td>{file.name}</td>
|
<td>{file.name}</td>
|
||||||
|
@ -69,4 +52,21 @@ const FileList = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const skeletonRows = [...Array(5)].map((c, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>
|
||||||
|
<Skeleton height={30} width={30} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Skeleton height={14} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Skeleton height={14} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Skeleton height={25} width={25} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
|
||||||
export default FileList;
|
export default FileList;
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
import { Button, Center, createStyles, Group, Text } from "@mantine/core";
|
import { Button, Center, createStyles, Group, Text } from "@mantine/core";
|
||||||
import { Dropzone as MantineDropzone } from "@mantine/dropzone";
|
import { Dropzone as MantineDropzone } from "@mantine/dropzone";
|
||||||
import getConfig from "next/config";
|
|
||||||
import { Dispatch, ForwardedRef, SetStateAction, useRef } from "react";
|
import { Dispatch, ForwardedRef, SetStateAction, useRef } from "react";
|
||||||
import { TbCloudUpload, TbUpload } from "react-icons/tb";
|
import { TbCloudUpload, TbUpload } from "react-icons/tb";
|
||||||
|
import useConfig from "../../hooks/config.hook";
|
||||||
import { FileUpload } from "../../types/File.type";
|
import { FileUpload } from "../../types/File.type";
|
||||||
import { byteStringToHumanSizeString } from "../../utils/math/byteStringToHumanSizeString.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",
|
||||||
|
@ -40,27 +38,31 @@ const Dropzone = ({
|
||||||
isUploading: boolean;
|
isUploading: boolean;
|
||||||
setFiles: Dispatch<SetStateAction<FileUpload[]>>;
|
setFiles: Dispatch<SetStateAction<FileUpload[]>>;
|
||||||
}) => {
|
}) => {
|
||||||
|
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={parseInt(publicRuntimeConfig.MAX_FILE_SIZE!)}
|
// Temporary fix for Dropzone issue (https://github.com/mantinedev/mantine/issues/3115)
|
||||||
|
getFilesFromEvent={(e) => {
|
||||||
|
return Promise.resolve([
|
||||||
|
...((e.target as EventTarget & HTMLInputElement)?.files as any),
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
maxSize={parseInt(config.get("MAX_FILE_SIZE"))}
|
||||||
onReject={(e) => {
|
onReject={(e) => {
|
||||||
toast.error(e[0].errors[0].message);
|
toast.error(e[0].errors[0].message);
|
||||||
}}
|
}}
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
openRef={openRef as ForwardedRef<() => void>}
|
openRef={openRef as ForwardedRef<() => void>}
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
if (files.length > 100) {
|
const newFiles = files.map((file) => {
|
||||||
toast.error("You can't upload more than 100 files per share.");
|
(file as FileUpload).uploadingProgress = 0;
|
||||||
} else {
|
return file as FileUpload;
|
||||||
const newFiles = files.map((file) => {
|
});
|
||||||
(file as FileUpload).uploadingProgress = 0;
|
setFiles(newFiles);
|
||||||
return file as FileUpload;
|
|
||||||
});
|
|
||||||
setFiles(newFiles);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
className={classes.dropzone}
|
className={classes.dropzone}
|
||||||
radius="md"
|
radius="md"
|
||||||
|
@ -75,8 +77,7 @@ const Dropzone = ({
|
||||||
<Text align="center" size="sm" mt="xs" color="dimmed">
|
<Text align="center" size="sm" mt="xs" color="dimmed">
|
||||||
Drag'n'drop files here to start your share. We can accept
|
Drag'n'drop files here to start your share. We can accept
|
||||||
only files that are less than{" "}
|
only files that are less than{" "}
|
||||||
{byteStringToHumanSizeString(publicRuntimeConfig.MAX_FILE_SIZE)} in
|
{byteStringToHumanSizeString(config.get("MAX_FILE_SIZE"))} in size.
|
||||||
size.
|
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</MantineDropzone>
|
</MantineDropzone>
|
||||||
|
|
|
@ -18,7 +18,6 @@ import {
|
||||||
import { useForm, yupResolver } from "@mantine/form";
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
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 getConfig from "next/config";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { TbAlertCircle } from "react-icons/tb";
|
import { TbAlertCircle } from "react-icons/tb";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
@ -26,11 +25,13 @@ import shareService from "../../../services/share.service";
|
||||||
import { ShareSecurity } from "../../../types/share.type";
|
import { ShareSecurity } from "../../../types/share.type";
|
||||||
import ExpirationPreview from "../ExpirationPreview";
|
import ExpirationPreview from "../ExpirationPreview";
|
||||||
|
|
||||||
const { publicRuntimeConfig } = getConfig();
|
|
||||||
|
|
||||||
const showCreateUploadModal = (
|
const showCreateUploadModal = (
|
||||||
modals: ModalsContextProps,
|
modals: ModalsContextProps,
|
||||||
isSignedIn: boolean,
|
options: {
|
||||||
|
isUserSignedIn: boolean;
|
||||||
|
ALLOW_UNAUTHENTICATED_SHARES: boolean;
|
||||||
|
ENABLE_EMAIL_RECIPIENTS: boolean;
|
||||||
|
},
|
||||||
uploadCallback: (
|
uploadCallback: (
|
||||||
id: string,
|
id: string,
|
||||||
expiration: string,
|
expiration: string,
|
||||||
|
@ -42,7 +43,7 @@ const showCreateUploadModal = (
|
||||||
title: <Title order={4}>Share</Title>,
|
title: <Title order={4}>Share</Title>,
|
||||||
children: (
|
children: (
|
||||||
<CreateUploadModalBody
|
<CreateUploadModalBody
|
||||||
isSignedIn={isSignedIn}
|
options={options}
|
||||||
uploadCallback={uploadCallback}
|
uploadCallback={uploadCallback}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -51,7 +52,7 @@ const showCreateUploadModal = (
|
||||||
|
|
||||||
const CreateUploadModalBody = ({
|
const CreateUploadModalBody = ({
|
||||||
uploadCallback,
|
uploadCallback,
|
||||||
isSignedIn,
|
options,
|
||||||
}: {
|
}: {
|
||||||
uploadCallback: (
|
uploadCallback: (
|
||||||
id: string,
|
id: string,
|
||||||
|
@ -59,12 +60,16 @@ const CreateUploadModalBody = ({
|
||||||
recipients: string[],
|
recipients: string[],
|
||||||
security: ShareSecurity
|
security: ShareSecurity
|
||||||
) => void;
|
) => void;
|
||||||
isSignedIn: boolean;
|
options: {
|
||||||
|
isUserSignedIn: boolean;
|
||||||
|
ALLOW_UNAUTHENTICATED_SHARES: boolean;
|
||||||
|
ENABLE_EMAIL_RECIPIENTS: boolean;
|
||||||
|
};
|
||||||
}) => {
|
}) => {
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
|
|
||||||
const [showNotSignedInAlert, setShowNotSignedInAlert] = useState(
|
const [showNotSignedInAlert, setShowNotSignedInAlert] = useState(
|
||||||
publicRuntimeConfig.ALLOW_UNAUTHENTICATED_SHARES == "true"
|
options.ENABLE_EMAIL_RECIPIENTS
|
||||||
);
|
);
|
||||||
|
|
||||||
const validationSchema = yup.object().shape({
|
const validationSchema = yup.object().shape({
|
||||||
|
@ -93,7 +98,7 @@ const CreateUploadModalBody = ({
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Group>
|
<Group>
|
||||||
{showNotSignedInAlert && !isSignedIn && (
|
{showNotSignedInAlert && !options.isUserSignedIn && (
|
||||||
<Alert
|
<Alert
|
||||||
withCloseButton
|
withCloseButton
|
||||||
onClose={() => setShowNotSignedInAlert(false)}
|
onClose={() => setShowNotSignedInAlert(false)}
|
||||||
|
@ -225,7 +230,7 @@ const CreateUploadModalBody = ({
|
||||||
{ExpirationPreview({ form })}
|
{ExpirationPreview({ form })}
|
||||||
</Text>
|
</Text>
|
||||||
<Accordion>
|
<Accordion>
|
||||||
{publicRuntimeConfig.EMAIL_RECIPIENTS_ENABLED == "true" && (
|
{options.ENABLE_EMAIL_RECIPIENTS && (
|
||||||
<Accordion.Item value="recipients" sx={{ borderBottom: "none" }}>
|
<Accordion.Item value="recipients" sx={{ borderBottom: "none" }}>
|
||||||
<Accordion.Control>Email recipients</Accordion.Control>
|
<Accordion.Control>Email recipients</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
|
|
14
frontend/src/hooks/config.hook.ts
Normal file
14
frontend/src/hooks/config.hook.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { createContext, useContext } from "react";
|
||||||
|
import configService from "../services/config.service";
|
||||||
|
import Config from "../types/config.type";
|
||||||
|
|
||||||
|
export const ConfigContext = createContext<Config[] | null>(null);
|
||||||
|
|
||||||
|
const useConfig = () => {
|
||||||
|
const configVariables = useContext(ConfigContext) as Config[];
|
||||||
|
return {
|
||||||
|
get: (key: string) => configService.get(key, configVariables),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useConfig;
|
|
@ -8,25 +8,33 @@ import { useColorScheme } from "@mantine/hooks";
|
||||||
import { ModalsProvider } from "@mantine/modals";
|
import { ModalsProvider } from "@mantine/modals";
|
||||||
import { NotificationsProvider } from "@mantine/notifications";
|
import { NotificationsProvider } from "@mantine/notifications";
|
||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Header from "../components/navBar/NavBar";
|
import Header from "../components/navBar/NavBar";
|
||||||
|
import useConfig, { ConfigContext } from "../hooks/config.hook";
|
||||||
import { UserContext } from "../hooks/user.hook";
|
import { UserContext } from "../hooks/user.hook";
|
||||||
import authService from "../services/auth.service";
|
import authService from "../services/auth.service";
|
||||||
|
import configService from "../services/config.service";
|
||||||
import userService from "../services/user.service";
|
import userService from "../services/user.service";
|
||||||
import GlobalStyle from "../styles/global.style";
|
import GlobalStyle from "../styles/global.style";
|
||||||
import globalStyle from "../styles/mantine.style";
|
import globalStyle from "../styles/mantine.style";
|
||||||
|
import Config from "../types/config.type";
|
||||||
import { CurrentUser } from "../types/user.type";
|
import { CurrentUser } from "../types/user.type";
|
||||||
import { GlobalLoadingContext } from "../utils/loading.util";
|
import { GlobalLoadingContext } from "../utils/loading.util";
|
||||||
|
|
||||||
function App({ Component, pageProps }: AppProps) {
|
function App({ Component, pageProps }: AppProps) {
|
||||||
const systemTheme = useColorScheme();
|
const systemTheme = useColorScheme();
|
||||||
|
const router = useRouter();
|
||||||
|
const config = useConfig();
|
||||||
|
|
||||||
const [colorScheme, setColorScheme] = useState<ColorScheme>();
|
const [colorScheme, setColorScheme] = useState<ColorScheme>();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [user, setUser] = useState<CurrentUser | null>(null);
|
const [user, setUser] = useState<CurrentUser | null>(null);
|
||||||
|
const [configVariables, setConfigVariables] = useState<Config[] | null>(null);
|
||||||
|
|
||||||
const getInitalData = async () => {
|
const getInitalData = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
setConfigVariables(await configService.list());
|
||||||
await authService.refreshAccessToken();
|
await authService.refreshAccessToken();
|
||||||
setUser(await userService.getCurrentUser());
|
setUser(await userService.getCurrentUser());
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
@ -37,6 +45,16 @@ function App({ Component, pageProps }: AppProps) {
|
||||||
getInitalData();
|
getInitalData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
configVariables &&
|
||||||
|
configVariables.filter((variable) => variable.key)[0].value == "false" &&
|
||||||
|
!["/auth/signUp", "/admin/setup"].includes(router.asPath)
|
||||||
|
) {
|
||||||
|
router.push(!user ? "/auth/signUp" : "/admin/setup");
|
||||||
|
}
|
||||||
|
}, [router.asPath]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setColorScheme(systemTheme);
|
setColorScheme(systemTheme);
|
||||||
}, [systemTheme]);
|
}, [systemTheme]);
|
||||||
|
@ -54,13 +72,15 @@ function App({ Component, pageProps }: AppProps) {
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<LoadingOverlay visible overlayOpacity={1} />
|
<LoadingOverlay visible overlayOpacity={1} />
|
||||||
) : (
|
) : (
|
||||||
<UserContext.Provider value={user}>
|
<ConfigContext.Provider value={configVariables}>
|
||||||
<LoadingOverlay visible={isLoading} overlayOpacity={1} />
|
<UserContext.Provider value={user}>
|
||||||
<Header />
|
<LoadingOverlay visible={isLoading} overlayOpacity={1} />
|
||||||
<Container>
|
<Header />
|
||||||
<Component {...pageProps} />
|
<Container>
|
||||||
</Container>
|
<Component {...pageProps} />
|
||||||
</UserContext.Provider>
|
</Container>
|
||||||
|
</UserContext.Provider>{" "}
|
||||||
|
</ConfigContext.Provider>
|
||||||
)}
|
)}
|
||||||
</GlobalLoadingContext.Provider>
|
</GlobalLoadingContext.Provider>
|
||||||
</ModalsProvider>
|
</ModalsProvider>
|
||||||
|
@ -69,9 +89,4 @@ function App({ Component, pageProps }: AppProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opts out of static site generation to use publicRuntimeConfig
|
|
||||||
App.getInitialProps = () => {
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
153
frontend/src/pages/account/index.tsx
Normal file
153
frontend/src/pages/account/index.tsx
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Container,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
PasswordInput,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
|
import { useModals } from "@mantine/modals";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import * as yup from "yup";
|
||||||
|
import useUser from "../../hooks/user.hook";
|
||||||
|
import authService from "../../services/auth.service";
|
||||||
|
import userService from "../../services/user.service";
|
||||||
|
import toast from "../../utils/toast.util";
|
||||||
|
|
||||||
|
const Account = () => {
|
||||||
|
const user = useUser();
|
||||||
|
const modals = useModals();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const accountForm = useForm({
|
||||||
|
initialValues: {
|
||||||
|
username: user?.username,
|
||||||
|
email: user?.email,
|
||||||
|
},
|
||||||
|
validate: yupResolver(
|
||||||
|
yup.object().shape({
|
||||||
|
email: yup.string().email(),
|
||||||
|
username: yup.string().min(3),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const passwordForm = useForm({
|
||||||
|
initialValues: {
|
||||||
|
oldPassword: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
validate: yupResolver(
|
||||||
|
yup.object().shape({
|
||||||
|
oldPassword: yup.string().min(8),
|
||||||
|
password: yup.string().min(8),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
router.push("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="sm">
|
||||||
|
<Title order={3} mb="xs">
|
||||||
|
My account
|
||||||
|
</Title>
|
||||||
|
<Paper withBorder p="xl">
|
||||||
|
<Title order={5} mb="xs">
|
||||||
|
Account Info
|
||||||
|
</Title>
|
||||||
|
<form
|
||||||
|
onSubmit={accountForm.onSubmit((values) =>
|
||||||
|
userService
|
||||||
|
.updateCurrentUser({
|
||||||
|
username: values.username,
|
||||||
|
email: values.email,
|
||||||
|
})
|
||||||
|
.then(() => toast.success("User updated successfully"))
|
||||||
|
.catch(toast.axiosError)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<TextInput
|
||||||
|
label="Username"
|
||||||
|
{...accountForm.getInputProps("username")}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Email"
|
||||||
|
{...accountForm.getInputProps("email")}
|
||||||
|
/>
|
||||||
|
<Group position="right">
|
||||||
|
<Button type="submit">Save</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
<Paper withBorder p="xl" mt="lg">
|
||||||
|
<Title order={5} mb="xs">
|
||||||
|
Password
|
||||||
|
</Title>
|
||||||
|
<form
|
||||||
|
onSubmit={passwordForm.onSubmit((values) =>
|
||||||
|
authService
|
||||||
|
.updatePassword(values.oldPassword, values.password)
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Password updated successfully");
|
||||||
|
passwordForm.reset();
|
||||||
|
})
|
||||||
|
.catch(toast.axiosError)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<PasswordInput
|
||||||
|
label="Old password"
|
||||||
|
{...passwordForm.getInputProps("oldPassword")}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
label="New password"
|
||||||
|
{...passwordForm.getInputProps("password")}
|
||||||
|
/>
|
||||||
|
<Group position="right">
|
||||||
|
<Button type="submit">Save</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
<Center mt={80}>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
onClick={() =>
|
||||||
|
modals.openConfirmModal({
|
||||||
|
title: "Account deletion",
|
||||||
|
children: (
|
||||||
|
<Text size="sm">
|
||||||
|
Do you really want to delete your account including all your
|
||||||
|
active shares?
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
|
||||||
|
labels: { confirm: "Delete", cancel: "Cancel" },
|
||||||
|
confirmProps: { color: "red" },
|
||||||
|
onConfirm: async () => {
|
||||||
|
await userService.removeCurrentUser();
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Delete Account
|
||||||
|
</Button>
|
||||||
|
</Center>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Account;
|
16
frontend/src/pages/admin/config.tsx
Normal file
16
frontend/src/pages/admin/config.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { Space, Title } from "@mantine/core";
|
||||||
|
import AdminConfigTable from "../../components/admin/AdminConfigTable";
|
||||||
|
|
||||||
|
const AdminConfig = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Title mb={30} order={3}>
|
||||||
|
Configuration
|
||||||
|
</Title>
|
||||||
|
<AdminConfigTable />
|
||||||
|
<Space h="xl" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminConfig;
|
62
frontend/src/pages/admin/index.tsx
Normal file
62
frontend/src/pages/admin/index.tsx
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import { Col, Container, createStyles, Grid, Paper, Text } from "@mantine/core";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { TbSettings, TbUsers } from "react-icons/tb";
|
||||||
|
|
||||||
|
const managementOptions = [
|
||||||
|
{
|
||||||
|
title: "User management",
|
||||||
|
icon: TbUsers,
|
||||||
|
route: "/admin/users",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Configuration",
|
||||||
|
icon: TbSettings,
|
||||||
|
route: "/admin/config",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
item: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
textAlign: "center",
|
||||||
|
height: 90,
|
||||||
|
"&:hover": {
|
||||||
|
boxShadow: `${theme.shadows.sm} !important`,
|
||||||
|
transform: "scale(1.01)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Admin = () => {
|
||||||
|
const { classes, theme } = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="xl">
|
||||||
|
<Paper withBorder radius="md" p={40}>
|
||||||
|
<Grid mt="md">
|
||||||
|
{managementOptions.map((item) => {
|
||||||
|
return (
|
||||||
|
<Col xs={6} key={item.route}>
|
||||||
|
<Paper
|
||||||
|
withBorder
|
||||||
|
component={Link}
|
||||||
|
href={item.route}
|
||||||
|
key={item.title}
|
||||||
|
className={classes.item}
|
||||||
|
>
|
||||||
|
<item.icon color={theme.colors.victoria[5]} size={35} />
|
||||||
|
<Text mt={7}>{item.title}</Text>
|
||||||
|
</Paper>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Admin;
|
50
frontend/src/pages/admin/setup.tsx
Normal file
50
frontend/src/pages/admin/setup.tsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { Button, Stack, Text, Title } from "@mantine/core";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import AdminConfigTable from "../../components/admin/AdminConfigTable";
|
||||||
|
import Logo from "../../components/Logo";
|
||||||
|
import useConfig from "../../hooks/config.hook";
|
||||||
|
import useUser from "../../hooks/user.hook";
|
||||||
|
import configService from "../../services/config.service";
|
||||||
|
|
||||||
|
const Setup = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const config = useConfig();
|
||||||
|
const user = useUser();
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
router.push("/auth/signUp");
|
||||||
|
return;
|
||||||
|
} else if (config.get("SETUP_FINISHED")) {
|
||||||
|
router.push("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack align="center">
|
||||||
|
<Logo height={80} width={80} />
|
||||||
|
<Title order={2}>Welcome to Pingvin Share</Title>
|
||||||
|
<Text>Let's customize Pingvin Share for you! </Text>
|
||||||
|
<AdminConfigTable />
|
||||||
|
<Button
|
||||||
|
loading={isLoading}
|
||||||
|
onClick={async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
await configService.finishSetup();
|
||||||
|
setIsLoading(false);
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
mb={70}
|
||||||
|
mt="lg"
|
||||||
|
>
|
||||||
|
Let me in
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Setup;
|
73
frontend/src/pages/admin/users.tsx
Normal file
73
frontend/src/pages/admin/users.tsx
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import { Button, Group, Space, Text, Title } from "@mantine/core";
|
||||||
|
import { useModals } from "@mantine/modals";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { TbPlus } from "react-icons/tb";
|
||||||
|
import ManageUserTable from "../../components/admin/ManageUserTable";
|
||||||
|
import showCreateUserModal from "../../components/admin/showCreateUserModal";
|
||||||
|
import userService from "../../services/user.service";
|
||||||
|
import User from "../../types/user.type";
|
||||||
|
import toast from "../../utils/toast.util";
|
||||||
|
|
||||||
|
const Users = () => {
|
||||||
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const modals = useModals();
|
||||||
|
|
||||||
|
const getUsers = () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
userService.list().then((users) => {
|
||||||
|
setUsers(users);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUser = (user: User) => {
|
||||||
|
modals.openConfirmModal({
|
||||||
|
title: `Delete ${user.username}?`,
|
||||||
|
children: (
|
||||||
|
<Text size="sm">
|
||||||
|
Do you really want to delete <b>{user.username}</b> and all his
|
||||||
|
shares?
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
labels: { confirm: "Delete", cancel: "Cancel" },
|
||||||
|
confirmProps: { color: "red" },
|
||||||
|
onConfirm: async () => {
|
||||||
|
userService
|
||||||
|
.remove(user.id)
|
||||||
|
.then(() => setUsers(users.filter((v) => v.id != user.id)))
|
||||||
|
.catch(toast.axiosError);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getUsers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Group position="apart" align="baseline" mb={20}>
|
||||||
|
<Title mb={30} order={3}>
|
||||||
|
User management
|
||||||
|
</Title>
|
||||||
|
<Button
|
||||||
|
onClick={() => showCreateUserModal(modals, getUsers)}
|
||||||
|
leftIcon={<TbPlus size={20} />}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<ManageUserTable
|
||||||
|
users={users}
|
||||||
|
getUsers={getUsers}
|
||||||
|
deleteUser={deleteUser}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
<Space h="xl" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Users;
|
|
@ -1,5 +1,5 @@
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import AuthForm from "../../components/auth/AuthForm";
|
import SignInForm from "../../components/auth/SignInForm";
|
||||||
import Meta from "../../components/Meta";
|
import Meta from "../../components/Meta";
|
||||||
import useUser from "../../hooks/user.hook";
|
import useUser from "../../hooks/user.hook";
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ const SignIn = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Sign In" />
|
<Meta title="Sign In" />
|
||||||
<AuthForm mode="signIn" />
|
<SignInForm />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,22 @@
|
||||||
import getConfig from "next/config";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import AuthForm from "../../components/auth/AuthForm";
|
import SignUpForm from "../../components/auth/SignUpForm";
|
||||||
import Meta from "../../components/Meta";
|
import Meta from "../../components/Meta";
|
||||||
|
import useConfig from "../../hooks/config.hook";
|
||||||
import useUser from "../../hooks/user.hook";
|
import useUser from "../../hooks/user.hook";
|
||||||
|
|
||||||
const { publicRuntimeConfig } = getConfig();
|
|
||||||
|
|
||||||
const SignUp = () => {
|
const SignUp = () => {
|
||||||
|
const config = useConfig();
|
||||||
const user = useUser();
|
const user = useUser();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
if (user) {
|
if (user) {
|
||||||
router.replace("/");
|
router.replace("/");
|
||||||
} else if (publicRuntimeConfig.ALLOW_REGISTRATION == "false") {
|
} else if (config.get("ALLOW_REGISTRATION") == "false") {
|
||||||
router.replace("/auth/signIn");
|
router.replace("/auth/signIn");
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Sign Up" />
|
<Meta title="Sign Up" />
|
||||||
<AuthForm mode="signUp" />
|
<SignUpForm />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,16 +8,14 @@ import {
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import getConfig from "next/config";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { TbCheck } from "react-icons/tb";
|
import { TbCheck } from "react-icons/tb";
|
||||||
import Meta from "../components/Meta";
|
import Meta from "../components/Meta";
|
||||||
|
import useConfig from "../hooks/config.hook";
|
||||||
import useUser from "../hooks/user.hook";
|
import useUser from "../hooks/user.hook";
|
||||||
|
|
||||||
const { publicRuntimeConfig } = getConfig();
|
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
inner: {
|
inner: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
@ -71,13 +69,14 @@ const useStyles = createStyles((theme) => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const config = useConfig();
|
||||||
const user = useUser();
|
const user = useUser();
|
||||||
|
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
if (user || publicRuntimeConfig.ALLOW_UNAUTHENTICATED_SHARES == "true") {
|
if (user || config.get("ALLOW_UNAUTHENTICATED_SHARES")) {
|
||||||
router.replace("/upload");
|
router.replace("/upload");
|
||||||
} else if (publicRuntimeConfig.SHOW_HOME_PAGE == "false") {
|
} else if (!config.get("SHOW_HOME_PAGE")) {
|
||||||
router.replace("/auth/signIn");
|
router.replace("/auth/signIn");
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Button, Group } from "@mantine/core";
|
import { Button, Group } from "@mantine/core";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import getConfig from "next/config";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Meta from "../components/Meta";
|
import Meta from "../components/Meta";
|
||||||
|
@ -9,13 +8,13 @@ import Dropzone from "../components/upload/Dropzone";
|
||||||
import FileList from "../components/upload/FileList";
|
import FileList from "../components/upload/FileList";
|
||||||
import showCompletedUploadModal from "../components/upload/modals/showCompletedUploadModal";
|
import showCompletedUploadModal from "../components/upload/modals/showCompletedUploadModal";
|
||||||
import showCreateUploadModal from "../components/upload/modals/showCreateUploadModal";
|
import showCreateUploadModal from "../components/upload/modals/showCreateUploadModal";
|
||||||
|
import useConfig from "../hooks/config.hook";
|
||||||
import useUser from "../hooks/user.hook";
|
import useUser from "../hooks/user.hook";
|
||||||
import shareService from "../services/share.service";
|
import shareService from "../services/share.service";
|
||||||
import { FileUpload } from "../types/File.type";
|
import { FileUpload } from "../types/File.type";
|
||||||
import { ShareSecurity } from "../types/share.type";
|
import { ShareSecurity } from "../types/share.type";
|
||||||
import toast from "../utils/toast.util";
|
import toast from "../utils/toast.util";
|
||||||
|
|
||||||
const { publicRuntimeConfig } = getConfig();
|
|
||||||
let share: any;
|
let share: any;
|
||||||
|
|
||||||
const Upload = () => {
|
const Upload = () => {
|
||||||
|
@ -23,6 +22,7 @@ const Upload = () => {
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
|
|
||||||
const user = useUser();
|
const user = useUser();
|
||||||
|
const config = useConfig();
|
||||||
const [files, setFiles] = useState<FileUpload[]>([]);
|
const [files, setFiles] = useState<FileUpload[]>([]);
|
||||||
const [isUploading, setisUploading] = useState(false);
|
const [isUploading, setisUploading] = useState(false);
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ const Upload = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [files]);
|
}, [files]);
|
||||||
if (!user && publicRuntimeConfig.ALLOW_UNAUTHENTICATED_SHARES == "false") {
|
if (!user && !config.get("ALLOW_UNAUTHENTICATED_SHARES")) {
|
||||||
router.replace("/");
|
router.replace("/");
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
@ -106,7 +106,19 @@ const Upload = () => {
|
||||||
loading={isUploading}
|
loading={isUploading}
|
||||||
disabled={files.length <= 0}
|
disabled={files.length <= 0}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
showCreateUploadModal(modals, user ? true : false, uploadFiles)
|
showCreateUploadModal(
|
||||||
|
modals,
|
||||||
|
{
|
||||||
|
isUserSignedIn: user ? true : false,
|
||||||
|
ALLOW_UNAUTHENTICATED_SHARES: config.get(
|
||||||
|
"ALLOW_UNAUTHENTICATED_SHARES"
|
||||||
|
),
|
||||||
|
ENABLE_EMAIL_RECIPIENTS: config.get(
|
||||||
|
"ENABLE_EMAIL_RECIPIENTS"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
uploadFiles
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Share
|
Share
|
||||||
|
|
|
@ -2,15 +2,22 @@ import { getCookie, setCookies } from "cookies-next";
|
||||||
import * as jose from "jose";
|
import * as jose from "jose";
|
||||||
import api from "./api.service";
|
import api from "./api.service";
|
||||||
|
|
||||||
const signIn = async (email: string, password: string) => {
|
const signIn = async (emailOrUsername: string, password: string) => {
|
||||||
const response = await api.post("auth/signIn", { email, password });
|
const emailOrUsernameBody = emailOrUsername.includes("@")
|
||||||
|
? { email: emailOrUsername }
|
||||||
|
: { username: emailOrUsername };
|
||||||
|
|
||||||
|
const response = await api.post("auth/signIn", {
|
||||||
|
...emailOrUsernameBody,
|
||||||
|
password,
|
||||||
|
});
|
||||||
setCookies("access_token", response.data.accessToken);
|
setCookies("access_token", response.data.accessToken);
|
||||||
setCookies("refresh_token", response.data.refreshToken);
|
setCookies("refresh_token", response.data.refreshToken);
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
const signUp = async (email: string, password: string) => {
|
const signUp = async (email: string, username: string, password: string) => {
|
||||||
return await api.post("auth/signUp", { email, password });
|
return await api.post("auth/signUp", { email, username, password });
|
||||||
};
|
};
|
||||||
|
|
||||||
const signOut = () => {
|
const signOut = () => {
|
||||||
|
@ -37,9 +44,14 @@ const refreshAccessToken = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updatePassword = async (oldPassword: string, password: string) => {
|
||||||
|
await api.patch("/auth/password", { oldPassword, password });
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
signIn,
|
signIn,
|
||||||
signUp,
|
signUp,
|
||||||
signOut,
|
signOut,
|
||||||
refreshAccessToken,
|
refreshAccessToken,
|
||||||
|
updatePassword,
|
||||||
};
|
};
|
||||||
|
|
43
frontend/src/services/config.service.ts
Normal file
43
frontend/src/services/config.service.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import Config, { AdminConfig } from "../types/config.type";
|
||||||
|
import api from "./api.service";
|
||||||
|
|
||||||
|
const list = async (): Promise<Config[]> => {
|
||||||
|
return (await api.get("/configs")).data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const listForAdmin = async (): Promise<AdminConfig[]> => {
|
||||||
|
return (await api.get("/configs/admin")).data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = async (
|
||||||
|
key: string,
|
||||||
|
value: string | number | boolean
|
||||||
|
): Promise<AdminConfig[]> => {
|
||||||
|
return (await api.patch(`/configs/admin/${key}`, { value })).data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const get = (key: string, configVariables: Config[]): any => {
|
||||||
|
if (!configVariables) return null;
|
||||||
|
|
||||||
|
const configVariable = configVariables.filter(
|
||||||
|
(variable) => variable.key == key
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
if (!configVariable) throw new Error(`Config variable ${key} not found`);
|
||||||
|
|
||||||
|
if (configVariable.type == "number") return parseInt(configVariable.value);
|
||||||
|
if (configVariable.type == "boolean") return configVariable.value == "true";
|
||||||
|
if (configVariable.type == "string") return configVariable.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const finishSetup = async (): Promise<AdminConfig[]> => {
|
||||||
|
return (await api.post("/configs/admin/finishSetup")).data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
list,
|
||||||
|
listForAdmin,
|
||||||
|
update,
|
||||||
|
get,
|
||||||
|
finishSetup,
|
||||||
|
};
|
|
@ -80,7 +80,7 @@ const uploadFile = async (
|
||||||
const response = await api.post(`shares/${shareId}/files`, formData, {
|
const response = await api.post(`shares/${shareId}/files`, formData, {
|
||||||
onUploadProgress: (progressEvent) => {
|
onUploadProgress: (progressEvent) => {
|
||||||
const uploadingProgress = Math.round(
|
const uploadingProgress = Math.round(
|
||||||
(100 * progressEvent.loaded) / progressEvent.total
|
(100 * progressEvent.loaded) / (progressEvent.total ?? 1)
|
||||||
);
|
);
|
||||||
if (uploadingProgress < 100) progressCallBack(uploadingProgress);
|
if (uploadingProgress < 100) progressCallBack(uploadingProgress);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,36 @@
|
||||||
import { CurrentUser } from "../types/user.type";
|
import {
|
||||||
|
CreateUser,
|
||||||
|
CurrentUser,
|
||||||
|
UpdateCurrentUser,
|
||||||
|
UpdateUser,
|
||||||
|
} from "../types/user.type";
|
||||||
import api from "./api.service";
|
import api from "./api.service";
|
||||||
import authService from "./auth.service";
|
import authService from "./auth.service";
|
||||||
|
|
||||||
|
const list = async () => {
|
||||||
|
return (await api.get("/users")).data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (user: CreateUser) => {
|
||||||
|
return (await api.post("/users", user)).data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = async (id: string, user: UpdateUser) => {
|
||||||
|
return (await api.patch(`/users/${id}`, user)).data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = async (id: string) => {
|
||||||
|
await api.delete(`/users/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCurrentUser = async (user: UpdateCurrentUser) => {
|
||||||
|
return (await api.patch("/users/me", user)).data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeCurrentUser = async () => {
|
||||||
|
await api.delete("/users/me");
|
||||||
|
};
|
||||||
|
|
||||||
const getCurrentUser = async (): Promise<CurrentUser | null> => {
|
const getCurrentUser = async (): Promise<CurrentUser | null> => {
|
||||||
try {
|
try {
|
||||||
await authService.refreshAccessToken();
|
await authService.refreshAccessToken();
|
||||||
|
@ -12,5 +41,11 @@ const getCurrentUser = async (): Promise<CurrentUser | null> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
list,
|
||||||
|
create,
|
||||||
|
update,
|
||||||
|
remove,
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
|
updateCurrentUser,
|
||||||
|
removeCurrentUser,
|
||||||
};
|
};
|
||||||
|
|
13
frontend/src/types/config.type.ts
Normal file
13
frontend/src/types/config.type.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
type Config = {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdminConfig = Config & {
|
||||||
|
updatedAt: Date;
|
||||||
|
secret: boolean;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Config;
|
|
@ -1,8 +1,29 @@
|
||||||
export default interface User {
|
type User = {
|
||||||
id: string;
|
id: string;
|
||||||
firstName?: string;
|
username: string;
|
||||||
lastName?: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
}
|
isAdmin: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export interface CurrentUser extends User {}
|
export type CreateUser = {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
isAdmin?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateUser = {
|
||||||
|
username?: string;
|
||||||
|
email?: string;
|
||||||
|
password?: string;
|
||||||
|
isAdmin?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateCurrentUser = {
|
||||||
|
username?: string;
|
||||||
|
email?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CurrentUser = User & {};
|
||||||
|
|
||||||
|
export default User;
|
||||||
|
|
|
@ -10,6 +10,9 @@ const error = (message: string) =>
|
||||||
message: message,
|
message: message,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const axiosError = (axiosError: any) =>
|
||||||
|
error(axiosError?.response?.data?.message ?? "An unknown error occured");
|
||||||
|
|
||||||
const success = (message: string) =>
|
const success = (message: string) =>
|
||||||
showNotification({
|
showNotification({
|
||||||
icon: <TbCheck />,
|
icon: <TbCheck />,
|
||||||
|
@ -22,5 +25,6 @@ const success = (message: string) =>
|
||||||
const toast = {
|
const toast = {
|
||||||
error,
|
error,
|
||||||
success,
|
success,
|
||||||
|
axiosError,
|
||||||
};
|
};
|
||||||
export default toast;
|
export default toast;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user