Skip to content

Commit d4b2370

Browse files
authored
Merge branch 'main' into workspaces
2 parents fe7df06 + b964cb0 commit d4b2370

32 files changed

+1016
-26
lines changed

coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func New(options *Options) http.Handler {
3838
})
3939
})
4040
r.Post("/login", users.loginWithPassword)
41+
r.Post("/logout", users.logout)
4142
r.Route("/users", func(r chi.Router) {
4243
r.Post("/", users.createInitialUser)
4344

coderd/users.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,20 @@ func (users *users) loginWithPassword(rw http.ResponseWriter, r *http.Request) {
237237
})
238238
}
239239

240+
// Clear the user's session cookie
241+
func (*users) logout(rw http.ResponseWriter, r *http.Request) {
242+
// Get a blank token cookie
243+
cookie := &http.Cookie{
244+
// MaxAge < 0 means to delete the cookie now
245+
MaxAge: -1,
246+
Name: httpmw.AuthCookie,
247+
Path: "/",
248+
}
249+
250+
http.SetCookie(rw, cookie)
251+
render.Status(r, http.StatusOK)
252+
}
253+
240254
// Generates a new ID and secret for an API key.
241255
func generateAPIKeyIDSecret() (id string, secret string, err error) {
242256
// Length of an API Key ID.

coderd/users_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package coderd_test
22

33
import (
44
"context"
5+
"net/http"
56
"testing"
67

78
"github.com/stretchr/testify/require"
89

910
"github.com/coder/coder/coderd"
1011
"github.com/coder/coder/coderd/coderdtest"
12+
"github.com/coder/coder/httpmw"
1113
)
1214

1315
func TestUsers(t *testing.T) {
@@ -75,3 +77,30 @@ func TestUsers(t *testing.T) {
7577
require.Len(t, orgs, 1)
7678
})
7779
}
80+
81+
func TestLogout(t *testing.T) {
82+
t.Parallel()
83+
84+
t.Run("LogoutShouldClearCookie", func(t *testing.T) {
85+
t.Parallel()
86+
87+
server := coderdtest.New(t)
88+
fullURL, err := server.URL.Parse("/api/v2/logout")
89+
require.NoError(t, err, "Server URL should parse successfully")
90+
91+
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, fullURL.String(), nil)
92+
require.NoError(t, err, "/logout request construction should succeed")
93+
94+
httpClient := &http.Client{}
95+
96+
response, err := httpClient.Do(req)
97+
require.NoError(t, err, "/logout request should succeed")
98+
response.Body.Close()
99+
100+
cookies := response.Cookies()
101+
require.Len(t, cookies, 1, "Exactly one cookie should be returned")
102+
103+
require.Equal(t, cookies[0].Name, httpmw.AuthCookie, "Cookie should be the auth cookie")
104+
require.Equal(t, cookies[0].MaxAge, -1, "Cookie should be set to delete")
105+
})
106+
}

codersdk/users.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ func (c *Client) LoginWithPassword(ctx context.Context, req coderd.LoginWithPass
4444
return resp, nil
4545
}
4646

47+
// Logout calls the /logout API
48+
// Call `ClearSessionToken()` to clear the session token of the client.
49+
func (c *Client) Logout(ctx context.Context) error {
50+
// Since `LoginWithPassword` doesn't actually set a SessionToken
51+
// (it requires a call to SetSessionToken), this is essentially a no-op
52+
res, err := c.request(ctx, http.MethodPost, "/api/v2/logout", nil)
53+
if err != nil {
54+
return err
55+
}
56+
defer res.Body.Close()
57+
return nil
58+
}
59+
4760
// User returns a user for the ID provided.
4861
// If the ID string is empty, the current user will be returned.
4962
func (c *Client) User(ctx context.Context, id string) (coderd.User, error) {

codersdk/users_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,12 @@ func TestUsers(t *testing.T) {
4747
require.NoError(t, err)
4848
require.Len(t, orgs, 1)
4949
})
50+
51+
t.Run("LogoutIsSuccessful", func(t *testing.T) {
52+
t.Parallel()
53+
server := coderdtest.New(t)
54+
_ = server.RandomInitialUser(t)
55+
err := server.Client.Logout(context.Background())
56+
require.NoError(t, err)
57+
})
5058
}

site/api.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@ interface LoginResponse {
22
session_token: string
33
}
44

5+
// This must be kept in sync with the `Project` struct in the back-end
6+
export interface Project {
7+
id: string
8+
created_at: string
9+
updated_at: string
10+
organization_id: string
11+
name: string
12+
provisioner: string
13+
active_version_id: string
14+
}
15+
516
export const login = async (email: string, password: string): Promise<LoginResponse> => {
617
const response = await fetch("/api/v2/login", {
718
method: "POST",
@@ -21,3 +32,16 @@ export const login = async (email: string, password: string): Promise<LoginRespo
2132

2233
return body
2334
}
35+
36+
export const logout = async (): Promise<void> => {
37+
const response = await fetch("/api/v2/logout", {
38+
method: "POST",
39+
})
40+
41+
if (!response.ok) {
42+
const body = await response.json()
43+
throw new Error(body.message)
44+
}
45+
46+
return
47+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { render, screen } from "@testing-library/react"
2+
import React from "react"
3+
import { ErrorSummary } from "./index"
4+
5+
describe("ErrorSummary", () => {
6+
it("renders", async () => {
7+
// When
8+
const error = new Error("test error message")
9+
render(<ErrorSummary error={error} />)
10+
11+
// Then
12+
const element = await screen.findByText("test error message", { exact: false })
13+
expect(element).toBeDefined()
14+
})
15+
})
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import React from "react"
2+
3+
export interface ErrorSummaryProps {
4+
error: Error
5+
}
6+
7+
export const ErrorSummary: React.FC<ErrorSummaryProps> = ({ error }) => {
8+
// TODO: More interesting error page
9+
return <div>{error.toString()}</div>
10+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Button from "@material-ui/core/Button"
2+
import { lighten, makeStyles } from "@material-ui/core/styles"
3+
import React from "react"
4+
5+
export interface HeaderButtonProps {
6+
readonly text: string
7+
readonly disabled?: boolean
8+
readonly onClick?: (event: MouseEvent) => void
9+
}
10+
11+
export const HeaderButton: React.FC<HeaderButtonProps> = (props) => {
12+
const styles = useStyles()
13+
14+
return (
15+
<Button
16+
className={styles.pageButton}
17+
variant="contained"
18+
onClick={(event: React.MouseEvent): void => {
19+
if (props.onClick) {
20+
props.onClick(event.nativeEvent)
21+
}
22+
}}
23+
disabled={props.disabled}
24+
component="button"
25+
>
26+
{props.text}
27+
</Button>
28+
)
29+
}
30+
31+
const useStyles = makeStyles((theme) => ({
32+
pageButton: {
33+
whiteSpace: "nowrap",
34+
backgroundColor: lighten(theme.palette.hero.main, 0.1),
35+
color: "#B5BFD2",
36+
},
37+
}))

site/components/Header/index.test.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { screen } from "@testing-library/react"
2+
import { render } from "./../../test_helpers"
3+
import React from "react"
4+
import { Header } from "./index"
5+
6+
describe("Header", () => {
7+
it("renders title and subtitle", async () => {
8+
// When
9+
render(<Header title="Title Test" subTitle="Subtitle Test" />)
10+
11+
// Then
12+
const titleElement = await screen.findByText("Title Test")
13+
expect(titleElement).toBeDefined()
14+
15+
const subTitleElement = await screen.findByText("Subtitle Test")
16+
expect(subTitleElement).toBeDefined()
17+
})
18+
19+
it("renders button if specified", async () => {
20+
// When
21+
render(<Header title="Title" action={{ text: "Button Test" }} />)
22+
23+
// Then
24+
const buttonElement = await screen.findByRole("button")
25+
expect(buttonElement).toBeDefined()
26+
expect(buttonElement.textContent).toEqual("Button Test")
27+
})
28+
})

0 commit comments

Comments
 (0)