diff --git a/src/browser/api.ts b/src/browser/api.ts index 77e76ca1..8186bb77 100644 --- a/src/browser/api.ts +++ b/src/browser/api.ts @@ -1,5 +1,12 @@ import { getBasepath } from "hookrouter" -import { Application, ApplicationsResponse, CreateSessionResponse, FilesResponse, RecentResponse } from "../common/api" +import { + Application, + ApplicationsResponse, + CreateSessionResponse, + FilesResponse, + LoginResponse, + RecentResponse, +} from "../common/api" import { ApiEndpoint, HttpCode, HttpError } from "../common/http" export interface AuthBody { @@ -33,7 +40,7 @@ const tryRequest = async (endpoint: string, options?: RequestInit): Promise => { +export const authenticate = async (body?: AuthBody): Promise => { const response = await tryRequest(ApiEndpoint.login, { method: "POST", body: JSON.stringify({ ...body, basePath: getBasepath() }), @@ -41,10 +48,7 @@ export const authenticate = async (body?: AuthBody): Promise => { "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", }, }) - const json = await response.json() - if (json && json.success) { - setAuthed(true) - } + return response.json() } export const getFiles = async (): Promise => { diff --git a/src/browser/app.tsx b/src/browser/app.tsx index b3f1271f..dd0a6e25 100644 --- a/src/browser/app.tsx +++ b/src/browser/app.tsx @@ -10,34 +10,33 @@ export interface AppProps { options: Options } +interface RedirectedApplication extends Application { + redirected?: boolean +} + +const origin = typeof window !== "undefined" ? window.location.origin + window.location.pathname : undefined + const App: React.FunctionComponent = (props) => { const [authed, setAuthed] = React.useState(props.options.authed) - const [app, setApp] = React.useState(props.options.app) + const [app, setApp] = React.useState(props.options.app) const [error, setError] = React.useState() if (typeof window !== "undefined") { - const url = new URL(window.location.origin + window.location.pathname + props.options.basePath) + const url = new URL(origin + props.options.basePath) setBasepath(normalize(url.pathname)) // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(window as any).setAuthed = (a: boolean): void => { if (authed !== a) { setAuthed(a) - // TEMP: Remove when no longer auto-loading VS Code. - if (a && !app) { - setApp({ - name: "VS Code", - path: "/", - embedPath: "/vscode-embed", - }) - } } } } React.useEffect(() => { - if (app && !isExecutableApplication(app)) { + if (app && !isExecutableApplication(app) && !app.redirected) { navigate(normalize(`${getBasepath()}/${app.path}/`, true)) + setApp({ ...app, redirected: true }) } }, [app]) @@ -51,7 +50,7 @@ const App: React.FunctionComponent = (props) => { undefined )} - {authed && app && app.embedPath ? ( + {authed && app && app.embedPath && app.redirected ? ( ) : ( undefined diff --git a/src/browser/components/modal.tsx b/src/browser/components/modal.tsx index df8c1881..6f5b63de 100644 --- a/src/browser/components/modal.tsx +++ b/src/browser/components/modal.tsx @@ -22,7 +22,6 @@ export interface ModalProps { enum Section { Browse, Home, - Login, Open, Recent, } @@ -103,7 +102,7 @@ export const Modal: React.FunctionComponent = (props) => { const content = (): React.ReactElement => { if (!props.authed) { - return + return } switch (section) { case Section.Recent: @@ -112,8 +111,6 @@ export const Modal: React.FunctionComponent = (props) => { return case Section.Browse: return - case Section.Login: - return case Section.Open: return default: @@ -140,9 +137,7 @@ export const Modal: React.FunctionComponent = (props) => { ) : ( - + undefined )}
diff --git a/src/browser/pages/home.tsx b/src/browser/pages/home.tsx index a2d38c5e..2af38ae7 100644 --- a/src/browser/pages/home.tsx +++ b/src/browser/pages/home.tsx @@ -1,6 +1,6 @@ import * as React from "react" import { Application } from "../../common/api" -import { authenticate } from "../api" +import { authenticate, setAuthed } from "../api" export interface HomeProps { app?: Application @@ -8,7 +8,9 @@ export interface HomeProps { export const Home: React.FunctionComponent = (props) => { React.useEffect(() => { - authenticate().catch(() => undefined) + authenticate() + .then(() => setAuthed(true)) + .catch(() => undefined) }, []) return ( diff --git a/src/browser/pages/login.tsx b/src/browser/pages/login.tsx index 03340b0a..3e44face 100644 --- a/src/browser/pages/login.tsx +++ b/src/browser/pages/login.tsx @@ -1,22 +1,36 @@ import * as React from "react" +import { Application } from "../../common/api" import { HttpError } from "../../common/http" -import { authenticate } from "../api" +import { authenticate, setAuthed } from "../api" import { FieldError } from "../components/error" +export interface LoginProps { + setApp(app: Application): void +} + /** * Login page. Will redirect on success. */ -export const Login: React.FunctionComponent = () => { +export const Login: React.FunctionComponent = (props) => { const [password, setPassword] = React.useState("") const [error, setError] = React.useState() async function handleSubmit(event: React.FormEvent): Promise { event.preventDefault() - authenticate({ password }).catch(setError) + authenticate({ password }) + .then((response) => { + if (response.app) { + props.setApp(response.app) + } + setAuthed(true) + }) + .catch(setError) } React.useEffect(() => { - authenticate().catch(() => undefined) + authenticate() + .then(() => setAuthed(true)) + .catch(() => undefined) }, []) return ( diff --git a/src/common/api.ts b/src/common/api.ts index aa89a626..d8829f55 100644 --- a/src/common/api.ts +++ b/src/common/api.ts @@ -23,11 +23,15 @@ export enum SessionError { } export interface LoginRequest { - password: string basePath: string + password: string } export interface LoginResponse { + /** + * An application to load immediately after logging in. + */ + app?: Application success: boolean } diff --git a/src/node/api/server.ts b/src/node/api/server.ts index 4d5ab4b4..ec1ac452 100644 --- a/src/node/api/server.ts +++ b/src/node/api/server.ts @@ -3,6 +3,7 @@ import * as http from "http" import * as net from "net" import * as ws from "ws" import { + Application, ApplicationsResponse, ClientMessage, FilesResponse, @@ -15,6 +16,12 @@ import { normalize } from "../../common/util" import { HttpProvider, HttpProviderOptions, HttpResponse, HttpServer, Route } from "../http" import { hash } from "../util" +export const Vscode: Application = { + name: "VS Code", + path: "/", + embedPath: "./vscode-embed", +} + /** * API HTTP provider. */ @@ -104,6 +111,8 @@ export class ApiHttpProvider extends HttpProvider { return { content: { success: true, + // TEMP: Auto-load VS Code. + app: Vscode, }, cookie: typeof password === "string" @@ -149,13 +158,7 @@ export class ApiHttpProvider extends HttpProvider { private async applications(): Promise> { return { content: { - applications: [ - { - name: "VS Code", - path: "/vscode", - embedPath: "/vscode-embed", - }, - ], + applications: [Vscode], }, } } diff --git a/src/node/app/server.tsx b/src/node/app/server.tsx index b14876e6..a2ec93eb 100644 --- a/src/node/app/server.tsx +++ b/src/node/app/server.tsx @@ -5,6 +5,7 @@ import * as ReactDOMServer from "react-dom/server" import App from "../../browser/app" import { HttpCode, HttpError } from "../../common/http" import { Options } from "../../common/util" +import { Vscode } from "../api/server" import { HttpProvider, HttpResponse, Route } from "../http" /** @@ -21,39 +22,40 @@ export class MainHttpProvider extends HttpProvider { return response } + case "/vscode": case "/": { if (route.requestPath !== "/index.html") { throw new HttpError("Not found", HttpCode.NotFound) } + const options: Options = { authed: !!this.authenticated(request), basePath: this.base(route), logLevel: logger.level, } - if (options.authed) { - // TEMP: Auto-load VS Code for now. In future versions we'll need to check - // the URL for the appropriate application to load, if any. - options.app = { - name: "VS Code", - path: "/", - embedPath: "/vscode-embed", - } + // TODO: Load other apps based on the URL as well. + if (route.base === Vscode.path && options.authed) { + options.app = Vscode } - const response = await this.getUtf8Resource(this.rootPath, "src/browser/index.html") - response.content = response.content - .replace(/{{COMMIT}}/g, this.options.commit) - .replace(/{{BASE}}/g, this.base(route)) - .replace(/"{{OPTIONS}}"/g, `'${JSON.stringify(options)}'`) - .replace(/{{COMPONENT}}/g, ReactDOMServer.renderToString()) - return response + return this.getRoot(route, options) } } return undefined } + public async getRoot(route: Route, options: Options): Promise { + const response = await this.getUtf8Resource(this.rootPath, "src/browser/index.html") + response.content = response.content + .replace(/{{COMMIT}}/g, this.options.commit) + .replace(/{{BASE}}/g, this.base(route)) + .replace(/"{{OPTIONS}}"/g, `'${JSON.stringify(options)}'`) + .replace(/{{COMPONENT}}/g, ReactDOMServer.renderToString()) + return response + } + public async handleWebSocket(): Promise { return undefined } diff --git a/src/node/http.ts b/src/node/http.ts index a4fb5258..7c023956 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -116,7 +116,6 @@ interface ProviderRoute extends Route { } export interface HttpProviderOptions { - readonly base: string readonly auth: AuthType readonly password?: string readonly commit: string @@ -154,7 +153,7 @@ export abstract class HttpProvider { * Get the base relative to the provided route. */ public base(route: Route): string { - const depth = route.fullPath ? (route.fullPath.match(/\//g) || []).length : 1 + const depth = ((route.fullPath + "/").match(/\//g) || []).length return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : "")) } @@ -404,7 +403,6 @@ export class HttpServer { new provider( { auth: this.options.auth || AuthType.None, - base: endpoint, commit: this.options.commit, password: this.options.password, },