Skip to content

Commit 6cebfa4

Browse files
committed
Generalize initial app logic
1 parent 205775a commit 6cebfa4

File tree

9 files changed

+78
-57
lines changed

9 files changed

+78
-57
lines changed

src/browser/api.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { getBasepath } from "hookrouter"
2-
import { Application, ApplicationsResponse, CreateSessionResponse, FilesResponse, RecentResponse } from "../common/api"
2+
import {
3+
Application,
4+
ApplicationsResponse,
5+
CreateSessionResponse,
6+
FilesResponse,
7+
LoginResponse,
8+
RecentResponse,
9+
} from "../common/api"
310
import { ApiEndpoint, HttpCode, HttpError } from "../common/http"
411

512
export interface AuthBody {
@@ -33,18 +40,15 @@ const tryRequest = async (endpoint: string, options?: RequestInit): Promise<Resp
3340
/**
3441
* Try authenticating.
3542
*/
36-
export const authenticate = async (body?: AuthBody): Promise<void> => {
43+
export const authenticate = async (body?: AuthBody): Promise<LoginResponse> => {
3744
const response = await tryRequest(ApiEndpoint.login, {
3845
method: "POST",
3946
body: JSON.stringify({ ...body, basePath: getBasepath() }),
4047
headers: {
4148
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
4249
},
4350
})
44-
const json = await response.json()
45-
if (json && json.success) {
46-
setAuthed(true)
47-
}
51+
return response.json()
4852
}
4953

5054
export const getFiles = async (): Promise<FilesResponse> => {

src/browser/app.tsx

+11-12
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,33 @@ export interface AppProps {
1010
options: Options
1111
}
1212

13+
interface RedirectedApplication extends Application {
14+
redirected?: boolean
15+
}
16+
17+
const origin = typeof window !== "undefined" ? window.location.origin + window.location.pathname : undefined
18+
1319
const App: React.FunctionComponent<AppProps> = (props) => {
1420
const [authed, setAuthed] = React.useState<boolean>(props.options.authed)
15-
const [app, setApp] = React.useState<Application | undefined>(props.options.app)
21+
const [app, setApp] = React.useState<RedirectedApplication | undefined>(props.options.app)
1622
const [error, setError] = React.useState<HttpError | Error | string>()
1723

1824
if (typeof window !== "undefined") {
19-
const url = new URL(window.location.origin + window.location.pathname + props.options.basePath)
25+
const url = new URL(origin + props.options.basePath)
2026
setBasepath(normalize(url.pathname))
2127

2228
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2329
;(window as any).setAuthed = (a: boolean): void => {
2430
if (authed !== a) {
2531
setAuthed(a)
26-
// TEMP: Remove when no longer auto-loading VS Code.
27-
if (a && !app) {
28-
setApp({
29-
name: "VS Code",
30-
path: "/",
31-
embedPath: "/vscode-embed",
32-
})
33-
}
3432
}
3533
}
3634
}
3735

3836
React.useEffect(() => {
39-
if (app && !isExecutableApplication(app)) {
37+
if (app && !isExecutableApplication(app) && !app.redirected) {
4038
navigate(normalize(`${getBasepath()}/${app.path}/`, true))
39+
setApp({ ...app, redirected: true })
4140
}
4241
}, [app])
4342

@@ -51,7 +50,7 @@ const App: React.FunctionComponent<AppProps> = (props) => {
5150
undefined
5251
)}
5352
<Modal app={app} setApp={setApp} authed={authed} error={error} setError={setError} />
54-
{authed && app && app.embedPath ? (
53+
{authed && app && app.embedPath && app.redirected ? (
5554
<iframe id="iframe" src={normalize(`./${app.embedPath}/`, true)}></iframe>
5655
) : (
5756
undefined

src/browser/components/modal.tsx

+2-7
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export interface ModalProps {
2222
enum Section {
2323
Browse,
2424
Home,
25-
Login,
2625
Open,
2726
Recent,
2827
}
@@ -103,7 +102,7 @@ export const Modal: React.FunctionComponent<ModalProps> = (props) => {
103102

104103
const content = (): React.ReactElement => {
105104
if (!props.authed) {
106-
return <Login />
105+
return <Login setApp={setApp} />
107106
}
108107
switch (section) {
109108
case Section.Recent:
@@ -112,8 +111,6 @@ export const Modal: React.FunctionComponent<ModalProps> = (props) => {
112111
return <Home app={props.app} />
113112
case Section.Browse:
114113
return <Browse />
115-
case Section.Login:
116-
return <Login />
117114
case Section.Open:
118115
return <Open app={props.app} setApp={setApp} />
119116
default:
@@ -140,9 +137,7 @@ export const Modal: React.FunctionComponent<ModalProps> = (props) => {
140137
</button>
141138
</>
142139
) : (
143-
<button className="link" onClick={(): void => setSection(Section.Login)}>
144-
Login
145-
</button>
140+
undefined
146141
)}
147142
</nav>
148143
<div className="footer">

src/browser/pages/home.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import * as React from "react"
22
import { Application } from "../../common/api"
3-
import { authenticate } from "../api"
3+
import { authenticate, setAuthed } from "../api"
44

55
export interface HomeProps {
66
app?: Application
77
}
88

99
export const Home: React.FunctionComponent<HomeProps> = (props) => {
1010
React.useEffect(() => {
11-
authenticate().catch(() => undefined)
11+
authenticate()
12+
.then(() => setAuthed(true))
13+
.catch(() => undefined)
1214
}, [])
1315

1416
return (

src/browser/pages/login.tsx

+18-4
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,36 @@
11
import * as React from "react"
2+
import { Application } from "../../common/api"
23
import { HttpError } from "../../common/http"
3-
import { authenticate } from "../api"
4+
import { authenticate, setAuthed } from "../api"
45
import { FieldError } from "../components/error"
56

7+
export interface LoginProps {
8+
setApp(app: Application): void
9+
}
10+
611
/**
712
* Login page. Will redirect on success.
813
*/
9-
export const Login: React.FunctionComponent = () => {
14+
export const Login: React.FunctionComponent<LoginProps> = (props) => {
1015
const [password, setPassword] = React.useState<string>("")
1116
const [error, setError] = React.useState<HttpError>()
1217

1318
async function handleSubmit(event: React.FormEvent<HTMLFormElement>): Promise<void> {
1419
event.preventDefault()
15-
authenticate({ password }).catch(setError)
20+
authenticate({ password })
21+
.then((response) => {
22+
if (response.app) {
23+
props.setApp(response.app)
24+
}
25+
setAuthed(true)
26+
})
27+
.catch(setError)
1628
}
1729

1830
React.useEffect(() => {
19-
authenticate().catch(() => undefined)
31+
authenticate()
32+
.then(() => setAuthed(true))
33+
.catch(() => undefined)
2034
}, [])
2135

2236
return (

src/common/api.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,15 @@ export enum SessionError {
2323
}
2424

2525
export interface LoginRequest {
26-
password: string
2726
basePath: string
27+
password: string
2828
}
2929

3030
export interface LoginResponse {
31+
/**
32+
* An application to load immediately after logging in.
33+
*/
34+
app?: Application
3135
success: boolean
3236
}
3337

src/node/api/server.ts

+10-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as http from "http"
33
import * as net from "net"
44
import * as ws from "ws"
55
import {
6+
Application,
67
ApplicationsResponse,
78
ClientMessage,
89
FilesResponse,
@@ -15,6 +16,12 @@ import { normalize } from "../../common/util"
1516
import { HttpProvider, HttpProviderOptions, HttpResponse, HttpServer, Route } from "../http"
1617
import { hash } from "../util"
1718

19+
export const Vscode: Application = {
20+
name: "VS Code",
21+
path: "/",
22+
embedPath: "./vscode-embed",
23+
}
24+
1825
/**
1926
* API HTTP provider.
2027
*/
@@ -104,6 +111,8 @@ export class ApiHttpProvider extends HttpProvider {
104111
return {
105112
content: {
106113
success: true,
114+
// TEMP: Auto-load VS Code.
115+
app: Vscode,
107116
},
108117
cookie:
109118
typeof password === "string"
@@ -149,13 +158,7 @@ export class ApiHttpProvider extends HttpProvider {
149158
private async applications(): Promise<HttpResponse<ApplicationsResponse>> {
150159
return {
151160
content: {
152-
applications: [
153-
{
154-
name: "VS Code",
155-
path: "/vscode",
156-
embedPath: "/vscode-embed",
157-
},
158-
],
161+
applications: [Vscode],
159162
},
160163
}
161164
}

src/node/app/server.tsx

+17-15
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as ReactDOMServer from "react-dom/server"
55
import App from "../../browser/app"
66
import { HttpCode, HttpError } from "../../common/http"
77
import { Options } from "../../common/util"
8+
import { Vscode } from "../api/server"
89
import { HttpProvider, HttpResponse, Route } from "../http"
910

1011
/**
@@ -21,39 +22,40 @@ export class MainHttpProvider extends HttpProvider {
2122
return response
2223
}
2324

25+
case "/vscode":
2426
case "/": {
2527
if (route.requestPath !== "/index.html") {
2628
throw new HttpError("Not found", HttpCode.NotFound)
2729
}
30+
2831
const options: Options = {
2932
authed: !!this.authenticated(request),
3033
basePath: this.base(route),
3134
logLevel: logger.level,
3235
}
3336

34-
if (options.authed) {
35-
// TEMP: Auto-load VS Code for now. In future versions we'll need to check
36-
// the URL for the appropriate application to load, if any.
37-
options.app = {
38-
name: "VS Code",
39-
path: "/",
40-
embedPath: "/vscode-embed",
41-
}
37+
// TODO: Load other apps based on the URL as well.
38+
if (route.base === Vscode.path && options.authed) {
39+
options.app = Vscode
4240
}
4341

44-
const response = await this.getUtf8Resource(this.rootPath, "src/browser/index.html")
45-
response.content = response.content
46-
.replace(/{{COMMIT}}/g, this.options.commit)
47-
.replace(/{{BASE}}/g, this.base(route))
48-
.replace(/"{{OPTIONS}}"/g, `'${JSON.stringify(options)}'`)
49-
.replace(/{{COMPONENT}}/g, ReactDOMServer.renderToString(<App options={options} />))
50-
return response
42+
return this.getRoot(route, options)
5143
}
5244
}
5345

5446
return undefined
5547
}
5648

49+
public async getRoot(route: Route, options: Options): Promise<HttpResponse> {
50+
const response = await this.getUtf8Resource(this.rootPath, "src/browser/index.html")
51+
response.content = response.content
52+
.replace(/{{COMMIT}}/g, this.options.commit)
53+
.replace(/{{BASE}}/g, this.base(route))
54+
.replace(/"{{OPTIONS}}"/g, `'${JSON.stringify(options)}'`)
55+
.replace(/{{COMPONENT}}/g, ReactDOMServer.renderToString(<App options={options} />))
56+
return response
57+
}
58+
5759
public async handleWebSocket(): Promise<undefined> {
5860
return undefined
5961
}

src/node/http.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@ interface ProviderRoute extends Route {
116116
}
117117

118118
export interface HttpProviderOptions {
119-
readonly base: string
120119
readonly auth: AuthType
121120
readonly password?: string
122121
readonly commit: string
@@ -154,7 +153,7 @@ export abstract class HttpProvider {
154153
* Get the base relative to the provided route.
155154
*/
156155
public base(route: Route): string {
157-
const depth = route.fullPath ? (route.fullPath.match(/\//g) || []).length : 1
156+
const depth = ((route.fullPath + "/").match(/\//g) || []).length
158157
return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : ""))
159158
}
160159

@@ -404,7 +403,6 @@ export class HttpServer {
404403
new provider(
405404
{
406405
auth: this.options.auth || AuthType.None,
407-
base: endpoint,
408406
commit: this.options.commit,
409407
password: this.options.password,
410408
},

0 commit comments

Comments
 (0)