feat: custom branding (#112)

* add first concept

* remove setup status

* split config page in multiple components

* add custom branding docs

* add test email button

* fix invalid email from header

* add migration

* mount images to host

* update docs

* remove unused endpoint

* run formatter
This commit is contained in:
Elias Schneider
2023-03-04 23:29:00 +01:00
committed by GitHub
parent f9840505b8
commit fddad3ef70
66 changed files with 908 additions and 623 deletions

View File

@@ -42,7 +42,7 @@ export class AuthController {
@Body() dto: AuthRegisterDTO,
@Res({ passthrough: true }) response: Response
) {
if (!this.config.get("ALLOW_REGISTRATION"))
if (!this.config.get("share.allowRegistration"))
throw new ForbiddenException("Registration is not allowed");
const result = await this.authService.signUp(dto);

View File

@@ -25,7 +25,7 @@ export class AuthService {
) {}
async signUp(dto: AuthRegisterDTO) {
const isFirstUser = this.config.get("SETUP_STATUS") == "STARTED";
const isFirstUser = (await this.prisma.user.count()) == 0;
const hash = await argon.hash(dto.password);
try {
@@ -38,10 +38,6 @@ export class AuthService {
},
});
if (isFirstUser) {
await this.config.changeSetupStatus("REGISTERED");
}
const { refreshToken, refreshTokenId } = await this.createRefreshToken(
user.id
);
@@ -161,7 +157,7 @@ export class AuthService {
},
{
expiresIn: "15min",
secret: this.config.get("JWT_SECRET"),
secret: this.config.get("internal.jwtSecret"),
}
);
}

View File

@@ -11,7 +11,7 @@ export class JwtGuard extends AuthGuard("jwt") {
try {
return (await super.canActivate(context)) as boolean;
} catch {
return this.config.get("ALLOW_UNAUTHENTICATED_SHARES");
return this.config.get("share.allowUnauthenticatedShares");
}
}
}

View File

@@ -9,10 +9,10 @@ import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: ConfigService, private prisma: PrismaService) {
config.get("JWT_SECRET");
config.get("internal.jwtSecret");
super({
jwtFromRequest: JwtStrategy.extractJWT,
secretOrKey: config.get("JWT_SECRET"),
secretOrKey: config.get("internal.jwtSecret"),
});
}

View File

@@ -1,4 +1,12 @@
import { Body, Controller, Get, Patch, Post, UseGuards } from "@nestjs/common";
import {
Body,
Controller,
Get,
Param,
Patch,
Post,
UseGuards,
} from "@nestjs/common";
import { SkipThrottle } from "@nestjs/throttler";
import { AdministratorGuard } from "src/auth/guard/isAdmin.guard";
import { JwtGuard } from "src/auth/guard/jwt.guard";
@@ -22,24 +30,20 @@ export class ConfigController {
return new ConfigDTO().fromList(await this.configService.list());
}
@Get("admin")
@Get("admin/:category")
@UseGuards(JwtGuard, AdministratorGuard)
async listForAdmin() {
async getByCategory(@Param("category") category: string) {
return new AdminConfigDTO().fromList(
await this.configService.listForAdmin()
await this.configService.getByCategory(category)
);
}
@Patch("admin")
@UseGuards(JwtGuard, AdministratorGuard)
async updateMany(@Body() data: UpdateConfigDTO[]) {
await this.configService.updateMany(data);
}
@Post("admin/finishSetup")
@UseGuards(JwtGuard, AdministratorGuard)
async finishSetup() {
return await this.configService.changeSetupStatus("FINISHED");
return new AdminConfigDTO().fromList(
await this.configService.updateMany(data)
);
}
@Post("admin/testEmail")

View File

@@ -14,9 +14,9 @@ export class ConfigService {
private prisma: PrismaService
) {}
get(key: string): any {
get(key: `${string}.${string}`): any {
const configVariable = this.configVariables.filter(
(variable) => variable.key == key
(variable) => `${variable.category}.${variable.name}` == key
)[0];
if (!configVariable) throw new Error(`Config variable ${key} not found`);
@@ -27,30 +27,51 @@ export class ConfigService {
return configVariable.value;
}
async listForAdmin() {
return await this.prisma.config.findMany({
async getByCategory(category: string) {
const configVariables = await this.prisma.config.findMany({
orderBy: { order: "asc" },
where: { locked: { equals: false } },
where: { category, locked: { equals: false } },
});
return configVariables.map((variable) => {
return {
key: `${variable.category}.${variable.name}`,
...variable,
};
});
}
async list() {
return await this.prisma.config.findMany({
const configVariables = await this.prisma.config.findMany({
where: { secret: { equals: false } },
});
return configVariables.map((variable) => {
return {
key: `${variable.category}.${variable.name}`,
...variable,
};
});
}
async updateMany(data: { key: string; value: string | number | boolean }[]) {
const response: Config[] = [];
for (const variable of data) {
await this.update(variable.key, variable.value);
response.push(await this.update(variable.key, variable.value));
}
return data;
return response;
}
async update(key: string, value: string | number | boolean) {
const configVariable = await this.prisma.config.findUnique({
where: { key },
where: {
name_category: {
category: key.split(".")[0],
name: key.split(".")[1],
},
},
});
if (!configVariable || configVariable.locked)
@@ -67,7 +88,12 @@ export class ConfigService {
}
const updatedVariable = await this.prisma.config.update({
where: { key },
where: {
name_category: {
category: key.split(".")[0],
name: key.split(".")[1],
},
},
data: { value: value.toString() },
});
@@ -75,15 +101,4 @@ export class ConfigService {
return updatedVariable;
}
async changeSetupStatus(status: "STARTED" | "REGISTERED" | "FINISHED") {
const updatedVariable = await this.prisma.config.update({
where: { key: "SETUP_STATUS" },
data: { value: status },
});
this.configVariables = await this.prisma.config.findMany();
return updatedVariable;
}
}

View File

@@ -2,6 +2,9 @@ import { Expose, plainToClass } from "class-transformer";
import { ConfigDTO } from "./config.dto";
export class AdminConfigDTO extends ConfigDTO {
@Expose()
name: string;
@Expose()
secret: boolean;
@@ -14,9 +17,6 @@ export class AdminConfigDTO extends ConfigDTO {
@Expose()
obscured: boolean;
@Expose()
category: string;
from(partial: Partial<AdminConfigDTO>) {
return plainToClass(AdminConfigDTO, partial, {
excludeExtraneousValues: true,

View File

@@ -8,16 +8,16 @@ export class EmailService {
constructor(private config: ConfigService) {}
getTransporter() {
if (!this.config.get("SMTP_ENABLED"))
if (!this.config.get("smtp.enabled"))
throw new InternalServerErrorException("SMTP is disabled");
return nodemailer.createTransport({
host: this.config.get("SMTP_HOST"),
port: parseInt(this.config.get("SMTP_PORT")),
secure: parseInt(this.config.get("SMTP_PORT")) == 465,
host: this.config.get("smtp.host"),
port: this.config.get("smtp.port"),
secure: this.config.get("smtp.port") == 465,
auth: {
user: this.config.get("SMTP_USERNAME"),
pass: this.config.get("SMTP_PASSWORD"),
user: this.config.get("smtp.username"),
pass: this.config.get("smtp.password"),
},
});
}
@@ -27,17 +27,19 @@ export class EmailService {
shareId: string,
creator?: User
) {
if (!this.config.get("ENABLE_SHARE_EMAIL_RECIPIENTS"))
if (!this.config.get("email.enableShareEmailRecipients"))
throw new InternalServerErrorException("Email service disabled");
const shareUrl = `${this.config.get("APP_URL")}/share/${shareId}`;
const shareUrl = `${this.config.get("general.appUrl")}/share/${shareId}`;
await this.getTransporter().sendMail({
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
from: `"${this.config.get("general.appName")}" <${this.config.get(
"smtp.email"
)}>`,
to: recipientEmail,
subject: this.config.get("SHARE_RECEPIENTS_EMAIL_SUBJECT"),
subject: this.config.get("email.shareRecipientsSubject"),
text: this.config
.get("SHARE_RECEPIENTS_EMAIL_MESSAGE")
.get("email.shareRecipientsMessage")
.replaceAll("\\n", "\n")
.replaceAll("{creator}", creator?.username ?? "Someone")
.replaceAll("{shareUrl}", shareUrl),
@@ -45,14 +47,16 @@ export class EmailService {
}
async sendMailToReverseShareCreator(recipientEmail: string, shareId: string) {
const shareUrl = `${this.config.get("APP_URL")}/share/${shareId}`;
const shareUrl = `${this.config.get("general.appUrl")}/share/${shareId}`;
await this.getTransporter().sendMail({
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
from: `"${this.config.get("general.appName")}" <${this.config.get(
"smtp.email"
)}>`,
to: recipientEmail,
subject: this.config.get("REVERSE_SHARE_EMAIL_SUBJECT"),
subject: this.config.get("email.reverseShareSubject"),
text: this.config
.get("REVERSE_SHARE_EMAIL_MESSAGE")
.get("email.reverseShareMessage")
.replaceAll("\\n", "\n")
.replaceAll("{shareUrl}", shareUrl),
});
@@ -60,28 +64,32 @@ export class EmailService {
async sendResetPasswordEmail(recipientEmail: string, token: string) {
const resetPasswordUrl = `${this.config.get(
"APP_URL"
"general.appUrl"
)}/auth/resetPassword/${token}`;
await this.getTransporter().sendMail({
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
from: `"${this.config.get("general.appName")}" <${this.config.get(
"smtp.email"
)}>`,
to: recipientEmail,
subject: this.config.get("RESET_PASSWORD_EMAIL_SUBJECT"),
subject: this.config.get("email.resetPasswordSubject"),
text: this.config
.get("RESET_PASSWORD_EMAIL_MESSAGE")
.get("email.resetPasswordMessage")
.replaceAll("{url}", resetPasswordUrl),
});
}
async sendInviteEmail(recipientEmail: string, password: string) {
const loginUrl = `${this.config.get("APP_URL")}/auth/signIn`;
const loginUrl = `${this.config.get("general.appUrl")}/auth/signIn`;
await this.getTransporter().sendMail({
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
from: `"${this.config.get("general.appName")}" <${this.config.get(
"smtp.email"
)}>`,
to: recipientEmail,
subject: this.config.get("INVITE_EMAIL_SUBJECT"),
subject: this.config.get("email.inviteSubject"),
text: this.config
.get("INVITE_EMAIL_MESSAGE")
.get("email.inviteMessage")
.replaceAll("{url}", loginUrl)
.replaceAll("{password}", password),
});
@@ -90,7 +98,9 @@ export class EmailService {
async sendTestMail(recipientEmail: string) {
try {
await this.getTransporter().sendMail({
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
from: `"${this.config.get("general.appName")}" <${this.config.get(
"smtp.email"
)}>`,
to: recipientEmail,
subject: "Test email",
text: "This is a test email",

View File

@@ -67,7 +67,7 @@ export class FileService {
const shareSizeSum = fileSizeSum + diskFileSize + buffer.byteLength;
if (
shareSizeSum > this.config.get("MAX_SHARE_SIZE") ||
shareSizeSum > this.config.get("share.maxSize") ||
(share.reverseShare?.maxShareSize &&
shareSizeSum > parseInt(share.reverseShare.maxShareSize))
) {

View File

@@ -31,7 +31,7 @@ export class ReverseShareController {
async create(@Body() body: CreateReverseShareDTO, @GetUser() user: User) {
const token = await this.reverseShareService.create(body, user.id);
const link = `${this.config.get("APP_URL")}/upload/${token}`;
const link = `${this.config.get("general.appUrl")}/upload/${token}`;
return { token, link };
}

View File

@@ -24,7 +24,7 @@ export class ReverseShareService {
)
.toDate();
const globalMaxShareSize = this.config.get("MAX_SHARE_SIZE");
const globalMaxShareSize = this.config.get("share.maxSize");
if (globalMaxShareSize < data.maxShareSize)
throw new BadRequestException(

View File

@@ -153,7 +153,7 @@ export class ShareService {
if (
share.reverseShare &&
this.config.get("SMTP_ENABLED") &&
this.config.get("smtp.enabled") &&
share.reverseShare.sendEmailNotification
) {
await this.emailService.sendMailToReverseShareCreator(
@@ -303,7 +303,7 @@ export class ShareService {
},
{
expiresIn: moment(expiration).diff(new Date(), "seconds") + "s",
secret: this.config.get("JWT_SECRET"),
secret: this.config.get("internal.jwtSecret"),
}
);
}
@@ -315,7 +315,7 @@ export class ShareService {
try {
const claims = this.jwtService.verify(token, {
secret: this.config.get("JWT_SECRET"),
secret: this.config.get("internal.jwtSecret"),
// Ignore expiration if expiration is 0
ignoreExpiration: moment(expiration).isSame(0),
});

View File

@@ -4,7 +4,7 @@ import { UserController } from "./user.controller";
import { UserSevice } from "./user.service";
@Module({
imports:[EmailModule],
imports: [EmailModule],
providers: [UserSevice],
controllers: [UserController],
})