diff --git a/src/app/router.ts b/src/app/router.ts index c3057e2..5318185 100644 --- a/src/app/router.ts +++ b/src/app/router.ts @@ -1,11 +1,13 @@ import { Elysia } from 'elysia' import deviceRouter from '~/modules/device/router' import authRouter from '~/modules/auth/router' +import topicsRouter from '~/modules/topics/router' export const router = new Elysia({ name: 'codebase.router', }) .use(authRouter) + .use(topicsRouter) .use(deviceRouter) .get('/', () => ({ message: 'This service is running as expected.', diff --git a/src/middlewares/basicAuth.ts b/src/middlewares/basicAuth.ts index af355e0..7734e52 100644 --- a/src/middlewares/basicAuth.ts +++ b/src/middlewares/basicAuth.ts @@ -1,22 +1,24 @@ -import { HTTPHeaders, StatusMap } from 'elysia' +import { Elysia } from 'elysia' import { basicAuth as credential } from '~/config' import { UnauthenticatedError } from '~/helpers/errors' import { logger } from '~/plugins' -export const basicAuthMiddleware = ( - headers: Record, - set: { - headers: HTTPHeaders - status?: number | keyof StatusMap - }, -) => { - const expectedAuth = Buffer.from(`${credential.username}:${credential.password}`).toString('base64') - if (headers.authorization !== `Basic ${expectedAuth}`) { - logger.debug({ - 'headers.authorization': headers.authorization, - }, 'Failed basic authentication attempt') - set.status = 401 - set.headers['WWW-Authenticate'] = 'Basic realm="Restricted Area"' - throw new UnauthenticatedError('Unauthenticated.') - } -} +export const basicAuthMacro = new Elysia() + .macro({ + verifyBasicAuth: (enabled: boolean) => ({ + beforeHandle({ set, headers }) { + if (!enabled) return + + const expectedAuth = Buffer.from(`${credential.username}:${credential.password}`).toString('base64') + + if (headers.authorization !== `Basic ${expectedAuth}`) { + logger.debug({ + 'headers.authorization': headers.authorization, + }, 'Failed basic authentication attempt') + set.status = 401 + set.headers['WWW-Authenticate'] = 'Basic realm="Restricted Area"' + throw new UnauthenticatedError('Unauthenticated.') + } + }, + }), + }) diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts index 3669332..b2433ea 100644 --- a/src/middlewares/index.ts +++ b/src/middlewares/index.ts @@ -1,2 +1,2 @@ -export { basicAuthMiddleware } from './basicAuth' +export { basicAuthMacro } from './basicAuth' export { bearerAuthMiddleware } from './bearerAuth' diff --git a/src/modules/auth/router.ts b/src/modules/auth/router.ts index 507f102..ec92e84 100644 --- a/src/modules/auth/router.ts +++ b/src/modules/auth/router.ts @@ -1,7 +1,7 @@ import { Elysia, t } from 'elysia' import { createMerchantBody, createMerchantResponseSchema } from './schema' import CreateMerchantService from './commands/service' -import { basicAuthMiddleware } from '~/middlewares/basicAuth' +import { basicAuthMacro } from '~/middlewares/basicAuth' import { DataAlreadyExistsError } from '~/helpers/errors' import { apiKeyAuthMacro } from '~/middlewares/apiKeyAuth' @@ -10,9 +10,8 @@ export const router = new Elysia({ detail: { tags: ['Auth'] }, prefix: '/auth', }) - .post('/get-key', async ({ body, headers, set }) => { - basicAuthMiddleware(headers, set) - + .use(basicAuthMacro) + .post('/get-key', async ({ body, set }) => { try { const result = await CreateMerchantService.createMerchant(body) return { @@ -39,6 +38,7 @@ export const router = new Elysia({ response: createMerchantResponseSchema, detail: { security: [{ basicAuth: [] }], + verifyBasicAuth: true, }, }) .use(apiKeyAuthMacro) diff --git a/src/modules/topics/commands/command.ts b/src/modules/topics/commands/command.ts new file mode 100644 index 0000000..4539d53 --- /dev/null +++ b/src/modules/topics/commands/command.ts @@ -0,0 +1,84 @@ +import { httpConfig } from '~/config' +import logger from '~/plugins/logger' +import { PatchTopic, PostDeletetopic } from '../schema' + +export async function postTopic(payload: PostDeletetopic) { + try { + const request = await fetch(`${httpConfig.baseUrl}/addTopic`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }) + if (request.status != 200) { + return { + status: request.status, + message: request.statusText, + data: null, + } + } + return { + status: request.status, + message: request.statusText, + data: await request.json(), + } + } catch (e) { + logger.error(e, 'ERROR') + throw e + } +} + +export async function delTopic(payload: PostDeletetopic) { + try { + const request = await fetch(`${httpConfig.baseUrl}/deleteTopic`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }) + if (request.status != 200) { + return { + status: request.status, + message: request.statusText, + data: null, + } + } + return { + status: request.status, + message: request.statusText, + data: await request.json(), + } + } catch (err) { + logger.error(err) + throw err + } +} + +export async function patchTopic(payload: PatchTopic) { + try { + const request = await fetch(`${httpConfig.baseUrl}/editTopic`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }) + if (request.status != 200) { + return { + status: request.status, + message: request.statusText, + data: null, + } + } + return { + status: request.status, + message: request.statusText, + data: await request.json(), + } + } catch (err) { + logger.error(err) + throw err + } +} diff --git a/src/modules/topics/commands/service.ts b/src/modules/topics/commands/service.ts new file mode 100644 index 0000000..5336c18 --- /dev/null +++ b/src/modules/topics/commands/service.ts @@ -0,0 +1,51 @@ +import { PatchTopic, PostDeleteTopicResponseSchema, PostDeletetopic } from '../schema' +import { delTopic, patchTopic, postTopic } from './command' +export default abstract class CommandService { + static async postTopic(body: PostDeletetopic): Promise { + const postRequest = await postTopic(body) + if (postRequest.status != 200) { + return { + status: postRequest.status, + message: postRequest.message, + data: null, + } + } + return { + status: 200, + message: 'Create topic success', + data: null, + } + } + + static async delTopic(body: PostDeletetopic): Promise { + const postRequest = await delTopic(body) + if (postRequest.status != 200) { + return { + status: postRequest.status, + message: postRequest.message, + data: null, + } + } + return { + status: 200, + message: 'Delete topic success', + data: null, + } + } + + static async patchTopic(body: PatchTopic): Promise { + const postRequest = await patchTopic(body) + if (postRequest.status != 200) { + return { + status: postRequest.status, + message: postRequest.message, + data: null, + } + } + return { + status: 200, + message: 'Update topic success', + data: null, + } + } +} diff --git a/src/modules/topics/queries/query.ts b/src/modules/topics/queries/query.ts new file mode 100644 index 0000000..667d853 --- /dev/null +++ b/src/modules/topics/queries/query.ts @@ -0,0 +1,57 @@ +import { httpConfig } from '~/config' +import logger from '~/plugins/logger' +import { ListTopics } from '../schema' + +export async function getCommands(payload: ListTopics) { + try { + const qParams = new URLSearchParams(payload).toString() + const request = await fetch(`${httpConfig.baseUrl}/listCommand?${qParams}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }) + if (request.status != 200) { + return { + status: request.status, + message: request.statusText, + data: null, + } + } + return { + status: request.status, + message: request.statusText, + data: await request.json(), + } + } catch (err) { + logger.error(err) + throw err + } +} + +export async function getStateReply(payload: ListTopics) { + try { + const qParams = new URLSearchParams(payload).toString() + const request = await fetch(`${httpConfig.baseUrl}/listState-reply?${qParams}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }) + if (request.status != 200) { + return { + status: request.status, + message: request.statusText, + data: null, + } + } + return { + status: request.status, + message: request.statusText, + data: await request.json(), + } + } catch (err) { + logger.error(err) + throw err + } +} diff --git a/src/modules/topics/queries/service.ts b/src/modules/topics/queries/service.ts new file mode 100644 index 0000000..3fdaa16 --- /dev/null +++ b/src/modules/topics/queries/service.ts @@ -0,0 +1,36 @@ +import { ListTopics } from '../schema' +import { getCommands, getStateReply } from './query' + +export default abstract class QueryService { + static async getCommands(payload: ListTopics) { + const result = await getCommands(payload) + if (result.status != 200) { + return { + status: result.status, + message: result.message, + data: null, + } + } + return { + status: 200, + message: 'Get commands success', + data: result.data.data, + } + } + + static async getStateReply(payload: ListTopics) { + const result = await getStateReply(payload) + if (result.status != 200) { + return { + status: result.status, + message: result.message, + data: null, + } + } + return { + status: 200, + message: 'Get state-reply success', + data: result.data.data, + } + } +} diff --git a/src/modules/topics/router.ts b/src/modules/topics/router.ts new file mode 100644 index 0000000..c968d05 --- /dev/null +++ b/src/modules/topics/router.ts @@ -0,0 +1,65 @@ +import { Elysia } from 'elysia' +import CommandService from './commands/service' +import { listTopicResponseSchema, listTopics, postDeleteTopic, postDeleteTopicResponseSchema } from './schema' +import QueryService from './queries/service' +import { basicAuthMacro } from '../../middlewares/basicAuth' + +export const router = new Elysia({ + name: 'modules.topics', + detail: { tags: ['Topics'] }, + prefix: '/topics/v1', +}) + .use(basicAuthMacro) + .get('/state-reply', async ({ query, set }) => { + const result = await QueryService.getStateReply(query) + if (result.status != 200) { + set.status = result.status + return result + } + return result + }, { + query: listTopics, + response: listTopicResponseSchema, + verifyBasicAuth: true, + }) + .get('/commands', async ({ query, set }) => { + const result = await QueryService.getCommands(query) + if (result.status != 200) { + set.status = result.status + return result + } + return result + }, { + query: listTopics, + response: listTopicResponseSchema, + verifyBasicAuth: true, + }) + .post('/', async ({ body, set }) => { + const result = await CommandService.postTopic(body) + if (result.status != 200) { + set.status = result.status + return result + } + return result + }, { + body: postDeleteTopic, + response: postDeleteTopicResponseSchema, + verifyBasicAuth: true, + }) + .patch('/', () => { + return 'Topics' + }) + .delete('/', async ({ body, set }) => { + const result = await CommandService.delTopic(body) + if (result.status != 200) { + set.status = result.status + return result + } + return result + }, { + body: postDeleteTopic, + response: postDeleteTopicResponseSchema, + verifyBasicAuth: true, + }) + +export default router diff --git a/src/modules/topics/schema.ts b/src/modules/topics/schema.ts new file mode 100644 index 0000000..90c661b --- /dev/null +++ b/src/modules/topics/schema.ts @@ -0,0 +1,54 @@ +import { z } from 'zod' + +export const postDeleteTopic = z.object({ + topic: z.string(), + type: z.enum(['state-reply', 'commands']), +}) +export type PostDeletetopic = z.infer + +export const patchTopic = z.object({ + oldTopic: z.string(), + newTopic: z.string(), + type: z.enum(['state-reply', 'commands']), +}) +export type PatchTopic = z.infer + +export const listTopics = z.object({ + topic: z.string().optional(), +}) +export type ListTopics = z.infer + +export const postDeleteTopicResponseSchema = z.object({ + status: z.number(), + message: z.string(), + data: z.any().nullable(), +}) +export type PostDeleteTopicResponseSchema = z.infer + +export const listTopicResponseSchema = z.object({ + status: z.number(), + data: z.array( + z.object({ + topic: z.string().nullable(), + }).nullable(), + ).nullable(), +}) +export type ListTopicResponseSchema = z.infer + +export const postDeleteTopicRequestSchema = { + body: postDeleteTopic, + response: { + 200: postDeleteTopicResponseSchema, + 400: postDeleteTopicResponseSchema, + 401: postDeleteTopicResponseSchema, + }, +} + +export const listTopicRequestSchema = { + query: listTopics, + response: { + 200: listTopicResponseSchema, + 400: listTopicResponseSchema, + 401: listTopicResponseSchema, + }, +} diff --git a/src/plugins/swagger.ts b/src/plugins/swagger.ts index 2b35eec..a6616f4 100644 --- a/src/plugins/swagger.ts +++ b/src/plugins/swagger.ts @@ -19,11 +19,12 @@ export const swaggerPlugin = openapi({ 'tags': [ { name: 'Auth', description: 'Auth API' }, { name: 'Device', description: 'Device API' }, + { name: 'Topics', description: 'Management Topic' }, ], // --- Grouping di atas tag --- 'x-tagGroups': [ { name: 'Auth', tags: ['Auth'] }, - { name: 'Device', tags: ['Device'] }, + { name: 'Device', tags: ['Device', 'Topics'] }, ], 'components': { securitySchemes: {