Skip to content

Commit 2595156

Browse files
committed
Add frontend
1 parent 53c2bf4 commit 2595156

File tree

6 files changed

+105
-10
lines changed

6 files changed

+105
-10
lines changed

site/src/api/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ export const getUser = async (): Promise<Types.UserResponse> => {
6565
return response.data
6666
}
6767

68+
export const getAuthMethods = async (): Promise<Types.AuthMethods> => {
69+
const response = await axios.get<Types.AuthMethods>("/api/v2/users/authmethods")
70+
return response.data
71+
}
72+
6873
export const getApiKey = async (): Promise<Types.APIKeyResponse> => {
6974
const response = await axios.post<Types.APIKeyResponse>("/api/v2/users/me/keys")
7075
return response.data

site/src/api/types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ export interface UserResponse {
1818
readonly name: string
1919
}
2020

21+
export interface AuthMethods {
22+
readonly password: boolean
23+
readonly github: boolean
24+
}
25+
2126
/**
2227
* `Organization` must be kept in sync with the go struct in organizations.go
2328
*/

site/src/components/SignIn/SignInForm.tsx

+19-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import Button from "@material-ui/core/Button"
12
import FormHelperText from "@material-ui/core/FormHelperText"
3+
import Link from "@material-ui/core/Link"
24
import { makeStyles } from "@material-ui/core/styles"
35
import TextField from "@material-ui/core/TextField"
46
import { FormikContextType, useFormik } from "formik"
57
import React from "react"
68
import * as Yup from "yup"
9+
import { AuthMethods } from "../../api/types"
10+
import { LoadingButton } from "../Button"
711
import { getFormHelpers, onChangeTrimmed } from "../Form"
8-
import { LoadingButton } from "./../Button"
912
import { Welcome } from "./Welcome"
1013

1114
/**
@@ -24,7 +27,8 @@ export const Language = {
2427
emailInvalid: "Please enter a valid email address.",
2528
emailRequired: "Please enter an email address.",
2629
authErrorMessage: "Incorrect email or password.",
27-
signIn: "Sign In",
30+
basicSignIn: "Sign In",
31+
githubSignIn: "GitHub",
2832
}
2933

3034
const validationSchema = Yup.object({
@@ -49,10 +53,11 @@ const useStyles = makeStyles((theme) => ({
4953
export interface SignInFormProps {
5054
isLoading: boolean
5155
authErrorMessage?: string
56+
authMethods?: AuthMethods
5257
onSubmit: ({ email, password }: { email: string; password: string }) => Promise<void>
5358
}
5459

55-
export const SignInForm: React.FC<SignInFormProps> = ({ isLoading, authErrorMessage, onSubmit }) => {
60+
export const SignInForm: React.FC<SignInFormProps> = ({ authMethods, isLoading, authErrorMessage, onSubmit }) => {
5661
const styles = useStyles()
5762

5863
const form: FormikContextType<BuiltInAuthFormValues> = useFormik<BuiltInAuthFormValues>({
@@ -76,6 +81,7 @@ export const SignInForm: React.FC<SignInFormProps> = ({ isLoading, authErrorMess
7681
className={styles.loginTextField}
7782
fullWidth
7883
label={Language.emailLabel}
84+
type="email"
7985
variant="outlined"
8086
/>
8187
<TextField
@@ -91,10 +97,19 @@ export const SignInForm: React.FC<SignInFormProps> = ({ isLoading, authErrorMess
9197
{authErrorMessage && <FormHelperText error>{Language.authErrorMessage}</FormHelperText>}
9298
<div className={styles.submitBtn}>
9399
<LoadingButton color="primary" loading={isLoading} fullWidth type="submit" variant="contained">
94-
{isLoading ? "" : Language.signIn}
100+
{isLoading ? "" : Language.basicSignIn}
95101
</LoadingButton>
96102
</div>
97103
</form>
104+
{authMethods?.github && (
105+
<div className={styles.submitBtn}>
106+
<Link href="/api/v2/users/oauth2/github/callback">
107+
<Button color="primary" disabled={isLoading} fullWidth type="submit" variant="contained">
108+
{Language.githubSignIn}
109+
</Button>
110+
</Link>
111+
</div>
112+
)}
98113
</>
99114
)
100115
}

site/src/pages/login.test.tsx

+29-2
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,23 @@ describe("SignInPage", () => {
1616
return res(ctx.status(401), ctx.json({ message: "no user here" }))
1717
}),
1818
)
19+
// only leave password auth enabled by default
20+
server.use(
21+
rest.get("/api/v2/users/auth", (req, res, ctx) => {
22+
return res(ctx.status(200), ctx.json({
23+
password: true,
24+
github: false,
25+
}))
26+
})
27+
)
1928
})
2029

2130
it("renders the sign-in form", async () => {
2231
// When
2332
render(<SignInPage />)
2433

2534
// Then
26-
await screen.findByText(Language.signIn)
35+
await screen.findByText(Language.basicSignIn)
2736
})
2837

2938
it("shows an error message if SignIn fails", async () => {
@@ -42,12 +51,30 @@ describe("SignInPage", () => {
4251
await userEvent.type(email, "test@coder.com")
4352
await userEvent.type(password, "password")
4453
// Click sign-in
45-
const signInButton = await screen.findByText(Language.signIn)
54+
const signInButton = await screen.findByText(Language.basicSignIn)
4655
act(() => signInButton.click())
4756

4857
// Then
4958
const errorMessage = await screen.findByText(Language.authErrorMessage)
5059
expect(errorMessage).toBeDefined()
5160
expect(history.location.pathname).toEqual("/login")
5261
})
62+
63+
it("shows github authentication when enabled", async () => {
64+
// Given
65+
server.use(
66+
rest.get("/api/v2/users/auth", async (req, res, ctx) => {
67+
return res(ctx.status(200), ctx.json({
68+
password: true,
69+
github: true,
70+
}))
71+
}),
72+
)
73+
74+
// When
75+
render(<SignInPage />)
76+
77+
// Then
78+
await screen.findByText(Language.githubSignIn)
79+
})
5380
})

site/src/pages/login.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const SignInPage: React.FC = () => {
4747
<div className={styles.root}>
4848
<div className={styles.layout}>
4949
<div className={styles.container}>
50-
<SignInForm isLoading={isLoading} authErrorMessage={authErrorMessage} onSubmit={onSubmit} />
50+
<SignInForm authMethods={authState.context.methods} isLoading={isLoading} authErrorMessage={authErrorMessage} onSubmit={onSubmit} />
5151
</div>
5252

5353
<Footer />

site/src/xServices/auth/authXService.ts

+46-3
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ import { displaySuccess } from "../../components/Snackbar"
66
export const Language = {
77
successProfileUpdate: "Updated preferences.",
88
}
9+
910
export interface AuthContext {
1011
getUserError?: Error | unknown
12+
getMethodsError?: Error | unknown
1113
authError?: Error | unknown
1214
updateProfileError?: Error | unknown
1315
me?: Types.UserResponse
16+
methods?: Types.AuthMethods
1417
}
1518

1619
export type AuthEvent =
@@ -19,10 +22,17 @@ export type AuthEvent =
1922
| { type: "UPDATE_PROFILE"; data: Types.UpdateProfileRequest }
2023

2124
export const authMachine =
22-
/** @xstate-layout N4IgpgJg5mDOIC5QEMCuAXAFgZXc9YAdLAJZQB2kA8hgMTYCSA4gHID6DLioADgPal0JPuW4gAHogCMABgCcAFkLyZAVilS5GgEwAOAMwA2ADQgAnogDsa5YcOXD2y7oWH92-QF9PptFlz4RKQUJORQDOS0ECJEoQBufADWQWTkEWL8gsKiSBKI7kraqpZSuh7aUvpy6vqmFghlcoT6qgb6GsUlctrevhg4eATEqaHhkWAAThN8E4Q8ADb4AGYzALbDFOm5mSRCImKSCLKKynJqGlpSekZ1iIaltvaOzq4FvSB+A4GEMOhCYQBVWCTKIxQjxJJEX4AWTAGQEu2yB0QrikhG0Mn0+l0ulUWJkhlatXMKKqynkCn0lOscnsUnenwCQ1+-ygQJBk2mswWyzWPzA6Fh8Ky+1yh202iacip2I8hjU9m0twQ1lUzSk9hkHlUCjU2kMDP6TJSFEgETm0yWJHmsQgNtoAIACgARACCABUAKJsR0AJSoADEGAAZT3CxGi0CHGTKmSG-yDE2UCDmniW61EVA8CD4UaO9P26KUcHkBLJQiMxMbZOpguZ7O5sL5vhWm0ICEAY1zIgA2jIALrhvY5KPSKSWNWKWQeaUKSXj5UY3SEUrdKqExy08fxr5DYI18gWlsZwhZnOs5utsC0TkzOaLdArCbrSvffdmw9p48208Ni919tSz4Lthz7QdtgRYdkQaLRCF0SxtAUcdpQnRxdGVXV9EIKdFBkGRdDkSxFGKHdjWrD96GYdgqABd0hyRMVpCxdFaSxVQZEsIxx0sSxlUI9Eql1DUJQnRQ5FIqt91GGh0FBYsIXLfcZPoyM8gQdwsIQuRdEMSlSm1DxlR1Zc1AIhDdJwrwfA+I1JJGMIZJvKY7x5R8+SUjAVJHNSijVdwCIUdQriJfReJJBAihkZpLAUJDikigwJW8azyD4CA4DEV891SahPIgkVvMOdRDEINxqTUbFCW05UJywhQDFUNd2gxQxxOsrKk1GLZeEghjRyOfUoqkPEOOlQj2mVVrtDgjUEPYkpcT0CTvhZUZ2QmLzoLnZVdA1OCiS0TjcU0VRluy00U0-OtwTtIhUs9ZyNvyiNCrHHi4InPCFE4wktQwvbQtiloWu0jFLDOpMPyPK8bp-W8np6groI0d74PYmRvqMdilXC1wSvYxRYsIwbrHB9rbLfHLLuhk8SFuzbGKOYasLRr6fux5UZWi2KCclXarL6BNKYu2tv3rc88zrBn+pxTTGsqULp2xKRlRO0qCJnDdJXuMnBd3SHqa-K9pbUgBaRDlVNzjyTwjpOM+1QDXJoXzoPE3DlN+rLakVwba1dpvvYjQIeraS8sRl7oPcQgicQicihxGQfaM9jlFaWdSgJDR6Wd-X3cQZdY8DhPdCThRLY8dUOPsBwDE4wjdGSzwgA */
25+
/** @xstate-layout N4IgpgJg5mDOIC5QEMCuAXAFgZXc9YAdLAJZQB2kA8hgMTYCSA4gHID6DLioADgPal0JPuW4gAHogDsABgCshOQA4AzABY5ARgBMUgJxrtcgDQgAnok0zNSwkpkrNKvQDY3aqS6kBfb6bRYuPhEpBQk5FAM5LQQIkThAG58ANYhZORRYvyCwqJIEohyMlKE2mrFKo7aLq5SJuaILtq2mjZqrboy2s4+fiABOHgExOnhkdFgAE6TfJOEPAA2+ABmswC2IxSZ+dkkQiJikgiyCsrqWroGRqYWCHoq2oQyDpouXa1qGmq+-hiDwYQYOghBEAKqwKYxOKERIpIhAgCyYCyAj2uUOiF0mieTik3UqMhqSleN0QhgUOhUbzUKhk9jKch+-T+QWGQJBUHBkKmMzmixW60BYHQSJROQO+SOUmxVKUniUcjkUgVLjlpOOCqecj0LyKmmVeiUTIGrPhwo5SKwfAgsChlBh5CSqSFIuFmGt8B2qP2eVARxUeNKCo02k0cllTRc6qUalsCtU731DikvV+gSGZuBY0t7pttB5s3mS3Qq0mG0Rbo9YrREr9iHUMkIUnKHSklQVKnqtz0BkImjUbmeShcVmejL6Jozm0oECi8xmyxIC3iEGXtFBAAUACIAQQAKgBRNgbgBKVAAYgwADIH6s+jEIOV6Qgj1oxnTBkfq7qNlxyT4aC4saaPcVLGiyU6hDOc48AuS5EKgPAQPgYwbnBa6xPasLOpOAJQZAMHoQhSEoREaF8Iuy4ILCADGKEiAA2jIAC6d7opKiBPi+rRtB+-5fg0CDqM+YafG8+jWLI2jgemeHpAR5DzhR8GEIhyEcuRlFgPm0yFvyJaCrhwz4bOimwcpy6qSRGlEdRjp8HRPpMaxXrir6BSPvo3Fvu0zT8Zo6oDmoTb-p8cjaFcsjqDJ-zGfJpn0Mw7BUKCe5sbWHm6CU2jWKGnhaFSeLqv2jZKMOehOE49i0v+MWmtOYw0OgdrxPZzpQU16XuUcAC0-aPGGSgVQOTS4ko2jfjGhC0gB1WeDIBguHVkGjBETU6byRYCmW06da5NbdZiKalLl+p-k4XgTYJNhxuVNKGCB77fEy5DWnAYhGWkFDUBgXUPoqLiKOoxIqGVejNKoxXPCUMgjZ8zj6sORoThBclhBE2y8N67F1o+xIvhoejavqEX-gFgl6IGFWOC2bzWNqy0AuyYxcpMf0cQghjqn+JT6GJeJynIqrI2msWZhalY2uzuN9dojwpn+epDvqHjRkUL7KBDEljiLzKyXF32mUpWkwquRCvQeuls-t94c606s8c0A5C00UjqqDja5cUXQRXiDiMwb0FmURpuWQW1tY25D7242jsxn+bi6IFhh2K4qjKP+fsqAHX1B8bKkkGb0seR0gNx87idu4JtIwzoxTdELzbheOov1SZhEWcR6moURxdHCOgPhsBoNDRDKju84hCfHS4ZAXPqg59OCn58ufeFODQPD2DY-FUqGsASmdShgvKP67nClrwgQu2EPIPb2V4+CVNf7w5TOphs22en2LDVrb9Ns4w8j1coU8iafDKJVWQagDDqmOsOKBVhXDgweNJb+ppL59TcH2ZQw1E5jSurcYK8CHCODfE0B4vRfBAA */
2326
createMachine(
2427
{
25-
context: { me: undefined, getUserError: undefined, authError: undefined, updateProfileError: undefined },
28+
context: {
29+
me: undefined,
30+
getUserError: undefined,
31+
authError: undefined,
32+
updateProfileError: undefined,
33+
methods: undefined,
34+
getMethodsError: undefined,
35+
},
2636
tsTypes: {} as import("./authXService.typegen").Typegen0,
2737
schema: {
2838
context: {} as AuthContext,
@@ -31,6 +41,9 @@ export const authMachine =
3141
getMe: {
3242
data: Types.UserResponse
3343
}
44+
getMethods: {
45+
data: Types.AuthMethods
46+
}
3447
signIn: {
3548
data: Types.LoginResponse
3649
}
@@ -81,6 +94,25 @@ export const authMachine =
8194
onError: [
8295
{
8396
actions: "assignGetUserError",
97+
target: "gettingMethods",
98+
},
99+
],
100+
},
101+
tags: "loading",
102+
},
103+
gettingMethods: {
104+
invoke: {
105+
src: "getMethods",
106+
id: "getMethods",
107+
onDone: [
108+
{
109+
actions: ["assignMethods", "clearGetMethodsError"],
110+
target: "signedOut",
111+
},
112+
],
113+
onError: [
114+
{
115+
actions: "assignGetMethodsError",
84116
target: "signedOut",
85117
},
86118
],
@@ -139,7 +171,7 @@ export const authMachine =
139171
onDone: [
140172
{
141173
actions: ["unassignMe", "clearAuthError"],
142-
target: "signedOut",
174+
target: "gettingMethods",
143175
},
144176
],
145177
onError: [
@@ -160,6 +192,7 @@ export const authMachine =
160192
},
161193
signOut: API.logout,
162194
getMe: API.getUser,
195+
getMethods: API.getAuthMethods,
163196
updateProfile: async (context, event) => {
164197
if (!context.me) {
165198
throw new Error("No current user found")
@@ -176,6 +209,16 @@ export const authMachine =
176209
...context,
177210
me: undefined,
178211
})),
212+
assignMethods: assign({
213+
methods: (_, event) => event.data,
214+
}),
215+
assignGetMethodsError: assign({
216+
getMethodsError: (_, event) => event.data,
217+
}),
218+
clearGetMethodsError: assign((context: AuthContext) => ({
219+
...context,
220+
getMethodsError: undefined,
221+
})),
179222
assignGetUserError: assign({
180223
getUserError: (_, event) => event.data,
181224
}),

0 commit comments

Comments
 (0)