lib/config.js - git - Webernet Explorer _X
git 🏠 Home 👤 Sign up Self hosted git, retro shell
★ otgit, code hosting from the year 2000 ★
📄 octuna / lib / config.js
215 lines · 7.1 KB · Raw
1const fs = require('fs');
2const path = require('path');
3 
4const DEFAULTS = {
5 site: {
6 name: 'git',
7 publicUrl: 'http://localhost:8080',
8 bindHost: '0.0.0.0',
9 port: 8080,
10 tagline: 'Self hosted git, retro shell'
11 },
12 auth: {
13 sessionSecret: '',
14 sessionMaxAgeDays: 365,
15 scryptN: 16384,
16 allowSignup: true
17 },
18 repos: {
19 maxPerUser: 100,
20 maxRepoSizeMB: 1024,
21 defaultVisibility: 'public',
22 allowPrivate: true,
23 defaultBranch: 'main',
24 maxRenderBlobMB: 1,
25 maxRenderDiffLines: 5000,
26 rootDir: 'data/repos'
27 },
28 limits: {
29 login: { max: 8, windowMin: 15 },
30 signup: { max: 5, windowMin: 60 },
31 write: { max: 60, windowMin: 10 },
32 push: { max: 20, windowMin: 10 },
33 uploadMaxMB: 100
34 },
35 security: {
36 trustProxy: false,
37 cookieSecure: 'auto',
38 csp: "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'none'; form-action 'self'"
39 },
40 ui: {
41 addressBarMode: 'real',
42 addressBarFixed: '',
43 marqueeText: 'otgit, code hosting from the year 2000',
44 footerText: 'Best viewed in any browser at 800x600',
45 themeColor: 'teal'
46 },
47 git: {
48 binPath: 'git',
49 httpBackendPath: ''
50 },
51 admin: {
52 user: '',
53 passwordHash: '',
54 passwordSalt: ''
55 },
56 log: {
57 level: 'info'
58 }
59};
60 
61const SCHEMA = {
62 'site.name': { type: 'string', min: 1 },
63 'site.publicUrl': { type: 'string', min: 1 },
64 'site.bindHost': { type: 'string', min: 1, restart: true },
65 'site.port': { type: 'number', min: 1, max: 65535, restart: true },
66 'site.tagline': { type: 'string' },
67 'auth.sessionSecret': { type: 'string', min: 22, restart: true, secret: true },
68 'auth.sessionMaxAgeDays': { type: 'number', min: 1, max: 3650 },
69 'auth.scryptN': { type: 'number', min: 1024, max: 1048576, restart: true },
70 'auth.allowSignup': { type: 'boolean' },
71 'repos.maxPerUser': { type: 'number', min: 1 },
72 'repos.maxRepoSizeMB': { type: 'number', min: 1 },
73 'repos.defaultVisibility': { type: 'enum', values: ['public', 'private'] },
74 'repos.allowPrivate': { type: 'boolean' },
75 'repos.defaultBranch': { type: 'string', min: 1 },
76 'repos.maxRenderBlobMB': { type: 'number', min: 1 },
77 'repos.maxRenderDiffLines': { type: 'number', min: 100 },
78 'repos.rootDir': { type: 'string', min: 1, restart: true },
79 'limits.login.max': { type: 'number', min: 1 },
80 'limits.login.windowMin': { type: 'number', min: 1 },
81 'limits.signup.max': { type: 'number', min: 1 },
82 'limits.signup.windowMin': { type: 'number', min: 1 },
83 'limits.write.max': { type: 'number', min: 1 },
84 'limits.write.windowMin': { type: 'number', min: 1 },
85 'limits.push.max': { type: 'number', min: 1 },
86 'limits.push.windowMin': { type: 'number', min: 1 },
87 'limits.uploadMaxMB': { type: 'number', min: 1 },
88 'security.trustProxy': { type: 'boolean' },
89 'security.cookieSecure': { type: 'enum', values: ['auto', 'true', 'false'] },
90 'security.csp': { type: 'string', min: 1 },
91 'ui.addressBarMode': { type: 'enum', values: ['real', 'fixed'] },
92 'ui.addressBarFixed': { type: 'string' },
93 'ui.marqueeText': { type: 'string' },
94 'ui.footerText': { type: 'string' },
95 'ui.themeColor': { type: 'enum', values: ['teal', 'navy', 'purple'] },
96 'git.binPath': { type: 'string', min: 1, restart: true },
97 'git.httpBackendPath': { type: 'string', restart: true },
98 'admin.user': { type: 'string', min: 1, restart: true },
99 'admin.passwordHash': { type: 'string', secret: true, restart: true },
100 'admin.passwordSalt': { type: 'string', secret: true, restart: true },
101 'log.level': { type: 'enum', values: ['debug', 'info', 'warn', 'error'] }
102};
103 
104const CONFIG_PATH = path.join(__dirname, '..', 'config.json');
105let current = null;
106 
107function clone(v) {
108 if (v === null || typeof v !== 'object') return v;
109 if (Array.isArray(v)) return v.map(clone);
110 const o = {};
111 for (const k of Object.keys(v)) o[k] = clone(v[k]);
112 return o;
113}
114 
115function deepMerge(a, b) {
116 const out = clone(a);
117 for (const k of Object.keys(b || {})) {
118 const bv = b[k];
119 if (bv !== null && typeof bv === 'object' && !Array.isArray(bv)
120 && out[k] && typeof out[k] === 'object' && !Array.isArray(out[k])) {
121 out[k] = deepMerge(out[k], bv);
122 } else {
123 out[k] = clone(bv);
124 }
125 }
126 return out;
127}
128 
129function parts(p) { return p.split('.'); }
130 
131function getAt(obj, p) {
132 let cur = obj;
133 for (const k of parts(p)) {
134 if (cur == null || typeof cur !== 'object') return undefined;
135 cur = cur[k];
136 }
137 return cur;
138}
139 
140function setAt(obj, p, value) {
141 const ks = parts(p);
142 let cur = obj;
143 for (let i = 0; i < ks.length - 1; i++) {
144 const k = ks[i];
145 if (cur[k] == null || typeof cur[k] !== 'object') cur[k] = {};
146 cur = cur[k];
147 }
148 cur[ks[ks.length - 1]] = value;
149}
150 
151function validate(p, value) {
152 const s = SCHEMA[p];
153 if (!s) throw new Error('unknown setting: ' + p);
154 if (s.type === 'string') {
155 if (typeof value !== 'string') throw new Error(p + ' must be string');
156 if (s.min != null && value.length < s.min) throw new Error(p + ' too short');
157 } else if (s.type === 'number') {
158 if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error(p + ' must be number');
159 if (s.min != null && value < s.min) throw new Error(p + ' below min');
160 if (s.max != null && value > s.max) throw new Error(p + ' above max');
161 } else if (s.type === 'boolean') {
162 if (typeof value !== 'boolean') throw new Error(p + ' must be boolean');
163 } else if (s.type === 'enum') {
164 if (!s.values.includes(value)) throw new Error(p + ' not in ' + s.values.join(','));
165 }
166}
167 
168function reload() {
169 if (!fs.existsSync(CONFIG_PATH)) { current = clone(DEFAULTS); return; }
170 current = deepMerge(DEFAULTS, JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')));
171}
172 
173function get(p) {
174 if (p == null) return clone(current);
175 return getAt(current, p);
176}
177 
178function set(p, value) {
179 validate(p, value);
180 setAt(current, p, value);
181 writeAtomic();
182}
183 
184function setMany(entries) {
185 for (const [p, v] of Object.entries(entries)) validate(p, v);
186 for (const [p, v] of Object.entries(entries)) setAt(current, p, v);
187 writeAtomic();
188}
189 
190function writeAtomic() {
191 const tmp = CONFIG_PATH + '.tmp';
192 fs.writeFileSync(tmp, JSON.stringify(current, null, 2));
193 fs.renameSync(tmp, CONFIG_PATH);
194}
195 
196function requiresRestart(p) { return !!(SCHEMA[p] && SCHEMA[p].restart); }
197function isSecret(p) { return !!(SCHEMA[p] && SCHEMA[p].secret); }
198 
199function publicView() {
200 const v = clone(current);
201 for (const p of Object.keys(SCHEMA)) {
202 if (SCHEMA[p].secret) setAt(v, p, '');
203 }
204 return v;
205}
206 
207function schema() { return clone(SCHEMA); }
208 
209reload();
210 
211module.exports = {
212 reload, get, set, setMany,
213 requiresRestart, isSecret, publicView, schema
214};
215 
Done 🔒 Internet