@canva/cli 0.0.1-beta.2 → 0.0.1-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/base/backend/routers/oauth.ts +393 -0
- package/templates/base/package.json +1 -1
- package/templates/base/utils/backend/bearer_middleware/bearer_middleware.ts +101 -0
- package/templates/base/utils/backend/bearer_middleware/index.ts +1 -0
- package/templates/base/utils/backend/bearer_middleware/tests/bearer_middleware.tests.ts +192 -0
- package/templates/base/utils/use_add_element.ts +48 -0
- package/templates/base/utils/use_feature_support.ts +28 -0
- package/templates/common/README.md +0 -67
- package/templates/dam/backend/server.ts +0 -7
- package/templates/dam/package.json +6 -6
- package/templates/dam/src/app.tsx +2 -135
- package/templates/gen_ai/README.md +40 -1
- package/templates/gen_ai/backend/routers/oauth.ts +393 -0
- package/templates/gen_ai/backend/server.ts +1 -1
- package/templates/gen_ai/package.json +5 -5
- package/templates/gen_ai/src/api/api.ts +44 -27
- package/templates/gen_ai/src/components/footer.tsx +9 -5
- package/templates/gen_ai/src/components/image_grid.tsx +8 -6
- package/templates/gen_ai/src/components/loading_results.tsx +8 -4
- package/templates/gen_ai/src/context/app_context.tsx +8 -2
- package/templates/gen_ai/src/services/auth.tsx +5 -10
- package/templates/gen_ai/utils/backend/bearer_middleware/bearer_middleware.ts +101 -0
- package/templates/gen_ai/utils/backend/bearer_middleware/index.ts +1 -0
- package/templates/gen_ai/utils/backend/bearer_middleware/tests/bearer_middleware.tests.ts +192 -0
- package/templates/hello_world/package.json +3 -2
- package/templates/hello_world/src/app.tsx +4 -2
- package/templates/hello_world/utils/use_add_element.ts +48 -0
- package/templates/hello_world/utils/use_feature_support.ts +28 -0
- package/templates/dam/backend/database/database.ts +0 -42
- package/templates/dam/backend/routers/auth.ts +0 -285
- package/templates/gen_ai/backend/routers/auth.ts +0 -285
- package/templates/gen_ai/utils/backend/jwt_middleware/index.ts +0 -1
- package/templates/gen_ai/utils/backend/jwt_middleware/jwt_middleware.ts +0 -229
- package/templates/gen_ai/utils/backend/jwt_middleware/tests/jwt_middleware.tests.ts +0 -630
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import * as crypto from "crypto";
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import * as express from "express";
|
|
4
|
+
import * as basicAuth from "express-basic-auth";
|
|
5
|
+
import { JSONFileDatabase } from "../database/database";
|
|
6
|
+
import { createBearerMiddleware } from "../../utils/backend/bearer_middleware";
|
|
7
|
+
import * as debug from "debug";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Prefix your start command with `DEBUG=express:route:oauth` to enable debug logging
|
|
11
|
+
* for this middleware
|
|
12
|
+
*/
|
|
13
|
+
const debugLogger = debug("express:route:oauth");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* These are the hard-coded credentials for logging in to this template.
|
|
17
|
+
*/
|
|
18
|
+
const USERNAME = "username";
|
|
19
|
+
const PASSWORD = "password";
|
|
20
|
+
|
|
21
|
+
const COOKIE_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
|
|
22
|
+
const TOKEN_EXPIRY_S = 4 * 60 * 60; // 4 hours
|
|
23
|
+
const REFRESH_EXPIRY_S = 30 * 24 * 3600; // 30 days
|
|
24
|
+
|
|
25
|
+
const CLIENT_ID = "client_id";
|
|
26
|
+
const CLIENT_SECRET = "client_secret";
|
|
27
|
+
const REDIRECT_URI = "https://www.canva.com/apps/oauth/authorized";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* For simplicity, we simply generate random token values and store them in our database
|
|
31
|
+
* for production you may want to use JWTs or other stateless methods.
|
|
32
|
+
*/
|
|
33
|
+
interface Token {
|
|
34
|
+
value: string;
|
|
35
|
+
expiry: number; // Unix timestamp
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface User {
|
|
39
|
+
id: string;
|
|
40
|
+
authorizationCode?: Token;
|
|
41
|
+
accessToken?: Token;
|
|
42
|
+
refreshToken?: Token;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface RedirectUriRecord {
|
|
46
|
+
redirectUri: string;
|
|
47
|
+
authorizationCode: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface Data {
|
|
51
|
+
users: User[];
|
|
52
|
+
redirect_uris: RedirectUriRecord[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* For more information on Authentication, refer to our documentation: {@link https://www.canva.dev/docs/apps/authenticating-users/#types-of-authentication}.
|
|
57
|
+
*/
|
|
58
|
+
export const createAuthRouter = () => {
|
|
59
|
+
/**
|
|
60
|
+
* Set up a database with a "users" table. In this example code, the
|
|
61
|
+
* database is simply a JSON file.
|
|
62
|
+
*/
|
|
63
|
+
const db = new JSONFileDatabase<Data>({ users: [], redirect_uris: [] });
|
|
64
|
+
|
|
65
|
+
const router = express.Router();
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The urlencoded middleware allows us to parse form data from the request body.
|
|
69
|
+
* This is required for the OAuth 2.0 Token Exchange endpoint.
|
|
70
|
+
*/
|
|
71
|
+
router.use(express.urlencoded({ extended: true }));
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* This is the OAuth 2.0 Authorization endpoint
|
|
75
|
+
* https://datatracker.ietf.org/doc/html/rfc6749#section-3.1
|
|
76
|
+
*
|
|
77
|
+
* This endpoint does not implement PKCE, which you would want to do
|
|
78
|
+
* for production.
|
|
79
|
+
*/
|
|
80
|
+
router.get(
|
|
81
|
+
"/auth/authorize",
|
|
82
|
+
basicAuth({
|
|
83
|
+
users: { [USERNAME]: PASSWORD },
|
|
84
|
+
challenge: true,
|
|
85
|
+
}),
|
|
86
|
+
async (req, res) => {
|
|
87
|
+
const redirectUri = decodeURIComponent(
|
|
88
|
+
req.query.redirect_uri?.toString() ?? REDIRECT_URI,
|
|
89
|
+
);
|
|
90
|
+
const clientId = req.query.client_id?.toString();
|
|
91
|
+
const responseType = req.query.response_type?.toString();
|
|
92
|
+
const state = req.query.state?.toString();
|
|
93
|
+
|
|
94
|
+
if (!clientId) {
|
|
95
|
+
return res.status(400).send("Missing required client ID parameter");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (clientId !== CLIENT_ID) {
|
|
99
|
+
return res.status(400).send("Invalid client ID");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (redirectUri !== REDIRECT_URI) {
|
|
103
|
+
return res.status(400).send(`Invalid redirect URI ${redirectUri}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const failureResponse = (error: string) => {
|
|
107
|
+
const params = new URLSearchParams({
|
|
108
|
+
error,
|
|
109
|
+
state: state || "",
|
|
110
|
+
});
|
|
111
|
+
res.redirect(302, `${redirectUri}?${params}`);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (responseType !== "code") {
|
|
115
|
+
return failureResponse("unsupported_response_type");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// You should implement PKCE to protect against authorization code interception attacks
|
|
119
|
+
// https://datatracker.ietf.org/doc/html/rfc7636
|
|
120
|
+
// this is left off for simplicity in this example
|
|
121
|
+
|
|
122
|
+
// Generate a unique authorization code
|
|
123
|
+
const authorizationCode = crypto.randomUUID();
|
|
124
|
+
|
|
125
|
+
// Load the database
|
|
126
|
+
const data = await db.read();
|
|
127
|
+
|
|
128
|
+
// Add the user to the database if they aren't there already
|
|
129
|
+
if (!data.users.find((user) => user.id === USERNAME)) {
|
|
130
|
+
data.users.push({
|
|
131
|
+
id: USERNAME,
|
|
132
|
+
authorizationCode: {
|
|
133
|
+
value: authorizationCode,
|
|
134
|
+
expiry: Date.now() + COOKIE_EXPIRY_MS,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
// If the user is there, update the authorization code
|
|
139
|
+
// In a production system you would allow at least one authorization code
|
|
140
|
+
// per user per client, but here we simplify and have one per user.
|
|
141
|
+
data.users = data.users.map((user) => {
|
|
142
|
+
if (user.id === USERNAME) {
|
|
143
|
+
return {
|
|
144
|
+
...user,
|
|
145
|
+
authorizationCode: {
|
|
146
|
+
value: authorizationCode,
|
|
147
|
+
expiry: Date.now() + COOKIE_EXPIRY_MS,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return user;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (
|
|
155
|
+
!data.redirect_uris.find((record) => record.redirectUri === redirectUri)
|
|
156
|
+
) {
|
|
157
|
+
data.redirect_uris.push({ redirectUri, authorizationCode });
|
|
158
|
+
} else {
|
|
159
|
+
data.redirect_uris = data.redirect_uris.map((record) => {
|
|
160
|
+
if (record.redirectUri === redirectUri) {
|
|
161
|
+
return { ...record, authorizationCode };
|
|
162
|
+
}
|
|
163
|
+
return record;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
await db.write(data);
|
|
167
|
+
|
|
168
|
+
res.redirect(
|
|
169
|
+
302,
|
|
170
|
+
`${redirectUri}?code=${authorizationCode}&state=${state}`,
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
interface AuthorizationExchangeRequest {
|
|
176
|
+
grant_type: "authorization_code";
|
|
177
|
+
code: string;
|
|
178
|
+
redirect_uri?: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface RefreshTokenExchangeRequest {
|
|
182
|
+
grant_type: "refresh_token";
|
|
183
|
+
refresh_token: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const failureResponse = (res: express.Response, error: string) => {
|
|
187
|
+
return res.status(400).send(`{"error": "${error}"}`);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const authorizationCodeExchange = async (
|
|
191
|
+
res: express.Response,
|
|
192
|
+
body: AuthorizationExchangeRequest,
|
|
193
|
+
) => {
|
|
194
|
+
const refresh_token = crypto.randomUUID();
|
|
195
|
+
const access_token = crypto.randomUUID();
|
|
196
|
+
|
|
197
|
+
const code = body.code?.toString();
|
|
198
|
+
|
|
199
|
+
if (!code) {
|
|
200
|
+
return failureResponse(res, "invalid_request");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Load the database
|
|
204
|
+
const data = await db.read();
|
|
205
|
+
const lastRedirectUrl = data.redirect_uris.find(
|
|
206
|
+
(record) => record.authorizationCode === code,
|
|
207
|
+
)?.redirectUri;
|
|
208
|
+
|
|
209
|
+
// The specification requires that if the redirect uri was present in the authorization request,
|
|
210
|
+
// it must be included and the same in the token exchange request
|
|
211
|
+
if (lastRedirectUrl !== body.redirect_uri?.toString()) {
|
|
212
|
+
return failureResponse(res, "invalid_request");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Find the user who was issued this authorization code
|
|
216
|
+
const user = data.users.find(
|
|
217
|
+
(user) => user.authorizationCode?.value === code,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (!user || !user.authorizationCode) {
|
|
221
|
+
return failureResponse(res, "invalid_grant");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (user.authorizationCode.expiry < Date.now()) {
|
|
225
|
+
return failureResponse(res, "invalid_grant");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Update the user with the new access token and refresh token
|
|
229
|
+
// and clear out their authorization code. Authorization codes
|
|
230
|
+
// are single use, so attempting to use the old one will now fail
|
|
231
|
+
data.users = data.users.map((user) => {
|
|
232
|
+
if (user.id === USERNAME) {
|
|
233
|
+
return {
|
|
234
|
+
...user,
|
|
235
|
+
authorizationCode: undefined,
|
|
236
|
+
accessToken: {
|
|
237
|
+
value: access_token,
|
|
238
|
+
expiry: Date.now() + TOKEN_EXPIRY_S * 1000,
|
|
239
|
+
},
|
|
240
|
+
refreshToken: {
|
|
241
|
+
value: refresh_token,
|
|
242
|
+
expiry: Date.now() + REFRESH_EXPIRY_S * 1000,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return user;
|
|
247
|
+
});
|
|
248
|
+
await db.write(data);
|
|
249
|
+
|
|
250
|
+
return res.status(200).send(
|
|
251
|
+
JSON.stringify({
|
|
252
|
+
access_token,
|
|
253
|
+
refresh_token,
|
|
254
|
+
token_type: "bearer",
|
|
255
|
+
expires_in: TOKEN_EXPIRY_S,
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const refreshTokenExchange = async (
|
|
261
|
+
res: express.Response,
|
|
262
|
+
body: RefreshTokenExchangeRequest,
|
|
263
|
+
) => {
|
|
264
|
+
const refresh_token = crypto.randomUUID();
|
|
265
|
+
const access_token = crypto.randomUUID();
|
|
266
|
+
|
|
267
|
+
const old_refresh_token = body.refresh_token?.toString();
|
|
268
|
+
if (!old_refresh_token) {
|
|
269
|
+
return failureResponse(res, "invalid_request");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Find the user who was issued this refresh token
|
|
273
|
+
const data = await db.read();
|
|
274
|
+
const user = data.users.find(
|
|
275
|
+
(user) => user.refreshToken?.value === old_refresh_token,
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
if (!user) {
|
|
279
|
+
return failureResponse(res, "invalid_grant");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Update the user with the new refresh token
|
|
283
|
+
// refresh tokens are single use, so attempting
|
|
284
|
+
// to use the old one will now fail
|
|
285
|
+
data.users = data.users.map((u) => {
|
|
286
|
+
if (u.id === user.id) {
|
|
287
|
+
return {
|
|
288
|
+
...u,
|
|
289
|
+
accessToken: {
|
|
290
|
+
value: access_token,
|
|
291
|
+
expiry: Date.now() + TOKEN_EXPIRY_S * 1000,
|
|
292
|
+
},
|
|
293
|
+
refreshToken: {
|
|
294
|
+
value: refresh_token,
|
|
295
|
+
expiry: Date.now() + REFRESH_EXPIRY_S * 1000,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
return u;
|
|
300
|
+
});
|
|
301
|
+
await db.write(data);
|
|
302
|
+
|
|
303
|
+
return res.status(200).send(
|
|
304
|
+
JSON.stringify({
|
|
305
|
+
access_token,
|
|
306
|
+
refresh_token,
|
|
307
|
+
token_type: "bearer",
|
|
308
|
+
expires_in: TOKEN_EXPIRY_S,
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* This endpoint is the Oauth 2.0 Token endpoint
|
|
315
|
+
* https://datatracker.ietf.org/doc/html/rfc6749#section-3.2
|
|
316
|
+
* It serves both exchanging authorization codes for access tokens (and refresh tokens), and
|
|
317
|
+
* exchanging refresh tokens for new access tokens (and refresh tokens)
|
|
318
|
+
*/
|
|
319
|
+
router.post(
|
|
320
|
+
"/auth/token",
|
|
321
|
+
basicAuth({
|
|
322
|
+
users: { [CLIENT_ID]: CLIENT_SECRET },
|
|
323
|
+
challenge: false,
|
|
324
|
+
}),
|
|
325
|
+
async (req, res) => {
|
|
326
|
+
const body = (await req.body) as
|
|
327
|
+
| AuthorizationExchangeRequest
|
|
328
|
+
| RefreshTokenExchangeRequest;
|
|
329
|
+
|
|
330
|
+
const grantType = body.grant_type?.toString();
|
|
331
|
+
|
|
332
|
+
if (!grantType) {
|
|
333
|
+
return failureResponse(res, "invalid_request");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (grantType !== "authorization_code" && grantType !== "refresh_token") {
|
|
337
|
+
return failureResponse(res, "unsupported_grant_type");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (grantType === "refresh_token") {
|
|
341
|
+
refreshTokenExchange(res, body as RefreshTokenExchangeRequest);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return authorizationCodeExchange(
|
|
345
|
+
res,
|
|
346
|
+
body as AuthorizationExchangeRequest,
|
|
347
|
+
);
|
|
348
|
+
},
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* TODO: Add this middleware to all routes that will receive requests from
|
|
353
|
+
* your app.
|
|
354
|
+
*/
|
|
355
|
+
const bearerMiddleware = createBearerMiddleware(
|
|
356
|
+
async (access_token: string): Promise<string | undefined> => {
|
|
357
|
+
const data = await db.read();
|
|
358
|
+
const user = data.users.find(
|
|
359
|
+
(user) => user.accessToken?.value === access_token,
|
|
360
|
+
);
|
|
361
|
+
debugLogger(
|
|
362
|
+
`User found: ${user?.id}, expires ${user?.accessToken?.expiry} compare ${Date.now()}`,
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const isAuthenticated =
|
|
366
|
+
user && user.accessToken && user.accessToken?.expiry > Date.now();
|
|
367
|
+
if (!isAuthenticated) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
return user.id;
|
|
371
|
+
},
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* All routes that start with /api will be protected by bearer authentication
|
|
376
|
+
*/
|
|
377
|
+
router.use("/api", bearerMiddleware);
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* This endpoint checks if a user is authenticated.
|
|
381
|
+
*/
|
|
382
|
+
router.post("/api/authentication/status", async (req, res) => {
|
|
383
|
+
// Check if the user is authenticated
|
|
384
|
+
const isAuthenticated = !!req.user_id;
|
|
385
|
+
|
|
386
|
+
// Return the authentication status
|
|
387
|
+
res.send({
|
|
388
|
+
isAuthenticated,
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
return router;
|
|
393
|
+
};
|
|
@@ -2,7 +2,7 @@ import * as express from "express";
|
|
|
2
2
|
import * as cors from "cors";
|
|
3
3
|
import { createBaseServer } from "../utils/backend/base_backend/create";
|
|
4
4
|
import { createImageRouter } from "./routers/image";
|
|
5
|
-
import { createAuthRouter } from "./routers/
|
|
5
|
+
import { createAuthRouter } from "./routers/oauth";
|
|
6
6
|
|
|
7
7
|
async function main() {
|
|
8
8
|
const router = express.Router();
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@canva/app-i18n-kit": "^0.0.1-beta.5",
|
|
20
|
-
"@canva/app-ui-kit": "^
|
|
21
|
-
"@canva/asset": "^
|
|
22
|
-
"@canva/design": "^1.
|
|
23
|
-
"@canva/platform": "^
|
|
24
|
-
"@canva/user": "^
|
|
20
|
+
"@canva/app-ui-kit": "^4.0.0",
|
|
21
|
+
"@canva/asset": "^2.0.0",
|
|
22
|
+
"@canva/design": "^2.1.0",
|
|
23
|
+
"@canva/platform": "^2.0.0",
|
|
24
|
+
"@canva/user": "^2.0.0",
|
|
25
25
|
"cookie-parser": "1.4.6",
|
|
26
26
|
"cors": "2.8.5",
|
|
27
27
|
"html-react-parser": "5.1.12",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Oauth } from "@canva/user";
|
|
2
2
|
import { POLLING_INTERVAL_IN_SECONDS } from "src/config";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -66,18 +66,21 @@ const endpoints = {
|
|
|
66
66
|
* @param {number} options.numberOfImages - The number of images to generate.
|
|
67
67
|
* @returns {Promise<QueueImageGenerationResponse>} A promise that resolves to the created job ID.
|
|
68
68
|
*/
|
|
69
|
-
export const queueImageGeneration = async (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
export const queueImageGeneration = async (
|
|
70
|
+
{
|
|
71
|
+
prompt,
|
|
72
|
+
numberOfImages,
|
|
73
|
+
}: {
|
|
74
|
+
prompt: string;
|
|
75
|
+
numberOfImages: number;
|
|
76
|
+
},
|
|
77
|
+
oauth: Oauth,
|
|
78
|
+
): Promise<QueueImageGenerationResponse> => {
|
|
76
79
|
const url = new URL(endpoints.queueImageGeneration, BACKEND_HOST);
|
|
77
80
|
url.searchParams.append("count", numberOfImages.toString());
|
|
78
81
|
url.searchParams.append("prompt", prompt);
|
|
79
82
|
|
|
80
|
-
const result: QueueImageGenerationResponse = await sendRequest(url);
|
|
83
|
+
const result: QueueImageGenerationResponse = await sendRequest(url, oauth);
|
|
81
84
|
|
|
82
85
|
return result;
|
|
83
86
|
};
|
|
@@ -87,11 +90,14 @@ export const queueImageGeneration = async ({
|
|
|
87
90
|
* @param {string} jobId - The ID of the job to retrieve status for.
|
|
88
91
|
* @returns {Promise<ImageGenerationResult>} A promise that resolves to the image generation result.
|
|
89
92
|
*/
|
|
90
|
-
export const getImageGenerationJobStatus = async (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
export const getImageGenerationJobStatus = async (
|
|
94
|
+
{
|
|
95
|
+
jobId,
|
|
96
|
+
}: {
|
|
97
|
+
jobId: string;
|
|
98
|
+
},
|
|
99
|
+
oauth: Oauth,
|
|
100
|
+
): Promise<ImageGenerationResult> => {
|
|
95
101
|
const url = new URL(endpoints.getImageGenerationJobStatus, BACKEND_HOST);
|
|
96
102
|
url.searchParams.append("jobId", jobId);
|
|
97
103
|
|
|
@@ -103,6 +109,7 @@ export const getImageGenerationJobStatus = async ({
|
|
|
103
109
|
try {
|
|
104
110
|
const response = (await sendRequest(
|
|
105
111
|
url,
|
|
112
|
+
oauth,
|
|
106
113
|
)) as ImageGenerationJobStatusResponse;
|
|
107
114
|
if (response.status === "completed") {
|
|
108
115
|
return { images: response.images, credits: response.credits };
|
|
@@ -131,12 +138,13 @@ export const getImageGenerationJobStatus = async ({
|
|
|
131
138
|
*/
|
|
132
139
|
export const cancelImageGenerationJob = async (
|
|
133
140
|
jobId: string,
|
|
141
|
+
oauth: Oauth,
|
|
134
142
|
): Promise<void> => {
|
|
135
143
|
const url = new URL(endpoints.cancelImageGenerationJob, BACKEND_HOST);
|
|
136
144
|
url.searchParams.append("jobId", jobId);
|
|
137
145
|
|
|
138
146
|
try {
|
|
139
|
-
await sendRequest(url, {
|
|
147
|
+
await sendRequest(url, oauth, {
|
|
140
148
|
method: "POST",
|
|
141
149
|
});
|
|
142
150
|
} catch {
|
|
@@ -148,23 +156,26 @@ export const cancelImageGenerationJob = async (
|
|
|
148
156
|
* Retrieves the remaining credits.
|
|
149
157
|
* @returns {Promise<RemainingCreditsResult>} - A promise that resolves to the remaining credits.
|
|
150
158
|
*/
|
|
151
|
-
export const getRemainingCredits =
|
|
152
|
-
|
|
153
|
-
|
|
159
|
+
export const getRemainingCredits = async (
|
|
160
|
+
oauth: Oauth,
|
|
161
|
+
): Promise<RemainingCreditsResult> => {
|
|
162
|
+
const url = new URL(endpoints.getRemainingCredits, BACKEND_HOST);
|
|
154
163
|
|
|
155
|
-
|
|
164
|
+
const result: RemainingCreditsResult = await sendRequest(url, oauth);
|
|
156
165
|
|
|
157
|
-
|
|
158
|
-
|
|
166
|
+
return result;
|
|
167
|
+
};
|
|
159
168
|
|
|
160
169
|
/**
|
|
161
170
|
* Purchases credits for the user.
|
|
162
171
|
* @returns {Promise<RemainingCreditsResult>} - A promise that resolves to the remaining credits after purchasing.
|
|
163
172
|
*/
|
|
164
|
-
export const purchaseCredits = async (
|
|
173
|
+
export const purchaseCredits = async (
|
|
174
|
+
oauth: Oauth,
|
|
175
|
+
): Promise<RemainingCreditsResult> => {
|
|
165
176
|
const url = new URL(endpoints.purchaseCredits, BACKEND_HOST);
|
|
166
177
|
|
|
167
|
-
const result: RemainingCreditsResult = await sendRequest(url, {
|
|
178
|
+
const result: RemainingCreditsResult = await sendRequest(url, oauth, {
|
|
168
179
|
method: "POST",
|
|
169
180
|
});
|
|
170
181
|
|
|
@@ -177,10 +188,12 @@ export const purchaseCredits = async (): Promise<RemainingCreditsResult> => {
|
|
|
177
188
|
*
|
|
178
189
|
* Note: You must register the provided endpoint via the Developer Portal.
|
|
179
190
|
*/
|
|
180
|
-
export const checkAuthenticationStatus = async (
|
|
191
|
+
export const checkAuthenticationStatus = async (
|
|
192
|
+
oauth: Oauth,
|
|
193
|
+
): Promise<LoggedInState> => {
|
|
181
194
|
try {
|
|
182
195
|
const url = new URL(endpoints.getLoggedInStatus, BACKEND_HOST);
|
|
183
|
-
const result: { isAuthenticated: string } = await sendRequest(url, {
|
|
196
|
+
const result: { isAuthenticated: string } = await sendRequest(url, oauth, {
|
|
184
197
|
method: "POST",
|
|
185
198
|
});
|
|
186
199
|
|
|
@@ -202,8 +215,12 @@ export const checkAuthenticationStatus = async (): Promise<LoggedInState> => {
|
|
|
202
215
|
* @param {RequestInit} [options] - Optional fetch options to be passed to the fetch function.
|
|
203
216
|
* @returns {Promise<Object>} - A promise that resolves to the response body.
|
|
204
217
|
*/
|
|
205
|
-
const sendRequest = async <T>(
|
|
206
|
-
|
|
218
|
+
const sendRequest = async <T>(
|
|
219
|
+
url: URL,
|
|
220
|
+
oauth: Oauth,
|
|
221
|
+
options?: RequestInit,
|
|
222
|
+
): Promise<T> => {
|
|
223
|
+
const token = (await oauth.getAccessToken())?.token;
|
|
207
224
|
const res = await fetch(url, {
|
|
208
225
|
headers: {
|
|
209
226
|
...options?.headers,
|
|
@@ -27,6 +27,7 @@ export const Footer = () => {
|
|
|
27
27
|
setIsLoadingImages,
|
|
28
28
|
remainingCredits,
|
|
29
29
|
setRemainingCredits,
|
|
30
|
+
oauth,
|
|
30
31
|
} = useAppContext();
|
|
31
32
|
const intl = useIntl();
|
|
32
33
|
|
|
@@ -74,10 +75,13 @@ export const Footer = () => {
|
|
|
74
75
|
|
|
75
76
|
setIsLoadingImages(true);
|
|
76
77
|
try {
|
|
77
|
-
const { jobId } = await queueImageGeneration(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
const { jobId } = await queueImageGeneration(
|
|
79
|
+
{
|
|
80
|
+
prompt: promptInput,
|
|
81
|
+
numberOfImages: NUMBER_OF_IMAGES_TO_GENERATE,
|
|
82
|
+
},
|
|
83
|
+
oauth,
|
|
84
|
+
);
|
|
81
85
|
|
|
82
86
|
setJobId(jobId);
|
|
83
87
|
} catch {
|
|
@@ -91,7 +95,7 @@ export const Footer = () => {
|
|
|
91
95
|
};
|
|
92
96
|
|
|
93
97
|
const onPurchaseMoreCredits = async () => {
|
|
94
|
-
const { credits } = await purchaseCredits();
|
|
98
|
+
const { credits } = await purchaseCredits(oauth);
|
|
95
99
|
|
|
96
100
|
setRemainingCredits(credits);
|
|
97
101
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Grid, ImageCard, Rows, Text } from "@canva/app-ui-kit";
|
|
2
2
|
import type { QueuedImage } from "@canva/asset";
|
|
3
3
|
import { upload } from "@canva/asset";
|
|
4
|
-
import {
|
|
4
|
+
import { addElementAtPoint, ui } from "@canva/design";
|
|
5
5
|
import type { ImageType } from "src/api";
|
|
6
6
|
import { useAppContext } from "src/context";
|
|
7
7
|
import * as styles from "styles/utils.css";
|
|
@@ -12,12 +12,13 @@ const THUMBNAIL_HEIGHT = 150;
|
|
|
12
12
|
const uploadImage = async (image: ImageType): Promise<QueuedImage> => {
|
|
13
13
|
// Upload the image using @canva/asset.
|
|
14
14
|
const queuedImage = await upload({
|
|
15
|
-
type: "
|
|
15
|
+
type: "image",
|
|
16
16
|
mimeType: "image/jpeg",
|
|
17
17
|
thumbnailUrl: image.thumbnail.url,
|
|
18
18
|
url: image.fullsize.url,
|
|
19
19
|
width: image.fullsize.width,
|
|
20
20
|
height: image.fullsize.height,
|
|
21
|
+
aiDisclosure: "app_generated",
|
|
21
22
|
});
|
|
22
23
|
|
|
23
24
|
return queuedImage;
|
|
@@ -34,8 +35,8 @@ export const ImageGrid = () => {
|
|
|
34
35
|
try {
|
|
35
36
|
parentNode?.classList.add(styles.hidden);
|
|
36
37
|
|
|
37
|
-
await ui.
|
|
38
|
-
type: "
|
|
38
|
+
await ui.startDragToPoint(event, {
|
|
39
|
+
type: "image",
|
|
39
40
|
resolveImageRef: () => uploadImage(image),
|
|
40
41
|
previewUrl: image.thumbnail.url,
|
|
41
42
|
previewSize: {
|
|
@@ -55,8 +56,9 @@ export const ImageGrid = () => {
|
|
|
55
56
|
const onImageClick = async (image: ImageType) => {
|
|
56
57
|
const queuedImage = await uploadImage(image);
|
|
57
58
|
|
|
58
|
-
await
|
|
59
|
-
type: "
|
|
59
|
+
await addElementAtPoint({
|
|
60
|
+
type: "image",
|
|
61
|
+
altText: { text: image.label, decorative: false },
|
|
60
62
|
ref: queuedImage.ref,
|
|
61
63
|
});
|
|
62
64
|
};
|
|
@@ -70,6 +70,7 @@ export const LoadingResults = ({
|
|
|
70
70
|
promptInput,
|
|
71
71
|
setGeneratedImages,
|
|
72
72
|
setRemainingCredits,
|
|
73
|
+
oauth,
|
|
73
74
|
} = useAppContext();
|
|
74
75
|
|
|
75
76
|
useEffect(() => {
|
|
@@ -82,9 +83,12 @@ export const LoadingResults = ({
|
|
|
82
83
|
const pollJobStatus = async () => {
|
|
83
84
|
if (jobId) {
|
|
84
85
|
try {
|
|
85
|
-
const { images, credits } = await getImageGenerationJobStatus(
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
const { images, credits } = await getImageGenerationJobStatus(
|
|
87
|
+
{
|
|
88
|
+
jobId,
|
|
89
|
+
},
|
|
90
|
+
oauth,
|
|
91
|
+
);
|
|
88
92
|
setGeneratedImages(images);
|
|
89
93
|
setRemainingCredits(credits);
|
|
90
94
|
// Clear the jobId after fetching images
|
|
@@ -117,7 +121,7 @@ export const LoadingResults = ({
|
|
|
117
121
|
]);
|
|
118
122
|
|
|
119
123
|
const onCancelClick = async () => {
|
|
120
|
-
await cancelImageGenerationJob(jobId);
|
|
124
|
+
await cancelImageGenerationJob(jobId, oauth);
|
|
121
125
|
setIsLoadingImages(false);
|
|
122
126
|
navigate(Paths.HOME);
|
|
123
127
|
};
|