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
8 changes: 6 additions & 2 deletions packages/api/src/clients/bot/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Client, ClientOptions } from '@microsoft/teams.common/http';

import { ClientSettings, DEFAULT_CLIENT_SETTINGS } from '../client-settings';

import { BotSignInClient } from './sign-in';
import { BotTokenClient } from './token';

Expand All @@ -16,8 +18,9 @@ export class BotClient {
this._http = v;
}
protected _http: Client;
protected _clientSettings: ClientSettings;

constructor(options?: Client | ClientOptions) {
constructor(options?: Client | ClientOptions, clientSettings?: ClientSettings) {
if (!options) {
this._http = new Client();
} else if ('request' in options) {
Expand All @@ -26,8 +29,9 @@ export class BotClient {
this._http = new Client(options);
}

this._clientSettings = {...DEFAULT_CLIENT_SETTINGS, ...(clientSettings ?? {})};
this.token = new BotTokenClient(this.http);
this.signIn = new BotSignInClient(this.http);
this.signIn = new BotSignInClient(this.http, this._clientSettings);
}
}

Expand Down
11 changes: 11 additions & 0 deletions packages/api/src/clients/bot/sign-in.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,15 @@ describe('BotSignInClient', () => {
'https://token.botframework.com/api/botsignin/GetSignInUrl?state=test'
);
});

it('should use regional endpoint', async () => {
const client = new BotSignInClient({}, { tokenUrl: 'https://europe.token.botframework.com' });
const spy = jest.spyOn(client.http, 'get').mockResolvedValueOnce({});

await client.getUrl({ state: 'test' });

expect(spy).toHaveBeenCalledWith(
'https://europe.token.botframework.com/api/botsignin/GetSignInUrl?state=test'
);
});
});
14 changes: 11 additions & 3 deletions packages/api/src/clients/bot/sign-in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import qs from 'qs';
import { Client, ClientOptions } from '@microsoft/teams.common/http';

import { SignInUrlResponse } from '../../models';
import { ClientSettings, DEFAULT_CLIENT_SETTINGS } from '../client-settings';

export const BOT_SIGNIN_ENDPOINTS = {
URL: 'api/botsignin/GetSignInUrl',
RESOURCE: 'api/botsignin/GetSignInResource',
};

export type GetBotSignInUrlParams = {
state: string;
Expand All @@ -26,21 +32,23 @@ export class BotSignInClient {
this._http = v;
}
protected _http: Client;
protected _clientSettings: ClientSettings;

constructor(options?: Client | ClientOptions) {
constructor(options?: Client | ClientOptions, clientSettings?: ClientSettings) {
if (!options) {
this._http = new Client();
} else if ('request' in options) {
this._http = options;
} else {
this._http = new Client(options);
}
this._clientSettings = {...DEFAULT_CLIENT_SETTINGS, ...(clientSettings ?? {})};
}

async getUrl(params: GetBotSignInUrlParams) {
const q = qs.stringify(params);
const res = await this.http.get<string>(
`https://token.botframework.com/api/botsignin/GetSignInUrl?${q}`
`${this._clientSettings.OAuthUrl}/${BOT_SIGNIN_ENDPOINTS.URL}?${q}`
);

return res.data;
Expand All @@ -49,7 +57,7 @@ export class BotSignInClient {
async getResource(params: GetBotSignInResourceParams) {
const q = qs.stringify(params);
const res = await this.http.get<SignInUrlResponse>(
`https://token.botframework.com/api/botsignin/GetSignInResource?${q}`
`${this._clientSettings.OAuthUrl}/${BOT_SIGNIN_ENDPOINTS.RESOURCE}?${q}`
);

return res.data;
Expand Down
13 changes: 13 additions & 0 deletions packages/api/src/clients/client-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type ClientSettings = {
/**
* the URL to use for managing user oauth tokens.
* Specify this value if you are using a regional bot.
* For e.g., https://europe.token.botframework.com
* Default is https://token.botframework.com
*/
readonly OAuthUrl?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also oauthUrl, or oauthBaseEndpoint? It's not really the full url. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, each of these should have an associated environment variable that populates them through a config, if not provided

};
Comment on lines +1 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

camelCase?


export const DEFAULT_CLIENT_SETTINGS: Required<ClientSettings> = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Instead of making all keys optional in ClientSettings, and making DEFAULT_CLIENT_SETTINGS Required, you could make all keys required in ClientSettings, and make all parameters Partial. This way, in places you need ClientSettings, the value of the keys isn't string | undefined.
For eg, in sign-in.ts, you have:

`${this._clientSettings.OAuthUrl}/${BOT_SIGNIN_ENDPOINTS.URL}?${q}`

here OauthUrl is actually string | undefined, which is less understandable than if it was just string.

OAuthUrl: 'https://token.botframework.com',
};
11 changes: 8 additions & 3 deletions packages/api/src/clients/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as http from '@microsoft/teams.common/http';

import { BotClient } from './bot';
import { ClientSettings, DEFAULT_CLIENT_SETTINGS } from './client-settings';
import { ConversationClient } from './conversation';
import { MeetingClient } from './meeting';
import { TeamClient } from './team';
Expand All @@ -26,8 +27,9 @@ export class Client {
this._http = v;
}
protected _http: http.Client;
protected _clientSettings: ClientSettings;

constructor(serviceUrl: string, options?: http.Client | http.ClientOptions) {
constructor(serviceUrl: string, options?: http.Client | http.ClientOptions, clientSettings?: ClientSettings) {
this.serviceUrl = serviceUrl;

if (!options) {
Expand All @@ -44,8 +46,10 @@ export class Client {
});
}

this.bots = new BotClient(this.http);
this.users = new UserClient(this.http);
this._clientSettings = {...DEFAULT_CLIENT_SETTINGS, ...(clientSettings ?? {})};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is copied in many places. Consider a helper method that takes in Partial<ClientSettings> and returns ClientSettings


this.bots = new BotClient(this.http, this._clientSettings);
this.users = new UserClient(this.http, this._clientSettings);
this.conversations = new ConversationClient(serviceUrl, this.http);
this.teams = new TeamClient(serviceUrl, this.http);
this.meetings = new MeetingClient(serviceUrl, this.http);
Expand All @@ -57,3 +61,4 @@ export * from './bot';
export * from './conversation';
export * from './meeting';
export * from './team';
export * from './client-settings';
8 changes: 6 additions & 2 deletions packages/api/src/clients/user/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Client, ClientOptions } from '@microsoft/teams.common/http';

import { ClientSettings, DEFAULT_CLIENT_SETTINGS } from '../client-settings';

import { UserTokenClient } from './token';

export class UserClient {
Expand All @@ -12,8 +14,9 @@ export class UserClient {
this._http = v;
}
protected _http: Client;
protected _clientSettings: ClientSettings;

constructor(options?: Client | ClientOptions) {
constructor(options?: Client | ClientOptions, clientSettings?: ClientSettings) {
if (!options) {
this._http = new Client();
} else if ('request' in options) {
Expand All @@ -22,7 +25,8 @@ export class UserClient {
this._http = new Client(options);
}

this.token = new UserTokenClient(this.http);
this._clientSettings = {...DEFAULT_CLIENT_SETTINGS, ...(clientSettings ?? {})};
this.token = new UserTokenClient(this.http, this._clientSettings);
}
}

Expand Down
101 changes: 101 additions & 0 deletions packages/api/src/clients/user/token.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,23 @@ describe('UserTokenClient', () => {
);
});

it('should use client options and regional endpoint', async () => {
const client = new UserTokenClient({}, { tokenUrl: 'https://europe.token.botframework.com' });
const spy = jest.spyOn(client.http, 'get').mockResolvedValueOnce({});

await client.get({
connectionName: 'graph',
userId: '1',
channelId: 'msteams',
code: '123',
});

expect(spy).toHaveBeenCalledWith(
'https://europe.token.botframework.com/api/usertoken/GetToken?connectionName=graph&userId=1&channelId=msteams&code=123'
);
});


it('should get token', async () => {
const client = new UserTokenClient();
const spy = jest.spyOn(client.http, 'get').mockResolvedValueOnce({});
Expand Down Expand Up @@ -74,6 +91,30 @@ describe('UserTokenClient', () => {
);
});


it('should get AAD token in regional endpoint', async () => {
const client = new UserTokenClient({}, { tokenUrl: 'https://europe.token.botframework.com' });
const spy = jest.spyOn(client.http, 'post').mockResolvedValueOnce({});

await client.getAad({
connectionName: 'graph',
userId: '1',
channelId: 'msteams',
resourceUrls: [],
});

expect(spy).toHaveBeenCalledWith(
'https://europe.token.botframework.com/api/usertoken/GetAadTokens?connectionName=graph&userId=1&channelId=msteams',
{
connectionName: 'graph',
userId: '1',
channelId: 'msteams',
resourceUrls: [],
}
);
});


it('should get token status', async () => {
const client = new UserTokenClient();
const spy = jest.spyOn(client.http, 'get').mockResolvedValueOnce({});
Expand All @@ -89,6 +130,21 @@ describe('UserTokenClient', () => {
);
});

it('should get token status with regional endpoint', async () => {
const client = new UserTokenClient({}, { tokenUrl: 'https://europe.token.botframework.com' });
const spy = jest.spyOn(client.http, 'get').mockResolvedValueOnce({});

await client.getStatus({
userId: '1',
channelId: 'msteams',
includeFilter: '',
});

expect(spy).toHaveBeenCalledWith(
'https://europe.token.botframework.com/api/usertoken/GetTokenStatus?userId=1&channelId=msteams&includeFilter='
);
});

it('should delete token', async () => {
const client = new UserTokenClient();
const spy = jest.spyOn(client.http, 'delete').mockResolvedValueOnce({});
Expand All @@ -111,6 +167,28 @@ describe('UserTokenClient', () => {
);
});

it('should delete token in regional endpoint', async () => {
const client = new UserTokenClient({}, { tokenUrl: 'https://europe.token.botframework.com' });
const spy = jest.spyOn(client.http, 'delete').mockResolvedValueOnce({});

await client.signOut({
channelId: 'msteams',
connectionName: 'graph',
userId: '1',
});

expect(spy).toHaveBeenCalledWith(
'https://europe.token.botframework.com/api/usertoken/SignOut?channelId=msteams&connectionName=graph&userId=1',
{
data: {
channelId: 'msteams',
connectionName: 'graph',
userId: '1',
},
}
);
});

it('should exchange token', async () => {
const client = new UserTokenClient();
const spy = jest.spyOn(client.http, 'post').mockResolvedValueOnce({});
Expand All @@ -133,4 +211,27 @@ describe('UserTokenClient', () => {
}
);
});

it('should exchange token with regional endpoint', async () => {
const client = new UserTokenClient({}, { tokenUrl: 'https://europe.token.botframework.com' });
const spy = jest.spyOn(client.http, 'post').mockResolvedValueOnce({});

await client.exchange({
channelId: 'msteams',
connectionName: 'graph',
userId: '1',
exchangeRequest: {
uri: 'http://localhost',
token: 'test',
},
});

expect(spy).toHaveBeenCalledWith(
'https://europe.token.botframework.com/api/usertoken/exchange?userId=1&connectionName=graph&channelId=msteams',
{
uri: 'http://localhost',
token: 'test',
}
);
});
});
24 changes: 18 additions & 6 deletions packages/api/src/clients/user/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import qs from 'qs';
import { Client, ClientOptions } from '@microsoft/teams.common/http';

import { ChannelID, TokenExchangeRequest, TokenResponse, TokenStatus } from '../../models';
import { ClientSettings, DEFAULT_CLIENT_SETTINGS } from '../client-settings';

export type GetUserTokenParams = {
userId: string;
Expand Down Expand Up @@ -37,6 +38,14 @@ export type ExchangeUserTokenParams = {
exchangeRequest: TokenExchangeRequest;
};

export const USER_TOKEN_ENDPOINTS = {
GET_TOKEN: 'api/usertoken/GetToken',
GET_AAD_TOKENS: 'api/usertoken/GetAadTokens',
GET_STATUS: 'api/usertoken/GetTokenStatus',
SIGN_OUT: 'api/usertoken/SignOut',
EXCHANGE: 'api/usertoken/exchange',
};

export class UserTokenClient {
get http() {
return this._http;
Expand All @@ -45,21 +54,24 @@ export class UserTokenClient {
this._http = v;
}
protected _http: Client;
protected _clientSettings: ClientSettings;

constructor(options?: Client | ClientOptions) {
constructor(options?: Client | ClientOptions, clientSettings?: ClientSettings) {
if (!options) {
this._http = new Client();
} else if ('request' in options) {
this._http = options;
} else {
this._http = new Client(options);
}

this._clientSettings = {...DEFAULT_CLIENT_SETTINGS, ...(clientSettings ?? {})};
}

async get(params: GetUserTokenParams) {
const q = qs.stringify(params);
const res = await this.http.get<TokenResponse>(
`https://token.botframework.com/api/usertoken/GetToken?${q}`
`${this._clientSettings.OAuthUrl}/${USER_TOKEN_ENDPOINTS.GET_TOKEN}?${q}`
);

return res.data;
Expand All @@ -68,7 +80,7 @@ export class UserTokenClient {
async getAad(params: GetUserAADTokenParams) {
const q = qs.stringify(params);
const res = await this.http.post<Record<string, TokenResponse>>(
`https://token.botframework.com/api/usertoken/GetAadTokens?${q}`,
`${this._clientSettings.OAuthUrl}/${USER_TOKEN_ENDPOINTS.GET_AAD_TOKENS}?${q}`,
params
);

Expand All @@ -78,7 +90,7 @@ export class UserTokenClient {
async getStatus(params: GetUserTokenStatusParams) {
const q = qs.stringify(params);
const res = await this.http.get<TokenStatus[]>(
`https://token.botframework.com/api/usertoken/GetTokenStatus?${q}`
`${this._clientSettings.OAuthUrl}/${USER_TOKEN_ENDPOINTS.GET_STATUS}?${q}`
);

return res.data;
Expand All @@ -87,7 +99,7 @@ export class UserTokenClient {
async signOut(params: SignOutUserParams) {
const q = qs.stringify(params);
const res = await this.http.delete<void>(
`https://token.botframework.com/api/usertoken/SignOut?${q}`,
`${this._clientSettings.OAuthUrl}/${USER_TOKEN_ENDPOINTS.SIGN_OUT}?${q}`,
{ data: params }
);

Expand All @@ -102,7 +114,7 @@ export class UserTokenClient {
});

const res = await this.http.post<TokenResponse>(
`https://token.botframework.com/api/usertoken/exchange?${q}`,
`${this._clientSettings.OAuthUrl}/${USER_TOKEN_ENDPOINTS.EXCHANGE}?${q}`,
params.exchangeRequest
);

Expand Down
Loading
Loading