Skip to content

Commit 3b9a94a

Browse files
authored
chore(pronto): support ?environment flag (#2026)
Next to supporting `?api_key={key}&token={token}` we now allow one to directly specify the `?environment` where Pronto will operate on. Example: `pronto.getstream.io/join/123?enviroment=demo` will connect the current Pronto instance to our demo env.
1 parent 4630936 commit 3b9a94a

File tree

7 files changed

+97
-86
lines changed

7 files changed

+97
-86
lines changed

sample-apps/react/react-dogfood/context/AppEnvironmentContext.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { useConnectedUser } from '@stream-io/video-react-sdk';
22
import { createContext, PropsWithChildren, useContext } from 'react';
3-
4-
export type AppEnvironment = 'pronto' | 'demo' | string;
3+
import type { AppEnvironment } from '../lib/environmentConfig';
54

65
const environmentOverride =
76
typeof window !== 'undefined' &&

sample-apps/react/react-dogfood/helpers/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { StreamVideoClient, User } from '@stream-io/video-react-sdk';
2-
import { AppEnvironment } from '../context/AppEnvironmentContext';
2+
import type { AppEnvironment } from '../lib/environmentConfig';
33
import {
44
CreateJwtTokenRequest,
55
CreateJwtTokenResponse,

sample-apps/react/react-dogfood/helpers/jwt.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ export const createToken = async (
4848
});
4949
};
5050

51-
export const decodeToken = (token: string): Record<string, any> => {
51+
export const decodeToken = (
52+
token: string,
53+
): Record<string, string | undefined> => {
5254
const [, payload] = token.split('.');
5355
if (!payload) throw new Error('Malformed token, missing payload');
5456
try {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
export type AppConfig = {
2+
apiKey: string;
3+
secret: string;
4+
// a link to the app that can be opened in a browser:
5+
// https://sample.app/rooms/join/{type}/{id}?user_id={userId}&user_name={user_name}&token={token}&api_key={api_key}
6+
// supported replacements:
7+
// - {type}, {id},
8+
// - {userId}, {user_name}, {token},
9+
// - {api_key}
10+
deepLink?: string;
11+
defaultCallType?: string;
12+
};
13+
14+
export type SampleAppCallConfig = {
15+
[appType: string]: AppConfig | undefined;
16+
};
17+
18+
export type AppEnvironment = 'pronto' | 'demo' | (string & {});
19+
20+
const config: SampleAppCallConfig = JSON.parse(
21+
process.env.SAMPLE_APP_CALL_CONFIG || '{}',
22+
);
23+
24+
// 'pronto' is a special environment that we ensure it exists
25+
if (!config['pronto']) {
26+
config.pronto = {
27+
apiKey: process.env.STREAM_API_KEY!,
28+
secret: process.env.STREAM_SECRET_KEY!,
29+
};
30+
}
31+
32+
export const getEnvironmentConfig = (env: AppEnvironment) => {
33+
const appConfig = config[env];
34+
if (!appConfig) {
35+
throw new Error(`Invalid environment: ${env}`);
36+
}
37+
38+
if (!appConfig.apiKey || !appConfig.secret) {
39+
throw new Error(`Environment '${env}' is not configured properly.`);
40+
}
41+
42+
return appConfig;
43+
};

sample-apps/react/react-dogfood/lib/getServerSideCredentialsProps.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { getServerSession } from 'next-auth';
33
import { authOptions } from '../pages/api/auth/[...nextauth]';
44
import { createToken, decodeToken } from '../helpers/jwt';
55
import type { User } from '@stream-io/video-react-sdk';
6+
import { getEnvironmentConfig } from './environmentConfig';
7+
import { getRandomName, sanitizeUserId } from './names';
68

79
export type ServerSideCredentialOptions = {
810
signInAutomatically?: boolean;
@@ -19,6 +21,7 @@ type QueryParams = {
1921
api_key?: string;
2022
token?: string;
2123
user_id?: string;
24+
environment?: string;
2225
};
2326

2427
export const getServerSideCredentialsPropsWithOptions =
@@ -46,13 +49,18 @@ export const getServerSideCredentialsPropsWithOptions =
4649

4750
const query = context.query as QueryParams;
4851

49-
const apiKey = query.api_key || (process.env.STREAM_API_KEY as string);
50-
const secretKey = process.env.STREAM_SECRET_KEY as string;
52+
const environment = query.environment || 'pronto';
53+
const appConfig = getEnvironmentConfig(environment);
54+
55+
const apiKey =
56+
query.api_key ||
57+
appConfig.apiKey ||
58+
(process.env.STREAM_API_KEY as string);
59+
const secretKey =
60+
appConfig.secret || (process.env.STREAM_SECRET_KEY as string);
5161
const gleapApiKey = (process.env.GLEAP_API_KEY as string) || null;
5262

53-
const userIdOverride =
54-
query.token &&
55-
(decodeToken(query.token)['user_id'] as string | undefined);
63+
const userIdOverride = query.token && decodeToken(query.token)['user_id'];
5664
const userId =
5765
userIdOverride || query.user_id || session.user?.streamUserId;
5866

@@ -66,8 +74,10 @@ export const getServerSideCredentialsPropsWithOptions =
6674
}
6775

6876
// Chat does not allow for Id's to include special characters
69-
const streamUserId = userId.replace(/[^_\-0-9a-zA-Z@]/g, '_');
77+
const streamUserId = sanitizeUserId(userId);
7078

79+
const isProntoSales =
80+
process.env.NEXT_PUBLIC_APP_ENVIRONMENT === 'pronto-sales';
7181
const token =
7282
query.token ||
7383
(await createToken(
@@ -76,9 +86,7 @@ export const getServerSideCredentialsPropsWithOptions =
7686
secretKey,
7787
session.user?.stream
7888
? {
79-
...(process.env.NEXT_PUBLIC_APP_ENVIRONMENT === 'pronto-sales'
80-
? { role: 'stream' }
81-
: {}),
89+
...(isProntoSales ? { role: 'stream' } : {}),
8290
name: session.user?.name,
8391
image: session.user?.image,
8492
email: session.user?.email,
@@ -92,7 +100,9 @@ export const getServerSideCredentialsPropsWithOptions =
92100
userToken: token,
93101
user: {
94102
id: streamUserId,
95-
...(session.user?.name ? { name: session.user.name } : {}),
103+
...(session.user?.name
104+
? { name: session.user.name }
105+
: { name: isProntoSales ? '' : getRandomName() }),
96106
...(session.user?.image ? { image: session.user.image } : {}),
97107
},
98108
gleapApiKey,

sample-apps/react/react-dogfood/pages/api/auth/create-token.ts

Lines changed: 17 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,6 @@
11
import { NextApiRequest, NextApiResponse } from 'next';
22
import { createToken, maxTokenValidityInSeconds } from '../../../helpers/jwt';
3-
import { SampleAppCallConfig } from '../call/sample';
4-
import type { AppEnvironment } from '../../../context/AppEnvironmentContext';
5-
6-
const config: SampleAppCallConfig = JSON.parse(
7-
process.env.SAMPLE_APP_CALL_CONFIG || '{}',
8-
);
9-
10-
// 'pronto' is a special environment that we ensure it exists
11-
if (!config['pronto']) {
12-
config.pronto = {
13-
apiKey: process.env.STREAM_API_KEY,
14-
secret: process.env.STREAM_SECRET_KEY,
15-
};
16-
}
3+
import { getEnvironmentConfig } from '../../../lib/environmentConfig';
174

185
export type CreateJwtTokenErrorResponse = {
196
error: string;
@@ -28,7 +15,7 @@ export type CreateJwtTokenResponse = {
2815

2916
export type CreateJwtTokenRequest = {
3017
user_id: string;
31-
environment?: AppEnvironment;
18+
environment?: string;
3219
/** @deprecated */
3320
api_key?: string;
3421
[key: string]: string | string[] | undefined;
@@ -53,22 +40,6 @@ const createJwtToken = async (
5340
else if (apiKeyFromRequest === '2g3htdemzwhg') environment = 'demo-flutter';
5441
}
5542

56-
const appConfig = config[(environment || 'demo') as AppEnvironment];
57-
if (!appConfig) {
58-
return error(res, `'environment' parameter is invalid.`);
59-
}
60-
61-
if (!appConfig.apiKey || !appConfig.secret) {
62-
return res.status(400).json({
63-
error: `environment: '${environment}' is not configured properly.`,
64-
});
65-
}
66-
67-
const { apiKey, secret: secretKey } = appConfig;
68-
if (!secretKey) {
69-
return error(res, `'api_key' parameter is invalid.`);
70-
}
71-
7243
if (!userId) {
7344
return error(res, `'user_id' is a mandatory query parameter.`);
7445
}
@@ -93,21 +64,30 @@ const createJwtToken = async (
9364
.map((cid) => cid.trim());
9465
}
9566
}
96-
67+
const appConfig = getEnvironmentConfig(environment || 'demo');
9768
const token = await createToken(
9869
userId,
99-
apiKey,
100-
secretKey,
70+
appConfig.apiKey,
71+
appConfig.secret,
10172
params as Record<string, string | string[]>,
10273
);
103-
return res.status(200).json({
74+
res.status(200).json({
10475
userId,
105-
apiKey,
76+
apiKey: appConfig.apiKey,
10677
token,
10778
});
10879
};
10980

110-
export default createJwtToken;
81+
export default async function createTokenApi(
82+
req: NextApiRequest,
83+
res: NextApiResponse,
84+
) {
85+
try {
86+
return await createJwtToken(req, res);
87+
} catch (e) {
88+
return error(res, (e as Error).message);
89+
}
90+
}
11191

11292
const error = (
11393
res: NextApiResponse,

sample-apps/react/react-dogfood/pages/api/call/sample.ts

Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
22
import { meetingId } from '../../../lib/idGenerators';
33
import { createToken } from '../../../helpers/jwt';
44
import { getRandomName } from '../../../lib/names';
5-
6-
export type AppConfig = {
7-
apiKey?: string;
8-
secret?: string;
9-
// a link to the app that can be opened in a browser:
10-
// https://sample.app/rooms/join/{type}/{id}?user_id={userId}&user_name={user_name}&token={token}&api_key={api_key}
11-
// supported replacements:
12-
// - {type}, {id},
13-
// - {userId}, {user_name}, {token},
14-
// - {api_key}
15-
deepLink?: string;
16-
defaultCallType?: string;
17-
};
18-
19-
export type SampleAppCallConfig = {
20-
[appType: string]: AppConfig | undefined;
21-
};
5+
import { getEnvironmentConfig } from '../../../lib/environmentConfig';
226

237
type CreateSampleAppCallResponse = {
248
apiKey: string;
@@ -38,35 +22,28 @@ type CreateSampleAppCallRequest = {
3822
call_type?: string;
3923
};
4024

41-
const config: SampleAppCallConfig = JSON.parse(
42-
process.env.SAMPLE_APP_CALL_CONFIG || '{}',
43-
);
44-
45-
export default async function createSampleAppCall(
25+
export default async function createSampleAppCallApi(
4626
req: NextApiRequest,
4727
res: NextApiResponse,
4828
) {
49-
if (!req.query['app_type']) {
29+
try {
30+
return await createSampleAppCall(req, res);
31+
} catch (e) {
5032
return res.status(400).json({
51-
error: 'app_type is a mandatory query parameter.',
33+
error: (e as Error).message,
5234
});
5335
}
36+
}
5437

55-
const data = req.query as CreateSampleAppCallRequest;
56-
const appConfig = config[data.app_type];
57-
if (!appConfig) {
38+
async function createSampleAppCall(req: NextApiRequest, res: NextApiResponse) {
39+
if (!req.query['app_type']) {
5840
return res.status(400).json({
59-
error: `app_type '${
60-
data.app_type
61-
}' is not supported. Supported types: ${Object.keys(config).join(', ')}`,
41+
error: 'app_type is a mandatory query parameter.',
6242
});
6343
}
6444

65-
if (!appConfig.apiKey || !appConfig.secret) {
66-
return res.status(400).json({
67-
error: `app_type '${data.app_type}' is not configured properly.`,
68-
});
69-
}
45+
const data = req.query as CreateSampleAppCallRequest;
46+
const appConfig = getEnvironmentConfig(data.app_type);
7047

7148
const userName = getRandomName();
7249
const userId = toUserId(userName);

0 commit comments

Comments
 (0)