Skip to content

Commit edb182e

Browse files
authored
Merge branch 'main' into v3_singleFetch
2 parents 359f494 + 3507073 commit edb182e

19 files changed

+2616
-2882
lines changed

app/root.tsx

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ import {
3636
} from './components/ui/dropdown-menu.tsx'
3737
import { Icon, href as iconsHref } from './components/ui/icon.tsx'
3838
import { EpicToaster } from './components/ui/sonner.tsx'
39-
import { ThemeSwitch, useTheme } from './routes/resources+/theme-switch.tsx'
39+
import {
40+
ThemeSwitch,
41+
useOptionalTheme,
42+
useTheme,
43+
} from './routes/resources+/theme-switch.tsx'
4044
import tailwindStyleSheetUrl from './styles/tailwind.css?url'
4145
import { getUserId, logout } from './utils/auth.server.ts'
4246
import { ClientHintCheck, getHints } from './utils/client-hints.tsx'
@@ -153,14 +157,14 @@ function Document({
153157
nonce,
154158
theme = 'light',
155159
env = {},
156-
allowIndexing = true,
157160
}: {
158161
children: React.ReactNode
159162
nonce: string
160163
theme?: Theme
161164
env?: Record<string, string | undefined>
162165
allowIndexing?: boolean
163166
}) {
167+
const allowIndexing = ENV.ALLOW_INDEXING !== 'false'
164168
return (
165169
<html lang="en" className={`${theme} h-full overflow-x-hidden`}>
166170
<head>
@@ -188,24 +192,29 @@ function Document({
188192
)
189193
}
190194

195+
export function Layout({ children }: { children: React.ReactNode }) {
196+
// if there was an error running the loader, data could be missing
197+
const data = useLoaderData<typeof loader | null>()
198+
const nonce = useNonce()
199+
const theme = useOptionalTheme()
200+
return (
201+
<Document nonce={nonce} theme={theme} env={data?.ENV}>
202+
{children}
203+
</Document>
204+
)
205+
}
206+
191207
function App() {
192208
const data = useLoaderData<typeof loader>()
193-
const nonce = useNonce()
194209
const user = useOptionalUser()
195210
const theme = useTheme()
196211
const matches = useMatches()
197212
const isOnSearchPage = matches.find((m) => m.id === 'routes/users+/index')
198213
const searchBar = isOnSearchPage ? null : <SearchBar status="idle" />
199-
const allowIndexing = data.ENV.ALLOW_INDEXING !== 'false'
200214
useToast(data.toast)
201215

202216
return (
203-
<Document
204-
nonce={nonce}
205-
theme={theme}
206-
allowIndexing={allowIndexing}
207-
env={data.ENV}
208-
>
217+
<>
209218
<div className="flex h-screen flex-col justify-between">
210219
<header className="container py-6">
211220
<nav className="flex flex-wrap items-center justify-between gap-4 sm:flex-nowrap md:gap-8">
@@ -237,7 +246,7 @@ function App() {
237246
</div>
238247
<EpicToaster closeButton position="top-center" theme={theme} />
239248
<EpicProgress />
240-
</Document>
249+
</>
241250
)
242251
}
243252

@@ -326,21 +335,6 @@ function UserDropdown() {
326335
)
327336
}
328337

329-
export function ErrorBoundary() {
330-
// the nonce doesn't rely on the loader so we can access that
331-
const nonce = useNonce()
332-
333-
// NOTE: you cannot use useLoaderData in an ErrorBoundary because the loader
334-
// likely failed to run so we have to do the best we can.
335-
// We could probably do better than this (it's possible the loader did run).
336-
// This would require a change in Remix.
337-
338-
// Just make sure your root route never errors out and you'll always be able
339-
// to give the user a better UX.
340-
341-
return (
342-
<Document nonce={nonce}>
343-
<GeneralErrorBoundary />
344-
</Document>
345-
)
346-
}
338+
// this is a last resort error boundary. There's not much useful information we
339+
// can offer at this level.
340+
export const ErrorBoundary = GeneralErrorBoundary

app/routes/_auth+/auth.$provider.callback.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ test('if a user is not logged in, but the connection exists and they have enable
193193
userId,
194194
},
195195
})
196-
const { otp: _otp, ...config } = generateTOTP()
196+
const { otp: _otp, ...config } = await generateTOTP()
197197
await prisma.verification.create({
198198
data: {
199199
type: twoFAVerificationType,

app/routes/_auth+/verify.server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ export async function prepareVerification({
8787
const verifyUrl = getRedirectToUrl({ request, type, target })
8888
const redirectTo = new URL(verifyUrl.toString())
8989

90-
const { otp, ...verificationConfig } = generateTOTP({
91-
algorithm: 'SHA256',
90+
const { otp, ...verificationConfig } = await generateTOTP({
91+
algorithm: 'SHA-256',
9292
// Leaving off 0, O, and I on purpose to avoid confusing users.
9393
charSet: 'ABCDEFGHJKLMNPQRSTUVWXYZ123456789',
9494
period,
@@ -128,7 +128,7 @@ export async function isCodeValid({
128128
select: { algorithm: true, secret: true, period: true, charSet: true },
129129
})
130130
if (!verification) return false
131-
const result = verifyTOTP({
131+
const result = await verifyTOTP({
132132
otp: code,
133133
...verification,
134134
})

app/routes/resources+/theme-switch.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ import { redirect, useFetcher, useFetchers } from '@remix-run/react'
66
import { ServerOnly } from 'remix-utils/server-only'
77
import { z } from 'zod'
88
import { Icon } from '#app/components/ui/icon.tsx'
9-
import { useHints } from '#app/utils/client-hints.tsx'
10-
import { useRequestInfo } from '#app/utils/request-info.ts'
9+
import { useHints, useOptionalHints } from '#app/utils/client-hints.tsx'
10+
import {
11+
useOptionalRequestInfo,
12+
useRequestInfo,
13+
} from '#app/utils/request-info.ts'
1114
import { type Theme, setTheme } from '#app/utils/theme.server.ts'
1215

1316
const ThemeFormSchema = z.object({
@@ -129,3 +132,13 @@ export function useTheme() {
129132
}
130133
return requestInfo.userPrefs.theme ?? hints.theme
131134
}
135+
136+
export function useOptionalTheme() {
137+
const optionalHints = useOptionalHints()
138+
const optionalRequestInfo = useOptionalRequestInfo()
139+
const optimisticMode = useOptimisticThemeMode()
140+
if (optimisticMode) {
141+
return optimisticMode === 'system' ? optionalHints?.theme : optimisticMode
142+
}
143+
return optionalRequestInfo?.userPrefs.theme ?? optionalHints?.theme
144+
}

app/routes/settings+/profile.two-factor.index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
2828

2929
export async function action({ request }: ActionFunctionArgs) {
3030
const userId = await requireUserId(request)
31-
const { otp: _otp, ...config } = generateTOTP()
31+
const { otp: _otp, ...config } = await generateTOTP()
3232
const verificationData = {
3333
...config,
3434
type: twoFAVerifyVerificationType,

app/routes/settings+/profile.two-factor.verify.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
6969
const issuer = new URL(getDomainUrl(request)).host
7070
const otpUri = getTOTPAuthUri({
7171
...verification,
72+
// OTP clients break with the `-` in the algorithm name.
73+
algorithm: verification.algorithm.replaceAll('-', ''),
7274
accountName: user.email,
7375
issuer,
7476
})

app/utils/client-hints.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import { clientHint as timeZoneHint } from '@epic-web/client-hints/time-zone'
1111
import { useRevalidator } from '@remix-run/react'
1212
import * as React from 'react'
13-
import { useRequestInfo } from './request-info.ts'
13+
import { useOptionalRequestInfo, useRequestInfo } from './request-info.ts'
1414

1515
const hintsUtils = getHintUtils({
1616
theme: colorSchemeHint,
@@ -28,6 +28,11 @@ export function useHints() {
2828
return requestInfo.hints
2929
}
3030

31+
export function useOptionalHints() {
32+
const requestInfo = useOptionalRequestInfo()
33+
return requestInfo?.hints
34+
}
35+
3136
/**
3237
* @returns inline script element that checks for client hints and sets cookies
3338
* if they are not set then reloads the page if any cookie was set to an

app/utils/providers/github.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class GitHubProvider implements AuthProvider {
3838
throw new Error('Email not found')
3939
}
4040
const username = profile.displayName
41-
const imageUrl = profile.photos[0].value
41+
const imageUrl = profile.photos[0]?.value
4242
return {
4343
email,
4444
id: profile.id,

app/utils/request-info.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@ import { useRouteLoaderData } from '@remix-run/react'
33
import { type loader as rootLoader } from '#app/root.tsx'
44

55
/**
6-
* @returns the request info from the root loader
6+
* @returns the request info from the root loader (throws an error if it does not exist)
77
*/
88
export function useRequestInfo() {
9+
const maybeRequestInfo = useOptionalRequestInfo()
10+
invariant(maybeRequestInfo, 'No requestInfo found in root loader')
11+
12+
return maybeRequestInfo
13+
}
14+
15+
export function useOptionalRequestInfo() {
916
const data = useRouteLoaderData<typeof rootLoader>('root')
10-
invariant(data?.requestInfo, 'No requestInfo found in root loader')
1117

12-
return data.requestInfo
18+
return data?.requestInfo
1319
}

docs/database.md

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -215,34 +215,6 @@ with more data without performing a migration, then it's a bit more involved.
215215

216216
## Backups
217217

218-
### LiteFS Cloud Backups
219-
220-
LiteFS Cloud is a service offered by Fly.io for managing backup and restore
221-
functionality.
222-
223-
This is the simplest method for backing up your database.
224-
225-
It offers the ability to restore your database to any point in time in the last
226-
30 days, with 5 minute granularity.
227-
228-
Fly.io has some great documentation on how to set this up:
229-
230-
- [Pricing](https://fly.io/docs/about/pricing/#litefs-cloud)
231-
- [LiteFS Cloud Setup](https://fly.io/docs/litefs/cloud-backups/)
232-
- [Restoring DB with LiteFS Cloud](https://fly.io/docs/litefs/cloud-restore/)
233-
- [Disaster Recovery with LiteFS Cloud](https://fly.io/docs/litefs/disaster-recovery/)
234-
235-
The following is a summary of the steps to set up LiteFS Cloud:
236-
237-
1. Create a LiteFS Cloud cluster in your Fly.io dashboard:
238-
https://fly.io/dashboard/personal/litefs
239-
- Take note of the auth token, you'll need it in the next step
240-
1. Set the `LITEFS_CLOUD_TOKEN` to the token from your dashboard:
241-
```sh
242-
fly secrets set LITEFS_CLOUD_TOKEN="LITEFS_CLOUD_TOKEN_HERE" --app [YOUR_APP_NAME]
243-
```
244-
1. You should now be able to restore backups from the LiteFS dashboard.
245-
246218
### Manual DB Backups
247219

248220
Manual DB backups can be taken/restored using `litefs` commands:

0 commit comments

Comments
 (0)