mirror of
https://github.com/swissmakers/swiss-datashare.git
synced 2026-03-21 16:53:26 +01:00
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 7dbdb6729034be5b083f126f854d5e1411735a54. * improve info box text --------- Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -44,6 +44,7 @@ yarn-error.log*
|
||||
/docs/build/
|
||||
/docs/.docusaurus
|
||||
/docs/.cache-loader
|
||||
/config.yaml
|
||||
|
||||
# Jetbrains specific (webstorm)
|
||||
.idea/**/**
|
||||
|
||||
@@ -16,6 +16,7 @@ Pingvin Share is a self-hosted file sharing platform and an alternative for WeTr
|
||||
- Reverse shares
|
||||
- OIDC and LDAP authentication
|
||||
- Integration with ClamAV for security scans
|
||||
- Different file providers: local storage and S3
|
||||
|
||||
## 🐧 Get to know Pingvin Share
|
||||
|
||||
|
||||
4039
backend/package-lock.json
generated
4039
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -50,7 +50,8 @@
|
||||
"rimraf": "^6.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.33.5",
|
||||
"ts-node": "^10.9.2"
|
||||
"ts-node": "^10.9.2",
|
||||
"yaml": "^2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.4.5",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Prisma, PrismaClient } from "@prisma/client";
|
||||
import * as crypto from "crypto";
|
||||
|
||||
const configVariables: ConfigVariables = {
|
||||
export const configVariables = {
|
||||
internal: {
|
||||
jwtSecret: {
|
||||
type: "string",
|
||||
@@ -181,12 +181,12 @@ const configVariables: ConfigVariables = {
|
||||
},
|
||||
searchQuery: {
|
||||
type: "string",
|
||||
defaultValue: ""
|
||||
defaultValue: "",
|
||||
},
|
||||
|
||||
adminGroups: {
|
||||
type: "string",
|
||||
defaultValue: ""
|
||||
defaultValue: "",
|
||||
},
|
||||
|
||||
fieldNameMemberOf: {
|
||||
@@ -196,18 +196,18 @@ const configVariables: ConfigVariables = {
|
||||
fieldNameEmail: {
|
||||
type: "string",
|
||||
defaultValue: "userPrincipalName",
|
||||
}
|
||||
},
|
||||
},
|
||||
oauth: {
|
||||
"allowRegistration": {
|
||||
allowRegistration: {
|
||||
type: "boolean",
|
||||
defaultValue: "true",
|
||||
},
|
||||
"ignoreTotp": {
|
||||
ignoreTotp: {
|
||||
type: "boolean",
|
||||
defaultValue: "true",
|
||||
},
|
||||
"disablePassword": {
|
||||
disablePassword: {
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
secret: false,
|
||||
@@ -376,7 +376,22 @@ const configVariables: ConfigVariables = {
|
||||
defaultValue: "",
|
||||
secret: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
} satisfies ConfigVariables;
|
||||
|
||||
export type YamlConfig = {
|
||||
[Category in keyof typeof configVariables]: {
|
||||
[Key in keyof (typeof configVariables)[Category]]: string;
|
||||
};
|
||||
} & {
|
||||
initUser: {
|
||||
enabled: string;
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
isAdmin: boolean;
|
||||
ldapDN: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ConfigVariables = {
|
||||
@@ -433,7 +448,7 @@ async function migrateConfigVariables() {
|
||||
for (const existingConfigVariable of existingConfigVariables) {
|
||||
const configVariable =
|
||||
configVariables[existingConfigVariable.category]?.[
|
||||
existingConfigVariable.name
|
||||
existingConfigVariable.name
|
||||
];
|
||||
|
||||
// Delete the config variable if it doesn't exist in the seed
|
||||
|
||||
@@ -2,12 +2,17 @@ import {
|
||||
BadRequestException,
|
||||
Inject,
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { Config } from "@prisma/client";
|
||||
import * as argon from "argon2";
|
||||
import { EventEmitter } from "events";
|
||||
import * as fs from "fs";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { stringToTimespan } from "src/utils/date.util";
|
||||
import { parse as yamlParse } from "yaml";
|
||||
import { YamlConfig } from "../../prisma/seed/config.seed";
|
||||
|
||||
/**
|
||||
* ConfigService extends EventEmitter to allow listening for config updates,
|
||||
@@ -15,6 +20,9 @@ import { stringToTimespan } from "src/utils/date.util";
|
||||
*/
|
||||
@Injectable()
|
||||
export class ConfigService extends EventEmitter {
|
||||
yamlConfig?: YamlConfig;
|
||||
logger = new Logger(ConfigService.name);
|
||||
|
||||
constructor(
|
||||
@Inject("CONFIG_VARIABLES") private configVariables: Config[],
|
||||
private prisma: PrismaService,
|
||||
@@ -22,6 +30,65 @@ export class ConfigService extends EventEmitter {
|
||||
super();
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.loadYamlConfig();
|
||||
|
||||
if (this.yamlConfig) {
|
||||
await this.migrateInitUser();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadYamlConfig() {
|
||||
let configFile: string = "";
|
||||
try {
|
||||
configFile = fs.readFileSync("../config.yaml", "utf8");
|
||||
} catch (e) {
|
||||
this.logger.log(
|
||||
"Config.yaml is not set. Falling back to UI configuration.",
|
||||
);
|
||||
}
|
||||
try {
|
||||
this.yamlConfig = yamlParse(configFile);
|
||||
if (this.yamlConfig) {
|
||||
for (const configVariable of this.configVariables) {
|
||||
const category = this.yamlConfig[configVariable.category];
|
||||
if (!category) continue;
|
||||
|
||||
configVariable.value = category[configVariable.name];
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
"Failed to parse config.yaml. Falling back to UI configuration: ",
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateInitUser(): Promise<void> {
|
||||
if (!this.yamlConfig.initUser.enabled) return;
|
||||
|
||||
const userCount = await this.prisma.user.count({
|
||||
where: { isAdmin: true },
|
||||
});
|
||||
if (userCount === 1) {
|
||||
this.logger.log(
|
||||
"Skip initial user creation. Admin user is already existent.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
await this.prisma.user.create({
|
||||
data: {
|
||||
email: this.yamlConfig.initUser.email,
|
||||
username: this.yamlConfig.initUser.username,
|
||||
password: this.yamlConfig.initUser.password
|
||||
? await argon.hash(this.yamlConfig.initUser.password)
|
||||
: null,
|
||||
isAdmin: this.yamlConfig.initUser.isAdmin,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get(key: `${string}.${string}`): any {
|
||||
const configVariable = this.configVariables.filter(
|
||||
(variable) => `${variable.category}.${variable.name}` == key,
|
||||
@@ -40,24 +107,22 @@ export class ConfigService extends EventEmitter {
|
||||
}
|
||||
|
||||
async getByCategory(category: string) {
|
||||
const configVariables = await this.prisma.config.findMany({
|
||||
orderBy: { order: "asc" },
|
||||
where: { category, locked: { equals: false } },
|
||||
});
|
||||
const configVariables = this.configVariables
|
||||
.filter((c) => !c.locked && category == c.category)
|
||||
.sort((c) => c.order);
|
||||
|
||||
return configVariables.map((variable) => {
|
||||
return {
|
||||
...variable,
|
||||
key: `${variable.category}.${variable.name}`,
|
||||
value: variable.value ?? variable.defaultValue,
|
||||
allowEdit: this.isEditAllowed(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async list() {
|
||||
const configVariables = await this.prisma.config.findMany({
|
||||
where: { secret: { equals: false } },
|
||||
});
|
||||
const configVariables = this.configVariables.filter((c) => !c.secret);
|
||||
|
||||
return configVariables.map((variable) => {
|
||||
return {
|
||||
@@ -69,16 +134,26 @@ export class ConfigService extends EventEmitter {
|
||||
}
|
||||
|
||||
async updateMany(data: { key: string; value: string | number | boolean }[]) {
|
||||
if (!this.isEditAllowed())
|
||||
throw new BadRequestException(
|
||||
"You are only allowed to update config variables via the config.yaml file",
|
||||
);
|
||||
|
||||
const response: Config[] = [];
|
||||
|
||||
for (const variable of data) {
|
||||
response.push(await this.update(variable.key, variable.value));
|
||||
response.push(await this.update(variable.key, variable.value));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async update(key: string, value: string | number | boolean) {
|
||||
if (!this.isEditAllowed())
|
||||
throw new BadRequestException(
|
||||
"You are only allowed to update config variables via the config.yaml file",
|
||||
);
|
||||
|
||||
const configVariable = await this.prisma.config.findUnique({
|
||||
where: {
|
||||
name_category: {
|
||||
@@ -143,4 +218,8 @@ export class ConfigService extends EventEmitter {
|
||||
throw new BadRequestException(validation.message);
|
||||
}
|
||||
}
|
||||
|
||||
isEditAllowed(): boolean {
|
||||
return this.yamlConfig === undefined || this.yamlConfig === null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ export class AdminConfigDTO extends ConfigDTO {
|
||||
@Expose()
|
||||
obscured: boolean;
|
||||
|
||||
@Expose()
|
||||
allowEdit: boolean;
|
||||
|
||||
from(partial: Partial<AdminConfigDTO>) {
|
||||
return plainToClass(AdminConfigDTO, partial, {
|
||||
excludeExtraneousValues: true,
|
||||
|
||||
226
config.example.yaml
Normal file
226
config.example.yaml
Normal file
@@ -0,0 +1,226 @@
|
||||
#This configuration is pre-filled with the default values.
|
||||
#You can remove keys you don't want to set. If a key is missing, the value set in the UI will be used; if that is also unset, the default value applies.
|
||||
|
||||
general:
|
||||
#Name of the application
|
||||
appName: Pingvin Share
|
||||
#On which URL Pingvin Share is available
|
||||
appUrl: http://localhost:3000
|
||||
#Whether to set the secure flag on cookies. If enabled, the site will not function when accessed over HTTP.
|
||||
secureCookies: "false"
|
||||
#Whether to show the home page
|
||||
showHomePage: "true"
|
||||
#Time in hours after which a user must log in again (default: 3 months).
|
||||
sessionDuration: 3 months
|
||||
share:
|
||||
#Whether registration is allowed
|
||||
allowRegistration: "true"
|
||||
#Whether unauthenticated users can create shares
|
||||
allowUnauthenticatedShares: "false"
|
||||
#Maximum share expiration. Set to 0 to allow unlimited expiration.
|
||||
maxExpiration: 0 days
|
||||
#Default length for the generated ID of a share. This value is also used to generate links for reverse shares. A value below 8 is not considered secure.
|
||||
shareIdLength: "8"
|
||||
#Maximum share size
|
||||
maxSize: "1000000000"
|
||||
#Adjust the level to balance between file size and compression speed. Valid values range from 0 to 9, with 0 being no compression and 9 being maximum compression.
|
||||
zipCompressionLevel: "9"
|
||||
#Adjust the chunk size for your uploads to balance efficiency and reliability according to your internet connection. Smaller chunks can enhance success rates for unstable connections, while larger chunks make uploads faster for stable connections.
|
||||
chunkSize: "10000000"
|
||||
#The share creation modal automatically appears when a user selects files, eliminating the need to manually click the button.
|
||||
autoOpenShareModal: "false"
|
||||
email:
|
||||
#Whether to allow email sharing with recipients. Only enable this if SMTP is activated.
|
||||
enableShareEmailRecipients: "false"
|
||||
#Subject of the email which gets sent to the share recipients.
|
||||
shareRecipientsSubject: Files shared with you
|
||||
#Message which gets sent to the share recipients. Available variables:
|
||||
# {creator} - The username of the creator of the share
|
||||
# {creatorEmail} - The email of the creator of the share
|
||||
# {shareUrl} - The URL of the share
|
||||
# {desc} - The description of the share
|
||||
# {expires} - The expiration date of the share
|
||||
# These variables will be replaced with the actual value.
|
||||
shareRecipientsMessage: >-
|
||||
Hey!
|
||||
|
||||
|
||||
{creator} ({creatorEmail}) shared some files with you, view or download the
|
||||
files with this link: {shareUrl}
|
||||
|
||||
|
||||
The share will expire {expires}.
|
||||
|
||||
|
||||
Note: {desc}
|
||||
|
||||
|
||||
Shared securely with Pingvin Share 🐧
|
||||
#Subject of the sent email when someone created a share with your reverse share link.
|
||||
reverseShareSubject: Reverse share link used
|
||||
#Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.
|
||||
reverseShareMessage: |-
|
||||
Hey!
|
||||
|
||||
A share was just created with your reverse share link: {shareUrl}
|
||||
|
||||
Shared securely with Pingvin Share 🐧
|
||||
#Subject of the sent email when a user requests a password reset.
|
||||
resetPasswordSubject: Pingvin Share password reset
|
||||
#Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.
|
||||
resetPasswordMessage: >-
|
||||
Hey!
|
||||
|
||||
|
||||
You requested a password reset. Click this link to reset your password:
|
||||
{url}
|
||||
|
||||
The link expires in a hour.
|
||||
|
||||
|
||||
Pingvin Share 🐧
|
||||
#Subject of the sent email when an admin invites a user.
|
||||
inviteSubject: Pingvin Share invite
|
||||
#Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL, {email} with the email and {password} with the users password.
|
||||
inviteMessage: >-
|
||||
Hey!
|
||||
|
||||
|
||||
You were invited to Pingvin Share. Click this link to accept the invite:
|
||||
{url}
|
||||
|
||||
|
||||
You can use the email "{email}" and the password "{password}" to sign in.
|
||||
|
||||
|
||||
Pingvin Share 🐧
|
||||
smtp:
|
||||
#Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.
|
||||
enabled: "false"
|
||||
#Only set this to true if you need to trust self signed certificates.
|
||||
allowUnauthorizedCertificates: "false"
|
||||
#Host of the SMTP server
|
||||
host: ""
|
||||
#Port of the SMTP server
|
||||
port: "0"
|
||||
#Email address from wich the emails get sent
|
||||
email: ""
|
||||
#Username of the SMTP server
|
||||
username: ""
|
||||
#Password of the SMTP server
|
||||
password: ""
|
||||
ldap:
|
||||
#Use LDAP authentication for user login
|
||||
enabled: "false"
|
||||
#URL of the LDAP server
|
||||
url: ""
|
||||
#Default user used to perform the user search
|
||||
bindDn: ""
|
||||
#Password used to perform the user search
|
||||
bindPassword: ""
|
||||
#Base location, where the user search will be performed
|
||||
searchBase: ""
|
||||
#The user query will be used to search the 'User base' for the LDAP user. %username% can be used as the placeholder for the user given input.
|
||||
searchQuery: ""
|
||||
#Group required for administrative access.
|
||||
adminGroups: ""
|
||||
#LDAP attribute name for the groups, an user is a member of. This is used when checking for the admin group.
|
||||
fieldNameMemberOf: memberOf
|
||||
#LDAP attribute name for the email of an user.
|
||||
fieldNameEmail: userPrincipalName
|
||||
oauth:
|
||||
#Allow users to register via social login
|
||||
allowRegistration: "true"
|
||||
#Whether to ignore TOTP when user using social login
|
||||
ignoreTotp: "true"
|
||||
#Whether to disable password login
|
||||
#Make sure that an OAuth provider is properly configured before activating this configuration to avoid being locked out.
|
||||
disablePassword: "false"
|
||||
#Whether GitHub login is enabled
|
||||
github-enabled: "false"
|
||||
#Client ID of the GitHub OAuth app
|
||||
github-clientId: ""
|
||||
#Client secret of the GitHub OAuth app
|
||||
github-clientSecret: ""
|
||||
#Whether Google login is enabled
|
||||
google-enabled: "false"
|
||||
#Client ID of the Google OAuth app
|
||||
google-clientId: ""
|
||||
#Client secret of the Google OAuth app
|
||||
google-clientSecret: ""
|
||||
#Whether Microsoft login is enabled
|
||||
microsoft-enabled: "false"
|
||||
#Tenant ID of the Microsoft OAuth app
|
||||
#common: Users with both a personal Microsoft account and a work or school account from Microsoft Entra ID can sign in to the application. organizations: Only users with work or school accounts from Microsoft Entra ID can sign in to the application.
|
||||
#consumers: Only users with a personal Microsoft account can sign in to the application.
|
||||
#domain name of the Microsoft Entra tenant or the tenant ID in GUID format: Only users from a specific Microsoft Entra tenant (directory members with a work or school account or directory guests with a personal Microsoft account) can sign in to the application.
|
||||
microsoft-tenant: common
|
||||
#Client ID of the Microsoft OAuth app
|
||||
microsoft-clientId: ""
|
||||
#Client secret of the Microsoft OAuth app
|
||||
microsoft-clientSecret: ""
|
||||
#Whether Discord login is enabled
|
||||
discord-enabled: "false"
|
||||
#Limit signing in to users in a specific server. Leave it blank to disable.
|
||||
discord-limitedGuild: ""
|
||||
#Limit signing in to specific users by their Discord ID. Leave it blank to disable.
|
||||
discord-limitedUsers: ""
|
||||
#Client ID of the Discord OAuth app
|
||||
discord-clientId: ""
|
||||
#Client secret of the Discord OAuth app
|
||||
discord-clientSecret: ""
|
||||
#Whether OpenID Connect login is enabled
|
||||
oidc-enabled: "false"
|
||||
#Discovery URI of the OpenID Connect OAuth app
|
||||
oidc-discoveryUri: ""
|
||||
#Whether the “Sign out” button will sign out from the OpenID Connect provider
|
||||
oidc-signOut: "false"
|
||||
#Scopes which should be requested from the OpenID Connect provider.
|
||||
oidc-scope: openid email profile
|
||||
#Username claim in OpenID Connect ID token. Leave it blank if you don't know what this config is.
|
||||
oidc-usernameClaim: ""
|
||||
#Must be a valid JMES path referencing an array of roles. Managing access rights using OpenID Connect roles is only recommended if no other identity provider is configured and password login is disabled. Leave it blank if you don't know what this config is.
|
||||
oidc-rolePath: ""
|
||||
#Role required for general access. Must be present in a user’s roles for them to log in. Leave it blank if you don't know what this config is.
|
||||
oidc-roleGeneralAccess: ""
|
||||
#Role required for administrative access. Must be present in a user’s roles for them to access the admin panel. Leave it blank if you don't know what this config is.
|
||||
oidc-roleAdminAccess: ""
|
||||
#Client ID of the OpenID Connect OAuth app
|
||||
oidc-clientId: ""
|
||||
#Client secret of the OpenID Connect OAuth app
|
||||
oidc-clientSecret: ""
|
||||
s3:
|
||||
#Whether S3 should be used to store the shared files instead of the local file system.
|
||||
enabled: "false"
|
||||
#The URL of the S3 bucket.
|
||||
endpoint: ""
|
||||
#The region of the S3 bucket.
|
||||
region: ""
|
||||
#The name of the S3 bucket.
|
||||
bucketName: ""
|
||||
#The default path which should be used to store the files in the S3 bucket.
|
||||
bucketPath: ""
|
||||
#The key which allows you to access the S3 bucket.
|
||||
key: ""
|
||||
#The secret which allows you to access the S3 bucket.
|
||||
secret: ""
|
||||
legal:
|
||||
#Whether to show a link to imprint and privacy policy in the footer.
|
||||
enabled: "false"
|
||||
#The text which should be shown in the imprint. Supports Markdown. Leave blank to link to an external imprint page.
|
||||
imprintText: ""
|
||||
#If you already have an imprint page you can link it here instead of using the text field.
|
||||
imprintUrl: ""
|
||||
#The text which should be shown in the privacy policy. Supports Markdown. Leave blank to link to an external privacy policy page.
|
||||
privacyPolicyText: ""
|
||||
#If you already have a privacy policy page you can link it here instead of using the text field.
|
||||
privacyPolicyUrl: ""
|
||||
#This configuration is used to create the initial user when the application is started for the first time.
|
||||
#Make sure to change at least the password as soon as you log in!
|
||||
initUser:
|
||||
enabled: false
|
||||
username: admin
|
||||
email: admin@example.com
|
||||
password: my-secure-password
|
||||
isAdmin: true
|
||||
ldapDN: ""
|
||||
12
docker-compose.local.yml
Normal file
12
docker-compose.local.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
pingvin-share:
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 3001:3000
|
||||
environment:
|
||||
- TRUST_PROXY=false
|
||||
volumes:
|
||||
- "./data:/opt/app/backend/data"
|
||||
- "./data/images:/opt/app/frontend/public/img"
|
||||
# - "./config.yaml:/opt/app/config.yaml"
|
||||
@@ -9,6 +9,7 @@ services:
|
||||
volumes:
|
||||
- "./data:/opt/app/backend/data"
|
||||
- "./data/images:/opt/app/frontend/public/img"
|
||||
# - "./config.yaml:/opt/app/config.yaml" # Add this line, if you want to configure pingvin-share via config file and not via UI
|
||||
|
||||
# To add ClamAV, to scan your shares for malicious files,
|
||||
# see https://stonith404.github.io/pingvin-share/setup/integrations/#clamav-docker-only
|
||||
|
||||
@@ -4,27 +4,21 @@ id: configuration
|
||||
|
||||
# Configuration
|
||||
|
||||
You can customize Pingvin Share by going to the configuration page in your admin dashboard `/admin/config`.
|
||||
## General configuration
|
||||
|
||||
## General
|
||||
There are plenty of settings you can adjust to your needs. Pingvin Share can be configured in two ways:
|
||||
|
||||
The **General** Tab will let you customize your Pingvin Share instance to your liking.
|
||||
### UI
|
||||
|
||||
### App name
|
||||
You can change the settings in the UI (`/admin/config`)
|
||||
|
||||
To change the name of your instance, insert any text into `App name`.
|
||||
### YAML file
|
||||
|
||||
### App URL
|
||||
You can set the configuration via a YAML file. If you choose this way, you won't be able to change the settings in the UI.
|
||||
|
||||
To make your App available trough your own **domain**, insert your specific domain and also subdomain if needed. Add an `https://` if you have an SSL certificate installed. If this is not the case, use `http://`.
|
||||
If you use Docker you can create a `config.yml` file based on the [`config.example.yaml`](https://github.com/stonith404/pingvin-share/blob/main/config.yaml) and mount it to `/opt/app/config.yaml` in the container.
|
||||
|
||||
### Show home page
|
||||
|
||||
If you don't like the **home page** Pingvin Share provides and you just want the upload tab to be the main page, toggle this to `true`.
|
||||
|
||||
### Logo
|
||||
|
||||
Not only you can change your instances name, but also the logo it shows everywhere. To do that, upload an image as `png` with a 1:1 aspect ratio.
|
||||
If you run Pingvin Share without Docker, you can create a `config.yml` file based on the [`config.example.yaml`](https://github.com/stonith404/pingvin-share/blob/main/config.yaml) in the root directory of the project.
|
||||
|
||||
---
|
||||
|
||||
@@ -50,9 +44,10 @@ For installation specific configuration, you can use environment variables. The
|
||||
| `API_URL` | `http://localhost:8080` | The URL of the backend for the frontend. |
|
||||
|
||||
#### Docker specific
|
||||
|
||||
Environment variables that are only available when running Pingvin Share with Docker.
|
||||
|
||||
| Variable | Default Value | Description |
|
||||
| ------------- | ------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `TRUST_PROXY` | `false` | Whether Pingvin Share is behind a reverse proxy. If set to `true`, the `X-Forwarded-For` header is trusted. |
|
||||
| `PUID` and `PGID` | `1000` | The user and group ID of the user who should run Pingvin Share inside the Docker container and owns the files that are mounted with the volume. You can get the `PUID` and `GUID` of your user on your host machine by using the command `id`. For more information see [this article](https://docs.linuxserver.io/general/understanding-puid-and-pgid/#using-the-variables). |
|
||||
| Variable | Default Value | Description |
|
||||
| ----------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `TRUST_PROXY` | `false` | Whether Pingvin Share is behind a reverse proxy. If set to `true`, the `X-Forwarded-For` header is trusted. |
|
||||
| `PUID` and `PGID` | `1000` | The user and group ID of the user who should run Pingvin Share inside the Docker container and owns the files that are mounted with the volume. You can get the `PUID` and `GUID` of your user on your host machine by using the command `id`. For more information see [this article](https://docs.linuxserver.io/general/understanding-puid-and-pgid/#using-the-variables). |
|
||||
|
||||
@@ -45,6 +45,7 @@ const AdminConfigInput = ({
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
disabled={!configVariable.allowEdit}
|
||||
{...form.getInputProps("stringValue")}
|
||||
onChange={(e) => onValueChange(configVariable, e.target.value)}
|
||||
/>
|
||||
@@ -53,6 +54,7 @@ const AdminConfigInput = ({
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
disabled={!configVariable.allowEdit}
|
||||
{...form.getInputProps("stringValue")}
|
||||
placeholder={configVariable.defaultValue}
|
||||
onChange={(e) => onValueChange(configVariable, e.target.value)}
|
||||
@@ -64,6 +66,7 @@ const AdminConfigInput = ({
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
disabled={!configVariable.allowEdit}
|
||||
autosize
|
||||
{...form.getInputProps("textValue")}
|
||||
placeholder={configVariable.defaultValue}
|
||||
@@ -73,6 +76,7 @@ const AdminConfigInput = ({
|
||||
{configVariable.type == "number" && (
|
||||
<NumberInput
|
||||
{...form.getInputProps("numberValue")}
|
||||
disabled={!configVariable.allowEdit}
|
||||
placeholder={configVariable.defaultValue}
|
||||
onChange={(number) => onValueChange(configVariable, number)}
|
||||
w={201}
|
||||
@@ -81,6 +85,7 @@ const AdminConfigInput = ({
|
||||
{configVariable.type == "filesize" && (
|
||||
<FileSizeInput
|
||||
{...form.getInputProps("numberValue")}
|
||||
disabled={!configVariable.allowEdit}
|
||||
value={parseInt(configVariable.value ?? configVariable.defaultValue)}
|
||||
onChange={(bytes) => onValueChange(configVariable, bytes)}
|
||||
w={201}
|
||||
@@ -89,6 +94,7 @@ const AdminConfigInput = ({
|
||||
{configVariable.type == "boolean" && (
|
||||
<>
|
||||
<Switch
|
||||
disabled={!configVariable.allowEdit}
|
||||
{...form.getInputProps("booleanValue", { type: "checkbox" })}
|
||||
onChange={(e) => onValueChange(configVariable, e.target.checked)}
|
||||
/>
|
||||
@@ -97,6 +103,7 @@ const AdminConfigInput = ({
|
||||
{configVariable.type == "timespan" && (
|
||||
<TimespanInput
|
||||
value={stringToTimespan(configVariable.value)}
|
||||
disabled={!configVariable.allowEdit}
|
||||
onChange={(timespan) =>
|
||||
onValueChange(configVariable, timespanToString(timespan))
|
||||
}
|
||||
|
||||
@@ -302,6 +302,8 @@ export default {
|
||||
"privacy.title": "Datenschutzerklärung",
|
||||
// END /privacy
|
||||
// /admin/config
|
||||
"admin.config.config-file-warning.title": "Konfigurationsdatei aktiv",
|
||||
"admin.config.config-file-warning.description": "Da Pingvin Share mit einer Konfigurationsdatei konfiguriert ist, kann die Konfiguration nicht über die Benutzeroberfläche geändert werden.",
|
||||
"admin.config.title": "Einstellungen",
|
||||
"admin.config.category.general": "Allgemein",
|
||||
"admin.config.category.share": "Freigabe",
|
||||
|
||||
@@ -414,6 +414,9 @@ export default {
|
||||
// END /privacy
|
||||
|
||||
// /admin/config
|
||||
"admin.config.config-file-warning.title": "Configuration file present",
|
||||
"admin.config.config-file-warning.description": "As you have a configured Pingvin Share with a configuration file, you can't change the configuration through the UI.",
|
||||
|
||||
"admin.config.title": "Configuration",
|
||||
"admin.config.category.general": "General",
|
||||
"admin.config.category.share": "Share",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Alert,
|
||||
AppShell,
|
||||
Box,
|
||||
Button,
|
||||
@@ -13,6 +14,7 @@ import { useMediaQuery } from "@mantine/hooks";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TbInfoCircle } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Meta from "../../../components/Meta";
|
||||
import AdminConfigInput from "../../../components/admin/configuration/AdminConfigInput";
|
||||
@@ -46,6 +48,10 @@ export default function AppShellDemo() {
|
||||
|
||||
const [logo, setLogo] = useState<File | null>(null);
|
||||
|
||||
const isEditingAllowed = (): boolean => {
|
||||
return !configVariables || configVariables[0].allowEdit;
|
||||
};
|
||||
|
||||
const saveConfigVariables = async () => {
|
||||
if (logo) {
|
||||
configService
|
||||
@@ -132,6 +138,17 @@ export default function AppShellDemo() {
|
||||
) : (
|
||||
<>
|
||||
<Stack>
|
||||
{!isEditingAllowed() && (
|
||||
<Alert
|
||||
mb={"lg"}
|
||||
variant="light"
|
||||
color="primary"
|
||||
title={t("admin.config.config-file-warning.title")}
|
||||
icon={<TbInfoCircle />}
|
||||
>
|
||||
<FormattedMessage id="admin.config.config-file-warning.description" />
|
||||
</Alert>
|
||||
)}
|
||||
<Title mb="md" order={3}>
|
||||
{t("admin.config.category." + categoryId)}
|
||||
</Title>
|
||||
|
||||
@@ -16,6 +16,7 @@ export type AdminConfig = Config & {
|
||||
secret: boolean;
|
||||
description: string;
|
||||
obscured: boolean;
|
||||
allowEdit: boolean;
|
||||
};
|
||||
|
||||
export type AdminConfigGroupedByCategory = {
|
||||
|
||||
66
scripts/generate-example-config.ts
Normal file
66
scripts/generate-example-config.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import * as fs from "fs";
|
||||
import * as yaml from "yaml";
|
||||
import { configVariables } from "../backend/prisma/seed/config.seed";
|
||||
import translations from "../frontend/src/i18n/translations/en-US";
|
||||
|
||||
// Prepare an object that only contains the categories, keys and values
|
||||
const configVariablesWithDefaultValues = {};
|
||||
for (const [category, variables] of Object.entries(configVariables)) {
|
||||
if (category == "internal") continue;
|
||||
for (const [variableName, { defaultValue }] of Object.entries(variables)) {
|
||||
if (!configVariablesWithDefaultValues[category]) {
|
||||
configVariablesWithDefaultValues[category] = {};
|
||||
}
|
||||
configVariablesWithDefaultValues[category][variableName] = defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
// As `initUser` is not part of the `configVariables` object, we add it manually
|
||||
configVariablesWithDefaultValues["initUser"] = {
|
||||
enabled: false,
|
||||
username: "admin",
|
||||
email: "admin@example.com",
|
||||
password: "my-secure-password",
|
||||
isAdmin: true,
|
||||
ldapDN: "",
|
||||
};
|
||||
|
||||
// Create the yaml document
|
||||
const doc: any = new yaml.Document(configVariablesWithDefaultValues);
|
||||
|
||||
// Add the descriptions imported from `en-US.ts` as comments
|
||||
for (const category of doc.contents.items) {
|
||||
// As `initUser` can't be configured from the UI, we have to add the description manually
|
||||
if (category.key.value === "initUser") {
|
||||
category.key.commentBefore =
|
||||
"This configuration is used to create the initial user when the application is started for the first time.\n";
|
||||
category.key.commentBefore +=
|
||||
"Make sure to change at least the password as soon as you log in!";
|
||||
}
|
||||
|
||||
for (const variable of category.value.items) {
|
||||
variable.key.commentBefore = getDescription(
|
||||
category.key.value,
|
||||
variable.key.value
|
||||
);
|
||||
}
|
||||
}
|
||||
doc.commentBefore =
|
||||
"This configuration is pre-filled with the default values.\n";
|
||||
doc.commentBefore +=
|
||||
"You can remove keys you don't want to set. If a key is missing, the value set in the UI will be used; if that is also unset, the default value applies.";
|
||||
|
||||
// Write the YAML content to a file
|
||||
fs.writeFileSync("../config.example.yaml", doc.toString({ indent: 2 }), "utf8");
|
||||
console.log("YAML file generated successfully!");
|
||||
|
||||
// Helper functions
|
||||
function getDescription(category: string, name: string) {
|
||||
return translations[
|
||||
`admin.config.${category}.${camelToKebab(name)}.description`
|
||||
];
|
||||
}
|
||||
|
||||
function camelToKebab(str: string) {
|
||||
return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
||||
}
|
||||
229
scripts/package-lock.json
generated
Normal file
229
scripts/package-lock.json
generated
Normal file
@@ -0,0 +1,229 @@
|
||||
{
|
||||
"name": "scripts",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "scripts",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"yaml": "^2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.5",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz",
|
||||
"integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-node": "dist/bin.js",
|
||||
"ts-node-cwd": "dist/bin-cwd.js",
|
||||
"ts-node-esm": "dist/bin-esm.js",
|
||||
"ts-node-script": "dist/bin-script.js",
|
||||
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||
"ts-script": "dist/bin-script-deprecated.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/core": ">=1.2.50",
|
||||
"@swc/wasm": ">=1.2.50",
|
||||
"@types/node": "*",
|
||||
"typescript": ">=2.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/wasm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
|
||||
"integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
scripts/package.json
Normal file
15
scripts/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "scripts",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"generate-example-config": "ts-node generate-example-config.ts"
|
||||
},
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"yaml": "^2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.5",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
}
|
||||
8
scripts/tsconfig.json
Normal file
8
scripts/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["/**/*.ts", "generate-example-config.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user