feat: new module create topic

This commit is contained in:
2026-02-05 13:58:02 +07:00
parent f56f87aa18
commit a6eaba5944
11 changed files with 376 additions and 24 deletions
+2
View File
@@ -1,11 +1,13 @@
import { Elysia } from 'elysia' import { Elysia } from 'elysia'
import deviceRouter from '~/modules/device/router' import deviceRouter from '~/modules/device/router'
import authRouter from '~/modules/auth/router' import authRouter from '~/modules/auth/router'
import topicsRouter from '~/modules/topics/router'
export const router = new Elysia({ export const router = new Elysia({
name: 'codebase.router', name: 'codebase.router',
}) })
.use(authRouter) .use(authRouter)
.use(topicsRouter)
.use(deviceRouter) .use(deviceRouter)
.get('/', () => ({ .get('/', () => ({
message: 'This service is running as expected.', message: 'This service is running as expected.',
+20 -18
View File
@@ -1,22 +1,24 @@
import { HTTPHeaders, StatusMap } from 'elysia' import { Elysia } from 'elysia'
import { basicAuth as credential } from '~/config' import { basicAuth as credential } from '~/config'
import { UnauthenticatedError } from '~/helpers/errors' import { UnauthenticatedError } from '~/helpers/errors'
import { logger } from '~/plugins' import { logger } from '~/plugins'
export const basicAuthMiddleware = ( export const basicAuthMacro = new Elysia()
headers: Record<string, string | undefined>, .macro({
set: { verifyBasicAuth: (enabled: boolean) => ({
headers: HTTPHeaders beforeHandle({ set, headers }) {
status?: number | keyof StatusMap if (!enabled) return
},
) => { const expectedAuth = Buffer.from(`${credential.username}:${credential.password}`).toString('base64')
const expectedAuth = Buffer.from(`${credential.username}:${credential.password}`).toString('base64')
if (headers.authorization !== `Basic ${expectedAuth}`) { if (headers.authorization !== `Basic ${expectedAuth}`) {
logger.debug({ logger.debug({
'headers.authorization': headers.authorization, 'headers.authorization': headers.authorization,
}, 'Failed basic authentication attempt') }, 'Failed basic authentication attempt')
set.status = 401 set.status = 401
set.headers['WWW-Authenticate'] = 'Basic realm="Restricted Area"' set.headers['WWW-Authenticate'] = 'Basic realm="Restricted Area"'
throw new UnauthenticatedError('Unauthenticated.') throw new UnauthenticatedError('Unauthenticated.')
} }
} },
}),
})
+1 -1
View File
@@ -1,2 +1,2 @@
export { basicAuthMiddleware } from './basicAuth' export { basicAuthMacro } from './basicAuth'
export { bearerAuthMiddleware } from './bearerAuth' export { bearerAuthMiddleware } from './bearerAuth'
+4 -4
View File
@@ -1,7 +1,7 @@
import { Elysia, t } from 'elysia' import { Elysia, t } from 'elysia'
import { createMerchantBody, createMerchantResponseSchema } from './schema' import { createMerchantBody, createMerchantResponseSchema } from './schema'
import CreateMerchantService from './commands/service' import CreateMerchantService from './commands/service'
import { basicAuthMiddleware } from '~/middlewares/basicAuth' import { basicAuthMacro } from '~/middlewares/basicAuth'
import { DataAlreadyExistsError } from '~/helpers/errors' import { DataAlreadyExistsError } from '~/helpers/errors'
import { apiKeyAuthMacro } from '~/middlewares/apiKeyAuth' import { apiKeyAuthMacro } from '~/middlewares/apiKeyAuth'
@@ -10,9 +10,8 @@ export const router = new Elysia({
detail: { tags: ['Auth'] }, detail: { tags: ['Auth'] },
prefix: '/auth', prefix: '/auth',
}) })
.post('/get-key', async ({ body, headers, set }) => { .use(basicAuthMacro)
basicAuthMiddleware(headers, set) .post('/get-key', async ({ body, set }) => {
try { try {
const result = await CreateMerchantService.createMerchant(body) const result = await CreateMerchantService.createMerchant(body)
return { return {
@@ -39,6 +38,7 @@ export const router = new Elysia({
response: createMerchantResponseSchema, response: createMerchantResponseSchema,
detail: { detail: {
security: [{ basicAuth: [] }], security: [{ basicAuth: [] }],
verifyBasicAuth: true,
}, },
}) })
.use(apiKeyAuthMacro) .use(apiKeyAuthMacro)
+84
View File
@@ -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
}
}
+51
View File
@@ -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<PostDeleteTopicResponseSchema> {
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<PostDeleteTopicResponseSchema> {
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<PostDeleteTopicResponseSchema> {
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,
}
}
}
+57
View File
@@ -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
}
}
+36
View File
@@ -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,
}
}
}
+65
View File
@@ -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
+54
View File
@@ -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<typeof postDeleteTopic>
export const patchTopic = z.object({
oldTopic: z.string(),
newTopic: z.string(),
type: z.enum(['state-reply', 'commands']),
})
export type PatchTopic = z.infer<typeof patchTopic>
export const listTopics = z.object({
topic: z.string().optional(),
})
export type ListTopics = z.infer<typeof listTopics>
export const postDeleteTopicResponseSchema = z.object({
status: z.number(),
message: z.string(),
data: z.any().nullable(),
})
export type PostDeleteTopicResponseSchema = z.infer<typeof postDeleteTopicResponseSchema>
export const listTopicResponseSchema = z.object({
status: z.number(),
data: z.array(
z.object({
topic: z.string().nullable(),
}).nullable(),
).nullable(),
})
export type ListTopicResponseSchema = z.infer<typeof listTopicResponseSchema>
export const postDeleteTopicRequestSchema = {
body: postDeleteTopic,
response: {
200: postDeleteTopicResponseSchema,
400: postDeleteTopicResponseSchema,
401: postDeleteTopicResponseSchema,
},
}
export const listTopicRequestSchema = {
query: listTopics,
response: {
200: listTopicResponseSchema,
400: listTopicResponseSchema,
401: listTopicResponseSchema,
},
}
+2 -1
View File
@@ -19,11 +19,12 @@ export const swaggerPlugin = openapi({
'tags': [ 'tags': [
{ name: 'Auth', description: 'Auth API' }, { name: 'Auth', description: 'Auth API' },
{ name: 'Device', description: 'Device API' }, { name: 'Device', description: 'Device API' },
{ name: 'Topics', description: 'Management Topic' },
], ],
// --- Grouping di atas tag --- // --- Grouping di atas tag ---
'x-tagGroups': [ 'x-tagGroups': [
{ name: 'Auth', tags: ['Auth'] }, { name: 'Auth', tags: ['Auth'] },
{ name: 'Device', tags: ['Device'] }, { name: 'Device', tags: ['Device', 'Topics'] },
], ],
'components': { 'components': {
securitySchemes: { securitySchemes: {