feat: Login via CLI (#298)

Fixes #210 - this isPR implements `coder login` in the case where the default user is already created.

This change adds:
- A prompt in the case where there is not an initial user that opens the server URL + requests a session token
  - This ports over some code from v1 for the `openURL` and `isWSL` functions to support opening the browser
- A `/api/v2/api-keys` endpoint that can be `POST`'d to in order to request a new api key for a user
  - This route was inspired by the v1 functionality
- A `cli-auth` route + page that shows the generated api key
- Tests for the new code + storybook for the new UI

The `/cli-auth` route, like in v1, is very minimal:

<img width="624" alt="Screen Shot 2022-02-16 at 5 05 07 PM" src="https://user-images.githubusercontent.com/88213859/154384627-78ab9841-27bf-490f-9bbe-23f8173c9e97.png">

And the terminal UX looks like this:

![2022-02-16 17 13 29](https://user-images.githubusercontent.com/88213859/154385225-509c78d7-840c-4cab-8f1e-074fede8f97e.gif)
This commit is contained in:
Bryan
2022-02-17 20:09:33 -08:00
committed by GitHub
parent 80c5c93d8a
commit 3f7781403d
14 changed files with 372 additions and 4 deletions
+13
View File
@@ -139,3 +139,16 @@ export const logout = async (): Promise<void> => {
return
}
export const getApiKey = async (): Promise<{ key: string }> => {
const response = await fetch("/api/v2/users/me/keys", {
method: "POST",
})
if (!response.ok) {
const body = await response.json()
throw new Error(body.message)
}
return await response.json()
}
@@ -0,0 +1,16 @@
import { Story } from "@storybook/react"
import React from "react"
import { CliAuthToken, CliAuthTokenProps } from "./CliAuthToken"
export default {
title: "SignIn/CliAuthToken",
component: CliAuthToken,
argTypes: {
sessionToken: { control: "text", defaultValue: "some-session-token" },
},
}
const Template: Story<CliAuthTokenProps> = (args) => <CliAuthToken {...args} />
export const Example = Template.bind({})
Example.args = {}
@@ -0,0 +1,16 @@
import React from "react"
import { screen } from "@testing-library/react"
import { render } from "../../test_helpers"
import { CliAuthToken } from "./CliAuthToken"
describe("CliAuthToken", () => {
it("renders content", async () => {
// When
render(<CliAuthToken sessionToken="test-token" />)
// Then
await screen.findByText("Session Token")
await screen.findByText("test-token")
})
})
+29
View File
@@ -0,0 +1,29 @@
import Paper from "@material-ui/core/Paper"
import Typography from "@material-ui/core/Typography"
import { makeStyles } from "@material-ui/core/styles"
import React from "react"
import { CodeExample } from "../CodeExample"
export interface CliAuthTokenProps {
sessionToken: string
}
export const CliAuthToken: React.FC<CliAuthTokenProps> = ({ sessionToken }) => {
const styles = useStyles()
return (
<Paper className={styles.container}>
<Typography className={styles.title}>Session Token</Typography>
<CodeExample code={sessionToken} />
</Paper>
)
}
const useStyles = makeStyles((theme) => ({
title: {
marginBottom: theme.spacing(2),
},
container: {
maxWidth: "680px",
padding: theme.spacing(2),
},
}))
+1
View File
@@ -1 +1,2 @@
export * from "./CliAuthToken"
export * from "./SignInForm"
+44
View File
@@ -0,0 +1,44 @@
import { makeStyles } from "@material-ui/core/styles"
import React, { useEffect, useState } from "react"
import { getApiKey } from "../api"
import { CliAuthToken } from "../components/SignIn"
import { FullScreenLoader } from "../components/Loader/FullScreenLoader"
import { useUser } from "../contexts/UserContext"
const CliAuthenticationPage: React.FC = () => {
const { me } = useUser(true)
const styles = useStyles()
const [apiKey, setApiKey] = useState<string | null>(null)
useEffect(() => {
if (me?.id) {
void getApiKey().then(({ key }) => {
setApiKey(key)
})
}
}, [me?.id])
if (!apiKey) {
return <FullScreenLoader />
}
return (
<div className={styles.root}>
<CliAuthToken sessionToken={apiKey} />
</div>
)
}
const useStyles = makeStyles(() => ({
root: {
width: "100vw",
height: "100vh",
display: "flex",
justifyContent: "center",
alignItems: "center",
},
}))
export default CliAuthenticationPage