From 28f979eeb2efd0f630f675db27f49a85afcded8f Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Mon, 4 Mar 2024 21:48:55 -0800 Subject: [PATCH] release 3.0.0-alpha.3 (#28) - routes/permission: added GET, POST, DELETE - permission.get: default search with deleted=0 - session.put: added - session: store user/group info in cookie (saves DB trips) - mysql(insert, select, update, delete): return just the query - lib/group.get: convert booleans - lib/user.get: convert booleans --- .gitmodules | 3 + .release | 1 + CHANGELOG.md | 16 ++++ README.md | 2 +- conf.d/http.yml | 3 +- conf.d/mysql.yml | 2 +- lib/group.js | 61 ++++++++------- lib/group.test.js | 13 ++-- lib/mysql.js | 44 ++++------- lib/mysql.test.js | 103 ++++++++++++++++---------- lib/permission.js | 38 +++++----- lib/permission.test.js | 14 ++-- lib/session.js | 36 +++++++-- lib/test/user.json | 1 + lib/user.js | 64 +++++++--------- lib/user.test.js | 12 +-- lib/util.js | 2 +- package.json | 8 +- routes/group.js | 1 + routes/group.test.js | 2 +- routes/index.js | 4 +- routes/permission.js | 120 ++++++++++++++++++++++++++++++ routes/permission.test.js | 144 ++++++++++++++++++++++++++++++++++++ routes/session.js | 40 +++++----- routes/session.test.js | 4 +- routes/test/permission.json | 39 +++++----- routes/user.test.js | 17 +---- test.js | 7 ++ test.sh | 1 + 29 files changed, 558 insertions(+), 244 deletions(-) create mode 100644 .gitmodules create mode 160000 .release create mode 100644 CHANGELOG.md create mode 100644 routes/permission.js create mode 100644 routes/permission.test.js diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a8e94cb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule ".release"] + path = .release + url = git@github.com:msimerson/.release.git diff --git a/.release b/.release new file mode 160000 index 0000000..8959a99 --- /dev/null +++ b/.release @@ -0,0 +1 @@ +Subproject commit 8959a99cbbd4ac0e6871386a81d15b06307d6528 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..76c646c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# CHANGES + +### Unreleased + +### 3.0.0-alpha.3 + +- routes/permission: added GET, POST, DELETE +- permission.get: default search with deleted=0 +- session.put: added +- session: store user/group info in cookie (saves DB trips) +- mysql(insert, select, update, delete): return just the query +- lib/group.get: convert booleans +- lib/user.get: convert booleans + + +[3.0.0-alpha.3]: https://github.com/NicTool/api/releases/tag/3.0.0-alpha.3 diff --git a/README.md b/README.md index 522e7ae..4556af4 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ or `npm run develop (development)` -will start up the HTTP service on the port specified in `conf.d/http.yml`. The default URL for the service is [http://localhost:3000](http://localhost:3000) and the API methods have documentation at [http://localhost:3000/documentation](http://localhost:3000/documentation). +will start up the HTTP service on the port specified in `conf.d/http.yml`. The default URL for the service is [http://localhost:3000](http://localhost:3000) and the API methods have documentation at [http://localhost:3000/documentation#/](http://localhost:3000/documentation#/). ## Using the API service diff --git a/conf.d/http.yml b/conf.d/http.yml index c16f85f..acd4d46 100644 --- a/conf.d/http.yml +++ b/conf.d/http.yml @@ -1,4 +1,3 @@ - default: host: localhost port: 3000 @@ -6,7 +5,7 @@ default: # https://hapi.dev/module/cookie/api/?v=12.0.1 name: sid-nictool password: af1b926a5e21f535c4f5b6c42941c4cf - ttl: 3600000 # 1 hour + ttl: 3600000 # 1 hour # domain: path: / clearInvalid: true diff --git a/conf.d/mysql.yml b/conf.d/mysql.yml index ba5e7ac..ff73d87 100644 --- a/conf.d/mysql.yml +++ b/conf.d/mysql.yml @@ -13,7 +13,7 @@ default: # settings below this line override default settings production: host: mysql - password: "********" + password: '********' # used for CI testing (GitHub Actions workflows) test: diff --git a/lib/group.js b/lib/group.js index 17157dd..8e3f376 100644 --- a/lib/group.js +++ b/lib/group.js @@ -2,6 +2,7 @@ import Mysql from './mysql.js' import { mapToDbColumn } from './util.js' const groupDbMap = { id: 'nt_group_id', parent_gid: 'parent_group_id' } +const boolFields = ['deleted'] class Group { constructor() { @@ -11,34 +12,31 @@ class Group { async create(args) { if (args.id) { const g = await this.get({ id: args.id }) - if (g.length) return g[0].id + if (g.length === 1) return g[0].id } - return await Mysql.insert( - `INSERT INTO nt_group`, - mapToDbColumn(args, groupDbMap), + return await Mysql.execute( + ...Mysql.insert(`nt_group`, mapToDbColumn(args, groupDbMap)), ) } async get(args) { - return await Mysql.select( - `SELECT nt_group_id AS id + const rows = await Mysql.execute( + ...Mysql.select( + `SELECT nt_group_id AS id , parent_group_id AS parent_gid , name - FROM nt_group WHERE`, - mapToDbColumn(args, groupDbMap), - ) - } - - async getAdmin(args) { - return await Mysql.select( - `SELECT nt_group_id AS id - , name - , parent_group_id AS parent_gid - , deleted - FROM nt_group WHERE`, - mapToDbColumn(args, groupDbMap), + , deleted + FROM nt_group`, + mapToDbColumn(args, groupDbMap), + ), ) + for (const r of rows) { + for (const b of boolFields) { + r[b] = r[b] === 1 + } + } + return rows } async put(args) { @@ -46,30 +44,29 @@ class Group { const id = args.id delete args.id // Mysql.debug(1) - const r = await Mysql.update( - `UPDATE nt_group SET`, - `WHERE nt_group_id=${id}`, - mapToDbColumn(args, groupDbMap), + const r = await Mysql.execute( + ...Mysql.update( + `nt_group`, + `nt_group_id=${id}`, + mapToDbColumn(args, groupDbMap), + ), ) // console.log(r) return r.changedRows === 1 } - async delete(args, val) { - const g = await this.getAdmin(args) - if (g.length !== 1) return false + async delete(args) { await Mysql.execute(`UPDATE nt_group SET deleted=? WHERE nt_group_id=?`, [ - val ?? 1, - g[0].id, + args.deleted ?? 1, + args.id, ]) return true } async destroy(args) { - const g = await this.getAdmin(args) - if (g.length === 1) { - await Mysql.execute(`DELETE FROM nt_group WHERE nt_group_id=?`, [g[0].id]) - } + return await Mysql.execute( + ...Mysql.delete(`nt_group`, { nt_group_id: args.id }), + ) } } diff --git a/lib/group.test.js b/lib/group.test.js index 8aaccac..7ec9da1 100644 --- a/lib/group.test.js +++ b/lib/group.test.js @@ -20,6 +20,7 @@ describe('group', function () { id: testCase.id, name: testCase.name, parent_gid: 0, + deleted: false, }) }) @@ -29,6 +30,7 @@ describe('group', function () { id: testCase.id, name: testCase.name, parent_gid: 0, + deleted: false, }) }) @@ -39,6 +41,7 @@ describe('group', function () { id: testCase.id, name: 'example.net', parent_gid: 0, + deleted: false, }, ]) assert.ok(await Group.put({ id: testCase.id, name: testCase.name })) @@ -46,10 +49,10 @@ describe('group', function () { it('deletes a group', async () => { assert.ok(await Group.delete({ id: testCase.id })) - let g = await Group.getAdmin({ id: testCase.id }) - assert.equal(g[0].deleted, 1) - await Group.delete({ id: testCase.id }, 0) // restore - g = await Group.getAdmin({ id: testCase.id }) - assert.equal(g[0].deleted, 0) + let g = await Group.get({ id: testCase.id, deleted: 1 }) + assert.equal(g[0]?.deleted, true) + await Group.delete({ id: testCase.id, deleted: 0 }) // restore + g = await Group.get({ id: testCase.id }) + assert.equal(g[0].deleted, false) }) }) diff --git a/lib/mysql.js b/lib/mysql.js index bb1857e..b55f35c 100644 --- a/lib/mysql.js +++ b/lib/mysql.js @@ -42,34 +42,26 @@ class Mysql { return rows } - async insert(query, params = {}) { - const skipExecute = params.skipExecute ?? false - delete params.skipExecute - - query += `(${Object.keys(params).join(',')}) VALUES(${Object.keys(params).map(() => '?')})` - - if (skipExecute) return query - return await this.execute(query, Object.values(params)) + insert(table, params = {}) { + return [ + `INSERT INTO ${table} (${Object.keys(params).join(',')}) VALUES(${Object.keys(params).map(() => '?')})`, + Object.values(params), + ] } - async select(query, params = {}) { - const skipExecute = params.skipExecute ?? false - delete params.skipExecute - - const [queryWhere, paramsArray] = this.whereConditions(query, params) - - if (skipExecute) return queryWhere - return await this.execute(queryWhere, paramsArray) + select(query, params = {}) { + return this.whereConditions(query, params) } - async update(query, where, params = {}) { - const skipExecute = params.skipExecute ?? false - delete params.skipExecute - - query += ` ${Object.keys(params).join('=?,')}=? ${where}` + update(table, where, params = {}) { + return [ + `UPDATE ${table} SET ${Object.keys(params).join('=?,')}=? WHERE ${where}`, + Object.values(params), + ] + } - if (skipExecute) return { q: query, p: Object.values(params) } - return await this.execute(query, Object.values(params)) + delete(table, params) { + return this.whereConditions(`DELETE FROM ${table}`, params) } whereConditions(query, params) { @@ -82,6 +74,7 @@ class Mysql { // Object to WHERE conditions let first = true for (const p in params) { + if (first) newQuery += ' WHERE' if (!first) newQuery += ' AND' newQuery += ` ${p}=?` paramsArray.push(params[p]) @@ -91,11 +84,6 @@ class Mysql { return [newQuery, paramsArray] } - async delete(query, params) { - const [queryWhere, paramsArray] = this.whereConditions(query, params) - return await this.execute(queryWhere, paramsArray) - } - async disconnect(dbh) { const d = dbh || this.dbh if (_debug) console.log(`MySQL connection id ${d.connection.connectionId}`) diff --git a/lib/mysql.test.js b/lib/mysql.test.js index 82a6144..5bb305c 100644 --- a/lib/mysql.test.js +++ b/lib/mysql.test.js @@ -20,57 +20,80 @@ describe('mysql', () => { }) } - it('SQL: formats SELECT queries', async () => { - const r = await Mysql.select(`SELECT * FROM nt_user WHERE`, { - last_name: 'Test', - skipExecute: true, - }) - assert.equal(r, `SELECT * FROM nt_user WHERE last_name=?`) + it('formats SELECT queries', () => { + assert.deepEqual( + Mysql.select(`SELECT * FROM nt_user`, { + last_name: 'Test', + }), + [`SELECT * FROM nt_user WHERE last_name=?`, ['Test']], + ) }) - it('SQL: formats INSERT queries', async () => { - const r = await Mysql.select(`INSERT INTO nt_user SET`, { + it('formats INSERT query', () => { + const r = Mysql.insert(`nt_user`, { first_name: 'uNite', last_name: 'Test', - skipExecute: true, }) - assert.equal(r, `INSERT INTO nt_user SET first_name=? AND last_name=?`) + assert.deepEqual(r, [ + `INSERT INTO nt_user (first_name,last_name) VALUES(?,?)`, + ['uNite', 'Test'], + ]) }) - it('SQL: formats UPDATE queries, 1', async () => { - const { q, p } = await Mysql.update( - `UPDATE nt_user SET`, - `WHERE nt_user_id=4096`, - { first_name: 'uNite', skipExecute: true }, - ) - assert.equal(q, `UPDATE nt_user SET first_name=? WHERE nt_user_id=4096`) - assert.deepEqual(p, ['uNite']) + describe('update', () => { + it('formats with one value', () => { + const r = Mysql.update(`nt_user`, `nt_user_id=4096`, { + first_name: 'uNite', + }) + assert.deepEqual(r, [ + `UPDATE nt_user SET first_name=? WHERE nt_user_id=4096`, + ['uNite'], + ]) + }) + + it('formats with two values', () => { + const r = Mysql.update(`nt_user`, `nt_user_id=4096`, { + last_name: 'Teste', + is_admin: 1, + }) + assert.deepEqual(r, [ + `UPDATE nt_user SET last_name=?,is_admin=? WHERE nt_user_id=4096`, + ['Teste', 1], + ]) + }) + + it('formats with three values', () => { + const r = Mysql.update(`nt_user`, `nt_user_id=4096`, { + first_name: 'Unit', + last_name: 'Test', + is_admin: 0, + }) + assert.deepEqual(r, [ + `UPDATE nt_user SET first_name=?,last_name=?,is_admin=? WHERE nt_user_id=4096`, + ['Unit', 'Test', 0], + ]) + }) }) - it('SQL: formats UPDATE queries, 2', async () => { - const { q, p } = await Mysql.update( - `UPDATE nt_user SET`, - `WHERE nt_user_id=4096`, - { last_name: 'Teste', is_admin: 1, skipExecute: true }, - ) - assert.equal( - q, - `UPDATE nt_user SET last_name=?,is_admin=? WHERE nt_user_id=4096`, - ) - assert.deepEqual(p, ['Teste', 1]) + describe('delete', () => { + it('no params', () => { + assert.deepEqual(Mysql.delete(`nt_user`, {}), [`DELETE FROM nt_user`, []]) + }) + + it('with params', () => { + assert.deepEqual(Mysql.delete(`nt_user`, { last_name: 'Test' }), [ + `DELETE FROM nt_user WHERE last_name=?`, + ['Test'], + ]) + }) }) - it('SQL: formats UPDATE queries, 3', async () => { - const { q, p } = await Mysql.update( - `UPDATE nt_user SET`, - `WHERE nt_user_id=4096`, - { first_name: 'Unit', last_name: 'Test', is_admin: 0, skipExecute: true }, - ) - assert.equal( - q, - `UPDATE nt_user SET first_name=?,last_name=?,is_admin=? WHERE nt_user_id=4096`, - ) - assert.deepEqual(p, ['Unit', 'Test', 0]) + it('executes formatted queries', async () => { + const [query, argsArray] = Mysql.select(`SELECT * FROM nt_options`) + const r = await Mysql.execute(query, argsArray) + assert.deepEqual(r[0].option_id, 1) + + // await Mysql.execute(...Mysql.select(`SELECT * FROM nt_options`)) }) it('disconnects', async () => { diff --git a/lib/permission.js b/lib/permission.js index 3235da6..e868640 100644 --- a/lib/permission.js +++ b/lib/permission.js @@ -20,9 +20,8 @@ class Permission { if (p) return p.id } - return await Mysql.insert( - `INSERT INTO nt_perm`, - mapToDbColumn(objectToDb(args), permDbMap), + return await Mysql.execute( + ...Mysql.insert(`nt_perm`, mapToDbColumn(objectToDb(args), permDbMap)), ) } @@ -34,9 +33,13 @@ class Permission { , p.perm_name AS name ${getPermFields()} , p.deleted - FROM nt_perm p WHERE` + FROM nt_perm p` // Mysql.debug(1) - const rows = await Mysql.select(query, mapToDbColumn(args, permDbMap)) + if (args.deleted === undefined) args.deleted = false + + const rows = await Mysql.execute( + ...Mysql.select(query, mapToDbColumn(args, permDbMap)), + ) if (rows.length === 0) return if (rows.length > 1) { throw new Error( @@ -59,7 +62,7 @@ class Permission { WHERE p.deleted=0 AND u.deleted=0 AND u.nt_user_id=?` - const rows = await Mysql.select(query, [args.uid]) + const rows = await Mysql.execute(...Mysql.select(query, [args.uid])) return dbToObject(rows[0]) } @@ -68,30 +71,27 @@ class Permission { const id = args.id delete args.id // Mysql.debug(1) - const r = await Mysql.update( - `UPDATE nt_perm SET`, - `WHERE nt_perm_id=${id}`, - mapToDbColumn(args, permDbMap), + const r = await Mysql.execute( + ...Mysql.update( + `nt_perm`, + `nt_perm_id=${id}`, + mapToDbColumn(args, permDbMap), + ), ) return r.changedRows === 1 } - async delete(args, val) { - const p = await this.get(args) - if (!p) return false + async delete(args) { await Mysql.execute(`UPDATE nt_perm SET deleted=? WHERE nt_perm_id=?`, [ - val ?? 1, + args.deleted ?? 1, args.id, ]) return true } async destroy(args) { - const p = await this.get(args) - if (!p) return false - return await Mysql.delete( - `DELETE FROM nt_perm WHERE`, - mapToDbColumn(args, permDbMap), + return await Mysql.execute( + ...Mysql.delete(`nt_perm`, mapToDbColumn(args, permDbMap)), ) } } diff --git a/lib/permission.test.js b/lib/permission.test.js index 9cdc163..aaf2984 100644 --- a/lib/permission.test.js +++ b/lib/permission.test.js @@ -1,12 +1,16 @@ import assert from 'node:assert/strict' import { describe, it, after, before } from 'node:test' -import Permission from './permission.js' +import Group from './group.js' import User from './user.js' -import permTestCase from './test/permission.json' with { type: 'json' } +import Permission from './permission.js' + +import groupTestCase from './test/group.json' with { type: 'json' } import userTestCase from './test/user.json' with { type: 'json' } +import permTestCase from './test/permission.json' with { type: 'json' } before(async () => { + await Group.create(groupTestCase) await User.create(userTestCase) }) @@ -58,9 +62,9 @@ describe('permission', function () { it('deletes a permission', async () => { assert.ok(await Permission.delete({ id: permTestCase.id })) - let p = await Permission.get({ id: permTestCase.id }) - assert.equal(p.deleted, true) - await Permission.delete({ id: permTestCase.id }, 0) // restore + let p = await Permission.get({ id: permTestCase.id, deleted: 1 }) + assert.equal(p?.deleted, true) + await Permission.delete({ id: permTestCase.id, deleted: 0 }) // restore p = await Permission.get({ id: permTestCase.id }) assert.equal(p.deleted, false) }) diff --git a/lib/session.js b/lib/session.js index 51f0366..a5d189e 100644 --- a/lib/session.js +++ b/lib/session.js @@ -16,9 +16,8 @@ class Session { const r = await this.get(args) if (r) return r.id - const id = await Mysql.insert( - `INSERT INTO nt_user_session`, - mapToDbColumn(args, sessionDbMap), + const id = await Mysql.execute( + ...Mysql.insert(`nt_user_session`, mapToDbColumn(args, sessionDbMap)), ) return id } @@ -49,10 +48,35 @@ class Session { return sessions[0] } + async put(args) { + if (!args.id) return false + + if (args.last_access) { + const p = await this.get({ id: args.id }) + // if less than 60 seconds old, do nothing + const now = parseInt(Date.now() / 1000, 10) + const oneMinuteAgo = now - 60 + // update only when +1 minute old (save DB writes) + if (p.last_access > oneMinuteAgo) return true + args.last_access = now + } + + const id = args.id + delete args.id + const r = await Mysql.execute( + ...Mysql.update( + `nt_user_session`, + `nt_user_session_id=${id}`, + mapToDbColumn(args, sessionDbMap), + ), + ) + // console.log(r) + return r.changedRows === 1 + } + async delete(args) { - const r = await Mysql.select( - `DELETE FROM nt_user_session WHERE`, - mapToDbColumn(args, sessionDbMap), + const r = await Mysql.execute( + ...Mysql.delete(`nt_user_session`, mapToDbColumn(args, sessionDbMap)), ) return r.affectedRows === 1 } diff --git a/lib/test/user.json b/lib/test/user.json index 9bb6989..bbc9fac 100644 --- a/lib/test/user.json +++ b/lib/test/user.json @@ -6,5 +6,6 @@ "password": "Wh@tA-Decent#P6ssw0rd", "first_name": "Unit", "last_name": "Test", + "is_admin": false, "deleted": false } diff --git a/lib/user.js b/lib/user.js index 7b621f9..fe10ee4 100644 --- a/lib/user.js +++ b/lib/user.js @@ -5,6 +5,7 @@ import Config from './config.js' import { mapToDbColumn } from './util.js' const userDbMap = { id: 'nt_user_id', gid: 'nt_group_id' } +const boolFields = ['is_admin', 'deleted'] class User { constructor(args = {}) { @@ -70,73 +71,62 @@ class User { args.password = await this.hashAuthPbkdf2(args.password, args.pass_salt) } - const userId = await Mysql.insert( - `INSERT INTO nt_user`, - mapToDbColumn(args, userDbMap), + const userId = await Mysql.execute( + ...Mysql.insert(`nt_user`, mapToDbColumn(args, userDbMap)), ) return userId } async get(args) { - return await Mysql.select( - `SELECT email + if (args.deleted === undefined) args.deleted = false + const rows = await Mysql.execute( + ...Mysql.select( + `SELECT email , first_name , last_name , nt_group_id AS gid , nt_user_id AS id , username , email - FROM nt_user WHERE`, - mapToDbColumn(args, userDbMap), - ) - } - - async getAdmin(args) { - return await Mysql.select( - `SELECT email - , first_name - , last_name - , nt_group_id AS gid - , nt_user_id AS id - , username - , password - , email , deleted - FROM nt_user WHERE`, - mapToDbColumn(args, userDbMap), + FROM nt_user`, + mapToDbColumn(args, userDbMap), + ), ) + for (const r of rows) { + for (const b of boolFields) { + r[b] = r[b] === 1 + } + } + return rows } async put(args) { if (!args.id) return false const id = args.id delete args.id - const r = await Mysql.update( - `UPDATE nt_user SET`, - `WHERE nt_user_id=${id}`, - mapToDbColumn(args, userDbMap), + const r = await Mysql.execute( + ...Mysql.update( + `nt_user`, + `nt_user_id=${id}`, + mapToDbColumn(args, userDbMap), + ), ) return r.changedRows === 1 } - async delete(args, val) { - const u = await this.getAdmin(args) - if (u.length !== 1) return false + async delete(args) { const r = await Mysql.execute( `UPDATE nt_user SET deleted=? WHERE nt_user_id=?`, - [val ?? 1, u[0].id], + [args.deleted ?? 1, args.id], ) return r.changedRows === 1 } async destroy(args) { - const u = await this.getAdmin(args) - if (u.length === 1) { - await Mysql.delete( - `DELETE FROM nt_user WHERE`, - mapToDbColumn({ id: u[0].id }, userDbMap), - ) - } + await Mysql.execute( + ...Mysql.delete(`nt_user`, mapToDbColumn({ id: args.id }, userDbMap)), + ) } generateSalt(length = 16) { diff --git a/lib/user.test.js b/lib/user.test.js index 6a77c30..152706a 100644 --- a/lib/user.test.js +++ b/lib/user.test.js @@ -17,7 +17,7 @@ after(async () => { function sanitize(u) { const r = JSON.parse(JSON.stringify(u)) - for (const f of ['deleted', 'password', 'pass_salt']) { + for (const f of ['password', 'pass_salt']) { delete r[f] } return r @@ -57,11 +57,11 @@ describe('user', function () { describe('DELETE', function () { it('deletes a user', async () => { assert.ok(await User.delete({ id: testCase.id })) - let u = await User.getAdmin({ id: testCase.id }) - assert.equal(u[0].deleted, 1) - await User.delete({ id: testCase.id }, 0) // restore - u = await User.getAdmin({ id: testCase.id }) - assert.equal(u[0].deleted, 0) + let u = await User.get({ id: testCase.id, deleted: 1 }) + assert.equal(u[0].deleted, true) + await User.delete({ id: testCase.id, deleted: 0 }) // restore + u = await User.get({ id: testCase.id }) + assert.equal(u[0].deleted, false) }) }) diff --git a/lib/util.js b/lib/util.js index 38347d9..cb06b11 100644 --- a/lib/util.js +++ b/lib/util.js @@ -24,7 +24,7 @@ const meta = { } function mapToDbColumn(args, maps) { - // create an instance, so we don't mangle the original + // create an instance, so we don't mangle the original args const newArgs = JSON.parse(JSON.stringify(args)) for (const [key, val] of Object.entries(maps)) { diff --git a/package.json b/package.json index f6e4295..0d79d33 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nictool/api", - "version": "3.0.0-alpha.2", + "version": "3.0.0-alpha.3", "description": "NicTool API", "main": "index.js", "type": "module", @@ -8,8 +8,8 @@ "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", + "prettier": "npx prettier *.js conf.d lib routes html --check", + "prettier:fix": "npx prettier *.js conf.d lib routes html --write", "start": "NODE_ENV=production node ./server", "develop": "NODE_ENV=development node ./server", "test": "./test.sh", @@ -43,7 +43,7 @@ "@hapi/hoek": "^11.0.4", "@hapi/inert": "^7.1.0", "@hapi/vision": "^7.0.3", - "@nictool/validate": "^0.7.3", + "@nictool/validate": "^0.7.4", "hapi-swagger": "^17.2.1", "mysql2": "^3.9.2", "qs": "^6.11.2", diff --git a/routes/group.js b/routes/group.js index ddd1ead..ffdbbaf 100644 --- a/routes/group.js +++ b/routes/group.js @@ -19,6 +19,7 @@ function GroupRoutes(server) { deleted: request.query.deleted ?? 0, id: parseInt(request.params.id, 10), }) + if (groups.length !== 1) { return h .response({ diff --git a/routes/group.test.js b/routes/group.test.js index d2668be..2f69a17 100644 --- a/routes/group.test.js +++ b/routes/group.test.js @@ -69,7 +69,7 @@ describe('group routes', () => { assert.equal(res.statusCode, 201) }) - it('GET /group', async () => { + it(`GET /group/${case2Id}`, async () => { const res = await server.inject({ method: 'GET', url: `/group/${case2Id}`, diff --git a/routes/index.js b/routes/index.js index 66abeed..9cdcc08 100644 --- a/routes/index.js +++ b/routes/index.js @@ -19,6 +19,7 @@ import pkgJson from '../package.json' with { type: 'json' } import GroupRoutes from './group.js' import { User, UserRoutes } from './user.js' import { Session, SessionRoutes } from './session.js' +import { PermissionRoutes } from './permission.js' let server @@ -61,7 +62,7 @@ async function setup() { cookie: httpCfg.cookie, validate: async (request, session) => { - const s = await Session.get({ id: session.nt_user_session_id }) + const s = await Session.get({ id: session.id }) if (!s) return { isValid: false } // invalid cookie // const account = await User.get({ id: s.nt_user_id }) @@ -82,6 +83,7 @@ async function setup() { GroupRoutes(server) UserRoutes(server) SessionRoutes(server) + PermissionRoutes(server) server.route({ method: '*', diff --git a/routes/permission.js b/routes/permission.js new file mode 100644 index 0000000..78369d4 --- /dev/null +++ b/routes/permission.js @@ -0,0 +1,120 @@ +import validate from '@nictool/validate' + +import Permission from '../lib/permission.js' +import { meta } from '../lib/util.js' + +function PermissionRoutes(server) { + server.route([ + { + method: 'GET', + path: '/permission/{id}', + options: { + validate: { + query: validate.permission.v3, + }, + response: { + schema: validate.permission.GET, + }, + tags: ['api'], + }, + handler: async (request, h) => { + const getArgs = { + deleted: request.query.deleted === true ? 1 : 0, + id: parseInt(request.params.id, 10), + } + + const permission = await Permission.get(getArgs) + + return h + .response({ + permission, + meta: { + api: meta.api, + msg: `here's your permission`, + }, + }) + .code(200) + }, + }, + { + method: 'POST', + path: '/permission', + options: { + validate: { + payload: validate.permission.POST, + }, + response: { + schema: validate.permission.GET, + }, + tags: ['api'], + }, + handler: async (request, h) => { + const pid = await Permission.create(request.payload) + if (!pid) { + console.log(`POST /permission oops`) // TODO + } + + const permission = await Permission.get({ id: pid }) + + return h + .response({ + permission, + meta: { + api: meta.api, + msg: `the permission was created`, + }, + }) + .code(201) + }, + }, + { + method: 'DELETE', + path: '/permission/{id}', + options: { + validate: { + query: validate.permission.DELETE, + }, + response: { + schema: validate.permission.GET, + }, + tags: ['api'], + }, + handler: async (request, h) => { + const permission = await Permission.get({ + deleted: request.query.deleted === true ? 1 : 0, + id: parseInt(request.params.id, 10), + }) + + if (!permission) { + return h + .response({ + meta: { + api: meta.api, + msg: `I couldn't find that permission`, + }, + }) + .code(404) + } + + await Permission.delete({ + id: permission.id, + deleted: 1, + }) + + return h + .response({ + permission, + meta: { + api: meta.api, + msg: `I deleted that permission`, + }, + }) + .code(200) + }, + }, + ]) +} + +export default PermissionRoutes + +export { Permission, PermissionRoutes } diff --git a/routes/permission.test.js b/routes/permission.test.js new file mode 100644 index 0000000..f595b67 --- /dev/null +++ b/routes/permission.test.js @@ -0,0 +1,144 @@ +import assert from 'node:assert/strict' +import { describe, it, before, after } from 'node:test' + +import { init } from './index.js' +import Group from '../lib/group.js' +import User from '../lib/user.js' +import Permission from '../lib/permission.js' + +import groupCase from './test/group.json' with { type: 'json' } +import userCase from './test/user.json' with { type: 'json' } +import permCase from './test/permission.json' with { type: 'json' } + +let server + +before(async () => { + server = await init() + await Group.create(groupCase) + await User.create(userCase) + await Permission.create(permCase) +}) + +let case2Id = 4094 + +after(async () => { + Permission.destroy({ id: case2Id }) + await server.stop() +}) + +describe('permission routes', () => { + let sessionCookie + + it('POST /session establishes a session', async () => { + const res = await server.inject({ + method: 'POST', + url: '/session', + payload: { + username: `${userCase.username}@${groupCase.name}`, + password: userCase.password, + }, + }) + assert.ok(res.headers['set-cookie'][0]) + sessionCookie = res.headers['set-cookie'][0].split(';')[0] + }) + + it(`GET /permission/${userCase.id}`, async () => { + const res = await server.inject({ + method: 'GET', + url: `/permission/${userCase.id}`, + headers: { + Cookie: sessionCookie, + }, + }) + // console.log(res.result) + assert.equal(res.statusCode, 200) + assert.equal(res.result.permission.zone.create, true) + assert.equal(res.result.permission.nameserver.create, false) + }) + + it(`POST /permission (${case2Id})`, async () => { + const testCase = JSON.parse(JSON.stringify(permCase)) + testCase.id = case2Id // make it unique + testCase.user.id = case2Id + testCase.group.id = case2Id + testCase.name = `Route Test Permission 2` + delete testCase.deleted + // console.log(testCase) + + const res = await server.inject({ + method: 'POST', + url: '/permission', + headers: { + Cookie: sessionCookie, + }, + payload: testCase, + }) + // console.log(res.result) + assert.equal(res.statusCode, 201) + assert.equal(res.result.permission.zone.create, true) + assert.equal(res.result.permission.nameserver.create, false) + }) + + it(`GET /permission/${case2Id}`, async () => { + const res = await server.inject({ + method: 'GET', + url: `/permission/${case2Id}`, + headers: { + Cookie: sessionCookie, + }, + }) + // console.log(res.result) + assert.equal(res.statusCode, 200) + assert.equal(res.result.permission.zone.create, true) + assert.equal(res.result.permission.nameserver.create, false) + }) + + it(`DELETE /permission/${case2Id}`, async () => { + const res = await server.inject({ + method: 'DELETE', + url: `/permission/${case2Id}`, + headers: { + Cookie: sessionCookie, + }, + }) + // console.log(res.result) + assert.equal(res.statusCode, 200) + }) + + it(`GET /permission/${case2Id}`, async () => { + const res = await server.inject({ + method: 'GET', + url: `/permission/${case2Id}`, + headers: { + Cookie: sessionCookie, + }, + }) + // console.log(res.result) + // assert.equal(res.statusCode, 200) + assert.equal(res.result.permission, undefined) + }) + + it(`GET /permission/${case2Id} (deleted)`, async () => { + const res = await server.inject({ + method: 'GET', + url: `/permission/${case2Id}?deleted=true`, + headers: { + Cookie: sessionCookie, + }, + }) + // console.log(res.result) + assert.equal(res.statusCode, 200) + assert.ok(res.result.permission) + }) + + it('DELETE /session', async () => { + const res = await server.inject({ + method: 'DELETE', + url: '/session', + headers: { + Cookie: sessionCookie, + }, + }) + assert.equal(res.statusCode, 200) + }) +}) diff --git a/routes/session.js b/routes/session.js index 6030a8f..46a6a09 100644 --- a/routes/session.js +++ b/routes/session.js @@ -1,8 +1,8 @@ import validate from '@nictool/validate' -import Group from '../lib/group.js' import User from '../lib/user.js' import Session from '../lib/session.js' + import { meta } from '../lib/util.js' function SessionRoutes(server) { @@ -17,15 +17,15 @@ function SessionRoutes(server) { tags: ['api'], }, handler: async (request, h) => { - const { nt_user_id, nt_user_session_id } = request.state['sid-nictool'] - const users = await User.get({ id: nt_user_id }) - const groups = await Group.get({ id: users[0].gid }) - delete users[0].gid + const { user, group, session } = request.state['sid-nictool'] + + Session.put({ id: session.id, last_access: true }) + return h .response({ - user: users[0], - group: groups[0], - session: { id: nt_user_session_id }, + user: user, + group: group, + session: { id: session.id }, meta: { api: meta.api, msg: `working on it`, @@ -54,14 +54,15 @@ function SessionRoutes(server) { } const sessId = await Session.create({ - nt_user_id: account.user.id, - nt_user_session: '3.0.0', + uid: account.user.id, + session: '3.0.0', last_access: parseInt(Date.now() / 1000, 10), }) request.cookieAuth.set({ - nt_user_id: account.user.id, - nt_user_session_id: sessId, + user: account.user, + group: account.group, + session: { id: sessId }, }) return h @@ -80,6 +81,15 @@ function SessionRoutes(server) { { method: 'DELETE', path: '/session', + options: { + validate: { + query: validate.session.DELETE, + }, + response: { + schema: validate.session.GET, + }, + tags: ['api'], + }, handler: (request, h) => { request.cookieAuth.clear() @@ -92,12 +102,6 @@ function SessionRoutes(server) { }) .code(200) }, - options: { - response: { - schema: validate.session.GET, - }, - tags: ['api'], - }, }, ]) } diff --git a/routes/session.test.js b/routes/session.test.js index 311ec69..5167290 100644 --- a/routes/session.test.js +++ b/routes/session.test.js @@ -4,15 +4,18 @@ import { describe, it, before, after } from 'node:test' import { init } from './index.js' import userCase from './test/user.json' with { type: 'json' } import groupCase from './test/group.json' with { type: 'json' } +import permCase from './test/permission.json' with { type: 'json' } import User from '../lib/user.js' import Group from '../lib/group.js' +import Permission from '../lib/permission.js' let server before(async () => { await Group.create(groupCase) await User.create(userCase) + await Permission.create(permCase) server = await init() }) @@ -86,7 +89,6 @@ describe('session routes', () => { }) assert.equal(res.request.auth.isAuthenticated, true) assert.equal(res.statusCode, 200) - // console.log(res.result) }) } }) diff --git a/routes/test/permission.json b/routes/test/permission.json index 52db656..e9a58bc 100644 --- a/routes/test/permission.json +++ b/routes/test/permission.json @@ -1,27 +1,22 @@ { "id": 4095, - "uid": 4095, - "gid": 4095, "inherit": true, - "name": "Route Test Permission", - "group_write": false, - "group_create": false, - "group_delete": false, - "zone_write": true, - "zone_create": true, - "zone_delegate": true, - "zone_delete": true, - "zonerecord_write": false, - "zonerecord_create": false, - "zonerecord_delegate": false, - "zonerecord_delete": false, - "user_write": false, - "user_create": false, - "user_delete": false, - "nameserver_write": false, - "nameserver_create": false, - "nameserver_delete": false, + "name": "Test Permission", "self_write": false, - "usable_ns": "", - "deleted": false + "deleted": false, + "group": { "id": 4095, "create": false, "write": false, "delete": false }, + "nameserver": { + "usable": [], + "create": false, + "write": false, + "delete": false + }, + "zone": { "create": true, "write": true, "delete": true, "delegate": true }, + "zonerecord": { + "create": false, + "write": false, + "delete": false, + "delegate": false + }, + "user": { "id": 4095, "create": false, "write": false, "delete": false } } diff --git a/routes/user.test.js b/routes/user.test.js index 2d5b298..32829a3 100644 --- a/routes/user.test.js +++ b/routes/user.test.js @@ -16,7 +16,10 @@ before(async () => { await User.create(userCase) }) +const userId2 = 4094 + after(async () => { + User.destroy({ id: userId2 }) await server.stop() }) @@ -62,8 +65,6 @@ describe('user routes', () => { assert.equal(res.statusCode, 200) }) - const userId2 = 4094 - it('POST /user', async () => { const testCase = JSON.parse(JSON.stringify(userCase)) testCase.id = userId2 // make it unique @@ -130,18 +131,6 @@ describe('user routes', () => { assert.equal(res.statusCode, 200) }) - it(`DELETE /user/${userId2}`, async () => { - const res = await server.inject({ - method: 'DELETE', - url: `/user/${userId2}?destroy=true`, - headers: { - Cookie: sessionCookie, - }, - }) - // console.log(res.result) - assert.equal(res.statusCode, 200) - }) - it('DELETE /session', async () => { const res = await server.inject({ method: 'DELETE', diff --git a/test.js b/test.js index 6ff6df0..27ab6a4 100644 --- a/test.js +++ b/test.js @@ -5,6 +5,7 @@ import path from 'node:path' import Group from './lib/group.js' import User from './lib/user.js' import Session from './lib/session.js' +import Permission from './lib/permission.js' import groupCase from './lib/test/group.json' with { type: 'json' } import userCase from './lib/test/user.json' with { type: 'json' } @@ -55,6 +56,7 @@ async function teardown() { await destroyTestSession() await destroyTestUser() await destroyTestGroup() + await destroyTestPermission() await User.mysql.disconnect() await Group.mysql.disconnect() process.exit(0) @@ -73,3 +75,8 @@ async function destroyTestUser() { async function destroyTestSession() { await Session.delete({ nt_user_id: userCase.id }) } + +async function destroyTestPermission() { + await Permission.destroy({ id: userCase.id }) + await Permission.destroy({ id: userCase.id - 1 }) +} diff --git a/test.sh b/test.sh index deb27f0..f3edafe 100755 --- a/test.sh +++ b/test.sh @@ -1,6 +1,7 @@ #!/bin/sh NODE="node --no-warnings=ExperimentalWarning" +$NODE test.js teardown $NODE test.js setup if [ "$1" = "watch" ]; then