Skip to content
This repository was archived by the owner on Jun 29, 2025. It is now read-only.

Commit 9dfb52a

Browse files
feat: add ability to configure application with a config file (#740)
* add config file possibility * revert port in docker compose * Update docker-compose.yml Co-authored-by: Elias Schneider <login@eliasschneider.com> * Update docker-compose.yml Co-authored-by: Elias Schneider <login@eliasschneider.com> * add attribute description to config file * remove email message config * add package to resolve errors * remove email messages from config * move config initialization to config module * revert unnecessary change * add order * improve alert * run formatter * remove unnecessary packages * remove unnecessary types * use logger * don't save yaml config to db * allowEdit if no yaml config is set * improve docs * fix allow edit state * remove unnecessary check and refactor code * restore old config file * add script that generates `config.example.yaml` automatically * allow config variables to be changed if they are not set in the `config.yml` * add back init user * Revert "allow config variables to be changed if they are not set in the `config.yml`" This reverts commit 7dbdb67. * improve info box text --------- Co-authored-by: Elias Schneider <login@eliasschneider.com>
1 parent f429142 commit 9dfb52a

File tree

21 files changed

+2743
-2104
lines changed

21 files changed

+2743
-2104
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ yarn-error.log*
4444
/docs/build/
4545
/docs/.docusaurus
4646
/docs/.cache-loader
47+
/config.yaml
4748

4849
# Jetbrains specific (webstorm)
4950
.idea/**/**

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Pingvin Share is a self-hosted file sharing platform and an alternative for WeTr
1616
- Reverse shares
1717
- OIDC and LDAP authentication
1818
- Integration with ClamAV for security scans
19+
- Different file providers: local storage and S3
1920

2021
## 🐧 Get to know Pingvin Share
2122

backend/package-lock.json

Lines changed: 2025 additions & 2068 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@
5050
"rimraf": "^6.0.1",
5151
"rxjs": "^7.8.1",
5252
"sharp": "^0.33.5",
53-
"ts-node": "^10.9.2"
53+
"ts-node": "^10.9.2",
54+
"yaml": "^2.7.0"
5455
},
5556
"devDependencies": {
5657
"@nestjs/cli": "^10.4.5",

backend/prisma/seed/config.seed.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Prisma, PrismaClient } from "@prisma/client";
22
import * as crypto from "crypto";
33

4-
const configVariables: ConfigVariables = {
4+
export const configVariables = {
55
internal: {
66
jwtSecret: {
77
type: "string",
@@ -181,12 +181,12 @@ const configVariables: ConfigVariables = {
181181
},
182182
searchQuery: {
183183
type: "string",
184-
defaultValue: ""
184+
defaultValue: "",
185185
},
186186

187187
adminGroups: {
188188
type: "string",
189-
defaultValue: ""
189+
defaultValue: "",
190190
},
191191

192192
fieldNameMemberOf: {
@@ -196,18 +196,18 @@ const configVariables: ConfigVariables = {
196196
fieldNameEmail: {
197197
type: "string",
198198
defaultValue: "userPrincipalName",
199-
}
199+
},
200200
},
201201
oauth: {
202-
"allowRegistration": {
202+
allowRegistration: {
203203
type: "boolean",
204204
defaultValue: "true",
205205
},
206-
"ignoreTotp": {
206+
ignoreTotp: {
207207
type: "boolean",
208208
defaultValue: "true",
209209
},
210-
"disablePassword": {
210+
disablePassword: {
211211
type: "boolean",
212212
defaultValue: "false",
213213
secret: false,
@@ -376,7 +376,22 @@ const configVariables: ConfigVariables = {
376376
defaultValue: "",
377377
secret: false,
378378
},
379-
}
379+
},
380+
} satisfies ConfigVariables;
381+
382+
export type YamlConfig = {
383+
[Category in keyof typeof configVariables]: {
384+
[Key in keyof (typeof configVariables)[Category]]: string;
385+
};
386+
} & {
387+
initUser: {
388+
enabled: string;
389+
username: string;
390+
email: string;
391+
password: string;
392+
isAdmin: boolean;
393+
ldapDN: string;
394+
};
380395
};
381396

382397
type ConfigVariables = {
@@ -433,7 +448,7 @@ async function migrateConfigVariables() {
433448
for (const existingConfigVariable of existingConfigVariables) {
434449
const configVariable =
435450
configVariables[existingConfigVariable.category]?.[
436-
existingConfigVariable.name
451+
existingConfigVariable.name
437452
];
438453

439454
// Delete the config variable if it doesn't exist in the seed

backend/src/config/config.service.ts

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,93 @@ import {
22
BadRequestException,
33
Inject,
44
Injectable,
5+
Logger,
56
NotFoundException,
67
} from "@nestjs/common";
78
import { Config } from "@prisma/client";
9+
import * as argon from "argon2";
810
import { EventEmitter } from "events";
11+
import * as fs from "fs";
912
import { PrismaService } from "src/prisma/prisma.service";
1013
import { stringToTimespan } from "src/utils/date.util";
14+
import { parse as yamlParse } from "yaml";
15+
import { YamlConfig } from "../../prisma/seed/config.seed";
1116

1217
/**
1318
* ConfigService extends EventEmitter to allow listening for config updates,
1419
* now only `update` event will be emitted.
1520
*/
1621
@Injectable()
1722
export class ConfigService extends EventEmitter {
23+
yamlConfig?: YamlConfig;
24+
logger = new Logger(ConfigService.name);
25+
1826
constructor(
1927
@Inject("CONFIG_VARIABLES") private configVariables: Config[],
2028
private prisma: PrismaService,
2129
) {
2230
super();
2331
}
2432

33+
async onModuleInit() {
34+
await this.loadYamlConfig();
35+
36+
if (this.yamlConfig) {
37+
await this.migrateInitUser();
38+
}
39+
}
40+
41+
private async loadYamlConfig() {
42+
let configFile: string = "";
43+
try {
44+
configFile = fs.readFileSync("../config.yaml", "utf8");
45+
} catch (e) {
46+
this.logger.log(
47+
"Config.yaml is not set. Falling back to UI configuration.",
48+
);
49+
}
50+
try {
51+
this.yamlConfig = yamlParse(configFile);
52+
if (this.yamlConfig) {
53+
for (const configVariable of this.configVariables) {
54+
const category = this.yamlConfig[configVariable.category];
55+
if (!category) continue;
56+
57+
configVariable.value = category[configVariable.name];
58+
}
59+
}
60+
} catch (e) {
61+
this.logger.error(
62+
"Failed to parse config.yaml. Falling back to UI configuration: ",
63+
e,
64+
);
65+
}
66+
}
67+
68+
private async migrateInitUser(): Promise<void> {
69+
if (!this.yamlConfig.initUser.enabled) return;
70+
71+
const userCount = await this.prisma.user.count({
72+
where: { isAdmin: true },
73+
});
74+
if (userCount === 1) {
75+
this.logger.log(
76+
"Skip initial user creation. Admin user is already existent.",
77+
);
78+
return;
79+
}
80+
await this.prisma.user.create({
81+
data: {
82+
email: this.yamlConfig.initUser.email,
83+
username: this.yamlConfig.initUser.username,
84+
password: this.yamlConfig.initUser.password
85+
? await argon.hash(this.yamlConfig.initUser.password)
86+
: null,
87+
isAdmin: this.yamlConfig.initUser.isAdmin,
88+
},
89+
});
90+
}
91+
2592
get(key: `${string}.${string}`): any {
2693
const configVariable = this.configVariables.filter(
2794
(variable) => `${variable.category}.${variable.name}` == key,
@@ -40,24 +107,22 @@ export class ConfigService extends EventEmitter {
40107
}
41108

42109
async getByCategory(category: string) {
43-
const configVariables = await this.prisma.config.findMany({
44-
orderBy: { order: "asc" },
45-
where: { category, locked: { equals: false } },
46-
});
110+
const configVariables = this.configVariables
111+
.filter((c) => !c.locked && category == c.category)
112+
.sort((c) => c.order);
47113

48114
return configVariables.map((variable) => {
49115
return {
50116
...variable,
51117
key: `${variable.category}.${variable.name}`,
52118
value: variable.value ?? variable.defaultValue,
119+
allowEdit: this.isEditAllowed(),
53120
};
54121
});
55122
}
56123

57124
async list() {
58-
const configVariables = await this.prisma.config.findMany({
59-
where: { secret: { equals: false } },
60-
});
125+
const configVariables = this.configVariables.filter((c) => !c.secret);
61126

62127
return configVariables.map((variable) => {
63128
return {
@@ -69,16 +134,26 @@ export class ConfigService extends EventEmitter {
69134
}
70135

71136
async updateMany(data: { key: string; value: string | number | boolean }[]) {
137+
if (!this.isEditAllowed())
138+
throw new BadRequestException(
139+
"You are only allowed to update config variables via the config.yaml file",
140+
);
141+
72142
const response: Config[] = [];
73143

74144
for (const variable of data) {
75-
response.push(await this.update(variable.key, variable.value));
145+
response.push(await this.update(variable.key, variable.value));
76146
}
77147

78148
return response;
79149
}
80150

81151
async update(key: string, value: string | number | boolean) {
152+
if (!this.isEditAllowed())
153+
throw new BadRequestException(
154+
"You are only allowed to update config variables via the config.yaml file",
155+
);
156+
82157
const configVariable = await this.prisma.config.findUnique({
83158
where: {
84159
name_category: {
@@ -143,4 +218,8 @@ export class ConfigService extends EventEmitter {
143218
throw new BadRequestException(validation.message);
144219
}
145220
}
221+
222+
isEditAllowed(): boolean {
223+
return this.yamlConfig === undefined || this.yamlConfig === null;
224+
}
146225
}

backend/src/config/dto/adminConfig.dto.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export class AdminConfigDTO extends ConfigDTO {
1717
@Expose()
1818
obscured: boolean;
1919

20+
@Expose()
21+
allowEdit: boolean;
22+
2023
from(partial: Partial<AdminConfigDTO>) {
2124
return plainToClass(AdminConfigDTO, partial, {
2225
excludeExtraneousValues: true,

0 commit comments

Comments
 (0)