Generalize initial app logic

This commit is contained in:
Asher 2020-02-05 18:47:00 -06:00
parent 205775ac97
commit 6cebfa469d
No known key found for this signature in database
GPG Key ID: D63C1EF81242354A
9 changed files with 78 additions and 57 deletions

View File

@ -1,5 +1,12 @@
import { getBasepath } from "hookrouter" 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" import { ApiEndpoint, HttpCode, HttpError } from "../common/http"
export interface AuthBody { export interface AuthBody {
@ -33,7 +40,7 @@ const tryRequest = async (endpoint: string, options?: RequestInit): Promise<Resp
/** /**
* Try authenticating. * Try authenticating.
*/ */
export const authenticate = async (body?: AuthBody): Promise<void> => { export const authenticate = async (body?: AuthBody): Promise<LoginResponse> => {
const response = await tryRequest(ApiEndpoint.login, { const response = await tryRequest(ApiEndpoint.login, {
method: "POST", method: "POST",
body: JSON.stringify({ ...body, basePath: getBasepath() }), body: JSON.stringify({ ...body, basePath: getBasepath() }),
@ -41,10 +48,7 @@ export const authenticate = async (body?: AuthBody): Promise<void> => {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8", "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
}, },
}) })
const json = await response.json() return response.json()
if (json && json.success) {
setAuthed(true)
}
} }
export const getFiles = async (): Promise<FilesResponse> => { export const getFiles = async (): Promise<FilesResponse> => {

View File

@ -10,34 +10,33 @@ export interface AppProps {
options: Options options: Options
} }
interface RedirectedApplication extends Application {
redirected?: boolean
}
const origin = typeof window !== "undefined" ? window.location.origin + window.location.pathname : undefined
const App: React.FunctionComponent<AppProps> = (props) => { const App: React.FunctionComponent<AppProps> = (props) => {
const [authed, setAuthed] = React.useState<boolean>(props.options.authed) const [authed, setAuthed] = React.useState<boolean>(props.options.authed)
const [app, setApp] = React.useState<Application | undefined>(props.options.app) const [app, setApp] = React.useState<RedirectedApplication | undefined>(props.options.app)
const [error, setError] = React.useState<HttpError | Error | string>() const [error, setError] = React.useState<HttpError | Error | string>()
if (typeof window !== "undefined") { 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)) setBasepath(normalize(url.pathname))
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
;(window as any).setAuthed = (a: boolean): void => { ;(window as any).setAuthed = (a: boolean): void => {
if (authed !== a) { if (authed !== a) {
setAuthed(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(() => { React.useEffect(() => {
if (app && !isExecutableApplication(app)) { if (app && !isExecutableApplication(app) && !app.redirected) {
navigate(normalize(`${getBasepath()}/${app.path}/`, true)) navigate(normalize(`${getBasepath()}/${app.path}/`, true))
setApp({ ...app, redirected: true })
} }
}, [app]) }, [app])
@ -51,7 +50,7 @@ const App: React.FunctionComponent<AppProps> = (props) => {
undefined undefined
)} )}
<Modal app={app} setApp={setApp} authed={authed} error={error} setError={setError} /> <Modal app={app} setApp={setApp} authed={authed} error={error} setError={setError} />
{authed && app && app.embedPath ? ( {authed && app && app.embedPath && app.redirected ? (
<iframe id="iframe" src={normalize(`./${app.embedPath}/`, true)}></iframe> <iframe id="iframe" src={normalize(`./${app.embedPath}/`, true)}></iframe>
) : ( ) : (
undefined undefined

View File

@ -22,7 +22,6 @@ export interface ModalProps {
enum Section { enum Section {
Browse, Browse,
Home, Home,
Login,
Open, Open,
Recent, Recent,
} }
@ -103,7 +102,7 @@ export const Modal: React.FunctionComponent<ModalProps> = (props) => {
const content = (): React.ReactElement => { const content = (): React.ReactElement => {
if (!props.authed) { if (!props.authed) {
return <Login /> return <Login setApp={setApp} />
} }
switch (section) { switch (section) {
case Section.Recent: case Section.Recent:
@ -112,8 +111,6 @@ export const Modal: React.FunctionComponent<ModalProps> = (props) => {
return <Home app={props.app} /> return <Home app={props.app} />
case Section.Browse: case Section.Browse:
return <Browse /> return <Browse />
case Section.Login:
return <Login />
case Section.Open: case Section.Open:
return <Open app={props.app} setApp={setApp} /> return <Open app={props.app} setApp={setApp} />
default: default:
@ -140,9 +137,7 @@ export const Modal: React.FunctionComponent<ModalProps> = (props) => {
</button> </button>
</> </>
) : ( ) : (
<button className="link" onClick={(): void => setSection(Section.Login)}> undefined
Login
</button>
)} )}
</nav> </nav>
<div className="footer"> <div className="footer">

View File

@ -1,6 +1,6 @@
import * as React from "react" import * as React from "react"
import { Application } from "../../common/api" import { Application } from "../../common/api"
import { authenticate } from "../api" import { authenticate, setAuthed } from "../api"
export interface HomeProps { export interface HomeProps {
app?: Application app?: Application
@ -8,7 +8,9 @@ export interface HomeProps {
export const Home: React.FunctionComponent<HomeProps> = (props) => { export const Home: React.FunctionComponent<HomeProps> = (props) => {
React.useEffect(() => { React.useEffect(() => {
authenticate().catch(() => undefined) authenticate()
.then(() => setAuthed(true))
.catch(() => undefined)
}, []) }, [])
return ( return (

View File

@ -1,22 +1,36 @@
import * as React from "react" import * as React from "react"
import { Application } from "../../common/api"
import { HttpError } from "../../common/http" import { HttpError } from "../../common/http"
import { authenticate } from "../api" import { authenticate, setAuthed } from "../api"
import { FieldError } from "../components/error" import { FieldError } from "../components/error"
export interface LoginProps {
setApp(app: Application): void
}
/** /**
* Login page. Will redirect on success. * Login page. Will redirect on success.
*/ */
export const Login: React.FunctionComponent = () => { export const Login: React.FunctionComponent<LoginProps> = (props) => {
const [password, setPassword] = React.useState<string>("") const [password, setPassword] = React.useState<string>("")
const [error, setError] = React.useState<HttpError>() const [error, setError] = React.useState<HttpError>()
async function handleSubmit(event: React.FormEvent<HTMLFormElement>): Promise<void> { async function handleSubmit(event: React.FormEvent<HTMLFormElement>): Promise<void> {
event.preventDefault() event.preventDefault()
authenticate({ password }).catch(setError) authenticate({ password })
.then((response) => {
if (response.app) {
props.setApp(response.app)
}
setAuthed(true)
})
.catch(setError)
} }
React.useEffect(() => { React.useEffect(() => {
authenticate().catch(() => undefined) authenticate()
.then(() => setAuthed(true))
.catch(() => undefined)
}, []) }, [])
return ( return (

View File

@ -23,11 +23,15 @@ export enum SessionError {
} }
export interface LoginRequest { export interface LoginRequest {
password: string
basePath: string basePath: string
password: string
} }
export interface LoginResponse { export interface LoginResponse {
/**
* An application to load immediately after logging in.
*/
app?: Application
success: boolean success: boolean
} }

View File

@ -3,6 +3,7 @@ import * as http from "http"
import * as net from "net" import * as net from "net"
import * as ws from "ws" import * as ws from "ws"
import { import {
Application,
ApplicationsResponse, ApplicationsResponse,
ClientMessage, ClientMessage,
FilesResponse, FilesResponse,
@ -15,6 +16,12 @@ import { normalize } from "../../common/util"
import { HttpProvider, HttpProviderOptions, HttpResponse, HttpServer, Route } from "../http" import { HttpProvider, HttpProviderOptions, HttpResponse, HttpServer, Route } from "../http"
import { hash } from "../util" import { hash } from "../util"
export const Vscode: Application = {
name: "VS Code",
path: "/",
embedPath: "./vscode-embed",
}
/** /**
* API HTTP provider. * API HTTP provider.
*/ */
@ -104,6 +111,8 @@ export class ApiHttpProvider extends HttpProvider {
return { return {
content: { content: {
success: true, success: true,
// TEMP: Auto-load VS Code.
app: Vscode,
}, },
cookie: cookie:
typeof password === "string" typeof password === "string"
@ -149,13 +158,7 @@ export class ApiHttpProvider extends HttpProvider {
private async applications(): Promise<HttpResponse<ApplicationsResponse>> { private async applications(): Promise<HttpResponse<ApplicationsResponse>> {
return { return {
content: { content: {
applications: [ applications: [Vscode],
{
name: "VS Code",
path: "/vscode",
embedPath: "/vscode-embed",
},
],
}, },
} }
} }

View File

@ -5,6 +5,7 @@ import * as ReactDOMServer from "react-dom/server"
import App from "../../browser/app" import App from "../../browser/app"
import { HttpCode, HttpError } from "../../common/http" import { HttpCode, HttpError } from "../../common/http"
import { Options } from "../../common/util" import { Options } from "../../common/util"
import { Vscode } from "../api/server"
import { HttpProvider, HttpResponse, Route } from "../http" import { HttpProvider, HttpResponse, Route } from "../http"
/** /**
@ -21,39 +22,40 @@ export class MainHttpProvider extends HttpProvider {
return response return response
} }
case "/vscode":
case "/": { case "/": {
if (route.requestPath !== "/index.html") { if (route.requestPath !== "/index.html") {
throw new HttpError("Not found", HttpCode.NotFound) throw new HttpError("Not found", HttpCode.NotFound)
} }
const options: Options = { const options: Options = {
authed: !!this.authenticated(request), authed: !!this.authenticated(request),
basePath: this.base(route), basePath: this.base(route),
logLevel: logger.level, logLevel: logger.level,
} }
if (options.authed) { // TODO: Load other apps based on the URL as well.
// TEMP: Auto-load VS Code for now. In future versions we'll need to check if (route.base === Vscode.path && options.authed) {
// the URL for the appropriate application to load, if any. options.app = Vscode
options.app = {
name: "VS Code",
path: "/",
embedPath: "/vscode-embed",
}
} }
const response = await this.getUtf8Resource(this.rootPath, "src/browser/index.html") return this.getRoot(route, options)
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(<App options={options} />))
return response
} }
} }
return undefined return undefined
} }
public async getRoot(route: Route, options: Options): Promise<HttpResponse> {
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(<App options={options} />))
return response
}
public async handleWebSocket(): Promise<undefined> { public async handleWebSocket(): Promise<undefined> {
return undefined return undefined
} }

View File

@ -116,7 +116,6 @@ interface ProviderRoute extends Route {
} }
export interface HttpProviderOptions { export interface HttpProviderOptions {
readonly base: string
readonly auth: AuthType readonly auth: AuthType
readonly password?: string readonly password?: string
readonly commit: string readonly commit: string
@ -154,7 +153,7 @@ export abstract class HttpProvider {
* Get the base relative to the provided route. * Get the base relative to the provided route.
*/ */
public base(route: Route): string { 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) : "")) return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : ""))
} }
@ -404,7 +403,6 @@ export class HttpServer {
new provider( new provider(
{ {
auth: this.options.auth || AuthType.None, auth: this.options.auth || AuthType.None,
base: endpoint,
commit: this.options.commit, commit: this.options.commit,
password: this.options.password, password: this.options.password,
}, },