Generalize initial app logic
This commit is contained in:
parent
205775ac97
commit
6cebfa469d
9 changed files with 78 additions and 57 deletions
|
@ -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> => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue