Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(minor): minor fixes #10

Merged
merged 15 commits into from
Jun 10, 2024
Merged
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## Refresh Token Rotation Demo

### Development

1. Add a new file `.env.local`

```sh
VITE_AUTH_URL=https://auth-api.app/api/authorize
VITE_TOKEN_URL=https://auth-api.app/api/oauth/token
VITE_BASE_URL=http://localhost:5173
```

2. Install packages

```sh
$ npm install
```

3. Run development server, default to http://localhost:5173

```sh
$ npm run dev
```

### Test

```sh
$ npm run test
$ npm run test:coverage # display coverage
```
18 changes: 8 additions & 10 deletions lib/__tests__/cookie.test.js → lib/__tests__/cookie.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, test, expect, beforeAll, afterAll, vi, afterEach } from 'vite
import { saveCookie, getCookie, deleteCookie, clearAllCookies } from '../cookie'

describe('cookie', () => {
let mockCookie = []
let mockCookie: string[] = []
beforeAll(() => {
vi.stubGlobal('document', {
get cookie() {
Expand All @@ -24,10 +24,6 @@ describe('cookie', () => {
})

describe('saveCookie', () => {
test('throws error on empty name', () => {
expect(() => saveCookie()).toThrowError('Cookie name is required')
})

test('saves empty value', () => {
saveCookie('a', '')
expect(document.cookie).toMatch('a=;')
Expand Down Expand Up @@ -90,11 +86,13 @@ describe('cookie', () => {

describe('clearAllCookies', () => {
test('clears all cookies', () => {
saveCookie('a', '12345')
saveCookie('b', '54321')
clearAllCookies()
expect(document.cookie).toMatch('a=;')
expect(document.cookie).toMatch('b=;')
saveCookie('state-a', '12345')
saveCookie('state-b', '54321')
saveCookie('c', '54321')
clearAllCookies("state-")
expect(document.cookie).toMatch('state-a=;')
expect(document.cookie).toMatch('state-b=;')
expect(document.cookie).toMatch('c=54321;')
})
})
})
File renamed without changes.
10 changes: 6 additions & 4 deletions lib/cookie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ export const deleteCookie = (name: string) => {
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT`
}

export const clearAllCookies = () => {
export const clearAllCookies = (prefix: string) => {
const cookies = document.cookie.split(";")
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i]
const eqPos = cookie.indexOf("=")
const name = eqPos > -1 ? cookie.substring(0, eqPos) : cookie
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT`
if (cookie.trim().startsWith(prefix)) {
const eqPos = cookie.indexOf("=")
const name = eqPos > -1 ? cookie.substring(0, eqPos) : cookie
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT`
}
}
}
33 changes: 30 additions & 3 deletions src/App.module.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,35 @@
.app {
}

.app-loginBtn {
position: absolute;
top: 1rem;
right: 1rem;
}

.debug-table {
width: 80vw;
text-align: left;

font-family: monospace;
line-height: 1.5;
}
.debug-table tr:nth-of-type(odd) {
background-color: lightgreen;

@media screen and (prefers-color-scheme: dark) {
background-color: green;
}
}
.debug-table td {
padding: 1rem;
}

.debug-itemName {
font-weight: bold;
vertical-align: baseline;
padding-right: 1rem;
width: 20%;
}
.debug-itemValue {
vertical-align: baseline;
word-break: break-all;
width: 80%;
}
16 changes: 0 additions & 16 deletions src/App.test.tsx

This file was deleted.

45 changes: 19 additions & 26 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect } from 'react'

import AuthContext from '@/contexts/AuthContext'
import { getPKCEStatus } from '@/utils/auth'
import { getAuthStage } from '@/utils/authStage'
import { clearSearchParams, getSearchParams } from '@/utils/route'
import { deleteStateCookie } from '@/utils/stateCookie'
import { AuthStage } from '@/types'
Expand All @@ -12,8 +13,6 @@ import useAuthContextValue from '@/hooks/useAuthContextValue'
import useGetAccessToken from '@/hooks/useGetAccessToken'

import s from './App.module.css'
import LogoutButton from './components/LogoutButton'
import { getAuthStage } from './utils/authStage'

function App() {
const { state, code } = getSearchParams()
Expand All @@ -39,7 +38,7 @@ function App() {
} else if (authStage.stage === AuthStage.BEFORE_AUTH_CODE) {
getATWithAuthCode(authStage.state, authStage.code, authStage.codeVerifier)
}
// once on mount only
// on stage change only
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [authStage.stage])

Expand All @@ -49,34 +48,28 @@ function App() {
state,
code,
codeVerifier,
cookie: document.cookie || null,
localStorage: JSON.stringify(localStorage, null, 2)
}
const isLoggedIn = authStage.stage === AuthStage.LOGGED_IN
|| authStage.stage === AuthStage.AFTER_AUTH_CODE

return (
<AuthContext.Provider value={authContext}>
<main className={s['app']}>
{
isLoggedIn ? <LogoutButton className={s['app-loginBtn']} />
: <LoginButton className={s['app-loginBtn']} />
}
<h1>Stage: {authStage.stage}</h1>
<pre>
{!isLoading && <>
<table>
<tbody>
{Object.entries(statuses).filter(([, v]) => !!v).map(([key, value]) => (
<tr key={key}>
<td className='debug-itemName'>{key}</td>
<td className="debug-longText">{`${value}`}</td>
</tr>
))}
</tbody>
</table>
</>
}
{error && <div className='debug-error'>{error}</div>}
</pre>
<LoginButton className={s['app-loginBtn']} />
<h1>{authStage.stage}</h1>
{error && <div className="error">{error}</div>}
{!isLoading && (
<table className={s['debug-table']}>
<tbody>
{Object.entries(statuses).map(([key, value]) => (
<tr key={key}>
<td className={s['debug-itemName']}>{key}</td>
<td className={s['debug-itemValue']}>{`${value ?? "<empty>"}`}</td>
</tr>
))}
</tbody>
</table>)}
{isLoading && <p>Loading...</p>}
</main>
</AuthContext.Provider>
)
Expand Down
115 changes: 115 additions & 0 deletions src/__snapshots__/App.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`App > renders with snapshot 1`] = `
<div>
<main
class="app"
>
<div
class="app-loginBtn"
>
<span
class="error"
/>
<button
type="submit"
>
Login
</button>
</div>
<h1>
Logged Out
</h1>
<table
class="debug-table"
>
<tbody>
<tr>
<td
class="debug-itemName"
>
accessToken
</td>
<td
class="debug-itemValue"
>
&lt;empty&gt;
</td>
</tr>
<tr>
<td
class="debug-itemName"
>
refreshToken
</td>
<td
class="debug-itemValue"
>
&lt;empty&gt;
</td>
</tr>
<tr>
<td
class="debug-itemName"
>
state
</td>
<td
class="debug-itemValue"
>
&lt;empty&gt;
</td>
</tr>
<tr>
<td
class="debug-itemName"
>
code
</td>
<td
class="debug-itemValue"
>
&lt;empty&gt;
</td>
</tr>
<tr>
<td
class="debug-itemName"
>
codeVerifier
</td>
<td
class="debug-itemValue"
>
&lt;empty&gt;
</td>
</tr>
<tr>
<td
class="debug-itemName"
>
cookie
</td>
<td
class="debug-itemValue"
>
&lt;empty&gt;
</td>
</tr>
<tr>
<td
class="debug-itemName"
>
localStorage
</td>
<td
class="debug-itemValue"
>
{}
</td>
</tr>
</tbody>
</table>
</main>
</div>
`;
19 changes: 19 additions & 0 deletions src/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

import { test, expect, describe } from 'vitest'
import { render } from '@testing-library/react'
import App from '../App'

test('runs vitest', () => {
expect(1).toBe(1)
})

describe('App', () => {
test('renders with snapshot', () => {
const screen = render(<App />)
expect(screen.container.children[0]).toMatchSnapshot()
})
})




Loading