mirror of
https://github.com/swissmakers/swiss-datashare.git
synced 2026-04-11 10:27:01 +02:00
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:
@@ -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);
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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))
|
||||
) {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { UserController } from "./user.controller";
|
||||
import { UserSevice } from "./user.service";
|
||||
|
||||
@Module({
|
||||
imports:[EmailModule],
|
||||
imports: [EmailModule],
|
||||
providers: [UserSevice],
|
||||
controllers: [UserController],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user