mirror of
https://github.com/stonith404/pingvin-share.git
synced 2025-02-19 01:55:48 -05:00
feat: add setup wizard
This commit is contained in:
parent
493705e4ef
commit
b579b8f330
32 changed files with 689 additions and 179 deletions
|
@ -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") SELECT "createdAt", "email", "id", "password", "updatedAt" 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,11 +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
|
||||||
isAdministrator Boolean @default(false)
|
password String
|
||||||
firstName String?
|
isAdmin Boolean @default(false)
|
||||||
lastName String?
|
|
||||||
|
|
||||||
shares Share[]
|
shares Share[]
|
||||||
refreshTokens RefreshToken[]
|
refreshTokens RefreshToken[]
|
||||||
|
@ -81,10 +80,10 @@ model ShareSecurity {
|
||||||
model Config {
|
model Config {
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
key String @id
|
key String @id
|
||||||
type String
|
type String
|
||||||
value String?
|
value String
|
||||||
default String
|
description String
|
||||||
secret Boolean @default(true)
|
secret Boolean @default(true)
|
||||||
locked Boolean @default(false)
|
locked Boolean @default(false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,79 +1,8 @@
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import configVariables from "../../src/configVariables";
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
const configVariables = [
|
|
||||||
{
|
|
||||||
key: "setupFinished",
|
|
||||||
type: "boolean",
|
|
||||||
default: "false",
|
|
||||||
secret: false,
|
|
||||||
locked: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "appUrl",
|
|
||||||
type: "string",
|
|
||||||
default: "http://localhost:3000",
|
|
||||||
secret: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "showHomePage",
|
|
||||||
type: "boolean",
|
|
||||||
default: "true",
|
|
||||||
secret: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "allowRegistration",
|
|
||||||
type: "boolean",
|
|
||||||
default: "true",
|
|
||||||
secret: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "allowUnauthenticatedShares",
|
|
||||||
type: "boolean",
|
|
||||||
default: "false",
|
|
||||||
secret: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "maxFileSize",
|
|
||||||
type: "number",
|
|
||||||
default: "1000000000",
|
|
||||||
secret: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "jwtSecret",
|
|
||||||
type: "string",
|
|
||||||
default: "long-random-string",
|
|
||||||
locked: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "emailRecipientsEnabled",
|
|
||||||
type: "boolean",
|
|
||||||
default: "false",
|
|
||||||
secret: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "smtpHost",
|
|
||||||
type: "string",
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "smtpPort",
|
|
||||||
type: "number",
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "smtpEmail",
|
|
||||||
type: "string",
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "smtpPassword",
|
|
||||||
type: "string",
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
for (const variable of configVariables) {
|
for (const variable of configVariables) {
|
||||||
const existingConfigVariable = await prisma.config.findUnique({
|
const existingConfigVariable = await prisma.config.findUnique({
|
||||||
|
@ -85,14 +14,6 @@ async function main() {
|
||||||
await prisma.config.create({
|
await prisma.config.create({
|
||||||
data: variable,
|
data: variable,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// Update the config variable if the default value has changed
|
|
||||||
if (existingConfigVariable.default != variable.default) {
|
|
||||||
await prisma.config.update({
|
|
||||||
where: { key: variable.key },
|
|
||||||
data: { default: variable.default },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { Module } from "@nestjs/common";
|
import { HttpException, HttpStatus, Module } from "@nestjs/common";
|
||||||
|
|
||||||
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 { JobsService } from "./jobs/jobs.service";
|
||||||
|
|
||||||
import { APP_GUARD } from "@nestjs/core";
|
import { APP_GUARD } from "@nestjs/core";
|
||||||
|
import { MulterModule } from "@nestjs/platform-express";
|
||||||
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
|
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
|
||||||
|
import { Request } from "express";
|
||||||
import { ConfigModule } from "./config/config.module";
|
import { ConfigModule } from "./config/config.module";
|
||||||
import { ConfigService } from "./config/config.service";
|
import { ConfigService } from "./config/config.service";
|
||||||
import { EmailModule } from "./email/email.module";
|
import { EmailModule } from "./email/email.module";
|
||||||
|
@ -25,6 +27,24 @@ import { UserController } from "./user/user.controller";
|
||||||
EmailModule,
|
EmailModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
|
MulterModule.registerAsync({
|
||||||
|
useFactory: (config: ConfigService) => ({
|
||||||
|
fileFilter: (req: Request, file, cb) => {
|
||||||
|
const maxFileSize = config.get("maxFileSize");
|
||||||
|
const requestFileSize = parseInt(req.headers["content-length"]);
|
||||||
|
const isValidFileSize = requestFileSize <= maxFileSize;
|
||||||
|
cb(
|
||||||
|
!isValidFileSize &&
|
||||||
|
new HttpException(
|
||||||
|
`File must be smaller than ${maxFileSize} bytes`,
|
||||||
|
HttpStatus.PAYLOAD_TOO_LARGE
|
||||||
|
),
|
||||||
|
isValidFileSize
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
ThrottlerModule.forRoot({
|
ThrottlerModule.forRoot({
|
||||||
ttl: 60,
|
ttl: 60,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
|
|
|
@ -27,7 +27,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("setupFinished"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -38,16 +40,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 }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,17 @@
|
||||||
|
import { PickType } from "@nestjs/swagger";
|
||||||
|
import { Expose } from "class-transformer";
|
||||||
|
import { IsEmail, Length, Matches } from "class-validator";
|
||||||
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, ["password"] as const) {
|
||||||
|
@Expose()
|
||||||
|
@Matches("^[a-zA-Z0-9_.]*$", undefined, {
|
||||||
|
message: "Username can only contain letters, numbers, dots and underscores",
|
||||||
|
})
|
||||||
|
@Length(3, 32)
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { PickType } from "@nestjs/swagger";
|
||||||
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, [
|
||||||
|
"username",
|
||||||
"email",
|
"email",
|
||||||
"password",
|
"password",
|
||||||
] as const) {}
|
] as const) {}
|
||||||
|
|
|
@ -3,9 +3,11 @@ import { User } from "@prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdministratorGuard implements CanActivate {
|
export class AdministratorGuard implements CanActivate {
|
||||||
canActivate(context: ExecutionContext): boolean {
|
canActivate(context: ExecutionContext) {
|
||||||
const { user }: { user: User } = context.switchToHttp().getRequest();
|
const { user }: { user: User } = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
return user.isAdministrator;
|
|
||||||
|
return user.isAdmin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ 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) {
|
||||||
console.log(config.get("jwtSecret"));
|
|
||||||
config.get("jwtSecret");
|
config.get("jwtSecret");
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
@ -17,11 +16,9 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async validate(payload: { sub: string }) {
|
async validate(payload: { sub: string }) {
|
||||||
console.log("vali");
|
|
||||||
const user: User = await this.prisma.user.findUnique({
|
const user: User = await this.prisma.user.findUnique({
|
||||||
where: { id: payload.sub },
|
where: { id: payload.sub },
|
||||||
});
|
});
|
||||||
console.log({ user });
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
import { Body, Controller, Get, Param, Patch, UseGuards } from "@nestjs/common";
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
import { AdministratorGuard } from "src/auth/guard/isAdmin.guard";
|
import { AdministratorGuard } from "src/auth/guard/isAdmin.guard";
|
||||||
|
import { JwtGuard } from "src/auth/guard/jwt.guard";
|
||||||
import { ConfigService } from "./config.service";
|
import { ConfigService } from "./config.service";
|
||||||
import { AdminConfigDTO } from "./dto/adminConfig.dto";
|
import { AdminConfigDTO } from "./dto/adminConfig.dto";
|
||||||
import { ConfigDTO } from "./dto/config.dto";
|
import { ConfigDTO } from "./dto/config.dto";
|
||||||
|
@ -15,7 +24,7 @@ export class ConfigController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("admin")
|
@Get("admin")
|
||||||
@UseGuards(AdministratorGuard)
|
@UseGuards(JwtGuard, AdministratorGuard)
|
||||||
async listForAdmin() {
|
async listForAdmin() {
|
||||||
return new AdminConfigDTO().fromList(
|
return new AdminConfigDTO().fromList(
|
||||||
await this.configService.listForAdmin()
|
await this.configService.listForAdmin()
|
||||||
|
@ -23,10 +32,16 @@ export class ConfigController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch("admin/:key")
|
@Patch("admin/:key")
|
||||||
@UseGuards(AdministratorGuard)
|
@UseGuards(JwtGuard, AdministratorGuard)
|
||||||
async update(@Param("key") key: string, @Body() data: UpdateConfigDTO) {
|
async update(@Param("key") key: string, @Body() data: UpdateConfigDTO) {
|
||||||
return new AdminConfigDTO().from(
|
return new AdminConfigDTO().from(
|
||||||
await this.configService.update(key, data.value)
|
await this.configService.update(key, data.value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post("admin/finishSetup")
|
||||||
|
@UseGuards(JwtGuard, AdministratorGuard)
|
||||||
|
async finishSetup() {
|
||||||
|
return await this.configService.finishSetup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,27 +21,21 @@ export class ConfigService {
|
||||||
|
|
||||||
if (!configVariable) throw new Error(`Config variable ${key} not found`);
|
if (!configVariable) throw new Error(`Config variable ${key} not found`);
|
||||||
|
|
||||||
const value = configVariable.value ?? configVariable.default;
|
if (configVariable.type == "number") return parseInt(configVariable.value);
|
||||||
|
if (configVariable.type == "boolean") return configVariable.value == "true";
|
||||||
if (configVariable.type == "number") return parseInt(value);
|
if (configVariable.type == "string") return configVariable.value;
|
||||||
if (configVariable.type == "boolean") return value == "true";
|
|
||||||
if (configVariable.type == "string") return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async listForAdmin() {
|
async listForAdmin() {
|
||||||
return await this.prisma.config.findMany();
|
return await this.prisma.config.findMany({
|
||||||
|
where: { locked: { equals: false } },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async list() {
|
async list() {
|
||||||
const configVariables = await this.prisma.config.findMany({
|
return await this.prisma.config.findMany({
|
||||||
where: { secret: { equals: false } },
|
where: { secret: { equals: false } },
|
||||||
});
|
});
|
||||||
|
|
||||||
return configVariables.map((configVariable) => {
|
|
||||||
if (!configVariable.value) configVariable.value = configVariable.default;
|
|
||||||
|
|
||||||
return configVariable;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(key: string, value: string | number | boolean) {
|
async update(key: string, value: string | number | boolean) {
|
||||||
|
@ -57,9 +51,20 @@ export class ConfigService {
|
||||||
`Config variable must be of type ${configVariable.type}`
|
`Config variable must be of type ${configVariable.type}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return await this.prisma.config.update({
|
const updatedVariable = await this.prisma.config.update({
|
||||||
where: { key },
|
where: { key },
|
||||||
data: { value: value.toString() },
|
data: { value: value.toString() },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.configVariables = await this.prisma.config.findMany();
|
||||||
|
|
||||||
|
return updatedVariable;
|
||||||
|
}
|
||||||
|
|
||||||
|
async finishSetup() {
|
||||||
|
return await this.prisma.config.update({
|
||||||
|
where: { key: "setupFinished" },
|
||||||
|
data: { value: "true" },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,17 +2,19 @@ import { Expose, plainToClass } from "class-transformer";
|
||||||
import { ConfigDTO } from "./config.dto";
|
import { ConfigDTO } from "./config.dto";
|
||||||
|
|
||||||
export class AdminConfigDTO extends ConfigDTO {
|
export class AdminConfigDTO extends ConfigDTO {
|
||||||
@Expose()
|
|
||||||
default: string;
|
|
||||||
|
|
||||||
@Expose()
|
@Expose()
|
||||||
secret: boolean;
|
secret: boolean;
|
||||||
|
|
||||||
@Expose()
|
@Expose()
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
description: string;
|
||||||
|
|
||||||
from(partial: Partial<AdminConfigDTO>) {
|
from(partial: Partial<AdminConfigDTO>) {
|
||||||
return plainToClass(AdminConfigDTO, partial, { excludeExtraneousValues: true });
|
return plainToClass(AdminConfigDTO, partial, {
|
||||||
|
excludeExtraneousValues: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fromList(partial: Partial<AdminConfigDTO>[]) {
|
fromList(partial: Partial<AdminConfigDTO>[]) {
|
||||||
|
|
88
backend/src/configVariables.ts
Normal file
88
backend/src/configVariables.ts
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import * as crypto from "crypto";
|
||||||
|
|
||||||
|
const configVariables = [
|
||||||
|
{
|
||||||
|
key: "setupFinished",
|
||||||
|
description: "Whether the setup has been finished",
|
||||||
|
type: "boolean",
|
||||||
|
value: "false",
|
||||||
|
secret: false,
|
||||||
|
locked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "appUrl",
|
||||||
|
description: "On which URL Pingvin Share is available",
|
||||||
|
type: "string",
|
||||||
|
value: "http://localhost:3000",
|
||||||
|
secret: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "showHomePage",
|
||||||
|
description: "Whether to show the home page",
|
||||||
|
type: "boolean",
|
||||||
|
value: "true",
|
||||||
|
secret: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "allowRegistration",
|
||||||
|
description: "Whether registration is allowed",
|
||||||
|
type: "boolean",
|
||||||
|
value: "true",
|
||||||
|
secret: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "allowUnauthenticatedShares",
|
||||||
|
description: "Whether unauthorized users can create shares",
|
||||||
|
type: "boolean",
|
||||||
|
value: "false",
|
||||||
|
secret: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "maxFileSize",
|
||||||
|
description: "Maximum file size in bytes",
|
||||||
|
type: "number",
|
||||||
|
value: "1000000000",
|
||||||
|
secret: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "jwtSecret",
|
||||||
|
description: "Long random string used to sign JWT tokens",
|
||||||
|
type: "string",
|
||||||
|
value: crypto.randomBytes(256).toString("base64"),
|
||||||
|
locked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "emailRecipientsEnabled",
|
||||||
|
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: "smtpHost",
|
||||||
|
description: "Host of the SMTP server",
|
||||||
|
type: "string",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "smtpPort",
|
||||||
|
description: "Port of the SMTP server",
|
||||||
|
type: "number",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "smtpEmail",
|
||||||
|
description: "Email address of the SMTP server",
|
||||||
|
type: "string",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "smtpPassword",
|
||||||
|
description: "Password of the SMTP server",
|
||||||
|
type: "string",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default configVariables;
|
|
@ -23,17 +23,13 @@ export class EmailService {
|
||||||
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 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 🐧`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ import { ShareDTO } from "src/share/dto/share.dto";
|
||||||
import { ShareOwnerGuard } from "src/share/guard/shareOwner.guard";
|
import { ShareOwnerGuard } from "src/share/guard/shareOwner.guard";
|
||||||
import { ShareSecurityGuard } from "src/share/guard/shareSecurity.guard";
|
import { ShareSecurityGuard } from "src/share/guard/shareSecurity.guard";
|
||||||
import { FileService } from "./file.service";
|
import { FileService } from "./file.service";
|
||||||
import { FileValidationPipe } from "./pipe/fileValidation.pipe";
|
|
||||||
|
|
||||||
@Controller("shares/:shareId/files")
|
@Controller("shares/:shareId/files")
|
||||||
export class FileController {
|
export class FileController {
|
||||||
|
@ -32,7 +31,7 @@ export class FileController {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
async create(
|
async create(
|
||||||
@UploadedFile(FileValidationPipe)
|
@UploadedFile()
|
||||||
file: Express.Multer.File,
|
file: Express.Multer.File,
|
||||||
@Param("shareId") shareId: string
|
@Param("shareId") shareId: string
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import { ArgumentMetadata, Injectable, PipeTransform } from "@nestjs/common";
|
import {
|
||||||
|
ArgumentMetadata,
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
PipeTransform,
|
||||||
|
} from "@nestjs/common";
|
||||||
import { ConfigService } from "src/config/config.service";
|
import { ConfigService } from "src/config/config.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FileValidationPipe implements PipeTransform {
|
export class FileValidationPipe implements PipeTransform {
|
||||||
constructor(private config: ConfigService) {}
|
constructor(private config: ConfigService) {}
|
||||||
async transform(value: any, metadata: ArgumentMetadata) {
|
async transform(value: any, metadata: ArgumentMetadata) {
|
||||||
// "value" is an object containing the file's attributes and metadata
|
if (value.size > this.config.get("maxFileSize"))
|
||||||
console.log(this.config.get("maxFileSize"));
|
throw new BadRequestException("File is ");
|
||||||
const oneKb = 1000;
|
return value;
|
||||||
return value.size < oneKb;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,18 +1,17 @@
|
||||||
import { Expose, plainToClass } from "class-transformer";
|
import { Expose, plainToClass } from "class-transformer";
|
||||||
import { IsEmail, IsNotEmpty, IsString } from "class-validator";
|
import { IsEmail, IsNotEmpty, IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
export class UserDTO {
|
export class UserDTO {
|
||||||
@Expose()
|
@Expose()
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@Expose()
|
@Expose()
|
||||||
firstName: string;
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
username: string;
|
||||||
|
|
||||||
@Expose()
|
@Expose()
|
||||||
lastName: string;
|
@IsOptional()
|
||||||
|
|
||||||
@Expose()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
|
@ -20,6 +19,9 @@ export class UserDTO {
|
||||||
@IsString()
|
@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 });
|
||||||
}
|
}
|
||||||
|
|
95
frontend/src/components/admin/AdminConfigTable.tsx
Normal file
95
frontend/src/components/admin/AdminConfigTable.tsx
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
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 = () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
configService.listForAdmin().then((configVariables) => {
|
||||||
|
setConfigVariables(configVariables);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getConfigVariables();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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;
|
|
@ -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((e) => toast.error(e.response.data.message));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default showUpdateConfigVariableModal;
|
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((e) => toast.error(e.response.data.message));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size={420} my={40}>
|
||||||
|
<Title
|
||||||
|
align="center"
|
||||||
|
sx={(theme) => ({
|
||||||
|
fontFamily: `Greycliff CF, ${theme.fontFamily}`,
|
||||||
|
fontWeight: 900,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Welcome back
|
||||||
|
</Title>
|
||||||
|
{config.get("allowRegistration") && (
|
||||||
|
<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;
|
|
@ -15,17 +15,19 @@ 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 AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => {
|
const SignUpForm = () => {
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
|
|
||||||
const validationSchema = yup.object().shape({
|
const validationSchema = yup.object().shape({
|
||||||
email: yup.string().email().required(),
|
email: yup.string().email().required(),
|
||||||
|
username: yup.string().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,12 +36,12 @@ 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((e) => toast.error(e.response.data.message));
|
||||||
};
|
};
|
||||||
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((e) => toast.error(e.response.data.message));
|
||||||
};
|
};
|
||||||
|
@ -53,33 +55,31 @@ const AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => {
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{mode == "signUp" ? "Sign up" : "Welcome back"}
|
Sign up
|
||||||
</Title>
|
</Title>
|
||||||
{config.get("allowRegistration") && (
|
{config.get("allowRegistration") && (
|
||||||
<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 } 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,16 @@ const ActionAvatar = () => {
|
||||||
>
|
>
|
||||||
My shares
|
My shares
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
{user!.isAdmin && (
|
||||||
|
<Menu.Item
|
||||||
|
component={Link}
|
||||||
|
href="/admin/config"
|
||||||
|
icon={<TbSettings size={14} />}
|
||||||
|
>
|
||||||
|
Administration
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
authService.signOut();
|
authService.signOut();
|
||||||
|
|
|
@ -8,9 +8,10 @@ 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 { ConfigContext } from "../hooks/config.hook";
|
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 configService from "../services/config.service";
|
||||||
|
@ -23,15 +24,17 @@ 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 [config, setConfig] = useState<Config[] | null>(null);
|
const [configVariables, setConfigVariables] = useState<Config[] | null>(null);
|
||||||
|
|
||||||
const getInitalData = async () => {
|
const getInitalData = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setConfig(await configService.getAll());
|
setConfigVariables(await configService.list());
|
||||||
await authService.refreshAccessToken();
|
await authService.refreshAccessToken();
|
||||||
setUser(await userService.getCurrentUser());
|
setUser(await userService.getCurrentUser());
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
@ -42,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]);
|
||||||
|
@ -59,7 +72,7 @@ function App({ Component, pageProps }: AppProps) {
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<LoadingOverlay visible overlayOpacity={1} />
|
<LoadingOverlay visible overlayOpacity={1} />
|
||||||
) : (
|
) : (
|
||||||
<ConfigContext.Provider value={config}>
|
<ConfigContext.Provider value={configVariables}>
|
||||||
<UserContext.Provider value={user}>
|
<UserContext.Provider value={user}>
|
||||||
<LoadingOverlay visible={isLoading} overlayOpacity={1} />
|
<LoadingOverlay visible={isLoading} overlayOpacity={1} />
|
||||||
<Header />
|
<Header />
|
||||||
|
|
13
frontend/src/pages/admin/config.tsx
Normal file
13
frontend/src/pages/admin/config.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { Space } from "@mantine/core";
|
||||||
|
import AdminConfigTable from "../../components/admin/AdminConfigTable";
|
||||||
|
|
||||||
|
const AdminConfig = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AdminConfigTable />
|
||||||
|
<Space h="xl" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminConfig;
|
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("setupFinished")) {
|
||||||
|
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;
|
|
@ -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,5 +1,5 @@
|
||||||
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 useConfig from "../../hooks/config.hook";
|
||||||
import useUser from "../../hooks/user.hook";
|
import useUser from "../../hooks/user.hook";
|
||||||
|
@ -16,7 +16,7 @@ const SignUp = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Sign Up" />
|
<Meta title="Sign Up" />
|
||||||
<AuthForm mode="signUp" />
|
<SignUpForm />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = () => {
|
||||||
|
|
|
@ -1,11 +1,24 @@
|
||||||
import Config from "../types/config.type";
|
import Config, { AdminConfig } from "../types/config.type";
|
||||||
import api from "./api.service";
|
import api from "./api.service";
|
||||||
|
|
||||||
const getAll = async (): Promise<Config[]> => {
|
const list = async (): Promise<Config[]> => {
|
||||||
return (await api.get("/configs")).data;
|
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 => {
|
const get = (key: string, configVariables: Config[]): any => {
|
||||||
|
if (!configVariables) return null;
|
||||||
|
|
||||||
const configVariable = configVariables.filter(
|
const configVariable = configVariables.filter(
|
||||||
(variable) => variable.key == key
|
(variable) => variable.key == key
|
||||||
)[0];
|
)[0];
|
||||||
|
@ -17,7 +30,14 @@ const get = (key: string, configVariables: Config[]): any => {
|
||||||
if (configVariable.type == "string") return configVariable.value;
|
if (configVariable.type == "string") return configVariable.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
const finishSetup = async (): Promise<AdminConfig[]> => {
|
||||||
getAll,
|
return (await api.post("/configs/admin/finishSetup")).data;
|
||||||
get,
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
list,
|
||||||
|
listForAdmin,
|
||||||
|
update,
|
||||||
|
get,
|
||||||
|
finishSetup,
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,4 +4,10 @@ type Config = {
|
||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AdminConfig = Config & {
|
||||||
|
updatedAt: Date;
|
||||||
|
secret: boolean;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default Config;
|
export default Config;
|
||||||
|
|
|
@ -3,6 +3,7 @@ export default interface User {
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
isAdmin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CurrentUser extends User {}
|
export interface CurrentUser extends User {}
|
||||||
|
|
Loading…
Add table
Reference in a new issue