From ba9db4251de0f47499f0eee5172f1c082af178c7 Mon Sep 17 00:00:00 2001 From: Richard Knoll Date: Wed, 20 Nov 2024 15:59:47 -0800 Subject: [PATCH 1/3] beginnings of minecraft cloud proxy work --- localtypings/pxteditor.d.ts | 84 ++++++++++++ pxtlib/auth.ts | 11 ++ webapp/public/cloudframe.html | 205 ++++++++++++++++++++++++++++++ webapp/src/app.tsx | 7 +- webapp/src/auth.ts | 10 +- webapp/src/headerbar.tsx | 2 +- webapp/src/minecraftAuthClient.ts | 204 +++++++++++++++++++++++++++++ 7 files changed, 518 insertions(+), 5 deletions(-) create mode 100644 webapp/public/cloudframe.html create mode 100644 webapp/src/minecraftAuthClient.ts diff --git a/localtypings/pxteditor.d.ts b/localtypings/pxteditor.d.ts index 26dd274455a3..0f458610f539 100644 --- a/localtypings/pxteditor.d.ts +++ b/localtypings/pxteditor.d.ts @@ -103,6 +103,7 @@ declare namespace pxt.editor { | "serviceworkerregistered" | "runeval" | "precachetutorial" + | "cloudproxy" // package extension messasges | ExtInitializeType @@ -1368,6 +1369,89 @@ declare namespace pxt.editor { } type AssetEditorEvent = AssetEditorRequestSaveEvent | AssetEditorReadyEvent; + + type CloudProject = { + id: string; + shareId?: string; + header: string; + text: string; + version: string; + }; + + interface BaseCloudProxyRequest extends EditorMessageRequest { + action: "cloudproxy"; + operation: string; + response: true; + } + + interface CloudProxyUserRequest extends BaseCloudProxyRequest { + operation: "user"; + } + + interface CloudProxyListRequest extends BaseCloudProxyRequest { + operation: "list"; + headerIds?: string[]; + } + + interface CloudProxyGetRequest extends BaseCloudProxyRequest { + operation: "get"; + headerId: string; + } + + interface CloudProxySetRequest extends BaseCloudProxyRequest { + operation: "set"; + project: CloudProject; + } + + interface CloudProxyDeleteRequest extends BaseCloudProxyRequest { + operation: "delete"; + headerId: string; + } + + type CloudProxyRequest = + | CloudProxyUserRequest + | CloudProxyListRequest + | CloudProxyGetRequest + | CloudProxySetRequest + | CloudProxyDeleteRequest; + + + interface BaseCloudProxyResponse extends EditorMessageResponse { + action: "cloudproxy"; + operation: string; + resp: pxt.auth.ApiResult; + } + + interface CloudProxyUserResponse extends BaseCloudProxyResponse { + operation: "user"; + } + + interface CloudProxyListResponse extends BaseCloudProxyResponse { + operation: "list"; + resp: pxt.auth.ApiResult; + } + + interface CloudProxyGetResponse extends BaseCloudProxyResponse { + operation: "get"; + resp: pxt.auth.ApiResult; + } + + interface CloudProxySetResponse extends BaseCloudProxyResponse { + operation: "set"; + resp: pxt.auth.ApiResult; + } + + interface CloudProxyDeleteResponse extends BaseCloudProxyResponse { + operation: "delete"; + resp: pxt.auth.ApiResult; + } + + type CloudProxyResponse = + | CloudProxyUserResponse + | CloudProxyListResponse + | CloudProxyGetResponse + | CloudProxySetResponse + | CloudProxyDeleteResponse; } declare namespace pxt.workspace { diff --git a/pxtlib/auth.ts b/pxtlib/auth.ts index 234f281ee2c7..0e7481c9fbda 100644 --- a/pxtlib/auth.ts +++ b/pxtlib/auth.ts @@ -116,6 +116,10 @@ namespace pxt.auth { return await setLocalStorageValueAsync(CSRF_TOKEN_KEY, token); } export async function hasAuthTokenAsync(): Promise { + if (hasCloudProxyIdentity()) { + cachedHasAuthToken = true; + return true; + } return !!(await getAuthTokenAsync()); } async function delAuthTokenAsync(): Promise { @@ -748,9 +752,16 @@ namespace pxt.auth { } export function hasIdentity(): boolean { + if (hasCloudProxyIdentity()) { + return true; + } return !authDisabled && !pxt.BrowserUtils.isPxtElectron() && identityProviders().length > 0; } + export function hasCloudProxyIdentity(): boolean { + return true; + } + function idpEnabled(idp: pxt.IdentityProviderId): boolean { return identityProviders().filter(prov => prov.id === idp).length > 0; } diff --git a/webapp/public/cloudframe.html b/webapp/public/cloudframe.html new file mode 100644 index 000000000000..2c661ac6193d --- /dev/null +++ b/webapp/public/cloudframe.html @@ -0,0 +1,205 @@ + + + + + + + + + + + +
+

+    
+ + + + diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index eab841f53d32..2eb88f39a763 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -82,6 +82,7 @@ import { Tour } from "./components/onboarding/Tour"; import { parseTourStepsAsync } from "./onboarding"; import { initGitHubDb } from "./idbworkspace"; import { BlockDefinition, CategoryNameID } from "./toolbox"; +import { MinecraftAuthClient } from "./minecraftAuthClient"; pxt.blocks.requirePxtBlockly = () => pxtblockly as any; pxt.blocks.requireBlockly = () => Blockly; @@ -5940,6 +5941,8 @@ document.addEventListener("DOMContentLoaded", async () => { pxt.setupWebConfig((window as any).pxtConfig); const config = pxt.webConfig + auth.overrideAuthClient(() => new MinecraftAuthClient()); + const optsQuery = Util.parseQueryString(window.location.href.toLowerCase()); if (optsQuery["dbg"] == "1") { pxt.setLogLevel(pxt.LogLevel.Debug); @@ -5963,11 +5966,11 @@ document.addEventListener("DOMContentLoaded", async () => { pxt.blocks.showBlockIdInTooltip = true; } - initGitHubDb(); - pxt.perf.measureStart("setAppTarget"); pkg.setupAppTarget((window as any).pxtTargetBundle); + initGitHubDb(); + // DO NOT put any async code before this line! The serviceworker must be initialized before // the window load event fires appcache.init(() => theEditor.reloadEditor()); diff --git a/webapp/src/auth.ts b/webapp/src/auth.ts index b735f55f2288..9417d3234b1c 100644 --- a/webapp/src/auth.ts +++ b/webapp/src/auth.ts @@ -2,6 +2,7 @@ import * as core from "./core"; import * as data from "./data"; import * as cloud from "./cloud"; import * as workspace from "./workspace"; +import { MinecraftAuthClient } from "./minecraftAuthClient"; /** * Virtual API keys @@ -35,7 +36,7 @@ export class Component extends data.Component { } } -class AuthClient extends pxt.auth.AuthClient { +export class AuthClient extends pxt.auth.AuthClient { protected async onSignedIn(): Promise { const state = await pxt.auth.getUserStateAsync(); core.infoNotification(lf("Signed in: {0}", pxt.auth.userName(state.profile))); @@ -151,12 +152,13 @@ function initVirtualApi() { } let authClientPromise: Promise; +let authClientFactory = () => new AuthClient(); async function clientAsync(): Promise { if (!pxt.auth.hasIdentity()) { return undefined; } if (authClientPromise) return authClientPromise; authClientPromise = new Promise(async (resolve, reject) => { - const cli = new AuthClient(); + const cli = authClientFactory(); await cli.initAsync(); await cli.authCheckAsync(); await cli.initialUserPreferencesAsync(); @@ -165,6 +167,10 @@ async function clientAsync(): Promise { return authClientPromise; } +export function overrideAuthClient(factory: () => AuthClient) { + authClientFactory = factory; +} + export function hasIdentity(): boolean { return pxt.auth.hasIdentity(); } diff --git a/webapp/src/headerbar.tsx b/webapp/src/headerbar.tsx index d873f3d8e90c..a123223813cb 100644 --- a/webapp/src/headerbar.tsx +++ b/webapp/src/headerbar.tsx @@ -239,7 +239,7 @@ export class HeaderBar extends data.Component { const { home, header, tutorialOptions } = this.props.parent.state; const isController = pxt.shell.isControllerMode(); const isNativeHost = cmds.isNativeHost(); - const hasIdentity = auth.hasIdentity(); + const hasIdentity = auth.hasIdentity() && !pxt.auth.hasCloudProxyIdentity(); const activeEditor = this.props.parent.isPythonActive() ? "Python" : (this.props.parent.isJavaScriptActive() ? "JavaScript" : "Blocks"); diff --git a/webapp/src/minecraftAuthClient.ts b/webapp/src/minecraftAuthClient.ts new file mode 100644 index 000000000000..396a01954551 --- /dev/null +++ b/webapp/src/minecraftAuthClient.ts @@ -0,0 +1,204 @@ +import { AuthClient } from "./auth"; + + +export class MinecraftAuthClient extends AuthClient { + protected pendingMessages: pxt.Map<(response: pxt.editor.CloudProxyResponse) => void> = {}; + protected preferences: pxt.auth.UserPreferences = {}; + + private pendingAuthCheck: Promise> = undefined; + + constructor() { + super(); + + window.addEventListener("message", ev => { + const message = ev.data; + + if (message.action === "cloudproxy") { + if (this.pendingMessages[message.id]) { + const response = message as pxt.editor.CloudProxyResponse; + this.pendingMessages[message.id](response); + delete this.pendingMessages[message.id]; + } + } + }); + } + + async apiAsync(url: string, data?: any, method?: string, authToken?: string): Promise> { + const match = /((?:\/[^\?\/]+)*)(\?.*)?/.exec(url); + + if (!match) { + throw new Error("Bad API format"); + } + + const path = match[1]; + const query = match[2]; + + if (!method) { + if (data) { + method = "POST"; + } + else { + method = "GET"; + } + } + + if (path === "/api/user") { + if (method === "DELETE") { + return ( + { + success: true, + statusCode: 200, + resp: undefined, + err: undefined + } + ); + } + } + else if (path === "/api/user/profile") { + return this.userAsync() as Promise>; + } + else if (path === "/api/user/preferences") { + if (method === "POST") { + this.preferences = { + ...this.preferences, + ...data + }; + } + return ( + { + success: true, + statusCode: 200, + resp: {...this.preferences} as T, + err: undefined + } + ); + } + else if (path === "/api/auth/login") { + return ( + { + success: false, + statusCode: 500, + resp: undefined, + err: "Not supported" + } + ); + } + else if (path === "/api/auth/logout") { + return ( + { + success: true, + statusCode: 200, + resp: {} as T, + err: undefined + } + ); + } + else if (path === "/api/user/project") { + + if (method === "GET") { + // LIST + let headerIds: string[]; + + if (query) { + const parsed = new URLSearchParams(query); + const list = parsed.get("projectIds"); + if (list) { + headerIds = list.split(","); + } + } + + return this.listAsync(headerIds) as Promise>; + } + else { + // SET + return this.setAsync(data) as Promise>; + } + } + else if (path === "/api/user/project/share") { + // TODO + } + else if (path.startsWith("/api/user/project/")) { + const headerId = path.substring(18); + + if (method === "POST") { + return this.setAsync(data) as Promise>; + } + else if (method === "GET") { + return this.getAsync(headerId) as Promise>; + } + } + + return ( + { + success: false, + statusCode: 500, + resp: undefined, + err: "Not supported" + } + ); + } + + // public async authCheckAsync(): Promise { + // if (!this.pendingAuthCheck) { + // this.pendingAuthCheck = this.userAsync(); + // } + // const user = await this.pendingAuthCheck; + + // if (user.success) { + // return user.resp; + // } + // return undefined; + // } + + protected postMessageAsync(message: Partial): Promise { + return new Promise((resolve, reject) => { + const toPost = { + ...message, + type: "pxthost", + action: "cloudproxy", + reponse: true, + id: "cloudproxy-" + crypto.randomUUID() + }; + + this.pendingMessages[toPost.id] = resolve as any; + + window.parent.postMessage(toPost, "*"); + }) + } + + protected async listAsync(headerIds?: string[]): Promise> { + const resp = await this.postMessageAsync({ + operation: "list", + headerIds + }); + + return resp.resp; + } + + protected async getAsync(headerId: string): Promise> { + const resp = await this.postMessageAsync({ + operation: "get", + headerId + }); + + return resp.resp; + } + + protected async setAsync(project: pxt.editor.CloudProject): Promise> { + const resp = await this.postMessageAsync({ + operation: "set", + project + }); + + return resp.resp; + } + + protected async userAsync(): Promise> { + const resp = await this.postMessageAsync({ + operation: "user", + }); + + return resp.resp; + } +} + From 5b48293494f7c2a6d4e61b11b3b89649c58097ba Mon Sep 17 00:00:00 2001 From: Richard Knoll Date: Thu, 21 Nov 2024 12:41:34 -0800 Subject: [PATCH 2/3] more cloudframe stuff --- webapp/public/cloudframe.html | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/webapp/public/cloudframe.html b/webapp/public/cloudframe.html index 2c661ac6193d..0e91a7ba18af 100644 --- a/webapp/public/cloudframe.html +++ b/webapp/public/cloudframe.html @@ -36,6 +36,7 @@ +

     
From d540f89c2312c86bdd1f2c6e2bce0f54f044e776 Mon Sep 17 00:00:00 2001 From: Richard Knoll Date: Thu, 21 Nov 2024 14:06:18 -0800 Subject: [PATCH 3/3] add target flag --- localtypings/pxtarget.d.ts | 1 + pxtlib/auth.ts | 8 ++++---- webapp/src/app.tsx | 6 ++++-- webapp/src/headerbar.tsx | 2 +- webapp/src/minecraftAuthClient.ts | 18 ++---------------- 5 files changed, 12 insertions(+), 23 deletions(-) diff --git a/localtypings/pxtarget.d.ts b/localtypings/pxtarget.d.ts index d0f1f29e7f15..10a3931fcbc2 100644 --- a/localtypings/pxtarget.d.ts +++ b/localtypings/pxtarget.d.ts @@ -524,6 +524,7 @@ declare namespace pxt { timeMachineDiffInterval?: number; // An interval in milliseconds at which to take diffs to store in project history. Defaults to 5 minutes timeMachineSnapshotInterval?: number; // An interval in milliseconds at which to take full project snapshots in project history. Defaults to 15 minutes adjustBlockContrast?: boolean; // If set to true, all block colors will automatically be adjusted to have a contrast ratio of 4.5 with text + ipcIdentityProxy?: boolean; // for use with the in game minecraft experience only. If true, proxies all identity API requests through the ipc channel } interface DownloadDialogTheme { diff --git a/pxtlib/auth.ts b/pxtlib/auth.ts index 0e7481c9fbda..01eaf2993e16 100644 --- a/pxtlib/auth.ts +++ b/pxtlib/auth.ts @@ -116,7 +116,7 @@ namespace pxt.auth { return await setLocalStorageValueAsync(CSRF_TOKEN_KEY, token); } export async function hasAuthTokenAsync(): Promise { - if (hasCloudProxyIdentity()) { + if (proxyIdentityThroughIPC()) { cachedHasAuthToken = true; return true; } @@ -752,14 +752,14 @@ namespace pxt.auth { } export function hasIdentity(): boolean { - if (hasCloudProxyIdentity()) { + if (proxyIdentityThroughIPC()) { return true; } return !authDisabled && !pxt.BrowserUtils.isPxtElectron() && identityProviders().length > 0; } - export function hasCloudProxyIdentity(): boolean { - return true; + export function proxyIdentityThroughIPC(): boolean { + return pxt.appTarget.appTheme.ipcIdentityProxy; } function idpEnabled(idp: pxt.IdentityProviderId): boolean { diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index 2eb88f39a763..a87f2a8d09b1 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -5941,8 +5941,6 @@ document.addEventListener("DOMContentLoaded", async () => { pxt.setupWebConfig((window as any).pxtConfig); const config = pxt.webConfig - auth.overrideAuthClient(() => new MinecraftAuthClient()); - const optsQuery = Util.parseQueryString(window.location.href.toLowerCase()); if (optsQuery["dbg"] == "1") { pxt.setLogLevel(pxt.LogLevel.Debug); @@ -5971,6 +5969,10 @@ document.addEventListener("DOMContentLoaded", async () => { initGitHubDb(); + if (pxt.auth.proxyIdentityThroughIPC()) { + auth.overrideAuthClient(() => new MinecraftAuthClient()); + } + // DO NOT put any async code before this line! The serviceworker must be initialized before // the window load event fires appcache.init(() => theEditor.reloadEditor()); diff --git a/webapp/src/headerbar.tsx b/webapp/src/headerbar.tsx index a123223813cb..5ca7ef056c35 100644 --- a/webapp/src/headerbar.tsx +++ b/webapp/src/headerbar.tsx @@ -239,7 +239,7 @@ export class HeaderBar extends data.Component { const { home, header, tutorialOptions } = this.props.parent.state; const isController = pxt.shell.isControllerMode(); const isNativeHost = cmds.isNativeHost(); - const hasIdentity = auth.hasIdentity() && !pxt.auth.hasCloudProxyIdentity(); + const hasIdentity = auth.hasIdentity() && !pxt.auth.proxyIdentityThroughIPC(); const activeEditor = this.props.parent.isPythonActive() ? "Python" : (this.props.parent.isJavaScriptActive() ? "JavaScript" : "Blocks"); diff --git a/webapp/src/minecraftAuthClient.ts b/webapp/src/minecraftAuthClient.ts index 396a01954551..d056b874a54b 100644 --- a/webapp/src/minecraftAuthClient.ts +++ b/webapp/src/minecraftAuthClient.ts @@ -120,10 +120,7 @@ export class MinecraftAuthClient extends AuthClient { else if (path.startsWith("/api/user/project/")) { const headerId = path.substring(18); - if (method === "POST") { - return this.setAsync(data) as Promise>; - } - else if (method === "GET") { + if (method === "GET") { return this.getAsync(headerId) as Promise>; } } @@ -138,18 +135,6 @@ export class MinecraftAuthClient extends AuthClient { ); } - // public async authCheckAsync(): Promise { - // if (!this.pendingAuthCheck) { - // this.pendingAuthCheck = this.userAsync(); - // } - // const user = await this.pendingAuthCheck; - - // if (user.success) { - // return user.resp; - // } - // return undefined; - // } - protected postMessageAsync(message: Partial): Promise { return new Promise((resolve, reject) => { const toPost = { @@ -162,6 +147,7 @@ export class MinecraftAuthClient extends AuthClient { this.pendingMessages[toPost.id] = resolve as any; + // TODO: send over ipc channel window.parent.postMessage(toPost, "*"); }) }