Skip to content

Commit

Permalink
feat(user-tokens): user tokens api (#428)
Browse files Browse the repository at this point in the history
* feat(user-tokens): user tokens api

* fix inject watch script

* it just works

* fix version bug when legacy disabled

* return id also

* don't always reload

* fix

* anotha one

* version bumps

* bump webchat
  • Loading branch information
samuelmasse committed Mar 30, 2022
1 parent dc1f159 commit 52028f8
Show file tree
Hide file tree
Showing 16 changed files with 139 additions and 16 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@botpress/messaging",
"version": "1.1.7",
"version": "1.1.8",
"description": "Botpress messaging repo",
"author": "Botpress, Inc.",
"license": "AGPL-3.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@botpress/messaging-client",
"version": "1.1.8",
"version": "1.1.9",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"source": "src/index.ts",
Expand Down
10 changes: 10 additions & 0 deletions packages/client/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ export abstract class MessagingChannelApi extends MessagingChannelBase {
}, undefined)
}

/**
* Creates a new user token
* @param clientId id of the client that owns the user
* @param userId id of the user
* @returns token that can be used to make user-level requests
*/
async createUserToken(clientId: uuid, userId: uuid): Promise<{ id: string; token: string }> {
return (await this.http.post('/users/tokens', { userId }, { headers: this.headers[clientId] })).data
}

/**
* Creates a new messaging conversation
* @param clientId id of the client that will own this conversation
Expand Down
9 changes: 9 additions & 0 deletions packages/client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,15 @@ export class MessagingClient extends ProtectedEmitter<{
return this.channel.getUser(this.clientId, id)
}

/**
* Creates a new user token
* @param userId id of the user
* @returns token that can be used to make user-level requests
*/
async createUserToken(userId: uuid): Promise<{ id: string; token: string }> {
return this.channel.createUserToken(this.clientId, userId)
}

/**
* Creates a new messaging conversation
* @param userId id of the user that starts this conversation
Expand Down
25 changes: 25 additions & 0 deletions packages/client/test/e2e/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ describe('Http Client', () => {
conversation?: Conversation
message?: Message
webhooks?: SyncWebhook[]
userToken?: { id: string; token: string }
} = {}
let client: MessagingClient
const webhooks = [{ url: 'http://un.known.url' }, { url: 'http://second.un.known.url' }]
Expand Down Expand Up @@ -210,6 +211,30 @@ describe('Http Client', () => {
})
})

describe('User Tokens', () => {
test('Should create a user token without throwing any error', async () => {
const userToken = await client.createUserToken(state.user!.id)

expect(userToken.id).toBeDefined()
expect(userToken.token).toBeDefined()
expect(userToken.token.startsWith(userToken.id)).toBeTruthy()

state.userToken = userToken
})

test('Should create a second user token for the same user', async () => {
const userToken = await client.createUserToken(state.user!.id)

expect(userToken.id).toBeDefined()
expect(userToken.token).toBeDefined()
expect(userToken.token.startsWith(userToken.id)).toBeTruthy()

// should be a different token
expect(userToken.id).not.toEqual(state.userToken?.id)
expect(userToken.token).not.toEqual(state.userToken?.token)
})
})

describe('Message', () => {
const payload = {
type: 'text',
Expand Down
6 changes: 3 additions & 3 deletions packages/inject/package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"name": "@botpress/webchat-inject",
"version": "0.3.3",
"version": "0.3.4",
"license": "AGPL-3.0",
"scripts": {
"build": "yarn && yarn run -T parcel build src/index.html src/inject.js --public-url ./",
"write:version": "node -p \"require('../webchat/package.json').version\" > ./dist/version.txt",
"watch": "yarn && yarn run -T parcel watch src/index.html src/inject.js",
"watch": "yarn && yarn run -T parcel watch src/index.html src/inject.js --public-url ./",
"dev": "yarn && yarn run -T parcel src/index.html src/inject.js",
"prepublish": "yarn run -T rimraf dist && yarn --immutable && yarn run -T parcel build src/index.html src/inject.js --public-url ./ && yarn write:version",
"analyze": "yarn run -T parcel build src/index.html --public-url ./ --reporter @parcel/reporter-bundle-analyzer"
Expand All @@ -21,7 +21,7 @@
"@types/react-dom": "^17.0.11"
},
"dependencies": {
"@botpress/webchat": "0.3.3",
"@botpress/webchat": "0.3.4",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
Expand Down
2 changes: 1 addition & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@botpress/messaging-server",
"version": "1.1.7",
"version": "1.1.8",
"main": "index.ts",
"license": "AGPL-3.0",
"scripts": {
Expand Down
4 changes: 4 additions & 0 deletions packages/server/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { HealthApi } from './health/api'
import { MappingApi } from './mapping/api'
import { MessageApi } from './messages/api'
import { SyncApi } from './sync/api'
import { UserTokenApi } from './user-tokens/api'
import { UserApi } from './users/api'

export class Api {
Expand All @@ -24,6 +25,7 @@ export class Api {
private syncs: SyncApi
private health: HealthApi
private users: UserApi
private userTokens: UserTokenApi
private conversations: ConversationApi
private messages: MessageApi
private mapping: MappingApi
Expand All @@ -39,6 +41,7 @@ export class Api {
this.syncs = new SyncApi(this.app.syncs, this.app.channels)
this.health = new HealthApi(this.app.health)
this.users = new UserApi(this.app.users)
this.userTokens = new UserTokenApi(this.app.users, this.app.userTokens)
this.conversations = new ConversationApi(this.app.users, this.app.conversations)
this.messages = new MessageApi(this.app.users, this.app.conversations, this.app.messages, this.app.converse)
this.mapping = new MappingApi(this.app.channels, this.app.conversations, this.app.mapping)
Expand Down Expand Up @@ -68,6 +71,7 @@ export class Api {
this.syncs.setup(this.manager)
this.health.setup(this.manager)
this.users.setup(this.manager)
this.userTokens.setup(this.manager)
this.conversations.setup(this.manager)
this.messages.setup(this.manager)
this.mapping.setup(this.manager)
Expand Down
8 changes: 5 additions & 3 deletions packages/server/src/sync/schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Channel } from '@botpress/messaging-channels'
import Joi from 'joi'
import yn from 'yn'
import { ReqSchema } from '../base/schema'

export const makeSyncRequestSchema = (channels: Channel[]) => {
Expand All @@ -14,10 +15,11 @@ export const makeSyncRequestSchema = (channels: Channel[]) => {
}

for (const [name, channels] of Object.entries(channelsByName)) {
const versionSchema = Joi.string().valid(...new Set(channels.map((x) => x.meta.version)))

channelsSchema[name] = Joi.object({
version: Joi.string()
.valid(...channels.map((x) => x.meta.version))
.optional()
// when legacy channels are enable, not supplying the version defaults to 0.1.0
version: yn(process.env.ENABLE_LEGACY_CHANNELS) ? versionSchema.optional() : versionSchema.required()
})
.options({
allowUnknown: true
Expand Down
29 changes: 29 additions & 0 deletions packages/server/src/user-tokens/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { uuid } from '@botpress/messaging-base'
import { Response } from 'express'
import { ApiManager } from '../base/api-manager'
import { ClientApiRequest } from '../base/auth/client'
import { UserService } from '../users/service'
import { Schema } from './schema'
import { UserTokenService } from './service'

export class UserTokenApi {
constructor(private users: UserService, private userTokens: UserTokenService) {}

setup(router: ApiManager) {
router.post('/users/tokens', Schema.Api.Create, this.create.bind(this))
}

async create(req: ClientApiRequest, res: Response) {
const userId = req.body.userId as uuid

const user = await this.users.fetch(userId)
if (!user || user.clientId !== req.clientId) {
return res.sendStatus(404)
}

const tokenRaw = await this.userTokens.generateToken()
const userToken = await this.userTokens.create(user.id, tokenRaw, undefined)

res.status(201).send({ id: userToken.id, token: `${userToken.id}.${tokenRaw}` })
}
}
12 changes: 12 additions & 0 deletions packages/server/src/user-tokens/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Joi from 'joi'
import { ReqSchema } from '../base/schema'

const Api = {
Create: ReqSchema({
body: { userId: Joi.string().guid().required() }
})
}

const Socket = {}

export const Schema = { Api, Socket }
2 changes: 1 addition & 1 deletion packages/webchat/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@botpress/webchat",
"version": "0.3.3",
"version": "0.3.4",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"source": "src/index.tsx",
Expand Down
18 changes: 16 additions & 2 deletions packages/webchat/src/core/socket.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default class BpSocket {

public onMessage!: (event: any) => void

constructor(config: Config) {
constructor(private config: Config) {
this.chatId = config.chatId
this.socket = new MessagingSocket({ url: config.messagingUrl, clientId: config.clientId })

Expand All @@ -29,7 +29,7 @@ export default class BpSocket {
}

public async connect(): Promise<void> {
const creds = window.BP_STORAGE.get<UserCredentials>('creds')
const creds = this.getCreds()
await this.socket.connect(creds)

if (this.socket.userId) {
Expand All @@ -39,4 +39,18 @@ export default class BpSocket {
this.postToParent('', { userId })
}
}

public async reload(config: Config) {
this.config = config
const creds = this.getCreds()

if (creds?.userId !== this.socket.userId || creds?.userToken !== this.socket.creds?.userToken) {
await this.socket.disconnect()
await this.connect()
}
}

private getCreds() {
return this.config.customUser || window.BP_STORAGE.get<UserCredentials>('creds')
}
}
15 changes: 13 additions & 2 deletions packages/webchat/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,16 @@ class Web extends React.Component<MainProps> {
case 'configure':
return this.props.updateConfig!(Object.assign({}, constants.DEFAULT_CONFIG, data.payload))
case 'mergeConfig':
return this.props.mergeConfig!(data.payload)
this.props.mergeConfig!(data.payload)

const oldUserId = this.socket.socket.userId
await this.socket.reload(data.payload)

if (this.socket.socket.userId !== oldUserId) {
this.props.resetConversation!()
await this.props.initializeChat!()
}
return
case 'sendPayload':
return this.props.sendData!(data.payload)
case 'event':
Expand Down Expand Up @@ -357,7 +366,8 @@ export default inject(({ store }: { store: RootStore }) => ({
fetchConversation: store.fetchConversation,
setIntlProvider: store.setIntlProvider,
setSocket: store.setSocket,
currentConversationId: store.currentConversationId
currentConversationId: store.currentConversationId,
resetConversation: store.resetConversation
}))(injectIntl(observer(Web)))

type MainProps = { store?: RootStore } & WrappedComponentProps &
Expand Down Expand Up @@ -396,4 +406,5 @@ type MainProps = { store?: RootStore } & WrappedComponentProps &
| 'setIntlProvider'
| 'setSocket'
| 'currentConversationId'
| 'resetConversation'
>
7 changes: 7 additions & 0 deletions packages/webchat/src/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,13 @@ export interface Config {
* @default ''
*/
googleMapsAPIKey?: string
/**
* Allows setting a custom user id
*/
customUser?: {
userId: string
userToken: string
}
}

export interface BotDetails {
Expand Down
4 changes: 2 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2487,7 +2487,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@botpress/webchat-inject@workspace:packages/inject"
dependencies:
"@botpress/webchat": 0.3.3
"@botpress/webchat": 0.3.4
"@parcel/config-default": ^2.2.1
"@parcel/reporter-bundle-analyzer": 2.2.1
"@parcel/transformer-typescript-tsc": ^2.2.1
Expand All @@ -2498,7 +2498,7 @@ __metadata:
languageName: unknown
linkType: soft

"@botpress/webchat@*, @botpress/webchat@0.3.3, @botpress/webchat@workspace:packages/webchat":
"@botpress/webchat@*, @botpress/webchat@0.3.4, @botpress/webchat@workspace:packages/webchat":
version: 0.0.0-use.local
resolution: "@botpress/webchat@workspace:packages/webchat"
dependencies:
Expand Down

0 comments on commit 52028f8

Please sign in to comment.