Skip to content

Commit 0fc9257

Browse files
authored
feat: toml backend (#49)
- add TOML storage backends - move mysql teardown/disconnect into mysql classes - fix: don't log sensitive information
1 parent 0ebfc25 commit 0fc9257

45 files changed

Lines changed: 500 additions & 213 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/copilot-instructions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
- Run formatting check: `npm run prettier`
1414
- Run coverage: `npm run test:coverage`
1515

16-
Local tests expect MySQL plus the NicTool schema. CI initializes it with `sh sql/init-mysql.sh`, and `test.sh` recreates fixtures before each run.
16+
Local tests expect MySQL plus the NicTool schema. CI initializes it with `sh sql/init-mysql.sh`, and `test/run.sh` recreates fixtures before each run.
1717

1818
## Architecture
1919

@@ -38,5 +38,5 @@ Local tests expect MySQL plus the NicTool schema. CI initializes it with `sh sql
3838
- Reuse the legacy-schema mapping helpers instead of hand-rolling field conversions. Most repos convert booleans, nested permission/export objects, and short API names into the older DB layout before writing and normalize them again on read.
3939
- When changing group or user behavior, check permission side effects too. Group creation/update touches permission rows, and user reads/write paths may change `inherit_group_permissions` handling.
4040
- Route tests use `init()` plus `server.inject()` instead of booting a live server. They usually establish auth by calling `POST /session` and then pass `Authorization: Bearer <token>` to protected routes.
41-
- The test entrypoint is `test.sh`, not raw `node --test`, when you need DB-backed behavior. It tears fixtures down, recreates them, and then runs the requested test target.
41+
- The test entrypoint is `test/run.sh`, not raw `node --test`, when you need DB-backed behavior. It tears fixtures down, recreates them, and then runs the requested test target.
4242
- Zone-record changes must preserve the existing record-field translation logic. Special cases like zero `weight`/`priority` retention for `SRV`, `URI`, `HTTPS`, and `SVCB` are intentional.

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ jobs:
9191

9292
test-docker:
9393
runs-on: ubuntu-latest
94+
permissions:
95+
contents: read
9496
steps:
9597
- uses: actions/checkout@v6
9698
- name: Generate .env
@@ -126,4 +128,4 @@ jobs:
126128
node-version: ${{ matrix.node-version }}
127129
- run: sh sql/init-mysql.sh
128130
- run: npm install
129-
- run: sh test.sh
131+
- run: sh test/run.sh

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,4 @@ dist
132132
package-lock.json
133133
.release/
134134
conf.d/*.pem
135+
CLAUDE.md

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
66

77
### Unreleased
88

9+
- move mysql teardown/disconnect into mysql classes
10+
- fix: don't log sensitive information
11+
- add: TOML stores for group, nameserver, permission, session
12+
913
### [3.0.0-alpha.11] - 2026-04-07
1014

1115
- decorate user & group with permissions

lib/config.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class Config {
1818
const str = await fs.readFile(`./conf.d/${name}.toml`, 'utf8')
1919
const cfg = parse(str)
2020
applyEnvOverrides(name, cfg)
21-
if (this.debug) console.debug(cfg)
21+
// if (this.debug) console.debug(cfg)
2222

2323
if (name === 'http') {
2424
const tls = await loadPEM('./conf.d')
@@ -37,7 +37,7 @@ class Config {
3737
const str = fsSync.readFileSync(`./conf.d/${name}.toml`, 'utf8')
3838
const cfg = parse(str)
3939
applyEnvOverrides(name, cfg)
40-
if (this.debug) console.debug(cfg)
40+
// if (this.debug) console.debug(cfg)
4141

4242
if (name === 'http') {
4343
const tls = loadPEMSync('./conf.d')
@@ -102,7 +102,9 @@ function loadPEMSync(dir) {
102102
}
103103

104104
function parsePEMBlocks(content) {
105-
const keyMatch = content.match(/-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z]+ )?PRIVATE KEY-----/)
105+
const keyMatch = content.match(
106+
/-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z]+ )?PRIVATE KEY-----/,
107+
)
106108
const certMatches = [...content.matchAll(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g)]
107109

108110
if (!keyMatch && !certMatches.length) return null

lib/config.test.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@ import { describe, it, before, after } from 'node:test'
33

44
import Config from './config.js'
55

6-
const envOverrideKeys = ['NICTOOL_DB_HOST', 'NICTOOL_DB_PORT', 'NICTOOL_DB_USER', 'NICTOOL_DB_USER_PASSWORD', 'NICTOOL_DB_NAME', 'NICTOOL_HTTP_HOST', 'NICTOOL_HTTP_PORT']
6+
const envOverrideKeys = [
7+
'NICTOOL_DB_HOST',
8+
'NICTOOL_DB_PORT',
9+
'NICTOOL_DB_USER',
10+
'NICTOOL_DB_USER_PASSWORD',
11+
'NICTOOL_DB_NAME',
12+
'NICTOOL_HTTP_HOST',
13+
'NICTOOL_HTTP_PORT',
14+
]
715

816
describe('config', () => {
917
const savedEnv = {}
@@ -27,19 +35,22 @@ describe('config', () => {
2735
describe('get', () => {
2836
it(`loads mysql config`, async () => {
2937
const cfg = await Config.get('mysql')
30-
delete cfg.password; delete cfg.user
38+
delete cfg.password
39+
delete cfg.user
3140
assert.deepEqual(cfg, mysqlCfg)
3241
})
3342

3443
it(`loads mysql config synchronously`, () => {
3544
const cfg = Config.getSync('mysql')
36-
delete cfg.password; delete cfg.user
45+
delete cfg.password
46+
delete cfg.user
3747
})
3848

3949
it(`loads mysql config (from cache)`, async () => {
4050
process.env.NODE_DEBUG = 1
4151
const cfg = await Config.get('mysql')
42-
delete cfg.password; delete cfg.user
52+
delete cfg.password
53+
delete cfg.user
4354
assert.deepEqual(cfg, mysqlCfg)
4455
process.env.NODE_DEBUG = ''
4556
})

lib/group/store/base.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ class GroupBase {
1616
this.debug = args?.debug ?? false
1717
}
1818

19+
disconnect() {
20+
// noop, for repos that need to clean up resources
21+
}
22+
1923
// -------------------------------------------------------------------------
2024
// Repo contract – subclasses must implement these
2125
// -------------------------------------------------------------------------

lib/group/store/mysql.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,13 @@ class Group extends GroupBase {
4141
async addToSubgroups(gid, parent_gid, rank = 1000) {
4242
if (!parent_gid || parent_gid === 0) return
4343

44-
await Mysql.execute(...Mysql.insert('nt_group_subgroups', {
45-
nt_group_id: parent_gid,
46-
nt_subgroup_id: gid,
47-
rank,
48-
}))
44+
await Mysql.execute(
45+
...Mysql.insert('nt_group_subgroups', {
46+
nt_group_id: parent_gid,
47+
nt_subgroup_id: gid,
48+
rank,
49+
}),
50+
)
4951

5052
const parent = await this.get({ id: parent_gid })
5153
if (parent.length === 1 && parent[0].parent_gid !== 0) {
@@ -73,9 +75,9 @@ class Group extends GroupBase {
7375
if (include_subgroups) {
7476
const subgroupRows = await Mysql.execute(
7577
'SELECT nt_subgroup_id FROM nt_group_subgroups WHERE nt_group_id = ?',
76-
[args.id]
78+
[args.id],
7779
)
78-
const gids = [args.id, ...subgroupRows.map(r => r.nt_subgroup_id)]
80+
const gids = [args.id, ...subgroupRows.map((r) => r.nt_subgroup_id)]
7981
where.push(`g.nt_group_id IN (${gids.join(',')})`)
8082
} else {
8183
where.push('g.nt_group_id = ?')
@@ -135,7 +137,7 @@ class Group extends GroupBase {
135137
if (perm) {
136138
await Permission.put({
137139
id: perm.id,
138-
nameserver: { usable: Array.isArray(usable_ns) ? usable_ns : [] }
140+
nameserver: { usable: Array.isArray(usable_ns) ? usable_ns : [] },
139141
})
140142
}
141143
}
@@ -163,6 +165,10 @@ class Group extends GroupBase {
163165
const r = await Mysql.execute(...Mysql.delete(`nt_group`, { nt_group_id: args.id }))
164166
return r.affectedRows === 1
165167
}
168+
169+
disconnect() {
170+
return this.mysql?.disconnect()
171+
}
166172
}
167173

168174
export default Group

lib/group/test/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Group from '../index.js'
66
import testCase from '../test/group.json' with { type: 'json' }
77

88
after(async () => {
9-
Group.mysql.disconnect()
9+
Group.disconnect()
1010
})
1111

1212
describe('group', function () {

lib/mysql.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,8 @@ class Mysql {
1212
}
1313

1414
async connect() {
15-
// if (this.dbh && this.dbh?.connection?.connectionId) return this.dbh;
16-
1715
const cfg = await Config.get('mysql')
18-
if (_debug) console.log(cfg)
19-
16+
// if (_debug) console.log(cfg)
2017
this.dbh = await mysql.createConnection(cfg)
2118
if (_debug) console.log(`MySQL connection id ${this.dbh.connection.connectionId}`)
2219
return this.dbh

0 commit comments

Comments
 (0)