1import config from '../models/Config.js';
2import node from '../models/Node.js';
3import test from '../models/Speedtests.js';
4import recommendations from '../models/Recommendations.js';
5import integration from '../models/IntegrationData.js';
6import { triggerEvent } from './integrations.js';
7import bcrypt from 'bcryptjs';
8import * as timer from '../tasks/timer.js';
9import cron from 'cron-validator';
10import db from '../config/database.js';
11import fs from 'node:fs';
12import path from 'node:path';
13import * as interfaces from '../util/loadInterfaces.js';
14
15const configDefaults = {
16 ping: "25",
17 download: "100",
18 upload: "50",
19 cron: "0 * * * *",
20 scheduleOffset: "true",
21 provider: "none",
22 ooklaId: "none",
23 libreId: "none",
24 libreUrl: "none",
25 password: "none",
26 passwordLevel: "none",
27 interface: "none",
28 retentionDays: "365"
29}
30
31const MAX_RETENTION_DAYS = 10000;
32
33export const insertDefaults = async () => {
34 let insert = [];
35 for (let key in configDefaults) {
36 if (key !== "interface" && !(await config.findOne({where: {key: key}})))
37 insert.push({key: key, value: configDefaults[key]});
38
39 if (key === "interface") {
40 const ips = Object.keys(interfaces.interfaces);
41 let ip = ips.length > 0 ? ips[0] : "none";
42
43 if (!(await config.findOne({where: {key: key}})))
44 insert.push({key: key, value: ip});
45 }
46 }
47
48 await config.bulkCreate(insert, {validate: true});
49}
50
51export const listAll = async () => {
52 return await config.findAll();
53}
54
55export const getValue = async (key) => {
56 return (await config.findByPk(key))?.value;
57}
58
59export const updateValue = async (key, newValue) => {
60 if ((await getValue(key)) === undefined) return undefined;
61
62 triggerEvent("configUpdated", {key: key, value: key === "password" ? "protected" : newValue})
63 .then(undefined);
64
65 return await config.update({value: newValue}, {where: {key: key}});
66}
67
68export const getUsedStorage = async () => {
69 let size = 0;
70
71 if (process.env.DB_TYPE === "mysql") {
72 const sizes = await db.query("SELECT table_name AS `Table`, ROUND((data_length + index_length), 2) AS `size` FROM information_schema.TABLES WHERE table_schema = ?;", {
73 replacements: [process.env.DB_NAME],
74 type: db.QueryTypes.SELECT
75 });
76 for (let i = 0; i < sizes.length; i++) {
77 size += parseFloat(sizes[i].size);
78 }
79 } else {
80 const STORAGE_PATH = path.join(process.cwd(), 'data', `storage${process.env.PREVIEW_MODE === "true" ? "_preview" : ""}.db`);
81
82 size = fs.statSync(STORAGE_PATH).size;
83 }
84
85 return {size, testCount: await test.count()};
86}
87
88export const validateInput = async (key, value) => {
89 if (!value?.toString()) return "You need to provide the new value";
90
91 if ((key === "ping" || key === "download" || key === "upload") && /[^0-9.]/.test(value))
92 return "You need to provide a number in order to change this";
93
94 if ((key === "ooklaId" || key === "libreId") && (/[^0-9]/.test(value) && value !== "none"))
95 return "You need to provide a number in order to change this";
96
97 if (key === "libreUrl" && value !== "none") {
98 try {
99 new URL(value);
100 } catch (e) {
101 return "You need to provide a valid URL";
102 }
103 }
104
105 if (key === "passwordLevel" && !["none", "read"].includes(value))
106 return "You need to provide either none or read-access";
107
108 if (key === "provider" && !["ookla", "libre", "cloudflare"].includes(value))
109 return "You need to provide a valid provider";
110
111 if (key === "ping")
112 value = value.toString().split(".")[0];
113
114 if (key === "password" && value !== "none")
115 value = await bcrypt.hash(value, 10);
116
117 if (key === "cron" && !cron.isValidCron(value.toString()))
118 return "Not a valid cron expression";
119
120 if (key === "scheduleOffset" && !["true", "false"].includes(value))
121 return "You need to provide either true or false";
122
123 if (key === "interface" && !Object.keys(interfaces.interfaces).includes(value))
124 return "The provided interface does not exist";
125
126 if (key === "retentionDays") {
127 if (/[^0-9-]/.test(value.toString()))
128 return "You need to provide a number in order to change this";
129
130 const num = parseInt(value);
131 if (isNaN(num))
132 return "You need to provide a valid number";
133
134 if (num <= 0) {
135 value = "0";
136 } else if (num > MAX_RETENTION_DAYS) {
137 return `Retention must be ${MAX_RETENTION_DAYS} days or less (use 0 for unlimited)`;
138 } else {
139 value = num.toString();
140 }
141 }
142
143 if (configDefaults[key] === undefined)
144 return "The provided key does not exist";
145
146 if (process.env.PREVIEW_MODE === "true" && (key === "password" || key === "passwordLevel"))
147 return "You can't change the password in preview mode";
148
149 return {value: value};
150}
151
152export const exportConfig = async () => {
153 let obj = {};
154 obj.config = {};
155
156 let configValues = await config.findAll();
157 for (let i = 0; i < configValues.length; i++) {
158 if (configValues[i].key === "password" || configValues[i].key === "interface") continue;
159 obj.config[configValues[i].key] = configValues[i].value;
160 }
161
162 obj.nodes = await node.findAll();
163 obj.recommendations = await recommendations.findAll();
164
165 obj.integrations = await integration.findAll();
166
167 return obj;
168}
169
170export const importConfig = async (obj) => {
171 let configValues = obj.config;
172 for (let key in configValues) {
173 if (configDefaults[key] === undefined) continue
174 if (key === "password") continue;
175
176 const validate = await validateInput(key, configValues[key]);
177 if (Object.keys(validate).length !== 1) return false;
178
179 if (key === "cron") {
180 timer.stopTimer();
181 timer.startTimer(configValues[key].toString());
182 }
183
184 await config.update({value: validate.value}, {where: {key: key}});
185 }
186
187 if (recommendations.length > 1) return false;
188
189 await node.destroy({where: {}});
190 await recommendations.destroy({where: {}});
191 await integration.destroy({where: {}});
192
193 try {
194 await node.bulkCreate(obj.nodes);
195
196 for (let i = 0; i < obj.integrations.length; i++) {
197 obj.integrations[i].data = JSON.parse(obj.integrations[i].data);
198 }
199
200 await integration.bulkCreate(obj.integrations);
201
202 await recommendations.bulkCreate(obj.recommendations);
203 } catch (e) {
204 return false;
205 }
206
207 return true;
208}
209
210export const factoryReset = async () => {
211 let configValues = await config.findAll();
212 for (let i = 0; i < configValues.length; i++) {
213 await config.update({value: configDefaults[configValues[i].key]}, {where: {key: configValues[i].key}});
214 }
215
216 await node.destroy({where: {}});
217 await recommendations.destroy({where: {}});
218 await integration.destroy({where: {}});
219
220 timer.stopTimer();
221 timer.startTimer(configDefaults.cron);
222
223 interfaces.requestInterfaces();
224
225 return true;
226}