Skip to content

Commit

Permalink
fix(merge): merge from remote
Browse files Browse the repository at this point in the history
  • Loading branch information
lewisblackburn committed Oct 27, 2024
1 parent 00f632c commit ec60894
Show file tree
Hide file tree
Showing 17 changed files with 25,718 additions and 25,441 deletions.
225 changes: 225 additions & 0 deletions '
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import {
json,
type LoaderFunctionArgs,
type HeadersFunction,
type LinksFunction,
type MetaFunction,
} from '@remix-run/node'
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useLoaderData,
} from '@remix-run/react'
import { withSentry } from '@sentry/remix'
import { HoneypotProvider } from 'remix-utils/honeypot/react'
// eslint-disable-next-line no-warning-comments
// FIXME: This and the file should be removed when sonnerStyles are fixed (https://github.com/epicweb-dev/epic-stack/issues/842)
import sonnerStyles from '#app/styles/sonner.css?url'
import appleTouchIconAssetUrl from './assets/favicons/apple-touch-icon.png'
import faviconAssetUrl from './assets/favicons/favicon.svg'
import Banner from './components/banner.tsx'
import { GeneralErrorBoundary } from './components/error-boundary.tsx'
import { EpicProgress } from './components/progress-bar.tsx'
import { useToast } from './components/toaster.tsx'
import { href as iconsHref } from './components/ui/icon.tsx'
import { EpicToaster } from './components/ui/sonner.tsx'
import {
ThemeSwitch,
useOptionalTheme,
useTheme,
} from './routes/resources+/theme-switch.tsx'
import tailwindStyleSheetUrl from './styles/tailwind.css?url'
import { getUserId, logout } from './utils/auth.server.ts'
import { ClientHintCheck, getHints } from './utils/client-hints.tsx'
import { prisma } from './utils/db.server.ts'
import { getEnv } from './utils/env.server.ts'
import { honeypot } from './utils/honeypot.server.ts'
import { combineHeaders, getDomainUrl } from './utils/misc.tsx'
import { useNonce } from './utils/nonce-provider.ts'
import { type Theme, getTheme } from './utils/theme.server.ts'
import { makeTimings, time } from './utils/timing.server.ts'
import { getToast } from './utils/toast.server.ts'

export const links: LinksFunction = () => {
return [
// Preload svg sprite as a resource to avoid render blocking
{ rel: 'preload', href: iconsHref, as: 'image' },
{
rel: 'icon',
href: '/favicon.ico',
sizes: '48x48',
},
{ rel: 'icon', type: 'image/svg+xml', href: faviconAssetUrl },
{ rel: 'apple-touch-icon', href: appleTouchIconAssetUrl },
{
rel: 'manifest',
href: '/site.webmanifest',
crossOrigin: 'use-credentials',
} as const, // necessary to make typescript happy
{ rel: 'stylesheet', href: tailwindStyleSheetUrl },
{ rel: 'stylesheet', href: sonnerStyles },
].filter(Boolean)
}

export const meta: MetaFunction<typeof loader> = ({ data }) => {
return [
{ title: data ? 'Metabase' : 'Error | Metabase' },
{ name: 'description', content: `Your own captain's log` },
]
}

export async function loader({ request }: LoaderFunctionArgs) {
const timings = makeTimings('root loader')
const userId = await time(() => getUserId(request), {
timings,
type: 'getUserId',
desc: 'getUserId in root',
})

const user = userId
? await time(
() =>
prisma.user.findUniqueOrThrow({
select: {
id: true,
name: true,
username: true,
image: { select: { id: true } },
initials: true,
roles: {
select: {
name: true,
permissions: {
select: { entity: true, action: true, access: true },
},
},
},
},
where: { id: userId },
}),
{ timings, type: 'find user', desc: 'find user in root' },
)
: null
if (userId && !user) {
console.info('something weird happened')
// something weird happened... The user is authenticated but we can't find
// them in the database. Maybe they were deleted? Let's log them out.
await logout({ request, redirectTo: '/' })
}
const { toast, headers: toastHeaders } = await getToast(request)
const honeyProps = honeypot.getInputProps()

return json(
{
user,
requestInfo: {
hints: getHints(request),
origin: getDomainUrl(request),
path: new URL(request.url).pathname,
userPrefs: {
theme: getTheme(request),
},
},
ENV: getEnv(),
toast,
honeyProps,
},
{
headers: combineHeaders(
{ 'Server-Timing': timings.toString() },
toastHeaders,
),
},
)
}

export const headers: HeadersFunction = ({ loaderHeaders }) => {
const headers = {
'Server-Timing': loaderHeaders.get('Server-Timing') ?? '',
}
return headers
}

function Document({
children,
nonce,
theme = 'light',
env = {},
}: {
children: React.ReactNode
nonce: string
theme?: Theme
env?: Record<string, string>
}) {
const allowIndexing = ENV.ALLOW_INDEXING !== 'false'
return (
<html lang="en" className={`${theme} h-full overflow-x-hidden`}>
<head>
<ClientHintCheck nonce={nonce} />
<Meta />
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
{allowIndexing ? null : (
<meta name="robots" content="noindex, nofollow" />
)}
<Links />
</head>
<body className="bg-background text-foreground">
{children}
<script
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `window.ENV = ${JSON.stringify(env)}`,
}}
/>
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
</body>
</html>
)
}

export function Layout({ children }: { children: React.ReactNode }) {
// if there was an error running the loader, data could be missing
const data = useLoaderData<typeof loader | null>()
const nonce = useNonce()
const theme = useOptionalTheme()
return (
<Document nonce={nonce} theme={theme} env={data?.ENV}>
{children}
</Document>
)
}

function App() {
const data = useLoaderData<typeof loader>()
const theme = useTheme()
useToast(data.toast)

return (
<>
<Banner />
<Outlet />
<EpicToaster closeButton position="top-center" theme={theme} />
<EpicProgress />
</>
)
}

function AppWithProviders() {
const data = useLoaderData<typeof loader>()
return (
<HoneypotProvider {...data.honeyProps}>
<App />
</HoneypotProvider>
)
}

export default withSentry(AppWithProviders)

// this is a last resort error boundary. There's not much useful information we
// can offer at this level.
export const ErrorBoundary = GeneralErrorBoundary
91 changes: 42 additions & 49 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ import { EpicProgress } from './components/progress-bar.tsx'
import { useToast } from './components/toaster.tsx'
import { href as iconsHref } from './components/ui/icon.tsx'
import { EpicToaster } from './components/ui/sonner.tsx'
import { useTheme } from './routes/resources+/theme-switch.tsx'
import {
ThemeSwitch,

Check warning on line 30 in app/root.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'ThemeSwitch' is defined but never used. Allowed unused vars must match /^ignored/u
useOptionalTheme,
useTheme,
} from './routes/resources+/theme-switch.tsx'
import tailwindStyleSheetUrl from './styles/tailwind.css?url'
import { getUserId, logout } from './utils/auth.server.ts'
import { ClientHintCheck, getHints } from './utils/client-hints.tsx'
Expand Down Expand Up @@ -77,27 +81,27 @@ export async function loader({ request }: LoaderFunctionArgs) {

const user = userId
? await time(
() =>
prisma.user.findUniqueOrThrow({
select: {
id: true,
name: true,
username: true,
image: { select: { id: true } },
initials: true,
roles: {
select: {
name: true,
permissions: {
select: { entity: true, action: true, access: true },
},
() =>
prisma.user.findUniqueOrThrow({
select: {
id: true,
name: true,
username: true,
image: { select: { id: true } },
initials: true,
roles: {
select: {
name: true,
permissions: {
select: { entity: true, action: true, access: true },
},
},
},
where: { id: userId },
}),
{ timings, type: 'find user', desc: 'find user in root' },
)
},
where: { id: userId },
}),
{ timings, type: 'find user', desc: 'find user in root' },
)
: null
if (userId && !user) {
console.info('something weird happened')
Expand Down Expand Up @@ -144,14 +148,13 @@ function Document({
nonce,
theme = 'light',
env = {},
allowIndexing = true,
}: {
children: React.ReactNode
nonce: string
theme?: Theme
env?: Record<string, string>
allowIndexing?: boolean
}) {
const allowIndexing = ENV.ALLOW_INDEXING !== 'false'
return (
<html lang="en" className={`${theme} h-full overflow-x-hidden`}>
<head>
Expand Down Expand Up @@ -179,25 +182,30 @@ function Document({
)
}

export function Layout({ children }: { children: React.ReactNode }) {
// if there was an error running the loader, data could be missing
const data = useLoaderData<typeof loader | null>()
const nonce = useNonce()
const theme = useOptionalTheme()
return (
<Document nonce={nonce} theme={theme} env={data?.ENV}>
{children}
</Document>
)
}

function App() {
const data = useLoaderData<typeof loader>()
const nonce = useNonce()
const theme = useTheme()
const allowIndexing = data.ENV.ALLOW_INDEXING !== 'false'
useToast(data.toast)

return (
<Document
nonce={nonce}
theme={theme}
allowIndexing={allowIndexing}
env={data.ENV}
>
<Outlet />
<>
<Banner />
<Outlet />
<EpicToaster closeButton position="top-center" theme={theme} />
<EpicProgress />
</Document>
</>
)
}

Expand All @@ -212,21 +220,6 @@ function AppWithProviders() {

export default withSentry(AppWithProviders)

export function ErrorBoundary() {
// the nonce doesn't rely on the loader so we can access that
const nonce = useNonce()

// NOTE: you cannot use useLoaderData in an ErrorBoundary because the loader
// likely failed to run so we have to do the best we can.
// We could probably do better than this (it's possible the loader did run).
// This would require a change in Remix.

// Just make sure your root route never errors out and you'll always be able
// to give the user a better UX.

return (
<Document nonce={nonce}>
<GeneralErrorBoundary />
</Document>
)
}
// this is a last resort error boundary. There's not much useful information we
// can offer at this level.
export const ErrorBoundary = GeneralErrorBoundary
2 changes: 1 addition & 1 deletion app/routes/_auth+/auth.$provider.callback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ test('if a user is not logged in, but the connection exists and they have enable
userId,
},
})
const { otp: _otp, ...config } = generateTOTP()
const { otp: _otp, ...config } = await generateTOTP()
await prisma.verification.create({
data: {
type: twoFAVerificationType,
Expand Down
6 changes: 3 additions & 3 deletions app/routes/_auth+/verify.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ export async function prepareVerification({
const verifyUrl = getRedirectToUrl({ request, type, target })
const redirectTo = new URL(verifyUrl.toString())

const { otp, ...verificationConfig } = generateTOTP({
algorithm: 'SHA256',
const { otp, ...verificationConfig } = await generateTOTP({
algorithm: 'SHA-256',
// Leaving off 0, O, and I on purpose to avoid confusing users.
charSet: 'ABCDEFGHJKLMNPQRSTUVWXYZ123456789',
period,
Expand Down Expand Up @@ -128,7 +128,7 @@ export async function isCodeValid({
select: { algorithm: true, secret: true, period: true, charSet: true },
})
if (!verification) return false
const result = verifyTOTP({
const result = await verifyTOTP({
otp: code,
...verification,
})
Expand Down
Loading

0 comments on commit ec60894

Please sign in to comment.