Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 10 additions & 16 deletions packages/backend/src/apps/aibots/actions/send-query/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,6 @@ const action: IRawAction = {
key: 'sendQuery',
description: 'Sends a query to your customised AI Bot',
arguments: [
{
label: 'Bot API key',
key: 'apiKey',
description: 'Enter the API key for your bot',
type: 'string' as const,
required: true,
variables: false,
},
{
label: 'Query',
key: 'query',
Expand All @@ -37,16 +29,19 @@ const action: IRawAction = {
try {
const parameters = parametersSchema.parse($.step.parameters)

const { query, apiKey } = parameters
const { query } = parameters

const headers = { 'X-ATLAS-Key': apiKey }
if (!$.auth.data?.apiKey) {
throw new StepError(
'API key is required',
'Please check that you have entered a valid API key',
)
}

// create a new chat
const response = await $.http.post(
`/chats`,
{ name: 'Chat from Plumber' },
{ headers },
)
const response = await $.http.post(`/chats`, {
name: 'Chat from Plumber',
})

// /chats/:chatId/messages requires FormData
const chatId = response.data.id
Expand All @@ -58,7 +53,6 @@ const action: IRawAction = {
`/chats/:chatId/messages`,
formData,
{
headers,
urlPathParams: { chatId },
},
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { z } from 'zod'

export const parametersSchema = z.object({
apiKey: z.string().min(1),
query: z.string().min(1),
})

Expand Down
16 changes: 16 additions & 0 deletions packages/backend/src/apps/aibots/auth/auth-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { IGlobalVariable } from '@plumber/types'

import { z } from 'zod'

import { screenNameSchema } from '@/helpers/app-auth-schema'

const authDataSchema = z.object({
screenName: screenNameSchema,
apiKey: z.string().trim().min(1, 'Empty API key').max(150),
})

export type AuthData = z.infer<typeof authDataSchema>

export function validateAuthData($: IGlobalVariable): AuthData {
return authDataSchema.parse($.auth?.data)
}
35 changes: 35 additions & 0 deletions packages/backend/src/apps/aibots/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { IUserAddedConnectionAuth } from '@plumber/types'

import isStillVerified from './is-still-verified'
import verifyCredentials from './verify-credentials'

const auth: IUserAddedConnectionAuth = {
connectionType: 'user-added' as const,

fields: [
{
key: 'screenName',
label: 'Label',
type: 'string' as const,
required: true,
readOnly: false,
},
{
key: 'apiKey',
label: 'API key',
type: 'string' as const,
required: true,
readOnly: false,
clickToCopy: false,
autoComplete: 'off' as const,
},
],

verifyCredentials,
isStillVerified,
connectionModalLabel: {
chooseConnectionLabel: 'Connect to AiBots',
},
}

export default auth
23 changes: 23 additions & 0 deletions packages/backend/src/apps/aibots/auth/is-still-verified.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { IGlobalVariable } from '@plumber/types'

import { ZodError } from 'zod'
import { fromZodError } from 'zod-validation-error'

import { validateAuthData } from './auth-data'
import { verifyApiKey } from './verify-api-key'

export default async function isStillVerified(
$: IGlobalVariable,
): Promise<boolean> {
try {
validateAuthData($)
await verifyApiKey($)
return true
} catch (error) {
if (error instanceof ZodError) {
// Auth data validation failed: throws message from first error
throw new Error(fromZodError(error).details[0].message)
}
throw error
}
}
19 changes: 19 additions & 0 deletions packages/backend/src/apps/aibots/auth/verify-api-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IGlobalVariable } from '@plumber/types'

export async function verifyApiKey($: IGlobalVariable): Promise<void> {
try {
/**
* NOTE: this is a hacky way to verify the API key.
* Calling this API endpoint creates a new chat in the AiBots UI,
* but this will have to do until there is a proper API endpoint for this.
*/
await $.http.post(`/chats`, { name: 'Verifiying API Key from Plumber' })
} catch (err) {
if (err.response?.status === 401) {
throw new Error(
'API key is invalid, please ensure you have entered the correct API key',
)
}
throw err
}
}
25 changes: 25 additions & 0 deletions packages/backend/src/apps/aibots/auth/verify-credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { IGlobalVariable } from '@plumber/types'

import { ZodError } from 'zod'
import { fromZodError } from 'zod-validation-error'

import { AuthData, validateAuthData } from './auth-data'
import { verifyApiKey } from './verify-api-key'

const verifyCredentials = async ($: IGlobalVariable) => {
try {
const authData: AuthData = validateAuthData($)
await verifyApiKey($)
await $.auth.set({
screenName: authData.screenName,
})
} catch (error) {
if (error instanceof ZodError) {
// Auth data validation failed: throws message from first error
throw new Error(fromZodError(error).details[0].message)
}
throw error
}
}

export default verifyCredentials
23 changes: 23 additions & 0 deletions packages/backend/src/apps/aibots/common/add-auth-header.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { TBeforeRequest } from '@plumber/types'

import logger from '@/helpers/logger'

const addAuthHeader: TBeforeRequest = async ($, requestConfig) => {
const apiKey = $.auth.data.apiKey

if (typeof apiKey !== 'string') {
logger.error({
event: 'auth-header-apikey-error',
error: `[AiBots] Unexpected API key type: ${typeof apiKey}`,
flowId: $.flow.id,
})
throw new Error('[AiBots] Missing API key')
}

// request config has headers by default already
requestConfig.headers.set('X-ATLAS-Key', apiKey)
requestConfig.baseURL = $.app.apiBaseUrl as string
return requestConfig
}

export default addAuthHeader
9 changes: 4 additions & 5 deletions packages/backend/src/apps/aibots/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { IApp } from '@plumber/types'

import addAuthHeader from './common/add-auth-header'
import actions from './actions'
import auth from './auth'

const app: IApp = {
name: 'AIBots',
Expand All @@ -13,12 +15,9 @@ const app: IApp = {
apiBaseUrl: 'https://api.aibots.gov.sg/v1.0/api',
primaryColor: '',
category: 'ai',
// TODO: add the actual auth here
// there is currently no verification available for aibots
// its done directly in the action itself
// (as requested by AiBots team)
// auth,
auth,
actions,
beforeRequest: [addAuthHeader],
}

export default app
Loading