From 7555113dd4aa0106578f75377bc2a417d1388e19 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sun, 18 Feb 2024 20:24:08 -0800 Subject: [PATCH] beginning of JSON API (#1) * partial user, group, & session * add GHA configs * bring in SQL schemas * ci: enable mysql service (ubuntu, macos, windows) * feat(config): copy properties recursively * session: get cookie params from config * move http config into conf.d/http.yml * ci: mark win as experimental * doc(README): add ci & coverage badges --- .eslintrc.yaml | 8 ++ .github/workflows/ci.yml | 110 ++++++++++++++++++++ .github/workflows/codeql.yml | 14 +++ .github/workflows/publish.yml | 18 ++++ .gitignore | 2 + .prettierrc.yml | 3 + README.md | 8 +- conf.d/http.yml | 4 + conf.d/mysql.yml | 27 +++++ conf.d/session.yml | 30 ++++++ html/index.html | 8 ++ lib/config.js | 48 +++++++++ lib/group.js | 39 +++++++ lib/mysql.js | 104 +++++++++++++++++++ lib/session.js | 44 ++++++++ lib/user.js | 185 ++++++++++++++++++++++++++++++++++ lib/util.js | 19 ++++ package.json | 47 +++++++++ routes/index.js | 96 ++++++++++++++++++ routes/user.js | 88 ++++++++++++++++ server.js | 5 + sql/01_nt_group.sql | 50 +++++++++ sql/02_nt_user.sql | 95 +++++++++++++++++ sql/04_nt_nameserver.sql | 101 +++++++++++++++++++ sql/06_resource_records.sql | 44 ++++++++ sql/08_nt_zone.sql | 77 ++++++++++++++ sql/09_nt_zone_record.sql | 69 +++++++++++++ sql/10_nt_perm.sql | 145 ++++++++++++++++++++++++++ sql/12_nt_options.sql | 16 +++ sql/90_nt_summary.sql | 30 ++++++ sql/init-mysql.sh | 27 +++++ sql/my-gha.cnf | 3 + test/config.js | 73 ++++++++++++++ test/fixtures/.setup.js | 41 ++++++++ test/fixtures/.teardown.js | 28 +++++ test/fixtures/group.json | 6 ++ test/fixtures/run.sh | 5 + test/fixtures/user.json | 9 ++ test/mysql.js | 24 +++++ test/routes.js | 40 ++++++++ test/user.js | 180 +++++++++++++++++++++++++++++++++ test/util.js | 20 ++++ 42 files changed, 1989 insertions(+), 1 deletion(-) create mode 100644 .eslintrc.yaml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .prettierrc.yml create mode 100644 conf.d/http.yml create mode 100644 conf.d/mysql.yml create mode 100644 conf.d/session.yml create mode 100644 html/index.html create mode 100644 lib/config.js create mode 100644 lib/group.js create mode 100644 lib/mysql.js create mode 100644 lib/session.js create mode 100644 lib/user.js create mode 100644 lib/util.js create mode 100644 package.json create mode 100644 routes/index.js create mode 100644 routes/user.js create mode 100644 server.js create mode 100644 sql/01_nt_group.sql create mode 100644 sql/02_nt_user.sql create mode 100644 sql/04_nt_nameserver.sql create mode 100644 sql/06_resource_records.sql create mode 100644 sql/08_nt_zone.sql create mode 100644 sql/09_nt_zone_record.sql create mode 100644 sql/10_nt_perm.sql create mode 100644 sql/12_nt_options.sql create mode 100644 sql/90_nt_summary.sql create mode 100755 sql/init-mysql.sh create mode 100644 sql/my-gha.cnf create mode 100644 test/config.js create mode 100644 test/fixtures/.setup.js create mode 100644 test/fixtures/.teardown.js create mode 100644 test/fixtures/group.json create mode 100755 test/fixtures/run.sh create mode 100644 test/fixtures/user.json create mode 100644 test/mysql.js create mode 100644 test/routes.js create mode 100644 test/user.js create mode 100644 test/util.js 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' } }) + }) + }) +})