diff --git a/.eslintrc.yaml b/.eslintrc.yaml
new file mode 100644
index 0000000..692dbd6
--- /dev/null
+++ b/.eslintrc.yaml
@@ -0,0 +1,8 @@
+env:
+ node: true
+ es2021: true
+extends: eslint:recommended
+parserOptions:
+ ecmaVersion: latest
+ sourceType: module
+rules: {}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..4a5c4a0
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,110 @@
+name: CI
+
+on:
+ push:
+ pull_request:
+
+env:
+ CI: true
+ NODE_ENV: test
+
+jobs:
+ lint:
+ uses: NicTool/.github/.github/workflows/lint.yml@main
+
+ coverage:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Start MySQL
+ run: sudo /etc/init.d/mysql start
+ - uses: actions/setup-node@v4
+ - uses: actions/checkout@v4
+ - run: npm install
+ - name: Initialize MySQL
+ run: sh sql/init-mysql.sh
+ - name: run coverage
+ run: npx -y c8 --reporter=lcov npm test
+ env:
+ NODE_ENV: cov
+ - name: codecov
+ uses: codecov/codecov-action@v2
+ - name: Coveralls
+ uses: coverallsapp/github-action@master
+ with:
+ github-token: ${{ secrets.github_token }}
+
+ get-lts:
+ runs-on: ubuntu-latest
+ steps:
+ - id: get
+ uses: msimerson/node-lts-versions@v1
+ outputs:
+ lts: ${{ steps.get.outputs.lts }}
+ active: ${{ steps.get.outputs.active }}
+
+ test:
+ needs: [ lint, get-lts ]
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest]
+ node-version: ${{ fromJson(needs.get-lts.outputs.active) }}
+ fail-fast: false
+ steps:
+ - name: Start MySQL
+ run: sudo /etc/init.d/mysql start
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ name: Node ${{ matrix.node-version }} on ${{ matrix.os }}
+ with:
+ node-version: ${{ matrix.node-version }}
+ - name: Initialize MySQL
+ run: sh sql/init-mysql.sh
+ - run: npm install
+ - run: npm test
+
+ test-mac:
+ needs: [ lint, get-lts ]
+ runs-on: macos-latest
+ strategy:
+ matrix:
+ node-version: ${{ fromJson(needs.get-lts.outputs.active) }}
+ fail-fast: false
+ steps:
+ - name: Install & Start MySQL
+ run: |
+ brew install mysql
+ brew tap homebrew/services
+ brew services start mysql
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ name: Node ${{ matrix.node-version }} on ${{ matrix.os }}
+ with:
+ node-version: ${{ matrix.node-version }}
+ - name: Initialize MySQL
+ run: sh sql/init-mysql.sh
+ - run: npm install
+ - run: npm test
+
+ test-win:
+ # if: false
+ needs: [ lint, get-lts ]
+ runs-on: windows-latest
+ strategy:
+ matrix:
+ node-version: ${{ fromJson(needs.get-lts.outputs.active) }}
+ experimental: [true]
+ fail-fast: false
+ steps:
+ - name: Install MySQL
+ run: |
+ choco install mysql
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ name: Node ${{ matrix.node-version }} on ${{ matrix.os }}
+ with:
+ node-version: ${{ matrix.node-version }}
+ - name: Initialize MySQL
+ run: sh sql/init-mysql.sh
+ - run: npm install
+ - run: npm test
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..46e21d1
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,14 @@
+name: CodeQL
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [main]
+ schedule:
+ - cron: '18 7 * * 4'
+
+jobs:
+ codeql:
+ uses: NicTool/.github/.github/workflows/codeql.yml@main
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..f79da52
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,18 @@
+name: publish
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - package.json
+ release:
+ types: [published]
+
+env:
+ CI: true
+
+jobs:
+ publish:
+ uses: NicTool/.github/.github/workflows/publish.yml@main
+ secrets: inherit
diff --git a/.gitignore b/.gitignore
index c6bba59..323e2c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -128,3 +128,5 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
+
+package-lock.json
diff --git a/.prettierrc.yml b/.prettierrc.yml
new file mode 100644
index 0000000..9b110b8
--- /dev/null
+++ b/.prettierrc.yml
@@ -0,0 +1,3 @@
+trailingComma: 'all'
+semi: false
+singleQuote: true
diff --git a/README.md b/README.md
index e0cd136..005f1d8 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,8 @@
-# api
+[![Build Status](https://github.com/NicTool/api/actions/workflows/ci.yml/badge.svg)](https://github.com/NicTool/api/actions/workflows/ci.yml)
+[![Coverage Status](https://coveralls.io/repos/github/NicTool/api/badge.svg)](https://coveralls.io/github/NicTool/api)
+
+# nt-api
+
nictool api
+
+
diff --git a/conf.d/http.yml b/conf.d/http.yml
new file mode 100644
index 0000000..9827a29
--- /dev/null
+++ b/conf.d/http.yml
@@ -0,0 +1,4 @@
+
+default:
+ host: localhost
+ port: 3000
diff --git a/conf.d/mysql.yml b/conf.d/mysql.yml
new file mode 100644
index 0000000..d60b3c1
--- /dev/null
+++ b/conf.d/mysql.yml
@@ -0,0 +1,27 @@
+
+default:
+ host: 127.0.0.1
+ port: 3306
+ user: nictool
+ database: nictool
+ timezone: +00:00
+ dateStrings:
+ - DATETIME
+ - TIMESTAMP
+ decimalNumbers: true
+
+production:
+ host: mysql
+ password: "********"
+
+test:
+ user: root
+ password: root
+
+cov:
+ user: root
+ password: root
+
+development:
+ password: StaySafeOutThere
+ # socketPath: /opt/local/var/run/mysql82/mysqld.sock
diff --git a/conf.d/session.yml b/conf.d/session.yml
new file mode 100644
index 0000000..5ae7901
--- /dev/null
+++ b/conf.d/session.yml
@@ -0,0 +1,30 @@
+
+default:
+ cookie:
+ # https://hapi.dev/module/cookie/api/?v=12.0.1
+ name: sid-nictool
+ password: af1b926a5e21f535c4f5b6c42941c4cf
+ # ttl:
+ # domain:
+ path: /
+ # clearInvalid: false
+ isSameSite: Strict
+ isSecure: true
+ isHttpOnly: true
+ keepAlive: false
+ # redirectTo:
+
+production:
+ cookie:
+ # Change to your own secret password. hint: openssl rand -hex 16
+ # password:
+
+test:
+ cookie:
+ isSecure: false
+ password: ^NicTool.Is,The#Best_Dns-Manager$
+
+development:
+ cookie:
+ isSecure: false
+ password: ^NicTool.Is,The#Best_Dns-Manager$
\ No newline at end of file
diff --git a/html/index.html b/html/index.html
new file mode 100644
index 0000000..73b58c9
--- /dev/null
+++ b/html/index.html
@@ -0,0 +1,8 @@
+
+
+ Index
+
+
+ Hello World.
+
+
diff --git a/lib/config.js b/lib/config.js
new file mode 100644
index 0000000..b63b369
--- /dev/null
+++ b/lib/config.js
@@ -0,0 +1,48 @@
+const fs = require('fs/promises')
+
+const YAML = require('yaml')
+
+class config {
+ constructor(opts = {}) {
+ this.cfg = {}
+ this.debug = process.env.NODE_DEBUG ? true : false
+ this.env = process.env.NODE_ENV ?? opts.env
+ if (this.debug) console.log(`debug: true, env: ${this.env}`)
+ }
+
+ async get(name, env) {
+ const cacheKey = [name, env ?? this.env].join(':')
+ if (this.cfg?.[cacheKey]) return this.cfg[cacheKey] // cached
+
+ const str = await fs.readFile(`./conf.d/${name}.yml`, 'utf8')
+ const cfg = YAML.parse(str)
+ // if (this.debug) console.log(cfg)
+
+ this.cfg[cacheKey] = applyDefaults(cfg[env ?? this.env], cfg.default)
+ return this.cfg[cacheKey]
+ }
+
+ getSync(name, env) {
+ const cacheKey = [name, env ?? this.env].join(':')
+ if (this.cfg?.[cacheKey]) return this.cfg[cacheKey] // cached
+
+ const str = require('fs').readFileSync(`./conf.d/${name}.yml`, 'utf8')
+ const cfg = YAML.parse(str)
+
+ this.cfg[cacheKey] = applyDefaults(cfg[env ?? this.env], cfg.default)
+ return this.cfg[cacheKey]
+ }
+}
+
+function applyDefaults(cfg = {}, defaults = {}) {
+ for (const d in defaults) {
+ if (cfg[d] === undefined) {
+ cfg[d] = defaults[d]
+ } else if (typeof cfg[d] === 'object' && typeof defaults[d] === 'object') {
+ cfg[d] = applyDefaults(cfg[d], defaults[d])
+ }
+ }
+ return cfg
+}
+
+module.exports = new config()
diff --git a/lib/group.js b/lib/group.js
new file mode 100644
index 0000000..4bc2e7e
--- /dev/null
+++ b/lib/group.js
@@ -0,0 +1,39 @@
+const mysql = require('./mysql')
+
+const validate = require('@nictool/nt-validate')
+
+class Group {
+ constructor() {}
+
+ async create(args) {
+ // console.log(`create`)
+ const { error } = validate.group.validate(args)
+ if (error) console.error(error)
+
+ const g = await this.read({ nt_group_id: args.nt_group_id })
+ if (g.length) {
+ // console.log(g)
+ return g[0].nt_group_id
+ }
+
+ const groupId = await mysql.insert(`INSERT INTO nt_group`, args)
+ return groupId
+ }
+
+ async read(args) {
+ return await mysql.select(`SELECT * FROM nt_group WHERE`, args)
+ }
+
+ async destroy(args) {
+ const g = await this.read({ nt_group_id: args.nt_group_id })
+ // console.log(g)
+ if (g.length === 1) {
+ await mysql.execute(`DELETE FROM nt_group WHERE nt_group_id=?`, [
+ g[0].nt_group_id,
+ ])
+ }
+ }
+}
+
+module.exports = new Group()
+module.exports._mysql = mysql
diff --git a/lib/mysql.js b/lib/mysql.js
new file mode 100644
index 0000000..4a56462
--- /dev/null
+++ b/lib/mysql.js
@@ -0,0 +1,104 @@
+// const crypto = require('crypto')
+const mysql = require('mysql2/promise')
+
+const util = require('./util')
+util.setEnv()
+const config = require('./config')
+
+class MySQL {
+ constructor() {
+ this._debug = config.debug
+ }
+
+ async connect() {
+ // if (this.dbh && this.dbh?.connection?.connectionId) return this.dbh;
+
+ const cfg = await config.get('mysql')
+ if (config.debug) console.log(cfg)
+
+ this.dbh = await mysql.createConnection(cfg)
+ if (config.debug)
+ console.log(`MySQL connection id ${this.dbh.connection.connectionId}`)
+ return this.dbh
+ }
+
+ async execute(query, paramsArray) {
+ if (!this.dbh || this.dbh?.connection?._closing) {
+ if (config.debug) console.log(`(re)connecting to MySQL`)
+ this.dbh = await this.connect()
+ }
+
+ // console.log(query)
+ // console.log(paramsArray)
+ const [rows, fields] = await this.dbh.execute(query, paramsArray)
+ if (this.debug()) {
+ if (fields) console.log(fields)
+ console.log(rows)
+ }
+
+ if (/^(REPLACE|INSERT) INTO/.test(query)) return rows.insertId
+
+ return rows
+ }
+
+ async insert(query, params = {}) {
+ if (!this.dbh || this.dbh?.connection?._closing) {
+ if (config.debug) console.log(`(re)connecting to MySQL`)
+ this.dbh = await this.connect()
+ }
+
+ query += `(${Object.keys(params).join(',')}) VALUES(${Object.keys(params).map(() => '?')})`
+
+ // console.log(query)
+ // console.log(Object.values(params))
+ const [rows, fields] = await this.dbh.execute(query, Object.values(params))
+ if (this.debug()) {
+ if (fields) console.log(fields)
+ console.log(rows)
+ }
+
+ return rows.insertId
+ }
+
+ async select(query, params = {}) {
+ if (!this.dbh || this.dbh?.connection?._closing) {
+ if (config.debug) console.log(`(re)connecting to MySQL`)
+ this.dbh = await this.connect()
+ }
+
+ let paramsArray = []
+ if (Array.isArray(params)) {
+ paramsArray = [...params]
+ } else if (typeof params === 'object' && !Array.isArray(params)) {
+ // Object to SQL. Eg. { id: 'sample' } -> SELECT...WHERE id=?, ['sample']
+ let first = true
+ for (const p in params) {
+ if (!first) query += ' AND'
+ query += ` ${p}=?`
+ paramsArray.push(params[p])
+ first = false
+ }
+ }
+
+ const [rows, fields] = await this.dbh.execute(query, paramsArray)
+ if (this.debug()) {
+ if (fields) console.log(fields)
+ console.log(rows)
+ }
+ return rows
+ }
+
+ async disconnect(dbh) {
+ const d = dbh || this.dbh
+ if (config.debug)
+ console.log(`MySQL connection id ${d.connection.connectionId}`)
+ await d.end()
+ }
+
+ debug(val) {
+ if (val !== undefined) this._debug = val
+ return this._debug
+ }
+}
+
+module.exports = new MySQL()
diff --git a/lib/session.js b/lib/session.js
new file mode 100644
index 0000000..da6ba2e
--- /dev/null
+++ b/lib/session.js
@@ -0,0 +1,44 @@
+const Mysql = require('./mysql')
+
+class Session {
+ constructor() {}
+
+ async create(args) {
+ const r = await this.read({ nt_user_session: args.nt_user_session })
+ if (r) return r
+
+ const query = `INSERT INTO nt_user_session`
+
+ const id = await Mysql.insert(query, {
+ nt_user_id: args.nt_user_id,
+ nt_user_session: args.nt_user_session,
+ last_access: parseInt(Date.now() / 1000, 10),
+ })
+
+ return id
+ }
+
+ async read(args) {
+ let query = `SELECT s.*
+ FROM nt_user_session s
+ LEFT JOIN nt_user u ON s.nt_user_id = u.nt_user_id
+ WHERE u.deleted=0`
+
+ const params = []
+ if (args.id) {
+ query += ` AND s.nt_user_session_id = ?`
+ params.push(args.id)
+ }
+ if (args.nt_user_session) {
+ query += ` AND s.nt_user_session = ?`
+ params.push(args.nt_user_session)
+ }
+
+ const sessions = await Mysql.execute(query, params)
+ // console.log(sessions)
+ return sessions[0]
+ }
+}
+
+module.exports = new Session()
+module.exports._mysql = Mysql
diff --git a/lib/user.js b/lib/user.js
new file mode 100644
index 0000000..da41c55
--- /dev/null
+++ b/lib/user.js
@@ -0,0 +1,185 @@
+const crypto = require('node:crypto')
+const validate = require('@nictool/nt-validate')
+
+const mysql = require('./mysql')
+
+class User {
+ constructor(args) {
+ this.debug = args?.debug ?? false
+ }
+
+ async authenticate(authTry) {
+ if (this.debug) console.log(authTry)
+ let [username, group] = authTry.username.split('@')
+ if (!group) group = 'NicTool'
+
+ const query = `SELECT nt_user.*, nt_group.name AS groupname
+ FROM nt_user, nt_group
+ WHERE nt_user.nt_group_id = nt_group.nt_group_id
+ AND nt_group.deleted=0
+ AND nt_user.deleted=0
+ AND nt_user.username = ?
+ AND nt_group.name = ?`
+
+ for (const u of await mysql.execute(query, [username, group])) {
+ if (
+ await this.validPassword(
+ authTry.password,
+ u.password,
+ authTry.username,
+ u.pass_salt,
+ )
+ )
+ return u
+ }
+ }
+
+ async create(args) {
+ const { error } = validate.user.validate(args)
+ if (error) console.error(error)
+
+ const u = await this.read({
+ nt_user_id: args.nt_user_id,
+ nt_group_id: args.nt_group_id,
+ })
+ if (u.length) {
+ // console.log(u)
+ return u[0].nt_user_id
+ }
+
+ if (args.password) {
+ if (!args.pass_salt) args.pass_salt = this.generateSalt()
+ args.password = await this.hashAuthPbkdf2(args.password, args.pass_salt)
+ }
+
+ const userId = await mysql.insert(`INSERT INTO nt_user`, args)
+ return userId
+ }
+
+ async read(args) {
+ return await mysql.select(
+ `SELECT email, first_name, last_name, nt_group_id, nt_user_id, username, email, deleted
+ FROM nt_user WHERE`,
+ args,
+ )
+ }
+
+ async delete(args, val) {
+ const u = await this.read({ nt_user_id: args.nt_user_id })
+ if (u.length === 1) {
+ await mysql.execute(`UPDATE nt_user SET deleted=? WHERE nt_user_id=?`, [
+ val ?? 1,
+ u[0].nt_user_id,
+ ])
+ }
+ }
+
+ async destroy(args) {
+ const u = await this.read({ nt_user_id: args.nt_user_id })
+ if (u.length === 1) {
+ await mysql.execute(`DELETE FROM nt_user WHERE nt_user_id=?`, [
+ u[0].nt_user_id,
+ ])
+ }
+ }
+
+ async get_perms(user_id) {
+ return await mysql.execute(
+ `
+ SELECT ${getPermFields()} FROM nt_perm
+ WHERE deleted=0
+ AND nt_user_id = ?`,
+ [user_id],
+ )
+ }
+
+ generateSalt(length = 16) {
+ const chars = Array.from({ length: 87 }, (_, i) =>
+ String.fromCharCode(i + 40),
+ ) // ASCII 40-126
+ let salt = ''
+ for (let i = 0; i < length; i++) {
+ salt += chars[Math.floor(Math.random() * 87)]
+ }
+ return salt
+ }
+
+ async hashAuthPbkdf2(pass, salt) {
+ return new Promise((resolve, reject) => {
+ // match the defaults for NicTool 2.x
+ crypto.pbkdf2(pass, salt, 5000, 32, 'sha512', (err, derivedKey) => {
+ if (err) return reject(err)
+ resolve(derivedKey.toString('hex'))
+ })
+ })
+ }
+
+ async validPassword(passTry, passDb, username, salt) {
+ if (!salt && passTry === passDb) return true // plain pass, TODO, encrypt!
+
+ if (salt) {
+ const hashed = await this.hashAuthPbkdf2(passTry, salt)
+ if (this.debug) console.log(`hashed: (${hashed === passDb}) ${hashed}`)
+ return hashed === passDb
+ }
+
+ // Check for HMAC SHA-1 password
+ if (/^[0-9a-f]{40}$/.test(passDb)) {
+ const digest = crypto
+ .createHmac('sha1', username.toLowerCase())
+ .update(passTry)
+ .digest('hex')
+ if (this.debug) console.log(`digest: (${digest === passDb}) ${digest}`)
+ return digest === passDb
+ }
+
+ return false
+ }
+
+ async getSession(sessionId) {
+ let query = `SELECT s.*
+ FROM nt_user_session s
+ LEFT JOIN nt_user u ON s.nt_user_id = u.nt_user_id
+ WHERE u.deleted=0
+ AND s.nt_user_session = ?`
+
+ const session = await mysql.execute(query, [sessionId])
+ if (this.debug) console.log(session)
+ return session[0]
+ }
+}
+
+module.exports = new User()
+module.exports._mysql = mysql
+
+function getPermFields() {
+ return (
+ `nt_perm.` +
+ [
+ 'group_write',
+ 'group_create',
+ 'group_delete',
+
+ 'zone_write',
+ 'zone_create',
+ 'zone_delegate',
+ 'zone_delete',
+
+ 'zonerecord_write',
+ 'zonerecord_create',
+ 'zonerecord_delegate',
+ 'zonerecord_delete',
+
+ 'user_write',
+ 'user_create',
+ 'user_delete',
+
+ 'nameserver_write',
+ 'nameserver_create',
+ 'nameserver_delete',
+
+ 'self_write',
+ 'usable_ns',
+ ].join(`, nt_perm.`)
+ )
+}
diff --git a/lib/util.js b/lib/util.js
new file mode 100644
index 0000000..2b43f29
--- /dev/null
+++ b/lib/util.js
@@ -0,0 +1,19 @@
+exports.setEnv = () => {
+ if (process.env.NODE_ENV !== undefined) return
+
+ switch (require('os').hostname()) {
+ case 'mbp.simerson.net':
+ case 'imac27.simerson.net':
+ process.env.NODE_ENV = 'development'
+ break
+ default:
+ process.env.NODE_ENV = 'test'
+ }
+ console.log(`NODE_ENV: ${process.env.NODE_ENV}`)
+}
+
+exports.meta = {
+ api: {
+ version: require('../package.json').version,
+ },
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4f6e5ff
--- /dev/null
+++ b/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "nt-api",
+ "version": "3.0.0",
+ "description": "NicTool API",
+ "main": "index.js",
+ "scripts": {
+ "format": "npm run lint:fix && npm run prettier:fix",
+ "lint": "npx eslint *.js **/*.js",
+ "lint:fix": "npm run lint -- --fix",
+ "prettier": "npx prettier *.js lib routes test html --check",
+ "prettier:fix": "npm run prettier -- --write",
+ "start": "NODE_ENV=production node ./server",
+ "test": "test/fixtures/run.sh",
+ "versions": "npx dependency-version-checker check",
+ "watch": "npm run test -- --watch"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/NicTool/api.git"
+ },
+ "keywords": [
+ "nictool",
+ "api",
+ "dns",
+ "management"
+ ],
+ "author": "Matt Simerson ",
+ "license": "BSD-3-Clause",
+ "bugs": {
+ "url": "https://github.com/NicTool/api/issues"
+ },
+ "homepage": "https://github.com/NicTool/api#readme",
+ "devDependencies": {
+ "eslint": "^8.56.0"
+ },
+ "dependencies": {
+ "@hapi/basic": "^7.0.2",
+ "@hapi/cookie": "^12.0.1",
+ "@hapi/hapi": "^21.3.3",
+ "@hapi/hoek": "^11.0.4",
+ "@hapi/inert": "^7.1.0",
+ "@nictool/nt-validate": "^0.6.1",
+ "mysql2": "^3.9.1",
+ "qs": "^6.11.2",
+ "yaml": "^2.3.4"
+ }
+}
diff --git a/routes/index.js b/routes/index.js
new file mode 100644
index 0000000..218dd7d
--- /dev/null
+++ b/routes/index.js
@@ -0,0 +1,96 @@
+'use strict'
+
+const path = require('node:path')
+
+const hapi = require('@hapi/hapi')
+const qs = require('qs')
+// const hoek = require('@hapi/hoek')
+// const validate = require('@nictool/nt-validate')
+
+const util = require('../lib/util')
+util.setEnv()
+const config = require('../lib/config')
+const user = require('../lib/user')
+const UserRoutes = require('./user')
+
+let server
+
+const setup = async () => {
+ const httpCfg = await config.get('http')
+
+ server = hapi.server({
+ port: httpCfg.port,
+ host: httpCfg.host,
+ query: {
+ parser: (query) => qs.parse(query),
+ },
+ routes: {
+ files: {
+ relativeTo: path.join(__dirname, 'html'),
+ },
+ },
+ })
+
+ await server.register(require('@hapi/basic'))
+ await server.register(require('@hapi/cookie'))
+ await server.register(require('@hapi/inert'))
+ const sessionCfg = await config.get('session')
+
+ server.auth.strategy('session', 'cookie', {
+ cookie: sessionCfg.cookie,
+
+ redirectTo: '/login',
+
+ validate: async (request, session) => {
+ // console.log(`validate session: ${session}`)
+ const account = await session.read({ nt_user_session: session })
+
+ if (!account) return { isValid: false } // invalid cookie
+
+ return { isValid: true, credentials: account }
+ },
+ })
+
+ server.auth.default('session')
+
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: (request) => {
+ return `Hello World! ${request?.auth?.credentials?.name}`
+ },
+ // options: {},
+ })
+
+ UserRoutes(server)
+
+ server.route({
+ method: '*',
+ path: '/{any*}',
+ handler: function (request, h) {
+ return h.response('404 Error! Page Not Found!').code(404)
+ },
+ })
+
+ server.events.on('stop', () => {
+ user._mysql.disconnect()
+ })
+}
+
+exports.init = async () => {
+ await setup()
+ await server.initialize()
+ return server
+}
+
+exports.start = async () => {
+ await setup()
+ await server.start()
+ console.log(`Server running at: ${server.info.uri}`)
+ return server
+}
+
+process.on('unhandledRejection', (err) => {
+ console.error(err)
+ process.exit(1)
+})
diff --git a/routes/user.js b/routes/user.js
new file mode 100644
index 0000000..783f1d2
--- /dev/null
+++ b/routes/user.js
@@ -0,0 +1,88 @@
+const schema = require('@nictool/nt-validate')
+
+const User = require('../lib/user')
+
+module.exports = (server) => {
+ server.route([
+ {
+ method: 'GET',
+ path: '/login',
+ options: {
+ auth: { mode: 'try' },
+ plugins: {
+ cookie: {
+ redirectTo: false,
+ },
+ },
+ handler: async (request, h) => {
+ if (request.auth.isAuthenticated) {
+ return h.redirect('/')
+ }
+
+ return 'You need to log in!'
+ },
+ },
+ },
+ {
+ method: 'POST',
+ path: '/login',
+ options: {
+ auth: { mode: 'try' },
+ handler: async (request, h) => {
+ const account = await User.authenticate(request.payload)
+ if (!account) return 'Invalid authentication'
+
+ // TODO: generate session
+
+ // console.log(account)
+
+ request.cookieAuth.set({ id: account.nt_user_id })
+ return h.redirect('/')
+ },
+ validate: {
+ payload: schema.login,
+ },
+ },
+ },
+ {
+ method: 'GET',
+ path: '/logout',
+ options: {
+ handler: (request, h) => {
+ request.cookieAuth.clear()
+ return h.redirect('/')
+ },
+ },
+ },
+ ])
+}
+
+/*
+ server.route({
+ method: 'POST', // GET PUT POST DELETE
+ path: '/login',
+ handler: (request, h) => {
+ // request.query
+ // request.params
+ // request.payload
+ // console.log(request.payload)
+ return 'Hello Login World!'
+ },
+ options: {
+ auth: { mode: 'try' },
+ // plugins: {
+ // cookie: {
+ // redirectTo: false,
+ // }
+ // },
+ // response: {},
+ validate: {
+ // headers: true,
+ // query: true,
+ params: validate.login,
+ // payload: true,
+ // state: true,
+ },
+ },
+ }),
+*/
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..f2b02f8
--- /dev/null
+++ b/server.js
@@ -0,0 +1,5 @@
+'use strict'
+
+const { start } = require('./routes/index')
+
+start()
diff --git a/sql/01_nt_group.sql b/sql/01_nt_group.sql
new file mode 100644
index 0000000..79ccee1
--- /dev/null
+++ b/sql/01_nt_group.sql
@@ -0,0 +1,50 @@
+#
+# Copyright 2001 Dajoba, LLC -
+# Copyright 2004-2024 The Network People, Inc.
+
+DROP TABLE IF EXISTS nt_group;
+CREATE TABLE `nt_group` (
+ nt_group_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ parent_group_id INT UNSIGNED NOT NULL DEFAULT 0,
+ name varchar(255) NOT NULL,
+ deleted tinyint(1) unsigned NOT NULL DEFAULT 0,
+ PRIMARY KEY (`nt_group_id`),
+ KEY `nt_group_idx1` (`parent_group_id`),
+ KEY `nt_group_idx2` (`name`(191)),
+ KEY `nt_group_idx3` (`deleted`)
+) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
+
+
+DROP TABLE IF EXISTS nt_group_log;
+CREATE TABLE nt_group_log(
+ nt_group_log_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ nt_group_id INT UNSIGNED NOT NULL,
+ nt_user_id INT UNSIGNED NOT NULL,
+ action ENUM('added','modified','deleted','moved') NOT NULL,
+ timestamp INT UNSIGNED NOT NULL,
+ modified_group_id INT UNSIGNED NOT NULL,
+ parent_group_id INT UNSIGNED,
+ name VARCHAR(255),
+ PRIMARY KEY (`nt_group_log_id`),
+ KEY `nt_group_log_idx1` (`nt_group_id`),
+ KEY `nt_group_log_idx2` (`timestamp`)
+ /* CONSTRAINT `nt_group_log_ibfk_1` FOREIGN KEY (`nt_group_id`) REFERENCES `nt_group` (`nt_group_id`) ON DELETE CASCADE ON UPDATE CASCADE */
+) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPRESSED;
+
+
+DROP TABLE IF EXISTS nt_group_subgroups;
+CREATE TABLE nt_group_subgroups(
+ nt_group_id INT UNSIGNED NOT NULL,
+ nt_subgroup_id INT UNSIGNED NOT NULL,
+ `rank` INT UNSIGNED NOT NULL,
+ KEY `nt_group_subgroups_idx1` (`nt_group_id`),
+ KEY `nt_group_subgroups_idx2` (`nt_subgroup_id`)
+ /* CONSTRAINT `nt_group_subgroups_ibfk_1` FOREIGN KEY (`nt_group_id`) REFERENCES `nt_group` (`nt_group_id`) ON DELETE CASCADE ON UPDATE CASCADE */
+) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
+
+INSERT INTO `nt_group` (`nt_group_id`, `parent_group_id`, `name`)
+VALUES
+ (1,0,'NicTool');
+INSERT INTO nt_group_log(nt_group_id, nt_user_id, action, timestamp, modified_group_id, parent_group_id)
+VALUES
+ (1, 1, 'added', UNIX_TIMESTAMP(), 1, 0);
diff --git a/sql/02_nt_user.sql b/sql/02_nt_user.sql
new file mode 100644
index 0000000..7e79d1b
--- /dev/null
+++ b/sql/02_nt_user.sql
@@ -0,0 +1,95 @@
+#
+# Copyright 2001 Damon Edwards, Abe Shelton & Greg Schueler
+# Copyright 2004-2024 The Network People, Inc.
+#
+# NicTool is free software; you can redistribute it and/or modify it under
+# the terms of the Affero General Public License as published by Affero,
+# Inc.; either version 1 of the License, or any later version.
+#
+# NicTool is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the Affero GPL for details.
+#
+# You should have received a copy of the Affero General Public License
+# along with this program; if not, write to Affero Inc., 521 Third St,
+# Suite 225, San Francisco, CA 94107, USA
+#
+
+
+DROP TABLE IF EXISTS nt_user;
+CREATE TABLE nt_user(
+ nt_user_id INT UNSIGNED AUTO_INCREMENT NOT NULL,
+ nt_group_id INT UNSIGNED NOT NULL,
+ first_name VARCHAR(120),
+ last_name VARCHAR(160),
+ username VARCHAR(200) NOT NULL,
+ password VARCHAR(1020) NOT NULL,
+ pass_salt VARCHAR(16),
+ email VARCHAR(400) NOT NULL,
+ is_admin TINYINT(1) UNSIGNED DEFAULT NULL,
+ deleted TINYINT(1) UNSIGNED DEFAULT 0 NOT NULL,
+ PRIMARY KEY (`nt_user_id`),
+ KEY `nt_user_idx1` (`username`(191),`password`(191)),
+ KEY `nt_user_idx2` (`deleted`)
+) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
+
+
+DROP TABLE IF EXISTS nt_user_log;
+CREATE TABLE nt_user_log(
+ nt_user_log_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ nt_group_id INT UNSIGNED NOT NULL,
+ nt_user_id INT UNSIGNED NOT NULL,
+ action ENUM('added','modified','deleted','moved') NOT NULL,
+ timestamp INT UNSIGNED NOT NULL,
+ modified_user_id INT UNSIGNED NOT NULL,
+ first_name VARCHAR(120),
+ last_name VARCHAR(160),
+ username VARCHAR(200),
+ password VARCHAR(1020),
+ pass_salt VARCHAR(16),
+ email VARCHAR(400)
+) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPRESSED;
+
+
+DROP TABLE IF EXISTS nt_user_session;
+CREATE TABLE nt_user_session(
+ nt_user_session_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ nt_user_id INT UNSIGNED NOT NULL,
+ nt_user_session VARCHAR(100) NOT NULL,
+ last_access INT UNSIGNED NOT NULL,
+ PRIMARY KEY (`nt_user_session_id`),
+ KEY `nt_user_session_idx1` (`nt_user_id`,`nt_user_session`)
+ /* CONSTRAINT `nt_user_session_ibfk_1` FOREIGN KEY (`nt_user_id`) REFERENCES `nt_user` (`nt_user_id`) ON DELETE CASCADE ON UPDATE CASCADE */
+) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
+
+DROP TABLE IF EXISTS nt_user_session_log;
+CREATE TABLE nt_user_session_log(
+ nt_user_session_log_id INT UNSIGNED AUTO_INCREMENT NOT NULL,
+ nt_user_id INT UNSIGNED NOT NULL,
+ action ENUM('login','logout','timeout') NOT NULL,
+ timestamp INT UNSIGNED NOT NULL,
+ nt_user_session_id INT UNSIGNED,
+ nt_user_session VARCHAR(100),
+ PRIMARY KEY (`nt_user_session_log_id`),
+ KEY `nt_user_id` (`nt_user_id`)
+ /* CONSTRAINT `nt_user_session_log_ibfk_1` FOREIGN KEY (`nt_user_id`) REFERENCES `nt_user` (`nt_user_id`) ON DELETE CASCADE ON UPDATE CASCADE */
+) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPRESSED;
+
+DROP TABLE IF EXISTS nt_user_global_log;
+CREATE TABLE nt_user_global_log(
+ nt_user_global_log_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ nt_user_id INT UNSIGNED NOT NULL,
+ timestamp INT UNSIGNED NOT NULL,
+ action ENUM('added','deleted','modified','moved','recovered','delegated','modified delegation','removed delegation') NOT NULL,
+ object ENUM('zone','group','user','nameserver','zone_record') NOT NULL,
+ object_id INT UNSIGNED NOT NULL,
+ target ENUM('zone','group','user','nameserver','zone_record') ,
+ target_id INT UNSIGNED ,
+ target_name VARCHAR(255),
+ log_entry_id INT UNSIGNED NOT NULL,
+ title VARCHAR(255),
+ description VARCHAR(255),
+ PRIMARY KEY (`nt_user_global_log_id`),
+ KEY `nt_user_global_log_idx1` (`nt_user_id`)
+ /* CONSTRAINT `nt_user_global_log_ibfk_1` FOREIGN KEY (`nt_user_id`) REFERENCES `nt_user` (`nt_user_id`) ON DELETE CASCADE ON UPDATE CASCADE */
+) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPRESSED;
diff --git a/sql/04_nt_nameserver.sql b/sql/04_nt_nameserver.sql
new file mode 100644
index 0000000..2e4841f
--- /dev/null
+++ b/sql/04_nt_nameserver.sql
@@ -0,0 +1,101 @@
+#
+# Copyright 2001 Dajoba, LLC -
+# Copyright 2004-2024 The Network People, Inc.
+
+DROP TABLE IF EXISTS nt_nameserver;
+CREATE TABLE nt_nameserver(
+ nt_nameserver_id SMALLINT UNSIGNED AUTO_INCREMENT NOT NULL,
+ nt_group_id INT UNSIGNED NOT NULL,
+ name VARCHAR(127) NOT NULL,
+ ttl INT UNSIGNED,
+ description VARCHAR(255),
+ address VARCHAR(127) NOT NULL,
+ address6 VARCHAR(127) DEFAULT NULL,
+ remote_login VARCHAR(127) DEFAULT NULL,
+ export_type_id INT UNSIGNED DEFAULT '1',
+ logdir VARCHAR(255),
+ datadir VARCHAR(255),
+ export_interval SMALLINT UNSIGNED,
+ export_serials tinyint(1) UNSIGNED NOT NULL DEFAULT '1',
+ export_status varchar(255) NULL DEFAULT NULL,
+ deleted TINYINT(1) UNSIGNED DEFAULT 0 NOT NULL,
+ PRIMARY KEY (`nt_nameserver_id`),
+ KEY `nt_nameserver_idx1` (`name`),
+ KEY `nt_nameserver_idx2` (`deleted`),
+ KEY `nt_group_id` (`nt_group_id`)
+ /* CONSTRAINT `nt_nameserver_ibfk_1` FOREIGN KEY (`nt_group_id`) REFERENCES `nt_group` (`nt_group_id`) ON DELETE CASCADE ON UPDATE CASCADE */
+) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
+
+
+DROP TABLE IF EXISTS nt_nameserver_log;
+CREATE TABLE nt_nameserver_log(
+ nt_nameserver_log_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ nt_group_id INT UNSIGNED NOT NULL,
+ nt_user_id INT UNSIGNED NOT NULL,
+ action ENUM('added','modified','deleted','moved') NOT NULL,
+ timestamp INT UNSIGNED NOT NULL,
+ nt_nameserver_id SMALLINT UNSIGNED NOT NULL,
+ name VARCHAR(127),
+ ttl INT UNSIGNED,
+ description VARCHAR(255),
+ address VARCHAR(127),
+ address6 VARCHAR(127),
+ export_type_id INT UNSIGNED DEFAULT '1',
+ logdir VARCHAR(255),
+ datadir VARCHAR(255),
+ export_interval SMALLINT UNSIGNED,
+ export_serials tinyint(1) UNSIGNED NOT NULL DEFAULT '1',
+ PRIMARY KEY (`nt_nameserver_log_id`),
+ KEY `nt_nameserver_log_idx1` (`nt_nameserver_id`),
+ KEY `nt_nameserver_log_idx2` (`timestamp`)
+) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPRESSED;
+
+
+DROP TABLE IF EXISTS nt_nameserver_export_type;
+CREATE TABLE `nt_nameserver_export_type` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(16) NOT NULL DEFAULT '',
+ `descr` varchar(56) NOT NULL DEFAULT '',
+ `url` varchar(128) DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) DEFAULT CHARSET=utf8mb4;
+
+INSERT INTO `nt_nameserver_export_type` (`id`, `name`, `descr`, `url`)
+VALUES (1,'djbdns','djbdns (tinydns & axfrdns)','cr.yp.to/djbdns.html'),
+ (2,'bind','BIND (zone files)', 'www.isc.org/downloads/bind/'),
+ (3,'maradns','MaraDNS', 'maradns.samiam.org'),
+ (4,'powerdns','PowerDNS','www.powerdns.com'),
+ (5,'bind-nsupdate','BIND (nsupdate protocol)',''),
+ (6,'NSD','Name Server Daemon (NSD)','www.nlnetlabs.nl/projects/nsd/'),
+ (7,'dynect','DynECT Standard DNS','dyn.com/managed-dns/'),
+ (8,'knot','Knot DNS','www.knot-dns.cz');
+
+INSERT INTO nt_nameserver(nt_group_id, name, ttl, description, address,
+ export_type_id, logdir, datadir, export_interval) values (1,'ns1.example.com.',86400,'ns east',
+ '198.93.97.188','1','/etc/tinydns-ns1/log/main/',
+ '/etc/tinydns-ns1/root/',120);
+INSERT INTO nt_nameserver(nt_group_id, name, ttl, description, address,
+ export_type_id, logdir, datadir, export_interval) values (1,'ns2.example.com.',86400,'ns west',
+ '216.133.235.6','1','/etc/tinydns-ns2/log/main/','/etc/tinydns-ns2/root/',120);
+INSERT INTO nt_nameserver(nt_group_id, name, ttl, description, address,
+ export_type_id, logdir, datadir, export_interval) values (1,'ns3.example.com.',86400,'ns test',
+ '127.0.0.1','2','/var/log', '/etc/namedb/master/',120);
+INSERT INTO nt_nameserver_log(nt_group_id,nt_user_id, action, timestamp, nt_nameserver_id) VALUES (1,1,'added',UNIX_TIMESTAMP(), 1);
+INSERT INTO nt_nameserver_log(nt_group_id,nt_user_id, action, timestamp, nt_nameserver_id) VALUES (1,1,'added',UNIX_TIMESTAMP(), 2);
+INSERT INTO nt_nameserver_log(nt_group_id,nt_user_id, action, timestamp, nt_nameserver_id) VALUES (1,1,'added',UNIX_TIMESTAMP(), 3);
+
+DROP TABLE IF EXISTS nt_nameserver_export_log;
+CREATE TABLE nt_nameserver_export_log(
+ nt_nameserver_export_log_id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ nt_nameserver_id SMALLINT UNSIGNED NOT NULL,
+ date_start timestamp NULL DEFAULT NULL,
+ date_end timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
+ copied tinyint(1) UNSIGNED NOT NULL DEFAULT 0,
+ message VARCHAR(256) NULL DEFAULT NULL,
+ success tinyint(1) UNSIGNED NULL DEFAULT NULL,
+ partial tinyint(1) UNSIGNED NOT NULL DEFAULT 0,
+ KEY `nt_nameserver_export_log_idx1` (`nt_nameserver_id`)
+) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPRESSED;
+
+DROP TABLE IF EXISTS nt_nameserver_qlog;
+DROP TABLE IF EXISTS nt_nameserver_qlogfile;
\ No newline at end of file
diff --git a/sql/06_resource_records.sql b/sql/06_resource_records.sql
new file mode 100644
index 0000000..e7ae115
--- /dev/null
+++ b/sql/06_resource_records.sql
@@ -0,0 +1,44 @@
+# Copyright 2004-2024 The Network People, Inc.
+
+DROP TABLE IF EXISTS resource_record_type;
+CREATE TABLE resource_record_type (
+ id smallint(2) unsigned NOT NULL,
+ name varchar(10) NOT NULL,
+ description varchar(55) NULL DEFAULT NULL,
+ reverse tinyint(1) UNSIGNED NOT NULL DEFAULT 1,
+ forward tinyint(1) UNSIGNED NOT NULL DEFAULT 1,
+ obsolete tinyint(1) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`id`),
+ UNIQUE `name` (`name`)
+) DEFAULT CHARSET=utf8mb4;
+
+INSERT INTO `resource_record_type` (`id`, `name`, `description`, `reverse`, `forward`, `obsolete`)
+VALUES
+ (1,'A','Address',1,1,0),
+ (2,'NS','Name Server',1,1,0),
+ (5,'CNAME','Canonical Name',1,1,0),
+ (6,'SOA','Start Of Authority',0,0,0),
+ (12,'PTR','Pointer',1,0,0),
+ (13,'HINFO','Host Info',0,0,1),
+ (15,'MX','Mail Exchanger',0,1,0),
+ (16,'TXT','Text',1,1,0),
+ (24,'SIG','Signature',0,0,0),
+ (25,'KEY','Key',0,0,0),
+ (28,'AAAA','Address IPv6',0,1,0),
+ (29,'LOC','Location',0,1,0),
+ (30,'NXT','Next',0,0,1),
+ (33,'SRV','Service',0,1,0),
+ (35,'NAPTR','Naming Authority Pointer',1,1,0),
+ (39,'DNAME','Delegation Name',0,0,0),
+ (43,'DS','Delegation Signer',1,1,0),
+ (44,'SSHFP','Secure Shell Key Fingerprints',0,1,0),
+ (46,'RRSIG','Resource Record Signature',0,1,0),
+ (47,'NSEC','Next Secure',0,1,0),
+ (48,'DNSKEY','DNS Public Key',0,1,0),
+ (50,'NSEC3','Next Secure v3',0,0,0),
+ (51,'NSEC3PARAM','NSEC3 Parameters',0,0,0),
+ (99,'SPF','Sender Policy Framework',0,0,1),
+ (250,'TSIG','Transaction Signature',0,0,0),
+ (252,'AXFR',NULL,0,0,0),
+ (256,'URI','URI',0,1,0),
+ (257,'CAA','Certification Authority Authorization',0,1,0);
diff --git a/sql/08_nt_zone.sql b/sql/08_nt_zone.sql
new file mode 100644
index 0000000..fe2ca3a
--- /dev/null
+++ b/sql/08_nt_zone.sql
@@ -0,0 +1,77 @@
+#
+# Copyright 2001 Damon Edwards, Abe Shelton & Greg Schueler
+# Copyright 2004-2024 The Network People, Inc.
+#
+# NicTool is free software; you can redistribute it and/or modify it under
+# the terms of the Affero General Public License as published by Affero,
+# Inc.; either version 1 of the License, or any later version.
+#
+# NicTool is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the Affero GPL for details.
+#
+# You should have received a copy of the Affero General Public License
+# along with this program; if not, write to Affero Inc., 521 Third St,
+# Suite 225, San Francisco, CA 94107, USA
+#
+
+
+DROP TABLE IF EXISTS nt_zone;
+CREATE TABLE nt_zone(
+ nt_zone_id INT UNSIGNED AUTO_INCREMENT NOT NULL,
+ nt_group_id INT UNSIGNED NOT NULL,
+ zone VARCHAR(255) NOT NULL,
+ mailaddr VARCHAR(127),
+ description VARCHAR(255),
+ serial INT UNSIGNED NOT NULL DEFAULT '1',
+ refresh INT UNSIGNED,
+ retry INT UNSIGNED,
+ expire INT UNSIGNED,
+ minimum INT UNSIGNED,
+ ttl INT UNSIGNED,
+ location VARCHAR(8) DEFAULT NULL,
+ last_modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
+ last_publish DATETIME DEFAULT NULL,
+ deleted TINYINT(1) UNSIGNED DEFAULT 0 NOT NULL,
+ PRIMARY KEY (`nt_zone_id`),
+ KEY `nt_zone_idx1` (`nt_group_id`),
+ KEY `nt_zone_idx2` (`deleted`),
+ KEY `nt_zone_idx3` (`zone`)
+) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPRESSED;
+
+
+DROP TABLE IF EXISTS nt_zone_log;
+CREATE TABLE nt_zone_log(
+ nt_zone_log_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ nt_group_id INT UNSIGNED NOT NULL,
+ nt_user_id INT UNSIGNED NOT NULL,
+ action ENUM('added','modified','deleted','moved','recovered') NOT NULL,
+ timestamp INT UNSIGNED NOT NULL,
+ nt_zone_id INT UNSIGNED NOT NULL,
+ zone VARCHAR(255) NOT NULL,
+ mailaddr VARCHAR(127),
+ description VARCHAR(255),
+ serial INT UNSIGNED,
+ refresh INT UNSIGNED,
+ retry INT UNSIGNED,
+ expire INT UNSIGNED,
+ minimum INT UNSIGNED,
+ ttl INT UNSIGNED,
+ location VARCHAR(8) DEFAULT NULL,
+ PRIMARY KEY (`nt_zone_log_id`),
+ KEY `nt_zone_log_idx1` (`timestamp`),
+ KEY `nt_zone_log_idx2` (`nt_zone_id`),
+ KEY `nt_zone_log_idx3` (`action`),
+ KEY `nt_group_id` (`nt_group_id`),
+ KEY `nt_user_id` (`nt_user_id`)
+ /* CONSTRAINT `nt_zone_log_ibfk_3` FOREIGN KEY (`nt_user_id`) REFERENCES `nt_user` (`nt_user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ ** CONSTRAINT `nt_zone_log_ibfk_1` FOREIGN KEY (`nt_zone_id`) REFERENCES `nt_zone` (`nt_zone_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ ** CONSTRAINT `nt_zone_log_ibfk_2` FOREIGN KEY (`nt_group_id`) REFERENCES `nt_group` (`nt_group_id`) ON DELETE CASCADE ON UPDATE CASCADE */
+) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPRESSED;
+
+
+CREATE TABLE nt_zone_nameserver (
+ nt_zone_id int(10) unsigned NOT NULL,
+ nt_nameserver_id smallint(5) unsigned NOT NULL,
+ UNIQUE KEY `zone_ns_id` (`nt_zone_id`,`nt_nameserver_id`)
+) DEFAULT CHARSET=utf8mb4;
diff --git a/sql/09_nt_zone_record.sql b/sql/09_nt_zone_record.sql
new file mode 100644
index 0000000..0214a21
--- /dev/null
+++ b/sql/09_nt_zone_record.sql
@@ -0,0 +1,69 @@
+#
+# Copyright 2001 Damon Edwards, Abe Shelton & Greg Schueler
+# Copyright 2004-2024 The Network People, Inc.
+#
+# NicTool is free software; you can redistribute it and/or modify it under
+# the terms of the Affero General Public License as published by Affero,
+# Inc.; either version 1 of the License, or any later version.
+#
+# NicTool is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the Affero GPL for details.
+#
+# You should have received a copy of the Affero General Public License
+# along with this program; if not, write to Affero Inc., 521 Third St,
+# Suite 225, San Francisco, CA 94107, USA
+#
+
+DROP TABLE IF EXISTS nt_zone_record;
+CREATE TABLE nt_zone_record(
+ nt_zone_record_id INT UNSIGNED AUTO_INCREMENT NOT NULL,
+ nt_zone_id INT UNSIGNED NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ ttl INT UNSIGNED NOT NULL DEFAULT 0,
+ description VARCHAR(255),
+ type_id SMALLINT(2) UNSIGNED NOT NULL,
+ address VARCHAR(5120) NOT NULL,
+ weight SMALLINT UNSIGNED,
+ priority SMALLINT UNSIGNED,
+ other VARCHAR(512),
+ location VARCHAR(2) DEFAULT NULL,
+ timestamp timestamp NULL DEFAULT NULL,
+ deleted TINYINT(1) UNSIGNED DEFAULT 0 NOT NULL,
+ PRIMARY KEY (`nt_zone_record_id`),
+ KEY `nt_zone_record_idx1` (`name`),
+ KEY `nt_zone_record_idx2` (address(191)),
+ KEY `nt_zone_record_idx3` (`nt_zone_id`),
+ KEY `nt_zone_record_idx4` (`deleted`)
+ /* CONSTRAINT `nt_zone_record_ibfk_1` FOREIGN KEY (`nt_zone_id`) REFERENCES `nt_zone` (`nt_zone_id`) ON DELETE CASCADE ON UPDATE CASCADE */
+) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPRESSED;
+
+
+DROP TABLE IF EXISTS nt_zone_record_log;
+CREATE TABLE nt_zone_record_log(
+ nt_zone_record_log_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ nt_zone_id INT UNSIGNED NOT NULL,
+ nt_user_id INT UNSIGNED NOT NULL,
+ action ENUM('added','modified','deleted','recovered') NOT NULL,
+ timestamp INT UNSIGNED NOT NULL,
+ nt_zone_record_id INT UNSIGNED NOT NULL,
+ name VARCHAR(255),
+ ttl INT UNSIGNED,
+ description VARCHAR(255),
+ type_id SMALLINT(2) UNSIGNED NOT NULL,
+ address VARCHAR(5120),
+ weight SMALLINT UNSIGNED,
+ priority SMALLINT UNSIGNED,
+ other VARCHAR(512),
+ location VARCHAR(2) DEFAULT NULL,
+ PRIMARY KEY (`nt_zone_record_log_id`),
+ KEY `nt_zone_record_log_idx1` (`timestamp`),
+ KEY `nt_zone_record_log_idx2` (`nt_zone_record_id`),
+ KEY `nt_zone_record_log_idx3` (`nt_zone_id`),
+ KEY `nt_zone_record_log_idx4` (`action`),
+ KEY `nt_user_id` (`nt_user_id`)
+ /* CONSTRAINT `nt_zone_record_log_ibfk_3` FOREIGN KEY (`nt_zone_record_id`) REFERENCES `nt_zone_record` (`nt_zone_record_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ ** CONSTRAINT `nt_zone_record_log_ibfk_1` FOREIGN KEY (`nt_zone_id`) REFERENCES `nt_zone` (`nt_zone_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ ** CONSTRAINT `nt_zone_record_log_ibfk_2` FOREIGN KEY (`nt_user_id`) REFERENCES `nt_user` (`nt_user_id`) ON DELETE CASCADE ON UPDATE CASCADE */
+) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPRESSED;
+
diff --git a/sql/10_nt_perm.sql b/sql/10_nt_perm.sql
new file mode 100644
index 0000000..5ab958e
--- /dev/null
+++ b/sql/10_nt_perm.sql
@@ -0,0 +1,145 @@
+#
+# Copyright 2001 Damon Edwards, Abe Shelton & Greg Schueler
+# Copyright 2004-2024 The Network People, Inc.
+#
+# NicTool is free software; you can redistribute it and/or modify it under
+# the terms of the Affero General Public License as published by Affero,
+# Inc.; either version 1 of the License, or any later version.
+#
+# NicTool is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the Affero GPL for details.
+#
+# You should have received a copy of the Affero General Public License
+# along with this program; if not, write to Affero Inc., 521 Third St,
+# Suite 225, San Francisco, CA 94107, USA
+#
+
+DROP TABLE IF EXISTS nt_perm;
+CREATE TABLE nt_perm(
+ nt_perm_id INT UNSIGNED AUTO_INCREMENT NOT NULL,
+ nt_group_id INT UNSIGNED DEFAULT NULL,
+ nt_user_id INT UNSIGNED DEFAULT NULL,
+ inherit_perm INT UNSIGNED DEFAULT NULL,
+ perm_name VARCHAR(50),
+
+ group_write TINYINT UNSIGNED NOT NULL DEFAULT 0,
+ group_create TINYINT UNSIGNED NOT NULL DEFAULT 0,
+ #group_delegate TINYINT UNSIGNED NOT NULL DEFAULT 0,
+ group_delete TINYINT UNSIGNED NOT NULL DEFAULT 0,
+
+ zone_write TINYINT UNSIGNED NOT NULL DEFAULT 0,
+ zone_create TINYINT UNSIGNED NOT NULL DEFAULT 0,
+ zone_delegate TINYINT UNSIGNED NOT NULL DEFAULT 0,
+ zone_delete TINYINT UNSIGNED NOT NULL DEFAULT 0,
+
+ zonerecord_write TINYINT UNSIGNED NOT NULL DEFAULT 0,
+ zonerecord_create TINYINT UNSIGNED NOT NULL DEFAULT 0,
+ zonerecord_delegate TINYINT UNSIGNED NOT NULL DEFAULT 0,
+ zonerecord_delete TINYINT UNSIGNED NOT NULL DEFAULT 0,
+
+ user_write TINYINT UNSIGNED NOT NULL DEFAULT 0,
+ user_create TINYINT UNSIGNED NOT NULL DEFAULT 0,
+ user_delete TINYINT UNSIGNED NOT NULL DEFAULT 0,
+
+ nameserver_write TINYINT UNSIGNED NOT NULL DEFAULT 0,
+ nameserver_create TINYINT UNSIGNED NOT NULL DEFAULT 0,
+ nameserver_delete TINYINT UNSIGNED NOT NULL DEFAULT 0,
+
+ self_write TINYINT UNSIGNED NOT NULL DEFAULT 0,
+
+ usable_ns VARCHAR(50),
+
+ deleted TINYINT(1) UNSIGNED DEFAULT 0 NOT NULL,
+
+ PRIMARY KEY (`nt_perm_id`),
+ KEY `nt_perm_idx1` (`nt_group_id`,`nt_user_id`),
+ KEY `nt_perm_idx2` (`nt_user_id`)
+) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
+
+INSERT into nt_perm VALUES(1,1,0,NULL,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,0);
+
+DROP TABLE IF EXISTS nt_delegate;
+CREATE TABLE nt_delegate(
+ #nt_delegate_id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ nt_group_id INT UNSIGNED NOT NULL,
+ nt_object_id INT UNSIGNED NOT NULL,
+ nt_object_type ENUM('ZONE','ZONERECORD','NAMESERVER','USER','GROUP') NOT NULL ,
+ delegated_by_id INT UNSIGNED NOT NULL,
+ delegated_by_name VARCHAR(50),
+
+ perm_write TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ perm_delete TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ perm_delegate TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+
+ zone_perm_add_records TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_delete_records TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+
+ # more specific access perms --- not used yet
+
+ zone_perm_modify_zone TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_modify_mailaddr TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_modify_desc TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_modify_minimum TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_modify_serial TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_modify_refresh TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_modify_retry TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_modify_expire TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_modify_ttl TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_modify_nameservers TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+
+ zonerecord_perm_modify_name TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zonerecord_perm_modify_type TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zonerecord_perm_modify_addr TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zonerecord_perm_modify_weight TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zonerecord_perm_modify_ttl TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zonerecord_perm_modify_desc TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+
+ deleted TINYINT(1) UNSIGNED DEFAULT 0 NOT NULL,
+ KEY `nt_delegate_idx1` (`nt_group_id`,`nt_object_id`,`nt_object_type`),
+ KEY `nt_delegate_idx2` (`nt_object_id`,`nt_object_type`)
+ /* CONSTRAINT `nt_delegate_ibfk_1` FOREIGN KEY (`nt_group_id`) REFERENCES `nt_group` (`nt_group_id`) ON DELETE CASCADE ON UPDATE CASCADE */
+);
+
+
+DROP TABLE IF EXISTS nt_delegate_log;
+CREATE TABLE nt_delegate_log(
+ nt_delegate_log_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ nt_user_id INT UNSIGNED NOT NULL,
+ nt_user_name VARCHAR(50),
+ action ENUM('delegated','modified','deleted') NOT NULL,
+ nt_object_type ENUM('ZONE','ZONERECORD','NAMESERVER','USER','GROUP') NOT NULL ,
+ nt_object_id INT UNSIGNED NOT NULL,
+ nt_group_id INT UNSIGNED NOT NULL,
+ timestamp INT UNSIGNED NOT NULL,
+
+ perm_write TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ perm_delete TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ perm_delegate TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+
+ zone_perm_add_records TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_delete_records TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+
+ # more specific access perms --- not used yet
+
+ zone_perm_modify_zone TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_modify_mailaddr TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_modify_desc TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_modify_minimum TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_modify_serial TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_modify_refresh TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_modify_retry TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_modify_expire TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_modify_ttl TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zone_perm_modify_nameservers TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+
+ zonerecord_perm_modify_name TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zonerecord_perm_modify_type TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zonerecord_perm_modify_addr TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zonerecord_perm_modify_weight TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zonerecord_perm_modify_ttl TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+ zonerecord_perm_modify_desc TINYINT UNSIGNED DEFAULT 1 NOT NULL
+
+ #delegating groups: not implemented yet
+ #group_perm_modify_name TINYINT UNSIGNED DEFAULT 1 NOT NULL,
+);
diff --git a/sql/12_nt_options.sql b/sql/12_nt_options.sql
new file mode 100644
index 0000000..107cf8c
--- /dev/null
+++ b/sql/12_nt_options.sql
@@ -0,0 +1,16 @@
+# Copyright 2004-2024 The Network People, Inc.
+
+DROP TABLE IF EXISTS nt_options;
+CREATE TABLE nt_options (
+ option_id int(11) unsigned NOT NULL auto_increment,
+ option_name varchar(64) NOT NULL default '',
+ option_value text NOT NULL,
+ PRIMARY KEY (`option_id`),
+ UNIQUE KEY `option_name` (`option_name`)
+) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
+
+INSERT INTO `nt_options`
+VALUES (1,'db_version','2.34'),
+ (2,'session_timeout','45'),
+ (3,'default_group','NicTool')
+ ;
diff --git a/sql/90_nt_summary.sql b/sql/90_nt_summary.sql
new file mode 100644
index 0000000..9840616
--- /dev/null
+++ b/sql/90_nt_summary.sql
@@ -0,0 +1,30 @@
+#
+# Copyright 2001 Damon Edwards, Abe Shelton & Greg Schueler
+# Copyright 2004-2024 The Network People, Inc.
+#
+# NicTool is free software; you can redistribute it and/or modify it under
+# the terms of the Affero General Public License as published by Affero,
+# Inc.; either version 1 of the License, or any later version.
+#
+# NicTool is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the Affero GPL for details.
+#
+# You should have received a copy of the Affero General Public License
+# along with this program; if not, write to Affero Inc., 521 Third St,
+# Suite 225, San Francisco, CA 94107, USA
+#
+
+DROP TABLE IF EXISTS nt_group_summary;
+DROP TABLE IF EXISTS nt_group_current_summary;
+DROP TABLE IF EXISTS nt_nameserver_general_summary;
+DROP TABLE IF EXISTS nt_nameserver_summary;
+DROP TABLE IF EXISTS nt_nameserver_current_summary;
+DROP TABLE IF EXISTS nt_user_general_summary;
+DROP TABLE IF EXISTS nt_user_summary;
+DROP TABLE IF EXISTS nt_user_current_summary;
+DROP TABLE IF EXISTS nt_zone_general_summary;
+DROP TABLE IF EXISTS nt_zone_summary;
+DROP TABLE IF EXISTS nt_zone_current_summary;
+DROP TABLE IF EXISTS nt_zone_record_summary;
+DROP TABLE IF EXISTS nt_zone_record_current_summary;
diff --git a/sql/init-mysql.sh b/sql/init-mysql.sh
new file mode 100755
index 0000000..0a43f1f
--- /dev/null
+++ b/sql/init-mysql.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+# configure MySQL in the GitHub runners
+case "$(uname -s)" in
+ Linux*)
+ export MYSQL_PWD=root
+ ;;
+ Darwin*)
+ mysqladmin --user=root --password='' --protocol=tcp password 'root'
+ export MYSQL_PWD="root"
+ ;;
+ CYGWIN*|MINGW*|MINGW32*|MSYS*)
+ export MYSQL_PWD=""
+ ;;
+esac
+
+# AUTH="--defaults-extra-file=./sql/my-gha.cnf"
+
+# mysql --user=root -e 'DROP DATABASE IF EXISTS nictool;' || exit 1
+mysql --user=root -e 'CREATE DATABASE nictool;' || exit 1
+
+for f in './sql/*.sql';
+do
+ cat $f | mysql --user=root nictool || exit 1
+done
+
+exit 0
\ No newline at end of file
diff --git a/sql/my-gha.cnf b/sql/my-gha.cnf
new file mode 100644
index 0000000..93acbe4
--- /dev/null
+++ b/sql/my-gha.cnf
@@ -0,0 +1,3 @@
+[client]
+user=root
+password=root
\ No newline at end of file
diff --git a/test/config.js b/test/config.js
new file mode 100644
index 0000000..92e6451
--- /dev/null
+++ b/test/config.js
@@ -0,0 +1,73 @@
+const assert = require('node:assert/strict')
+const { describe, it } = require('node:test')
+
+const config = require('../lib/config')
+
+describe('config', function () {
+ describe('get', function () {
+ it(`loads mysql test config`, async function () {
+ const cfg = await config.get('mysql', 'test')
+ assert.deepEqual(cfg, mysqlTestCfg)
+ })
+
+ it(`loads mysql test config syncronously`, function () {
+ const cfg = config.getSync('mysql', 'test')
+ assert.deepEqual(cfg, mysqlTestCfg)
+ })
+
+ it(`loads mysql cov config`, async function () {
+ const cfg = await config.get('mysql', 'cov')
+ assert.deepEqual(cfg, mysqlTestCfg)
+ })
+
+ it(`loads mysql cov config (from cache)`, async function () {
+ process.env.NODE_DEBUG = 1
+ const cfg = await config.get('mysql', 'cov')
+ assert.deepEqual(cfg, mysqlTestCfg)
+ process.env.NODE_DEBUG = ''
+ })
+
+ it(`loads session test config`, async function () {
+ const cfg = await config.get('session', 'test')
+ assert.deepEqual(cfg, sessCfg)
+ })
+
+ it(`loads session test config syncronously`, function () {
+ const cfg = config.getSync('session', 'test')
+ assert.deepEqual(cfg, sessCfg)
+ })
+
+ it(`loads http test config syncronously`, function () {
+ const cfg = config.getSync('http', 'test')
+ assert.deepEqual(cfg, httpCfg)
+ })
+ })
+})
+
+const mysqlTestCfg = {
+ host: '127.0.0.1',
+ port: 3306,
+ user: 'root',
+ password: 'root',
+ database: 'nictool',
+ timezone: '+00:00',
+ dateStrings: ['DATETIME', 'TIMESTAMP'],
+ decimalNumbers: true,
+}
+
+const sessCfg = {
+ cookie: {
+ isHttpOnly: true,
+ isSameSite: 'Strict',
+ isSecure: false,
+ name: 'sid-nictool',
+ password: '^NicTool.Is,The#Best_Dns-Manager$',
+ path: '/',
+ },
+ keepAlive: false,
+}
+
+const httpCfg = {
+ host: 'localhost',
+ port: 3000,
+}
diff --git a/test/fixtures/.setup.js b/test/fixtures/.setup.js
new file mode 100644
index 0000000..b2fc10b
--- /dev/null
+++ b/test/fixtures/.setup.js
@@ -0,0 +1,41 @@
+const group = require('../../lib/group')
+const user = require('../../lib/user')
+// const session = require('../../lib/session')
+
+const userCase = require('./user.json')
+const groupCase = require('./group.json')
+
+const setup = async () => {
+ await createTestGroup()
+ await createTestUser()
+ // await createTestSession()
+ await user._mysql.disconnect()
+ await group._mysql.disconnect()
+ process.exit()
+}
+
+setup()
+
+async function createTestGroup() {
+ let g = group.read({ nt_group_id: groupCase.nt_group_id })
+ if (g.length === 1) return
+
+ await group.create(groupCase)
+}
+
+async function createTestUser() {
+ let u = await user.read({ nt_user_id: userCase.nt_user_id })
+ if (u.length === 1) return
+
+ const instance = JSON.parse(JSON.stringify(userCase))
+ instance.password = 'Wh@tA-Decent#P6ssw0rd'
+
+ await user.create(instance)
+}
+
+async function createTestSession() {
+ this.sessionId = await session.create({
+ nt_user_id: userCase.nt_user_id,
+ nt_user_session: 12345,
+ })
+}
diff --git a/test/fixtures/.teardown.js b/test/fixtures/.teardown.js
new file mode 100644
index 0000000..23129d3
--- /dev/null
+++ b/test/fixtures/.teardown.js
@@ -0,0 +1,28 @@
+const group = require('../../lib/group')
+const user = require('../../lib/user')
+// const session = require('../../lib/session')
+const userCase = require('./user.json')
+const groupCase = require('./group.json')
+
+const teardown = async () => {
+ // await destroyTestSession()
+ await destroyTestUser()
+ await destroyTestGroup()
+ await user._mysql.disconnect()
+ await group._mysql.disconnect()
+ process.exit()
+}
+
+teardown()
+
+async function destroyTestGroup() {
+ await group.destroy({ nt_group_id: groupCase.nt_group_id })
+}
+
+async function destroyTestUser() {
+ await user.destroy({ nt_user_id: userCase.nt_user_id })
+}
+
+async function destroyTestSession() {
+ // await session.destroy({ nt_user_id: ... })
+}
diff --git a/test/fixtures/group.json b/test/fixtures/group.json
new file mode 100644
index 0000000..1b684d5
--- /dev/null
+++ b/test/fixtures/group.json
@@ -0,0 +1,6 @@
+{
+ "nt_group_id": 4096,
+ "parent_group_id": 0,
+ "name": "example.com",
+ "deleted": false
+}
diff --git a/test/fixtures/run.sh b/test/fixtures/run.sh
new file mode 100755
index 0000000..9c6fa25
--- /dev/null
+++ b/test/fixtures/run.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+node test/fixtures/.setup.js
+node --test
+node test/fixtures/.teardown.js
\ No newline at end of file
diff --git a/test/fixtures/user.json b/test/fixtures/user.json
new file mode 100644
index 0000000..af849fe
--- /dev/null
+++ b/test/fixtures/user.json
@@ -0,0 +1,9 @@
+{
+ "nt_group_id": 4096,
+ "nt_user_id": 4096,
+ "username": "unit-test",
+ "email": "unit-test@example.com",
+ "first_name": "Unit",
+ "last_name": "Test",
+ "deleted": false
+}
diff --git a/test/mysql.js b/test/mysql.js
new file mode 100644
index 0000000..f78a7bf
--- /dev/null
+++ b/test/mysql.js
@@ -0,0 +1,24 @@
+const assert = require('node:assert/strict')
+const { describe, it } = require('node:test')
+
+const mysql = require('../lib/mysql')
+
+describe('mysql', () => {
+ it('connects', async () => {
+ this.dbh = await mysql.connect()
+ assert.ok(this.dbh.connection.connectionId)
+ })
+
+ if (process.env.NODE_ENV === 'cov') {
+ it('is noisy when debug=true', async () => {
+ mysql.debug(true)
+ await mysql.execute(`SHOW DATABASES`)
+ await mysql.select(`SELECT * FROM nt_group`)
+ })
+ }
+
+ it('disconnects', async () => {
+ assert.ok(this.dbh.connection.connectionId)
+ await mysql.disconnect(this.dbh)
+ })
+})
diff --git a/test/routes.js b/test/routes.js
new file mode 100644
index 0000000..866dae0
--- /dev/null
+++ b/test/routes.js
@@ -0,0 +1,40 @@
+const assert = require('node:assert/strict')
+const { describe, it, before, after } = require('node:test')
+
+const { init } = require('../routes')
+const userCase = require('./fixtures/user.json')
+
+before(async () => {
+ this.server = await init()
+})
+
+after(async () => {
+ await this.server.stop()
+})
+
+describe('routes', () => {
+ describe('GET /login', () => {
+ it('responds with 200', async () => {
+ const res = await this.server.inject({
+ method: 'GET',
+ url: '/login',
+ })
+ assert.deepEqual(res.statusCode, 200)
+ })
+ })
+
+ describe('POST /login', () => {
+ it('responds with 302', async () => {
+ const res = await this.server.inject({
+ method: 'POST',
+ url: '/login',
+ payload: {
+ username: `${userCase.username}@example.com`,
+ password: 'Wh@tA-Decent#P6ssw0rd',
+ },
+ })
+ // console.log(res.result)
+ assert.deepEqual(res.statusCode, 302)
+ })
+ })
+})
diff --git a/test/user.js b/test/user.js
new file mode 100644
index 0000000..cac8401
--- /dev/null
+++ b/test/user.js
@@ -0,0 +1,180 @@
+const assert = require('node:assert/strict')
+const { describe, it, before, after } = require('node:test')
+
+const session = require('../lib/session')
+const user = require('../lib/user')
+
+const userCase = require('./fixtures/user.json')
+
+before(async () => {
+ this.sessionId = await session.create({
+ nt_user_id: userCase.nt_user_id,
+ nt_user_session: 12345,
+ })
+
+ let users = await user.read({ nt_user_id: userCase.nt_user_id })
+ if (users.length === 1) return
+
+ const instance = JSON.parse(JSON.stringify(userCase))
+ instance.password = 'Wh@tA-Decent#P6ssw0rd'
+
+ await user.create(instance)
+})
+
+after(async () => {
+ // user._mysql.disconnect()
+ session._mysql.disconnect()
+})
+
+describe('user', function () {
+ describe('read', function () {
+ it('finds existing user by nt_user_id', async () => {
+ const u = await user.read({ nt_user_id: 4096 })
+ // console.log(u)
+ assert.deepEqual(u[0], {
+ nt_group_id: 4096,
+ nt_user_id: 4096,
+ username: 'unit-test',
+ email: 'unit-test@example.com',
+ first_name: 'Unit',
+ last_name: 'Test',
+ deleted: 0,
+ })
+ })
+
+ it('finds existing user by username', async () => {
+ const u = await user.read({ username: 'unit-test' })
+ // console.log(u)
+ assert.deepEqual(u[0], {
+ nt_group_id: 4096,
+ nt_user_id: 4096,
+ username: 'unit-test',
+ email: 'unit-test@example.com',
+ first_name: 'Unit',
+ last_name: 'Test',
+ deleted: 0,
+ })
+ })
+
+ it('deletes a user', async () => {
+ await user.delete({ nt_user_id: 4096 })
+ let u = await user.read({ nt_user_id: 4096 })
+ assert.equal(u[0].deleted, 1)
+ await user.delete({ nt_user_id: 4096 }, 0) // restore
+ u = await user.read({ nt_user_id: 4096 })
+ assert.equal(u[0].deleted, 0)
+ })
+ })
+
+ describe('get_perms', function () {
+ it.skip('gets user permissions', async () => {
+ const p = await user.get_perms(242)
+ assert.deepEqual(p[0], {
+ group_create: 1,
+ group_delete: 1,
+ group_write: 1,
+ nameserver_create: 0,
+ nameserver_delete: 0,
+ nameserver_write: 0,
+ self_write: 1,
+ usable_ns: null,
+ user_create: 1,
+ user_delete: 1,
+ user_write: 1,
+ zone_create: 1,
+ zone_delegate: 1,
+ zone_delete: 1,
+ zone_write: 1,
+ zonerecord_create: 1,
+ zonerecord_delegate: 1,
+ zonerecord_delete: 1,
+ zonerecord_write: 1,
+ })
+ })
+ })
+
+ describe('validPassword', function () {
+ it('auths user with plain text password', async () => {
+ const r = await user.validPassword('test', 'test', 'demo', '')
+ assert.equal(r, true)
+ })
+
+ it('auths valid pbkdb2 password', async () => {
+ const r = await user.validPassword(
+ 'YouGuessedIt!',
+ '050cfa70c3582be0d5bfae25138a8486dc2e6790f39bc0c4e111223ba6034432',
+ 'unit-test',
+ '(ICzAm2.QfCa6.MN',
+ )
+ assert.equal(r, true)
+ })
+
+ it('rejects invalid pbkdb2 password', async () => {
+ const r = await user.validPassword(
+ 'YouMissedIt!',
+ '050cfa70c3582be0d5bfae25138a8486dc2e6790f39bc0c4e111223ba6034432',
+ 'unit-test',
+ '(ICzAm2.QfCa6.MN',
+ )
+ assert.equal(r, false)
+ })
+
+ it('auths valid SHA1 password', async () => {
+ const r = await user.validPassword(
+ 'OhNoYouDont',
+ '083007777a5241d01abba70c938c60d80be60027',
+ 'unit-test',
+ )
+ assert.equal(r, true)
+ })
+
+ it('rejects invalid SHA1 password', async () => {
+ const r = await user.validPassword(
+ 'OhNoYouDont',
+ '083007777a5241d01abba7Oc938c60d80be60027',
+ 'unit-test',
+ )
+ assert.equal(r, false)
+ })
+ })
+
+ describe('authenticate', () => {
+ it.todo('rejects invalid user', () => {})
+
+ it.todo('rejects invalid pass', () => {})
+
+ it('accepts a valid username & password', async () => {
+ const u = await user.authenticate({
+ username: 'unit-test@example.com',
+ password: 'Wh@tA-Decent#P6ssw0rd',
+ })
+ assert.ok(u)
+ })
+ })
+})
+
+describe('session', function () {
+ // session._mysql.debug(true)
+
+ describe('create', () => {
+ it('creates a login session', async () => {
+ const s = await session.create({
+ nt_user_id: userCase.nt_user_id,
+ nt_user_session: 12345,
+ })
+ assert.ok(s)
+ })
+ })
+
+ describe('read', function () {
+ it('finds a session by ID', async () => {
+ const s = await session.read({ id: this.sessionId })
+ assert.ok(s)
+ })
+
+ it('finds a session by session', async () => {
+ const s = await session.read({ nt_user_session: 12345 })
+ assert.ok(s)
+ })
+ })
+})
diff --git a/test/util.js b/test/util.js
new file mode 100644
index 0000000..3eeb4e0
--- /dev/null
+++ b/test/util.js
@@ -0,0 +1,20 @@
+const assert = require('node:assert/strict')
+const { describe, it } = require('node:test')
+
+const util = require('../lib/util')
+
+describe('util', function () {
+ describe('setEnv', function () {
+ it('sets process.env.NODE_ENV', async () => {
+ assert.equal(process.env.NODE_ENV, undefined)
+ util.setEnv()
+ assert.ok(process.env.NODE_ENV)
+ })
+ })
+
+ describe('meta', () => {
+ it('returns the package version', () => {
+ assert.deepEqual(util.meta, { api: { version: '3.0.0' } })
+ })
+ })
+})