mirror of
https://github.com/stonith404/pingvin-share.git
synced 2024-07-02 07:20:38 +02:00
feat: add new config strategy to backend
This commit is contained in:
parent
13f98cc32c
commit
1b5e53ff7e
|
@ -9,6 +9,9 @@
|
||||||
"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": "npx 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.1.2",
|
||||||
"@nestjs/config": "^2.2.0",
|
"@nestjs/config": "^2.2.0",
|
||||||
|
|
|
@ -77,3 +77,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?
|
||||||
|
default String
|
||||||
|
secret Boolean @default(true)
|
||||||
|
locked Boolean @default(false)
|
||||||
|
}
|
||||||
|
|
118
backend/prisma/seed/config.seed.ts
Normal file
118
backend/prisma/seed/config.seed.ts
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
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() {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
} 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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,11 +1,14 @@
|
||||||
import { Module } from "@nestjs/common";
|
import { 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 { JobsService } from "./jobs/jobs.service";
|
||||||
|
|
||||||
import { APP_GUARD } from "@nestjs/core";
|
import { APP_GUARD } from "@nestjs/core";
|
||||||
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
|
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
|
||||||
|
import { ConfigModule } from "./config/config.module";
|
||||||
|
import { ConfigService } from "./config/config.service";
|
||||||
|
import { EmailModule } from "./email/email.module";
|
||||||
import { FileController } from "./file/file.controller";
|
import { FileController } from "./file/file.controller";
|
||||||
import { FileModule } from "./file/file.module";
|
import { FileModule } from "./file/file.module";
|
||||||
import { PrismaModule } from "./prisma/prisma.module";
|
import { PrismaModule } from "./prisma/prisma.module";
|
||||||
|
@ -13,7 +16,6 @@ import { PrismaService } from "./prisma/prisma.service";
|
||||||
import { ShareController } from "./share/share.controller";
|
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 { UserController } from "./user/user.controller";
|
||||||
import { EmailModule } from "./email/email.module";
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -22,7 +24,7 @@ import { EmailModule } from "./email/email.module";
|
||||||
FileModule,
|
FileModule,
|
||||||
EmailModule,
|
EmailModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule,
|
||||||
ThrottlerModule.forRoot({
|
ThrottlerModule.forRoot({
|
||||||
ttl: 60,
|
ttl: 60,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
|
@ -30,8 +32,16 @@ import { EmailModule } from "./email/email.module";
|
||||||
ScheduleModule.forRoot(),
|
ScheduleModule.forRoot(),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
ConfigService,
|
||||||
PrismaService,
|
PrismaService,
|
||||||
JobsService,
|
JobsService,
|
||||||
|
{
|
||||||
|
provide: "CONFIG_VARIABLES",
|
||||||
|
useFactory: async (prisma: PrismaService) => {
|
||||||
|
return await prisma.config.findMany();
|
||||||
|
},
|
||||||
|
inject: [PrismaService],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: APP_GUARD,
|
provide: APP_GUARD,
|
||||||
useClass: ThrottlerGuard,
|
useClass: ThrottlerGuard,
|
||||||
|
|
|
@ -5,8 +5,8 @@ import {
|
||||||
HttpCode,
|
HttpCode,
|
||||||
Post,
|
Post,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { Throttle } from "@nestjs/throttler";
|
import { Throttle } from "@nestjs/throttler";
|
||||||
|
import { ConfigService } from "src/config/config.service";
|
||||||
import { AuthService } from "./auth.service";
|
import { AuthService } from "./auth.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";
|
||||||
|
@ -21,8 +21,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("allowRegistration"))
|
||||||
throw new ForbiddenException("Registration is not allowed");
|
throw new ForbiddenException("Registration is not allowed");
|
||||||
return this.authService.signUp(dto);
|
return this.authService.signUp(dto);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,12 @@ import {
|
||||||
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";
|
||||||
|
@ -68,7 +68,7 @@ export class AuthService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
expiresIn: "15min",
|
expiresIn: "15min",
|
||||||
secret: this.config.get("JWT_SECRET"),
|
secret: this.config.get("jwtSecret"),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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("allowUnauthenticatedShares");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,27 @@
|
||||||
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) {
|
||||||
|
console.log(config.get("jwtSecret"));
|
||||||
|
config.get("jwtSecret");
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
secretOrKey: config.get("JWT_SECRET"),
|
secretOrKey: config.get("jwtSecret"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
18
backend/src/config/config.controller.ts
Normal file
18
backend/src/config/config.controller.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { Controller, Get } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "./config.service";
|
||||||
|
import { ConfigDTO } from "./dto/config.dto";
|
||||||
|
|
||||||
|
@Controller("configs")
|
||||||
|
export class ConfigController {
|
||||||
|
constructor(private configService: ConfigService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async list() {
|
||||||
|
return new ConfigDTO().fromList(await this.configService.list())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("admin")
|
||||||
|
async listForAdmin() {
|
||||||
|
return await this.configService.listForAdmin();
|
||||||
|
}
|
||||||
|
}
|
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 {}
|
41
backend/src/config/config.service.ts
Normal file
41
backend/src/config/config.service.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { Inject, Injectable } 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`);
|
||||||
|
|
||||||
|
const value = configVariable.value ?? configVariable.default;
|
||||||
|
|
||||||
|
if (configVariable.type == "number") return parseInt(value);
|
||||||
|
if (configVariable.type == "boolean") return value == "true";
|
||||||
|
if (configVariable.type == "string") return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listForAdmin() {
|
||||||
|
return await this.prisma.config.findMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
async list() {
|
||||||
|
const configVariables = await this.prisma.config.findMany({
|
||||||
|
where: { secret: { equals: false } },
|
||||||
|
});
|
||||||
|
|
||||||
|
return configVariables.map((configVariable) => {
|
||||||
|
if (!configVariable.value) configVariable.value = configVariable.default;
|
||||||
|
|
||||||
|
return configVariable;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
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 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,34 +1,35 @@
|
||||||
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("emailRecepientsEnabled"))
|
||||||
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 ?
|
const creatorIdentifier = creator
|
||||||
creator.firstName && creator.lastName
|
? creator.firstName && creator.lastName
|
||||||
? `${creator.firstName} ${creator.lastName}`
|
? `${creator.firstName} ${creator.lastName}`
|
||||||
: creator.email : "A Pingvin Share user";
|
: 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",
|
||||||
|
|
|
@ -2,7 +2,6 @@ import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
Param,
|
Param,
|
||||||
ParseFilePipeBuilder,
|
|
||||||
Post,
|
Post,
|
||||||
Res,
|
Res,
|
||||||
StreamableFile,
|
StreamableFile,
|
||||||
|
@ -19,6 +18,7 @@ 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,13 +32,7 @@ export class FileController {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
async create(
|
async create(
|
||||||
@UploadedFile(
|
@UploadedFile(FileValidationPipe)
|
||||||
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()
|
||||||
|
@ -78,14 +78,14 @@ export class FileService {
|
||||||
return fs.createReadStream(`./data/uploads/shares/${shareId}/archive.zip`);
|
return fs.createReadStream(`./data/uploads/shares/${shareId}/archive.zip`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFileDownloadUrl(shareId: string, fileId: string) {
|
async 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}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateFileDownloadToken(shareId: string, fileId: string) {
|
async generateFileDownloadToken(shareId: string, fileId: string) {
|
||||||
if (fileId == "zip") fileId = undefined;
|
if (fileId == "zip") fileId = undefined;
|
||||||
|
|
||||||
return this.jwtService.sign(
|
return this.jwtService.sign(
|
||||||
|
@ -95,15 +95,15 @@ export class FileService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
expiresIn: "10min",
|
expiresIn: "10min",
|
||||||
secret: this.config.get("JWT_SECRET"),
|
secret: this.config.get("jwtSecret"),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyFileDownloadToken(shareId: string, token: string) {
|
async verifyFileDownloadToken(shareId: string, token: string) {
|
||||||
try {
|
try {
|
||||||
const claims = this.jwtService.verify(token, {
|
const claims = this.jwtService.verify(token, {
|
||||||
secret: this.config.get("JWT_SECRET"),
|
secret: this.config.get("jwtSecret"),
|
||||||
});
|
});
|
||||||
return claims.shareId == shareId;
|
return claims.shareId == shareId;
|
||||||
} catch {
|
} catch {
|
||||||
|
|
13
backend/src/file/pipe/fileValidation.pipe.ts
Normal file
13
backend/src/file/pipe/fileValidation.pipe.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { ArgumentMetadata, 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) {
|
||||||
|
// "value" is an object containing the file's attributes and metadata
|
||||||
|
console.log(this.config.get("maxFileSize"));
|
||||||
|
const oneKb = 1000;
|
||||||
|
return value.size < oneKb;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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: {
|
||||||
|
|
|
@ -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";
|
||||||
|
@ -235,7 +235,7 @@ export class ShareService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
expiresIn: moment(expiration).diff(new Date(), "seconds") + "s",
|
expiresIn: moment(expiration).diff(new Date(), "seconds") + "s",
|
||||||
secret: this.config.get("JWT_SECRET"),
|
secret: this.config.get("jwtSecret"),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -247,7 +247,7 @@ export class ShareService {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const claims = this.jwtService.verify(token, {
|
const claims = this.jwtService.verify(token, {
|
||||||
secret: this.config.get("JWT_SECRET"),
|
secret: this.config.get("jwtSecret"),
|
||||||
// Ignore expiration if expiration is 0
|
// Ignore expiration if expiration is 0
|
||||||
ignoreExpiration: moment(expiration).isSame(0),
|
ignoreExpiration: moment(expiration).isSame(0),
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Block a user