Skip to content

Commit

Permalink
Topics / k1ch / Introduce Admin API: GET /clients (#119)
Browse files Browse the repository at this point in the history
* chore: remove PGPool from db layer admin-client

* feat: db layer / introduce getClients

* feat: introduce GET /clients

* chore: remove unused file and minor clean up

* chore: bump version to 2.2.0

* chore: minor clean ups
  • Loading branch information
k1ch authored Sep 6, 2024
1 parent 89caa16 commit 7eca106
Show file tree
Hide file tree
Showing 11 changed files with 151 additions and 61 deletions.
43 changes: 28 additions & 15 deletions database/layer/admin-client.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const { PGPool } = require('./pg_pool')
const pool = new PGPool()
const { usherDb } = require('./knex')
const { pgErrorHandler } = require('../utils/pgErrorHandler')

Expand All @@ -17,24 +15,24 @@ const { pgErrorHandler } = require('../utils/pgErrorHandler')
const insertClient = async (tenantName, clientId, name, description, secret) => {
try {
// validate tenant name and get tenant key
let sql = 'SELECT t.key from usher.tenants t WHERE t.name = $1'
const tenants = await pool.query(sql, [tenantName])
let sql = 'SELECT t.key from usher.tenants t WHERE t.name = ?'
const tenants = await usherDb.raw(sql, [tenantName])
if (tenants.rows.length === 0) {
throw new Error('Invalid tenant name')
}
const tenantKey = tenants.rows[0].key

// insert client record
sql = 'INSERT INTO usher.clients (client_id, name, description, secret) VALUES ($1, $2, $3, $4) returning key'
const results = await pool.query(sql, [clientId, name, description, secret])
sql = 'INSERT INTO usher.clients (client_id, name, description, secret) VALUES (?, ?, ?, ?) returning key'
const results = await usherDb.raw(sql, [clientId, name, description, secret])
const clientKey = results.rows[0].key
// relate client to tenant
sql = 'INSERT INTO usher.tenantclients (tenantkey, clientkey) VALUES ($1, $2)'
await pool.query(sql, [tenantKey, clientKey])
sql = 'INSERT INTO usher.tenantclients (tenantkey, clientkey) VALUES (?, ?)'
await usherDb.raw(sql, [tenantKey, clientKey])

// return client
sql = 'SElECT c.client_id, c.name, c.description, c.secret FROM usher.clients c WHERE c.client_id=$1'
const role = await pool.query(sql, [clientId])
sql = 'SElECT * FROM usher.clients c WHERE c.client_id=?'
const role = await usherDb.raw(sql, [clientId])
return role.rows[0]
} catch (error) {
if (error.message === 'duplicate key value violates unique constraint "clients_client_id_uq"') {
Expand All @@ -52,9 +50,9 @@ const insertClient = async (tenantName, clientId, name, description, secret) =>
* @returns client object
*/
const getClient = async (clientId) => {
const sql = 'SElECT c.client_id, c.name, c.description, c.secret FROM usher.clients c WHERE c.client_id = $1'
const sql = 'SElECT * FROM usher.clients c WHERE c.client_id = ?'
try {
const results = await pool.query(sql, [clientId])
const results = await usherDb.raw(sql, [clientId])
if (results.rowCount === 0) {
throw new Error(`No results for client_id ${clientId}`)
}
Expand Down Expand Up @@ -86,17 +84,17 @@ const updateClientByClientId = async (clientId, { client_id, name, description,
description,
secret,
updated_at: new Date(),
}).returning(['client_id', 'name', 'description', 'secret'])
}).returning('*')
return updatedClient
} catch (err) {
throw pgErrorHandler(err)
}
}

const deleteClientByClientId = async (clientId) => {
const sql = 'DELETE FROM usher.clients WHERE client_id = $1'
const sql = 'DELETE FROM usher.clients WHERE client_id = ?'
try {
const results = await pool.query(sql, [clientId])
const results = await usherDb.raw(sql, [clientId])
if (results.rowCount === 1) {
return 'Delete successful'
} else {
Expand All @@ -107,9 +105,24 @@ const deleteClientByClientId = async (clientId) => {
}
}

/**
* Retrieve a list of all clients
*
* @returns {Promise<Array>} - A promise that resolves to an array of clients
* @throws {Error} - If there is an error during the retrieval process
*/
const getClients = async () => {
try {
return await usherDb('clients').select('*')
} catch (err) {
throw pgErrorHandler(err)
}
}

module.exports = {
insertClient,
getClient,
updateClientByClientId,
deleteClientByClientId,
getClients,
}
4 changes: 2 additions & 2 deletions database/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion database/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dmgt-tech/the-usher-server-database",
"version": "2.1.2",
"version": "2.2.0",
"description": "Database layer for TheUsher",
"scripts": {
"test": "mocha --exit",
Expand Down
20 changes: 20 additions & 0 deletions database/test/db-admin-client.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const assert = require('node:assert')
const { describe, it } = require('mocha')
const { usherDb } = require('../layer/knex')
const adminClients = require('../layer/admin-client')

describe('Admin client view', () => {
describe('Test getClients', () => {
it('Should return all the clients', async () => {
const { count: totalCount } = await usherDb('clients').count('*').first()
const clients = await adminClients.getClients()
assert.equal(clients.length, Number(totalCount))
})

it('Returned clients should include all the table columns', async () => {
const columns = Object.keys(await usherDb('clients').columnInfo())
const [client] = await adminClients.getClients()
assert.ok(columns.every((col) => col in client))
})
})
})
6 changes: 3 additions & 3 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dmgt-tech/the-usher-server",
"version": "2.1.2",
"version": "2.2.0",
"description": "The Usher Authorization Server",
"engines": {
"node": ">=18"
Expand Down
19 changes: 19 additions & 0 deletions server/src/api_endpoints/clients/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,28 @@ const updateClient = async (req, res, next) => {
}
}

/**
* HTTP Request handler
* Returns a list of clients
*
* @param {Object} req - The request object
* @param {Object} res - The response object to send 200 statusCode and a list of clients
* @param {Function} next - The next middleware function
* @returns {Promise<void>} - A promise that resolves to void when client is updated
*/
const getClients = async (req, res, next) => {
try {
const clients = await dbAdminRole.getClients()
res.status(200).send(clients)
} catch ({ httpStatusCode = 500, message }) {
return next(createError(httpStatusCode, { message }))
}
}

module.exports = {
createClient,
deleteClient,
getClient,
updateClient,
getClients,
}
29 changes: 0 additions & 29 deletions server/src/api_endpoints/endpoint_clients.js

This file was deleted.

12 changes: 4 additions & 8 deletions server/src/api_endpoints/personas/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@ const createPersonaPermissions = async (req, res, next) => {
try {
const { persona_key: personaKey } = req.params
const permissionKeys = [...new Set(req.body)]
await Promise.all([
checkPersonaExists(personaKey),
checkPersonaPermissionsValidity(personaKey, permissionKeys),
])
await checkPersonaExists(personaKey)
await checkPersonaPermissionsValidity(personaKey, permissionKeys)
await dbAdminPersonaPermissions.insertPersonaPermissions(personaKey, permissionKeys)
const locationUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`
res.set('Location', locationUrl)
Expand All @@ -33,10 +31,8 @@ const createPersonaPermissions = async (req, res, next) => {
const deletePersonaPermission = async (req, res, next) => {
try {
const { persona_key: personaKey, permission_key: permissionKey } = req.params
await Promise.all([
checkPersonaExists(personaKey),
checkPermissionExists(permissionKey),
])
await checkPersonaExists(personaKey)
await checkPermissionExists(permissionKey)
await dbAdminPersonaPermissions.deletePersonaPermission(personaKey, permissionKey)
res.status(204).send()
} catch ({ httpStatusCode = 500, message }) {
Expand Down
33 changes: 33 additions & 0 deletions server/test/endpoint_clients.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,37 @@ describe('Admin Clients Endpoint Test', () => {
await usherDb('clients').where({ client_id: testClient.client_id }).del()
})
})

describe('Get Clients', () => {
/**
* GET /clients
* HTTP request to get list of clients
*
* @param {Object} header - The request headers
* @returns {Promise<fetch.response>} - A Promise which resolves to fetch.response
*/
const getClients = async (header = requestHeaders) => {
return await fetch(`${url}/clients`, {
method: 'GET',
headers: header,
})
}

it('should return 200, and list of all clients', async () => {
const response = await getClients()
assert.equal(response.status, 200)
const clients = await response.json();
assert.ok(!!clients.length)
})

it('should return 401, unauthorized token', async () => {
const userAccessToken = await getTestUser1IdPToken()
const response = await getClients(
{
...requestHeaders,
Authorization: `Bearer ${userAccessToken}`
})
assert.equal(response.status, 401)
})
})
})
42 changes: 40 additions & 2 deletions server/the-usher-openapi-spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ info:
license:
name: MIT
url: https://opensource.org/licenses/MIT
version: 2.1.1
version: 2.2.0
externalDocs:
description: GitHub Repository
url: https://github.com/DMGT-TECH/the-usher-server
Expand Down Expand Up @@ -800,6 +800,28 @@ paths:
$ref: '#/components/responses/InternalError'

/clients:
get:
'x-swagger-router-controller': 'clients/index'
operationId: getClients
summary: Get a list of Clients
tags:
- Client Admin APIs
security:
- bearerAdminAuth: []
responses:
200:
description: Returns a list of clients
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Client"
401:
$ref: '#/components/responses/Unauthorized'
500:
$ref: '#/components/responses/InternalError'

post:
'x-swagger-router-controller': 'clients/index'
operationId: createClient
Expand Down Expand Up @@ -940,6 +962,7 @@ paths:
tags:
- Client Admin APIs
security:
- bearerAdminAuth: []
- bearerClientAdminAuth: []
requestBody:
required: true
Expand Down Expand Up @@ -1217,23 +1240,36 @@ components:
Client:
type: object
properties:
key:
type: integer
minimum: 1
format: int32
client_id:
$ref: '#/components/schemas/EntityNameDef'
name:
$ref: '#/components/schemas/EntityNameDef'
description:
$ref: '#/components/schemas/EntityDescriptionDef'
type: string
secret:
type: string
maxLength: 50
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
required:
- client_id
- name
example:
key: 1
client_id: newsletter-app
name: The Newsletter App
description: Application for reading the newsletter on a mobile device
secret: secretphraseused
created_at: '2024-08-13T20:59:37.072Z'
updated_at: '2024-08-13T20:59:37.072Z'

ArrayOfSelfClients:
type: array
Expand Down Expand Up @@ -1270,8 +1306,10 @@ components:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
required:
- key
- tenantkey
Expand Down

0 comments on commit 7eca106

Please sign in to comment.