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 { 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<Resp
|
|||
/**
|
||||
* Try authenticating.
|
||||
*/
|
||||
export const authenticate = async (body?: AuthBody): Promise<void> => {
|
||||
export const authenticate = async (body?: AuthBody): Promise<LoginResponse> => {
|
||||
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<void> => {
|
|||
"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<FilesResponse> => {
|
||||
|
|
|
@ -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<AppProps> = (props) => {
|
||||
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>()
|
||||
|
||||
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<AppProps> = (props) => {
|
|||
undefined
|
||||
)}
|
||||
<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>
|
||||
) : (
|
||||
undefined
|
||||
|
|
|
@ -22,7 +22,6 @@ export interface ModalProps {
|
|||
enum Section {
|
||||
Browse,
|
||||
Home,
|
||||
Login,
|
||||
Open,
|
||||
Recent,
|
||||
}
|
||||
|
@ -103,7 +102,7 @@ export const Modal: React.FunctionComponent<ModalProps> = (props) => {
|
|||
|
||||
const content = (): React.ReactElement => {
|
||||
if (!props.authed) {
|
||||
return <Login />
|
||||
return <Login setApp={setApp} />
|
||||
}
|
||||
switch (section) {
|
||||
case Section.Recent:
|
||||
|
@ -112,8 +111,6 @@ export const Modal: React.FunctionComponent<ModalProps> = (props) => {
|
|||
return <Home app={props.app} />
|
||||
case Section.Browse:
|
||||
return <Browse />
|
||||
case Section.Login:
|
||||
return <Login />
|
||||
case Section.Open:
|
||||
return <Open app={props.app} setApp={setApp} />
|
||||
default:
|
||||
|
@ -140,9 +137,7 @@ export const Modal: React.FunctionComponent<ModalProps> = (props) => {
|
|||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button className="link" onClick={(): void => setSection(Section.Login)}>
|
||||
Login
|
||||
</button>
|
||||
undefined
|
||||
)}
|
||||
</nav>
|
||||
<div className="footer">
|
||||
|
|
|
@ -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<HomeProps> = (props) => {
|
||||
React.useEffect(() => {
|
||||
authenticate().catch(() => undefined)
|
||||
authenticate()
|
||||
.then(() => setAuthed(true))
|
||||
.catch(() => undefined)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
|
|
@ -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<LoginProps> = (props) => {
|
||||
const [password, setPassword] = React.useState<string>("")
|
||||
const [error, setError] = React.useState<HttpError>()
|
||||
|
||||
async function handleSubmit(event: React.FormEvent<HTMLFormElement>): Promise<void> {
|
||||
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 (
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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<HttpResponse<ApplicationsResponse>> {
|
||||
return {
|
||||
content: {
|
||||
applications: [
|
||||
{
|
||||
name: "VS Code",
|
||||
path: "/vscode",
|
||||
embedPath: "/vscode-embed",
|
||||
},
|
||||
],
|
||||
applications: [Vscode],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(<App options={options} />))
|
||||
return response
|
||||
return this.getRoot(route, options)
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
return undefined
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue