diff --git a/.gitignore b/.gitignore
index 26bf1f5..c94b87b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@ workspace/
**/public/build
**/playwright-report
data.db
+test.db
/playground
**/tsconfig.tsbuildinfo
**/*.tsbuildinfo
diff --git a/exercises/01.basics/01.problem.install-and-configure/package.json b/exercises/01.basics/01.problem.install-and-configure/package.json
deleted file mode 100644
index d2c7f32..0000000
--- a/exercises/01.basics/01.problem.install-and-configure/package.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "name": "exercises_01.basics_01.problem.install-and-configure"
-}
diff --git a/exercises/01.basics/01.solution.install-and-configure/package.json b/exercises/01.basics/01.solution.install-and-configure/package.json
deleted file mode 100644
index 5fa9319..0000000
--- a/exercises/01.basics/01.solution.install-and-configure/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "exercises_01.basics_01.solution.install-and-configure",
- "scripts": {
- "test:e2e": "playwright test"
- },
- "devDependencies": {
- "@playwright/test": "^1.53.2"
- }
-}
diff --git a/exercises/01.basics/README.mdx b/exercises/01.basics/README.mdx
deleted file mode 100644
index a5e602c..0000000
--- a/exercises/01.basics/README.mdx
+++ /dev/null
@@ -1 +0,0 @@
-# Basics
\ No newline at end of file
diff --git a/exercises/01.basics/01.problem.install-and-configure/README.mdx b/exercises/01.fundamentals/01.problem.install-and-configure/README.mdx
similarity index 100%
rename from exercises/01.basics/01.problem.install-and-configure/README.mdx
rename to exercises/01.fundamentals/01.problem.install-and-configure/README.mdx
diff --git a/exercises/01.fundamentals/01.problem.install-and-configure/package.json b/exercises/01.fundamentals/01.problem.install-and-configure/package.json
new file mode 100644
index 0000000..a00a70d
--- /dev/null
+++ b/exercises/01.fundamentals/01.problem.install-and-configure/package.json
@@ -0,0 +1,3 @@
+{
+ "name": "exercises_01.fundamentals_01.problem.install-and-configure"
+}
diff --git a/exercises/01.basics/01.problem.install-and-configure/tsconfig.json b/exercises/01.fundamentals/01.problem.install-and-configure/tsconfig.json
similarity index 100%
rename from exercises/01.basics/01.problem.install-and-configure/tsconfig.json
rename to exercises/01.fundamentals/01.problem.install-and-configure/tsconfig.json
diff --git a/exercises/03.guides/01.solution.recording-interactions/README.mdx b/exercises/01.fundamentals/01.solution.install-and-configure/README.mdx
similarity index 56%
rename from exercises/03.guides/01.solution.recording-interactions/README.mdx
rename to exercises/01.fundamentals/01.solution.install-and-configure/README.mdx
index 5a6da63..ec449c1 100644
--- a/exercises/03.guides/01.solution.recording-interactions/README.mdx
+++ b/exercises/01.fundamentals/01.solution.install-and-configure/README.mdx
@@ -1,4 +1,6 @@
-# Recording interactions
+# Install & configure
+
+- Mention the explicit `workers` value in `playwright.config.ts`.
Good job! 👏
diff --git a/exercises/01.fundamentals/01.solution.install-and-configure/package.json b/exercises/01.fundamentals/01.solution.install-and-configure/package.json
new file mode 100644
index 0000000..4a374e4
--- /dev/null
+++ b/exercises/01.fundamentals/01.solution.install-and-configure/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "exercises_01.fundamentals_01.solution.install-and-configure",
+ "scripts": {
+ "test:e2e": "playwright test"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.57.0"
+ }
+}
diff --git a/exercises/01.basics/01.solution.install-and-configure/playwright.config.ts b/exercises/01.fundamentals/01.solution.install-and-configure/playwright.config.ts
similarity index 85%
rename from exercises/01.basics/01.solution.install-and-configure/playwright.config.ts
rename to exercises/01.fundamentals/01.solution.install-and-configure/playwright.config.ts
index 0399057..7f796f7 100644
--- a/exercises/01.basics/01.solution.install-and-configure/playwright.config.ts
+++ b/exercises/01.fundamentals/01.solution.install-and-configure/playwright.config.ts
@@ -11,4 +11,5 @@ export default defineConfig({
],
fullyParallel: true,
forbidOnly: !!process.env.CI,
+ workers: process.env.CI ? 1 : undefined,
})
diff --git a/exercises/01.basics/01.solution.install-and-configure/tests/epicweb.test.ts b/exercises/01.fundamentals/01.solution.install-and-configure/tests/epicweb.test.ts
similarity index 100%
rename from exercises/01.basics/01.solution.install-and-configure/tests/epicweb.test.ts
rename to exercises/01.fundamentals/01.solution.install-and-configure/tests/epicweb.test.ts
diff --git a/exercises/01.basics/01.solution.install-and-configure/tsconfig.json b/exercises/01.fundamentals/01.solution.install-and-configure/tsconfig.json
similarity index 100%
rename from exercises/01.basics/01.solution.install-and-configure/tsconfig.json
rename to exercises/01.fundamentals/01.solution.install-and-configure/tsconfig.json
diff --git a/exercises/01.basics/02.problem.running-the-app/.env b/exercises/01.fundamentals/02.problem.running-the-app/.env
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/.env
rename to exercises/01.fundamentals/02.problem.running-the-app/.env
diff --git a/exercises/01.basics/02.problem.running-the-app/.env.example b/exercises/01.fundamentals/02.problem.running-the-app/.env.example
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/.env.example
rename to exercises/01.fundamentals/02.problem.running-the-app/.env.example
diff --git a/exercises/01.basics/02.problem.running-the-app/.github/PULL_REQUEST_TEMPLATE.md b/exercises/01.fundamentals/02.problem.running-the-app/.github/PULL_REQUEST_TEMPLATE.md
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/.github/PULL_REQUEST_TEMPLATE.md
rename to exercises/01.fundamentals/02.problem.running-the-app/.github/PULL_REQUEST_TEMPLATE.md
diff --git a/exercises/01.basics/02.problem.running-the-app/.github/workflows/deploy.yml b/exercises/01.fundamentals/02.problem.running-the-app/.github/workflows/deploy.yml
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/.github/workflows/deploy.yml
rename to exercises/01.fundamentals/02.problem.running-the-app/.github/workflows/deploy.yml
diff --git a/exercises/01.basics/02.problem.running-the-app/.gitignore b/exercises/01.fundamentals/02.problem.running-the-app/.gitignore
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/.gitignore
rename to exercises/01.fundamentals/02.problem.running-the-app/.gitignore
diff --git a/exercises/01.basics/02.problem.running-the-app/.npmrc b/exercises/01.fundamentals/02.problem.running-the-app/.npmrc
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/.npmrc
rename to exercises/01.fundamentals/02.problem.running-the-app/.npmrc
diff --git a/exercises/01.basics/02.problem.running-the-app/.prettierignore b/exercises/01.fundamentals/02.problem.running-the-app/.prettierignore
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/.prettierignore
rename to exercises/01.fundamentals/02.problem.running-the-app/.prettierignore
diff --git a/exercises/01.basics/02.problem.running-the-app/.vscode/extensions.json b/exercises/01.fundamentals/02.problem.running-the-app/.vscode/extensions.json
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/.vscode/extensions.json
rename to exercises/01.fundamentals/02.problem.running-the-app/.vscode/extensions.json
diff --git a/exercises/01.basics/02.problem.running-the-app/.vscode/remix.code-snippets b/exercises/01.fundamentals/02.problem.running-the-app/.vscode/remix.code-snippets
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/.vscode/remix.code-snippets
rename to exercises/01.fundamentals/02.problem.running-the-app/.vscode/remix.code-snippets
diff --git a/exercises/01.basics/02.problem.running-the-app/.vscode/settings.json b/exercises/01.fundamentals/02.problem.running-the-app/.vscode/settings.json
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/.vscode/settings.json
rename to exercises/01.fundamentals/02.problem.running-the-app/.vscode/settings.json
diff --git a/exercises/01.basics/02.problem.running-the-app/README.mdx b/exercises/01.fundamentals/02.problem.running-the-app/README.mdx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/README.mdx
rename to exercises/01.fundamentals/02.problem.running-the-app/README.mdx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/assets/favicons/apple-touch-icon.png b/exercises/01.fundamentals/02.problem.running-the-app/app/assets/favicons/apple-touch-icon.png
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/assets/favicons/apple-touch-icon.png
rename to exercises/01.fundamentals/02.problem.running-the-app/app/assets/favicons/apple-touch-icon.png
diff --git a/exercises/01.basics/02.problem.running-the-app/app/assets/favicons/favicon.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/assets/favicons/favicon.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/assets/favicons/favicon.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/assets/favicons/favicon.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/error-boundary.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/error-boundary.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/error-boundary.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/error-boundary.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/floating-toolbar.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/floating-toolbar.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/floating-toolbar.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/floating-toolbar.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/forms.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/forms.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/forms.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/forms.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/progress-bar.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/progress-bar.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/progress-bar.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/progress-bar.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/search-bar.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/search-bar.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/search-bar.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/search-bar.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/spacer.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/spacer.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/spacer.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/spacer.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/toaster.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/toaster.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/toaster.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/toaster.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/ui/README.md b/exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/README.md
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/ui/README.md
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/README.md
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/ui/button.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/button.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/ui/button.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/button.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/ui/checkbox.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/checkbox.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/ui/checkbox.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/checkbox.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/ui/dropdown-menu.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/dropdown-menu.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/ui/dropdown-menu.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/dropdown-menu.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/ui/icon.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/icon.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/ui/icon.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/icon.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/ui/input-otp.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/input-otp.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/ui/input-otp.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/input-otp.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/ui/input.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/input.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/ui/input.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/input.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/ui/label.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/label.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/ui/label.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/label.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/ui/sonner.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/sonner.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/ui/sonner.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/sonner.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/ui/status-button.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/status-button.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/ui/status-button.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/status-button.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/ui/textarea.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/textarea.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/ui/textarea.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/textarea.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/ui/tooltip.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/tooltip.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/ui/tooltip.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/ui/tooltip.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/components/user-dropdown.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/components/user-dropdown.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/components/user-dropdown.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/components/user-dropdown.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/entry.client.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/entry.client.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/entry.client.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/entry.client.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/entry.server.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/entry.server.tsx
similarity index 99%
rename from exercises/01.basics/02.problem.running-the-app/app/entry.server.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/entry.server.tsx
index 99fdd4b..8d8b1de 100644
--- a/exercises/01.basics/02.problem.running-the-app/app/entry.server.tsx
+++ b/exercises/01.fundamentals/02.problem.running-the-app/app/entry.server.tsx
@@ -88,6 +88,7 @@ export default async function handleRequest(...args: DocRequestArgs) {
},
},
},
+ xFrameOptions: false,
})
resolve(
diff --git a/exercises/01.basics/02.problem.running-the-app/app/root.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/root.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/root.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/root.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes.ts b/exercises/01.fundamentals/02.problem.running-the-app/app/routes.ts
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes.ts
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes.ts
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/$.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/$.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/$.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/$.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/auth.$provider.callback.test.ts b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/auth.$provider.callback.test.ts
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/auth.$provider.callback.test.ts
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/auth.$provider.callback.test.ts
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/auth.$provider.callback.ts b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/auth.$provider.callback.ts
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/auth.$provider.callback.ts
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/auth.$provider.callback.ts
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/auth_.$provider.ts b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/auth_.$provider.ts
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/auth_.$provider.ts
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/auth_.$provider.ts
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/forgot-password.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/forgot-password.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/forgot-password.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/forgot-password.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/login.server.ts b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/login.server.ts
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/login.server.ts
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/login.server.ts
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/login.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/login.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/login.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/login.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/logout.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/logout.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/logout.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/logout.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/onboarding.server.ts b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/onboarding.server.ts
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/onboarding.server.ts
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/onboarding.server.ts
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/onboarding.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/onboarding.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/onboarding.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/onboarding.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/onboarding_.$provider.server.ts b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/onboarding_.$provider.server.ts
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/onboarding_.$provider.server.ts
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/onboarding_.$provider.server.ts
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/onboarding_.$provider.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/onboarding_.$provider.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/onboarding_.$provider.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/onboarding_.$provider.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/reset-password.server.ts b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/reset-password.server.ts
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/reset-password.server.ts
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/reset-password.server.ts
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/reset-password.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/reset-password.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/reset-password.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/reset-password.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/signup.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/signup.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/signup.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/signup.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/verify.server.ts b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/verify.server.ts
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/verify.server.ts
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/verify.server.ts
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/verify.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/verify.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/verify.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/verify.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/webauthn+/authentication.ts b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/webauthn+/authentication.ts
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/webauthn+/authentication.ts
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/webauthn+/authentication.ts
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/webauthn+/registration.ts b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/webauthn+/registration.ts
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/webauthn+/registration.ts
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/webauthn+/registration.ts
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/webauthn+/utils.server.ts b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/webauthn+/utils.server.ts
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_auth+/webauthn+/utils.server.ts
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_auth+/webauthn+/utils.server.ts
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/about.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/about.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/about.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/about.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/index.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/index.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/index.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/index.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/docker.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/docker.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/docker.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/docker.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/eslint.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/eslint.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/eslint.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/eslint.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/faker.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/faker.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/faker.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/faker.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/fly.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/fly.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/fly.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/fly.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/github.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/github.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/github.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/github.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/logos.ts b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/logos.ts
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/logos.ts
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/logos.ts
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/msw.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/msw.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/msw.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/msw.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/playwright.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/playwright.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/playwright.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/playwright.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/prettier.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/prettier.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/prettier.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/prettier.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/prisma.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/prisma.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/prisma.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/prisma.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/radix.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/radix.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/radix.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/radix.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/react-email.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/react-email.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/react-email.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/react-email.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/remix.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/remix.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/remix.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/remix.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/resend.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/resend.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/resend.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/resend.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/sentry.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/sentry.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/sentry.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/sentry.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/shadcn-ui.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/shadcn-ui.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/shadcn-ui.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/shadcn-ui.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/sqlite.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/sqlite.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/sqlite.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/sqlite.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/stars.jpg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/stars.jpg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/stars.jpg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/stars.jpg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/tailwind.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/tailwind.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/tailwind.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/tailwind.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/testing-library.png b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/testing-library.png
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/testing-library.png
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/testing-library.png
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/typescript.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/typescript.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/typescript.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/typescript.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/vitest.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/vitest.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/vitest.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/vitest.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/zod.svg b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/zod.svg
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/logos/zod.svg
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/logos/zod.svg
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/privacy.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/privacy.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/privacy.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/privacy.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/support.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/support.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/support.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/support.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/tos.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/tos.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_marketing+/tos.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_marketing+/tos.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_seo+/robots[.]txt.ts b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_seo+/robots[.]txt.ts
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_seo+/robots[.]txt.ts
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_seo+/robots[.]txt.ts
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/_seo+/sitemap[.]xml.ts b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/_seo+/sitemap[.]xml.ts
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/_seo+/sitemap[.]xml.ts
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/_seo+/sitemap[.]xml.ts
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/admin+/cache.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/admin+/cache.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/admin+/cache.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/admin+/cache.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/admin+/cache_.lru.$cacheKey.ts b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/admin+/cache_.lru.$cacheKey.ts
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/admin+/cache_.lru.$cacheKey.ts
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/admin+/cache_.lru.$cacheKey.ts
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/admin+/cache_.sqlite.$cacheKey.ts b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/admin+/cache_.sqlite.$cacheKey.ts
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/admin+/cache_.sqlite.$cacheKey.ts
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/admin+/cache_.sqlite.$cacheKey.ts
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/admin+/cache_.sqlite.server.ts b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/admin+/cache_.sqlite.server.ts
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/admin+/cache_.sqlite.server.ts
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/admin+/cache_.sqlite.server.ts
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/admin+/cache_.sqlite.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/admin+/cache_.sqlite.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/admin+/cache_.sqlite.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/admin+/cache_.sqlite.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/me.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/me.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/me.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/me.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/resources+/download-user-data.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/resources+/download-user-data.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/resources+/download-user-data.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/resources+/download-user-data.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/resources+/healthcheck.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/resources+/healthcheck.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/resources+/healthcheck.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/resources+/healthcheck.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/resources+/images.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/resources+/images.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/resources+/images.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/resources+/images.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/resources+/theme-switch.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/resources+/theme-switch.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/resources+/theme-switch.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/resources+/theme-switch.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.change-email.server.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.change-email.server.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.change-email.server.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.change-email.server.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.change-email.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.change-email.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.change-email.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.change-email.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.connections.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.connections.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.connections.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.connections.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.index.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.index.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.index.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.index.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.passkeys.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.passkeys.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.passkeys.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.passkeys.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.password.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.password.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.password.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.password.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.password_.create.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.password_.create.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.password_.create.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.password_.create.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.photo.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.photo.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.photo.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.photo.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.two-factor.disable.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.two-factor.disable.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.two-factor.disable.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.two-factor.disable.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.two-factor.index.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.two-factor.index.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.two-factor.index.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.two-factor.index.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.two-factor.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.two-factor.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.two-factor.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.two-factor.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.two-factor.verify.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.two-factor.verify.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/settings+/profile.two-factor.verify.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/settings+/profile.two-factor.verify.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/users+/$username.test.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username.test.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/users+/$username.test.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username.test.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/users+/$username.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/users+/$username.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/users+/$username_+/__note-editor.server.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username_+/__note-editor.server.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/users+/$username_+/__note-editor.server.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username_+/__note-editor.server.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/users+/$username_+/__note-editor.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username_+/__note-editor.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/users+/$username_+/__note-editor.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username_+/__note-editor.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/users+/$username_+/notes.$noteId.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username_+/notes.$noteId.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/users+/$username_+/notes.$noteId.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username_+/notes.$noteId.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/users+/$username_+/notes.$noteId_.edit.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/users+/$username_+/notes.index.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username_+/notes.index.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/users+/$username_+/notes.index.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username_+/notes.index.tsx
diff --git a/exercises/01.basics/02.problem.running-the-app/app/routes/users+/$username_+/notes.new.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username_+/notes.new.tsx
similarity index 100%
rename from exercises/01.basics/02.problem.running-the-app/app/routes/users+/$username_+/notes.new.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username_+/notes.new.tsx
diff --git a/exercises/02.test-setup/01.problem.custom-fixtures/app/routes/users+/$username_+/notes.tsx b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username_+/notes.tsx
similarity index 96%
rename from exercises/02.test-setup/01.problem.custom-fixtures/app/routes/users+/$username_+/notes.tsx
rename to exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username_+/notes.tsx
index 90e7b3c..ded41ca 100644
--- a/exercises/02.test-setup/01.problem.custom-fixtures/app/routes/users+/$username_+/notes.tsx
+++ b/exercises/01.fundamentals/02.problem.running-the-app/app/routes/users+/$username_+/notes.tsx
@@ -51,7 +51,10 @@ export default function NotesRoute({ loaderData }: Route.ComponentProps) {
{ownerDisplayName}'s Notes
-
+
{isOwner ? (
-
+
{isOwner ? (
-
+
{isOwner ? (
{
const githubUser = await insertGitHubUser()
const email = githubUser.primaryEmail.toLowerCase()
- const { userId } = await setupUser({ ...createUser(), email })
+ const { userId } = await setupUser({ ...generateUserInfo(), email })
const request = await setupRequest({ code: githubUser.code })
const response = await loader({ request, params: PARAMS, context: {} })
@@ -141,7 +141,7 @@ test('gives an error if the account is already connected to another user', async
const githubUser = await insertGitHubUser()
await prisma.user.create({
data: {
- ...createUser(),
+ ...generateUserInfo(),
connections: {
create: {
providerName: GITHUB_PROVIDER_NAME,
@@ -245,7 +245,7 @@ async function setupRequest({
return request
}
-async function setupUser(userData = createUser()) {
+async function setupUser(userData = generateUserInfo()) {
const session = await prisma.session.create({
data: {
expirationDate: getSessionExpirationDate(),
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/auth.$provider.callback.ts b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/auth.$provider.callback.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/auth.$provider.callback.ts
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/auth.$provider.callback.ts
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/auth_.$provider.ts b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/auth_.$provider.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/auth_.$provider.ts
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/auth_.$provider.ts
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/forgot-password.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/forgot-password.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/forgot-password.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/forgot-password.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/login.server.ts b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/login.server.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/login.server.ts
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/login.server.ts
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/login.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/login.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/login.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/login.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/logout.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/logout.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/logout.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/logout.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/onboarding.server.ts b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/onboarding.server.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/onboarding.server.ts
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/onboarding.server.ts
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/onboarding.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/onboarding.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/onboarding.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/onboarding.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/onboarding_.$provider.server.ts b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/onboarding_.$provider.server.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/onboarding_.$provider.server.ts
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/onboarding_.$provider.server.ts
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/onboarding_.$provider.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/onboarding_.$provider.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/onboarding_.$provider.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/onboarding_.$provider.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/reset-password.server.ts b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/reset-password.server.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/reset-password.server.ts
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/reset-password.server.ts
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/reset-password.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/reset-password.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/reset-password.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/reset-password.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/signup.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/signup.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/signup.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/signup.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/verify.server.ts b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/verify.server.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/verify.server.ts
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/verify.server.ts
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/verify.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/verify.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/verify.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/verify.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/webauthn+/authentication.ts b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/webauthn+/authentication.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/webauthn+/authentication.ts
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/webauthn+/authentication.ts
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/webauthn+/registration.ts b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/webauthn+/registration.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/webauthn+/registration.ts
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/webauthn+/registration.ts
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/webauthn+/utils.server.ts b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/webauthn+/utils.server.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/webauthn+/utils.server.ts
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_auth+/webauthn+/utils.server.ts
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/about.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/about.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/about.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/about.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/index.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/index.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/index.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/index.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/docker.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/docker.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/docker.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/docker.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/eslint.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/eslint.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/eslint.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/eslint.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/faker.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/faker.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/faker.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/faker.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/fly.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/fly.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/fly.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/fly.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/github.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/github.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/github.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/github.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/logos.ts b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/logos.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/logos.ts
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/logos.ts
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/msw.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/msw.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/msw.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/msw.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/playwright.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/playwright.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/playwright.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/playwright.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/prettier.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/prettier.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/prettier.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/prettier.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/prisma.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/prisma.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/prisma.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/prisma.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/radix.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/radix.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/radix.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/radix.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/react-email.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/react-email.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/react-email.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/react-email.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/remix.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/remix.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/remix.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/remix.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/resend.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/resend.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/resend.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/resend.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/sentry.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/sentry.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/sentry.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/sentry.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/shadcn-ui.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/shadcn-ui.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/shadcn-ui.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/shadcn-ui.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/sqlite.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/sqlite.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/sqlite.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/sqlite.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/stars.jpg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/stars.jpg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/stars.jpg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/stars.jpg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/tailwind.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/tailwind.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/tailwind.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/tailwind.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/testing-library.png b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/testing-library.png
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/testing-library.png
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/testing-library.png
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/typescript.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/typescript.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/typescript.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/typescript.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/vitest.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/vitest.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/vitest.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/vitest.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/zod.svg b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/zod.svg
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/logos/zod.svg
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/logos/zod.svg
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/privacy.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/privacy.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/privacy.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/privacy.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/support.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/support.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/support.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/support.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/tos.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/tos.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_marketing+/tos.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_marketing+/tos.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_seo+/robots[.]txt.ts b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_seo+/robots[.]txt.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_seo+/robots[.]txt.ts
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_seo+/robots[.]txt.ts
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_seo+/sitemap[.]xml.ts b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_seo+/sitemap[.]xml.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_seo+/sitemap[.]xml.ts
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/_seo+/sitemap[.]xml.ts
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/admin+/cache.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/admin+/cache.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/admin+/cache.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/admin+/cache.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/admin+/cache_.lru.$cacheKey.ts b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/admin+/cache_.lru.$cacheKey.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/admin+/cache_.lru.$cacheKey.ts
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/admin+/cache_.lru.$cacheKey.ts
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/admin+/cache_.sqlite.$cacheKey.ts b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/admin+/cache_.sqlite.$cacheKey.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/admin+/cache_.sqlite.$cacheKey.ts
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/admin+/cache_.sqlite.$cacheKey.ts
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/admin+/cache_.sqlite.server.ts b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/admin+/cache_.sqlite.server.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/admin+/cache_.sqlite.server.ts
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/admin+/cache_.sqlite.server.ts
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/admin+/cache_.sqlite.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/admin+/cache_.sqlite.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/admin+/cache_.sqlite.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/admin+/cache_.sqlite.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/me.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/me.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/me.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/me.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/resources+/download-user-data.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/resources+/download-user-data.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/resources+/download-user-data.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/resources+/download-user-data.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/resources+/healthcheck.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/resources+/healthcheck.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/resources+/healthcheck.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/resources+/healthcheck.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/resources+/images.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/resources+/images.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/resources+/images.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/resources+/images.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/resources+/theme-switch.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/resources+/theme-switch.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/resources+/theme-switch.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/resources+/theme-switch.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.change-email.server.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.change-email.server.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.change-email.server.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.change-email.server.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.change-email.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.change-email.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.change-email.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.change-email.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.connections.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.connections.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.connections.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.connections.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.index.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.index.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.index.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.index.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.passkeys.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.passkeys.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.passkeys.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.passkeys.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.password.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.password.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.password.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.password.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.password_.create.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.password_.create.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.password_.create.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.password_.create.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.photo.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.photo.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.photo.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.photo.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.two-factor.disable.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.two-factor.disable.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.two-factor.disable.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.two-factor.disable.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.two-factor.index.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.two-factor.index.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.two-factor.index.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.two-factor.index.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.two-factor.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.two-factor.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.two-factor.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.two-factor.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.two-factor.verify.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.two-factor.verify.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/settings+/profile.two-factor.verify.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/settings+/profile.two-factor.verify.tsx
diff --git a/exercises/03.guides/02.solution.test-annotations/app/routes/users+/$username.test.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username.test.tsx
similarity index 93%
rename from exercises/03.guides/02.solution.test-annotations/app/routes/users+/$username.test.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username.test.tsx
index b0784bd..0884a24 100644
--- a/exercises/03.guides/02.solution.test-annotations/app/routes/users+/$username.test.tsx
+++ b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username.test.tsx
@@ -10,7 +10,7 @@ import { loader as rootLoader } from '#app/root.tsx'
import { getSessionExpirationDate, sessionKey } from '#app/utils/auth.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { authSessionStorage } from '#app/utils/session.server.ts'
-import { createUser, getUserImages } from '#tests/db-utils.ts'
+import { generateUserInfo, getUserImages } from '#tests/db-utils.ts'
import { default as UsernameRoute, loader } from './$username.tsx'
test('The user profile when not logged in as self', async () => {
@@ -19,7 +19,7 @@ test('The user profile when not logged in as self', async () => {
userImages[faker.number.int({ min: 0, max: userImages.length - 1 })]
const user = await prisma.user.create({
select: { id: true, username: true, name: true },
- data: { ...createUser(), image: { create: userImage } },
+ data: { ...generateUserInfo(), image: { create: userImage } },
})
const App = createRoutesStub([
{
@@ -44,7 +44,7 @@ test('The user profile when logged in as self', async () => {
userImages[faker.number.int({ min: 0, max: userImages.length - 1 })]
const user = await prisma.user.create({
select: { id: true, username: true, name: true },
- data: { ...createUser(), image: { create: userImage } },
+ data: { ...generateUserInfo(), image: { create: userImage } },
})
const session = await prisma.session.create({
select: { id: true },
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username_+/__note-editor.server.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username_+/__note-editor.server.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username_+/__note-editor.server.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username_+/__note-editor.server.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username_+/__note-editor.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username_+/__note-editor.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username_+/__note-editor.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username_+/__note-editor.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username_+/notes.$noteId.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username_+/notes.$noteId.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username_+/notes.$noteId.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username_+/notes.$noteId.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username_+/notes.$noteId_.edit.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username_+/notes.index.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username_+/notes.index.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username_+/notes.index.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username_+/notes.index.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username_+/notes.new.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username_+/notes.new.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username_+/notes.new.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username_+/notes.new.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username_+/notes.tsx b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username_+/notes.tsx
similarity index 96%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username_+/notes.tsx
rename to exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username_+/notes.tsx
index 90e7b3c..ded41ca 100644
--- a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username_+/notes.tsx
+++ b/exercises/01.fundamentals/03.solution.custom-fixtures/app/routes/users+/$username_+/notes.tsx
@@ -51,7 +51,10 @@ export default function NotesRoute({ loaderData }: Route.ComponentProps) {
{ownerDisplayName}'s Notes
-
+
{isOwner ? (
| null | undefined
+
+export function ErrorList({
+ id,
+ errors,
+}: {
+ errors?: ListOfErrors
+ id?: string
+}) {
+ const errorsToRender = errors?.filter(Boolean)
+ if (!errorsToRender?.length) return null
+ return (
+
+ {errorsToRender.map((e) => (
+
+ {e}
+
+ ))}
+
+ )
+}
+
+export function Field({
+ labelProps,
+ inputProps,
+ errors,
+ className,
+}: {
+ labelProps: React.LabelHTMLAttributes
+ inputProps: React.InputHTMLAttributes
+ errors?: ListOfErrors
+ className?: string
+}) {
+ const fallbackId = useId()
+ const id = inputProps.id ?? fallbackId
+ const errorId = errors?.length ? `${id}-error` : undefined
+ return (
+
+
+
+
+ {errorId ? : null}
+
+
+ )
+}
+
+export function OTPField({
+ labelProps,
+ inputProps,
+ errors,
+ className,
+}: {
+ labelProps: React.LabelHTMLAttributes
+ inputProps: Partial
+ errors?: ListOfErrors
+ className?: string
+}) {
+ const fallbackId = useId()
+ const id = inputProps.id ?? fallbackId
+ const errorId = errors?.length ? `${id}-error` : undefined
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {errorId ? : null}
+
+
+ )
+}
+
+export function TextareaField({
+ labelProps,
+ textareaProps,
+ errors,
+ className,
+}: {
+ labelProps: React.LabelHTMLAttributes
+ textareaProps: React.TextareaHTMLAttributes
+ errors?: ListOfErrors
+ className?: string
+}) {
+ const fallbackId = useId()
+ const id = textareaProps.id ?? textareaProps.name ?? fallbackId
+ const errorId = errors?.length ? `${id}-error` : undefined
+ return (
+
+
+
+
+ {errorId ? : null}
+
+
+ )
+}
+
+export function CheckboxField({
+ labelProps,
+ buttonProps,
+ errors,
+ className,
+}: {
+ labelProps: React.ComponentProps<'label'>
+ buttonProps: CheckboxProps & {
+ name: string
+ form: string
+ value?: string
+ }
+ errors?: ListOfErrors
+ className?: string
+}) {
+ const { key, defaultChecked, ...checkboxProps } = buttonProps
+ const fallbackId = useId()
+ const checkedValue = buttonProps.value ?? 'on'
+ const input = useInputControl({
+ key,
+ name: buttonProps.name,
+ formId: buttonProps.form,
+ initialValue: defaultChecked ? checkedValue : undefined,
+ })
+ const id = buttonProps.id ?? fallbackId
+ const errorId = errors?.length ? `${id}-error` : undefined
+
+ return (
+
+
+ {
+ input.change(state.valueOf() ? checkedValue : '')
+ buttonProps.onCheckedChange?.(state)
+ }}
+ onFocus={(event) => {
+ input.focus()
+ buttonProps.onFocus?.(event)
+ }}
+ onBlur={(event) => {
+ input.blur()
+ buttonProps.onBlur?.(event)
+ }}
+ type="button"
+ />
+
+
+
+ {errorId ? : null}
+
+
+ )
+}
diff --git a/exercises/02.test-setup/03.problem.authentication/app/components/progress-bar.tsx b/exercises/02.authentication/01.problem.basic/app/components/progress-bar.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/components/progress-bar.tsx
rename to exercises/02.authentication/01.problem.basic/app/components/progress-bar.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/components/search-bar.tsx b/exercises/02.authentication/01.problem.basic/app/components/search-bar.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/components/search-bar.tsx
rename to exercises/02.authentication/01.problem.basic/app/components/search-bar.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/components/spacer.tsx b/exercises/02.authentication/01.problem.basic/app/components/spacer.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/components/spacer.tsx
rename to exercises/02.authentication/01.problem.basic/app/components/spacer.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/components/toaster.tsx b/exercises/02.authentication/01.problem.basic/app/components/toaster.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/components/toaster.tsx
rename to exercises/02.authentication/01.problem.basic/app/components/toaster.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/components/ui/README.md b/exercises/02.authentication/01.problem.basic/app/components/ui/README.md
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/components/ui/README.md
rename to exercises/02.authentication/01.problem.basic/app/components/ui/README.md
diff --git a/exercises/02.test-setup/03.problem.authentication/app/components/ui/button.tsx b/exercises/02.authentication/01.problem.basic/app/components/ui/button.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/components/ui/button.tsx
rename to exercises/02.authentication/01.problem.basic/app/components/ui/button.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/components/ui/checkbox.tsx b/exercises/02.authentication/01.problem.basic/app/components/ui/checkbox.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/components/ui/checkbox.tsx
rename to exercises/02.authentication/01.problem.basic/app/components/ui/checkbox.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/components/ui/dropdown-menu.tsx b/exercises/02.authentication/01.problem.basic/app/components/ui/dropdown-menu.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/components/ui/dropdown-menu.tsx
rename to exercises/02.authentication/01.problem.basic/app/components/ui/dropdown-menu.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/components/ui/icon.tsx b/exercises/02.authentication/01.problem.basic/app/components/ui/icon.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/components/ui/icon.tsx
rename to exercises/02.authentication/01.problem.basic/app/components/ui/icon.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/components/ui/input-otp.tsx b/exercises/02.authentication/01.problem.basic/app/components/ui/input-otp.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/components/ui/input-otp.tsx
rename to exercises/02.authentication/01.problem.basic/app/components/ui/input-otp.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/components/ui/input.tsx b/exercises/02.authentication/01.problem.basic/app/components/ui/input.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/components/ui/input.tsx
rename to exercises/02.authentication/01.problem.basic/app/components/ui/input.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/components/ui/label.tsx b/exercises/02.authentication/01.problem.basic/app/components/ui/label.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/components/ui/label.tsx
rename to exercises/02.authentication/01.problem.basic/app/components/ui/label.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/components/ui/sonner.tsx b/exercises/02.authentication/01.problem.basic/app/components/ui/sonner.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/components/ui/sonner.tsx
rename to exercises/02.authentication/01.problem.basic/app/components/ui/sonner.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/components/ui/status-button.tsx b/exercises/02.authentication/01.problem.basic/app/components/ui/status-button.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/components/ui/status-button.tsx
rename to exercises/02.authentication/01.problem.basic/app/components/ui/status-button.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/components/ui/textarea.tsx b/exercises/02.authentication/01.problem.basic/app/components/ui/textarea.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/components/ui/textarea.tsx
rename to exercises/02.authentication/01.problem.basic/app/components/ui/textarea.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/components/ui/tooltip.tsx b/exercises/02.authentication/01.problem.basic/app/components/ui/tooltip.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/components/ui/tooltip.tsx
rename to exercises/02.authentication/01.problem.basic/app/components/ui/tooltip.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/components/user-dropdown.tsx b/exercises/02.authentication/01.problem.basic/app/components/user-dropdown.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/components/user-dropdown.tsx
rename to exercises/02.authentication/01.problem.basic/app/components/user-dropdown.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/entry.client.tsx b/exercises/02.authentication/01.problem.basic/app/entry.client.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/entry.client.tsx
rename to exercises/02.authentication/01.problem.basic/app/entry.client.tsx
diff --git a/exercises/02.authentication/01.problem.basic/app/entry.server.tsx b/exercises/02.authentication/01.problem.basic/app/entry.server.tsx
new file mode 100644
index 0000000..8d8b1de
--- /dev/null
+++ b/exercises/02.authentication/01.problem.basic/app/entry.server.tsx
@@ -0,0 +1,143 @@
+import crypto from 'node:crypto'
+import { PassThrough } from 'node:stream'
+import { styleText } from 'node:util'
+import { contentSecurity } from '@nichtsam/helmet/content'
+import { createReadableStreamFromReadable } from '@react-router/node'
+import * as Sentry from '@sentry/react-router'
+import { isbot } from 'isbot'
+import { renderToPipeableStream } from 'react-dom/server'
+import {
+ ServerRouter,
+ type LoaderFunctionArgs,
+ type ActionFunctionArgs,
+ type HandleDocumentRequestFunction,
+} from 'react-router'
+import { getEnv, init } from './utils/env.server.ts'
+import { getInstanceInfo } from './utils/litefs.server.ts'
+import { NonceProvider } from './utils/nonce-provider.ts'
+import { makeTimings } from './utils/timing.server.ts'
+
+export const streamTimeout = 5000
+
+init()
+global.ENV = getEnv()
+
+const MODE = process.env.NODE_ENV ?? 'development'
+
+type DocRequestArgs = Parameters
+
+export default async function handleRequest(...args: DocRequestArgs) {
+ const [request, responseStatusCode, responseHeaders, reactRouterContext] =
+ args
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ responseHeaders.set('fly-primary-instance', primaryInstance)
+ responseHeaders.set('fly-instance', currentInstance)
+
+ if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
+ responseHeaders.append('Document-Policy', 'js-profiling')
+ }
+
+ const callbackName = isbot(request.headers.get('user-agent'))
+ ? 'onAllReady'
+ : 'onShellReady'
+
+ const nonce = crypto.randomBytes(16).toString('hex')
+ return new Promise(async (resolve, reject) => {
+ let didError = false
+ // NOTE: this timing will only include things that are rendered in the shell
+ // and will not include suspended components and deferred loaders
+ const timings = makeTimings('render', 'renderToPipeableStream')
+
+ const { pipe, abort } = renderToPipeableStream(
+
+
+ ,
+ {
+ [callbackName]: () => {
+ const body = new PassThrough()
+ responseHeaders.set('Content-Type', 'text/html')
+ responseHeaders.append('Server-Timing', timings.toString())
+
+ contentSecurity(responseHeaders, {
+ crossOriginEmbedderPolicy: false,
+ contentSecurityPolicy: {
+ // NOTE: Remove reportOnly when you're ready to enforce this CSP
+ reportOnly: true,
+ directives: {
+ fetch: {
+ 'connect-src': [
+ MODE === 'development' ? 'ws:' : undefined,
+ process.env.SENTRY_DSN ? '*.sentry.io' : undefined,
+ "'self'",
+ ],
+ 'font-src': ["'self'"],
+ 'frame-src': ["'self'"],
+ 'img-src': ["'self'", 'data:'],
+ 'script-src': [
+ "'strict-dynamic'",
+ "'self'",
+ `'nonce-${nonce}'`,
+ ],
+ 'script-src-attr': [`'nonce-${nonce}'`],
+ },
+ },
+ },
+ xFrameOptions: false,
+ })
+
+ resolve(
+ new Response(createReadableStreamFromReadable(body), {
+ headers: responseHeaders,
+ status: didError ? 500 : responseStatusCode,
+ }),
+ )
+ pipe(body)
+ },
+ onShellError: (err: unknown) => {
+ reject(err)
+ },
+ onError: () => {
+ didError = true
+ },
+ nonce,
+ },
+ )
+
+ setTimeout(abort, streamTimeout + 5000)
+ })
+}
+
+export async function handleDataRequest(response: Response) {
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ response.headers.set('fly-primary-instance', primaryInstance)
+ response.headers.set('fly-instance', currentInstance)
+
+ return response
+}
+
+export function handleError(
+ error: unknown,
+ { request }: LoaderFunctionArgs | ActionFunctionArgs,
+): void {
+ // Skip capturing if the request is aborted as Remix docs suggest
+ // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
+ if (request.signal.aborted) {
+ return
+ }
+
+ if (error instanceof Error) {
+ console.error(styleText('red', String(error.stack)))
+ } else {
+ console.error(error)
+ }
+
+ Sentry.captureException(error)
+}
diff --git a/exercises/02.test-setup/03.problem.authentication/app/root.tsx b/exercises/02.authentication/01.problem.basic/app/root.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/root.tsx
rename to exercises/02.authentication/01.problem.basic/app/root.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes.ts b/exercises/02.authentication/01.problem.basic/app/routes.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes.ts
rename to exercises/02.authentication/01.problem.basic/app/routes.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/$.tsx b/exercises/02.authentication/01.problem.basic/app/routes/$.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/$.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/$.tsx
diff --git a/exercises/03.guides/03.solution.blocking-unneeded-requests/app/routes/_auth+/auth.$provider.callback.test.ts b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/auth.$provider.callback.test.ts
similarity index 97%
rename from exercises/03.guides/03.solution.blocking-unneeded-requests/app/routes/_auth+/auth.$provider.callback.test.ts
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/auth.$provider.callback.test.ts
index 643e453..3765dd7 100644
--- a/exercises/03.guides/03.solution.blocking-unneeded-requests/app/routes/_auth+/auth.$provider.callback.test.ts
+++ b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/auth.$provider.callback.test.ts
@@ -9,7 +9,7 @@ import { GITHUB_PROVIDER_NAME } from '#app/utils/connections.tsx'
import { prisma } from '#app/utils/db.server.ts'
import { authSessionStorage } from '#app/utils/session.server.ts'
import { generateTOTP } from '#app/utils/totp.server.ts'
-import { createUser } from '#tests/db-utils.ts'
+import { generateUserInfo } from '#tests/db-utils.ts'
import { insertGitHubUser, deleteGitHubUsers } from '#tests/mocks/github.ts'
import { server } from '#tests/mocks/index.ts'
import { consoleError } from '#tests/setup/setup-test-env.ts'
@@ -109,7 +109,7 @@ test(`when a user is logged in and has already connected, it doesn't do anything
test('when a user exists with the same email, create connection and make session', async () => {
const githubUser = await insertGitHubUser()
const email = githubUser.primaryEmail.toLowerCase()
- const { userId } = await setupUser({ ...createUser(), email })
+ const { userId } = await setupUser({ ...generateUserInfo(), email })
const request = await setupRequest({ code: githubUser.code })
const response = await loader({ request, params: PARAMS, context: {} })
@@ -141,7 +141,7 @@ test('gives an error if the account is already connected to another user', async
const githubUser = await insertGitHubUser()
await prisma.user.create({
data: {
- ...createUser(),
+ ...generateUserInfo(),
connections: {
create: {
providerName: GITHUB_PROVIDER_NAME,
@@ -245,7 +245,7 @@ async function setupRequest({
return request
}
-async function setupUser(userData = createUser()) {
+async function setupUser(userData = generateUserInfo()) {
const session = await prisma.session.create({
data: {
expirationDate: getSessionExpirationDate(),
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/auth.$provider.callback.ts b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/auth.$provider.callback.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/auth.$provider.callback.ts
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/auth.$provider.callback.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/auth_.$provider.ts b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/auth_.$provider.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/auth_.$provider.ts
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/auth_.$provider.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/forgot-password.tsx b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/forgot-password.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/forgot-password.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/forgot-password.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/login.server.ts b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/login.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/login.server.ts
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/login.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/login.tsx b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/login.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/login.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/login.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/logout.tsx b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/logout.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/logout.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/logout.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/onboarding.server.ts b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/onboarding.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/onboarding.server.ts
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/onboarding.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/onboarding.tsx b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/onboarding.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/onboarding.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/onboarding.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/onboarding_.$provider.server.ts b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/onboarding_.$provider.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/onboarding_.$provider.server.ts
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/onboarding_.$provider.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/onboarding_.$provider.tsx b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/onboarding_.$provider.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/onboarding_.$provider.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/onboarding_.$provider.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/reset-password.server.ts b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/reset-password.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/reset-password.server.ts
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/reset-password.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/reset-password.tsx b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/reset-password.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/reset-password.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/reset-password.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/signup.tsx b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/signup.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/signup.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/signup.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/verify.server.ts b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/verify.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/verify.server.ts
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/verify.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/verify.tsx b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/verify.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/verify.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/verify.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/webauthn+/authentication.ts b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/webauthn+/authentication.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/webauthn+/authentication.ts
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/webauthn+/authentication.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/webauthn+/registration.ts b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/webauthn+/registration.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/webauthn+/registration.ts
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/webauthn+/registration.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/webauthn+/utils.server.ts b/exercises/02.authentication/01.problem.basic/app/routes/_auth+/webauthn+/utils.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/webauthn+/utils.server.ts
rename to exercises/02.authentication/01.problem.basic/app/routes/_auth+/webauthn+/utils.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/about.tsx b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/about.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/about.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/about.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/index.tsx b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/index.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/index.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/index.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/docker.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/docker.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/docker.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/docker.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/eslint.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/eslint.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/eslint.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/eslint.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/faker.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/faker.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/faker.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/faker.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/fly.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/fly.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/fly.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/fly.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/github.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/github.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/github.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/github.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/logos.ts b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/logos.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/logos.ts
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/logos.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/msw.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/msw.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/msw.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/msw.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/playwright.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/playwright.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/playwright.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/playwright.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/prettier.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/prettier.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/prettier.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/prettier.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/prisma.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/prisma.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/prisma.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/prisma.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/radix.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/radix.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/radix.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/radix.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/react-email.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/react-email.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/react-email.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/react-email.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/remix.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/remix.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/remix.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/remix.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/resend.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/resend.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/resend.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/resend.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/sentry.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/sentry.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/sentry.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/sentry.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/shadcn-ui.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/shadcn-ui.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/shadcn-ui.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/shadcn-ui.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/sqlite.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/sqlite.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/sqlite.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/sqlite.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/stars.jpg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/stars.jpg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/stars.jpg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/stars.jpg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/tailwind.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/tailwind.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/tailwind.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/tailwind.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/testing-library.png b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/testing-library.png
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/testing-library.png
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/testing-library.png
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/typescript.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/typescript.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/typescript.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/typescript.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/vitest.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/vitest.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/vitest.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/vitest.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/zod.svg b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/zod.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/logos/zod.svg
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/logos/zod.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/privacy.tsx b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/privacy.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/privacy.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/privacy.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/support.tsx b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/support.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/support.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/support.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/tos.tsx b/exercises/02.authentication/01.problem.basic/app/routes/_marketing+/tos.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_marketing+/tos.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/_marketing+/tos.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_seo+/robots[.]txt.ts b/exercises/02.authentication/01.problem.basic/app/routes/_seo+/robots[.]txt.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_seo+/robots[.]txt.ts
rename to exercises/02.authentication/01.problem.basic/app/routes/_seo+/robots[.]txt.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_seo+/sitemap[.]xml.ts b/exercises/02.authentication/01.problem.basic/app/routes/_seo+/sitemap[.]xml.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_seo+/sitemap[.]xml.ts
rename to exercises/02.authentication/01.problem.basic/app/routes/_seo+/sitemap[.]xml.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/admin+/cache.tsx b/exercises/02.authentication/01.problem.basic/app/routes/admin+/cache.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/admin+/cache.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/admin+/cache.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/admin+/cache_.lru.$cacheKey.ts b/exercises/02.authentication/01.problem.basic/app/routes/admin+/cache_.lru.$cacheKey.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/admin+/cache_.lru.$cacheKey.ts
rename to exercises/02.authentication/01.problem.basic/app/routes/admin+/cache_.lru.$cacheKey.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/admin+/cache_.sqlite.$cacheKey.ts b/exercises/02.authentication/01.problem.basic/app/routes/admin+/cache_.sqlite.$cacheKey.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/admin+/cache_.sqlite.$cacheKey.ts
rename to exercises/02.authentication/01.problem.basic/app/routes/admin+/cache_.sqlite.$cacheKey.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/admin+/cache_.sqlite.server.ts b/exercises/02.authentication/01.problem.basic/app/routes/admin+/cache_.sqlite.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/admin+/cache_.sqlite.server.ts
rename to exercises/02.authentication/01.problem.basic/app/routes/admin+/cache_.sqlite.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/admin+/cache_.sqlite.tsx b/exercises/02.authentication/01.problem.basic/app/routes/admin+/cache_.sqlite.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/admin+/cache_.sqlite.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/admin+/cache_.sqlite.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/me.tsx b/exercises/02.authentication/01.problem.basic/app/routes/me.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/me.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/me.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/resources+/download-user-data.tsx b/exercises/02.authentication/01.problem.basic/app/routes/resources+/download-user-data.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/resources+/download-user-data.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/resources+/download-user-data.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/resources+/healthcheck.tsx b/exercises/02.authentication/01.problem.basic/app/routes/resources+/healthcheck.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/resources+/healthcheck.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/resources+/healthcheck.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/resources+/images.tsx b/exercises/02.authentication/01.problem.basic/app/routes/resources+/images.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/resources+/images.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/resources+/images.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/resources+/theme-switch.tsx b/exercises/02.authentication/01.problem.basic/app/routes/resources+/theme-switch.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/resources+/theme-switch.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/resources+/theme-switch.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.change-email.server.tsx b/exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.change-email.server.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.change-email.server.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.change-email.server.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.change-email.tsx b/exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.change-email.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.change-email.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.change-email.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.connections.tsx b/exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.connections.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.connections.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.connections.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.index.tsx b/exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.index.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.index.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.index.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.passkeys.tsx b/exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.passkeys.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.passkeys.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.passkeys.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.password.tsx b/exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.password.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.password.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.password.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.password_.create.tsx b/exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.password_.create.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.password_.create.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.password_.create.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.photo.tsx b/exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.photo.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.photo.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.photo.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.tsx b/exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.two-factor.disable.tsx b/exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.two-factor.disable.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.two-factor.disable.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.two-factor.disable.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.two-factor.index.tsx b/exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.two-factor.index.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.two-factor.index.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.two-factor.index.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.two-factor.tsx b/exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.two-factor.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.two-factor.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.two-factor.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.two-factor.verify.tsx b/exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.two-factor.verify.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/settings+/profile.two-factor.verify.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/settings+/profile.two-factor.verify.tsx
diff --git a/exercises/03.guides/03.solution.blocking-unneeded-requests/app/routes/users+/$username.test.tsx b/exercises/02.authentication/01.problem.basic/app/routes/users+/$username.test.tsx
similarity index 93%
rename from exercises/03.guides/03.solution.blocking-unneeded-requests/app/routes/users+/$username.test.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/users+/$username.test.tsx
index b0784bd..0884a24 100644
--- a/exercises/03.guides/03.solution.blocking-unneeded-requests/app/routes/users+/$username.test.tsx
+++ b/exercises/02.authentication/01.problem.basic/app/routes/users+/$username.test.tsx
@@ -10,7 +10,7 @@ import { loader as rootLoader } from '#app/root.tsx'
import { getSessionExpirationDate, sessionKey } from '#app/utils/auth.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { authSessionStorage } from '#app/utils/session.server.ts'
-import { createUser, getUserImages } from '#tests/db-utils.ts'
+import { generateUserInfo, getUserImages } from '#tests/db-utils.ts'
import { default as UsernameRoute, loader } from './$username.tsx'
test('The user profile when not logged in as self', async () => {
@@ -19,7 +19,7 @@ test('The user profile when not logged in as self', async () => {
userImages[faker.number.int({ min: 0, max: userImages.length - 1 })]
const user = await prisma.user.create({
select: { id: true, username: true, name: true },
- data: { ...createUser(), image: { create: userImage } },
+ data: { ...generateUserInfo(), image: { create: userImage } },
})
const App = createRoutesStub([
{
@@ -44,7 +44,7 @@ test('The user profile when logged in as self', async () => {
userImages[faker.number.int({ min: 0, max: userImages.length - 1 })]
const user = await prisma.user.create({
select: { id: true, username: true, name: true },
- data: { ...createUser(), image: { create: userImage } },
+ data: { ...generateUserInfo(), image: { create: userImage } },
})
const session = await prisma.session.create({
select: { id: true },
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username.tsx b/exercises/02.authentication/01.problem.basic/app/routes/users+/$username.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/users+/$username.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username_+/__note-editor.server.tsx b/exercises/02.authentication/01.problem.basic/app/routes/users+/$username_+/__note-editor.server.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username_+/__note-editor.server.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/users+/$username_+/__note-editor.server.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username_+/__note-editor.tsx b/exercises/02.authentication/01.problem.basic/app/routes/users+/$username_+/__note-editor.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username_+/__note-editor.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/users+/$username_+/__note-editor.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username_+/notes.$noteId.tsx b/exercises/02.authentication/01.problem.basic/app/routes/users+/$username_+/notes.$noteId.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username_+/notes.$noteId.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/users+/$username_+/notes.$noteId.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username_+/notes.$noteId_.edit.tsx b/exercises/02.authentication/01.problem.basic/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username_+/notes.index.tsx b/exercises/02.authentication/01.problem.basic/app/routes/users+/$username_+/notes.index.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username_+/notes.index.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/users+/$username_+/notes.index.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username_+/notes.new.tsx b/exercises/02.authentication/01.problem.basic/app/routes/users+/$username_+/notes.new.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username_+/notes.new.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/users+/$username_+/notes.new.tsx
diff --git a/exercises/02.authentication/01.problem.basic/app/routes/users+/$username_+/notes.tsx b/exercises/02.authentication/01.problem.basic/app/routes/users+/$username_+/notes.tsx
new file mode 100644
index 0000000..ded41ca
--- /dev/null
+++ b/exercises/02.authentication/01.problem.basic/app/routes/users+/$username_+/notes.tsx
@@ -0,0 +1,105 @@
+import { invariantResponse } from '@epic-web/invariant'
+import { Img } from 'openimg/react'
+import { Link, NavLink, Outlet } from 'react-router'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { cn, getUserImgSrc } from '#app/utils/misc.tsx'
+import { useOptionalUser } from '#app/utils/user.ts'
+import { type Route } from './+types/notes.ts'
+
+export async function loader({ params }: Route.LoaderArgs) {
+ const owner = await prisma.user.findFirst({
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ image: { select: { objectKey: true } },
+ notes: { select: { id: true, title: true } },
+ },
+ where: { username: params.username },
+ })
+
+ invariantResponse(owner, 'Owner not found', { status: 404 })
+
+ return { owner }
+}
+
+export default function NotesRoute({ loaderData }: Route.ComponentProps) {
+ const user = useOptionalUser()
+ const isOwner = user?.id === loaderData.owner.id
+ const ownerDisplayName = loaderData.owner.name ?? loaderData.owner.username
+ const navLinkDefaultClassName =
+ 'line-clamp-2 block rounded-l-full py-2 pl-8 pr-6 text-base lg:text-xl'
+ return (
+
+
+
+
+
+
+
+ {ownerDisplayName}'s Notes
+
+
+
+ {isOwner ? (
+
+
+ cn(navLinkDefaultClassName, isActive && 'bg-accent')
+ }
+ >
+ New Note
+
+
+ ) : null}
+ {loaderData.owner.notes.map((note) => (
+
+
+ cn(navLinkDefaultClassName, isActive && 'bg-accent')
+ }
+ >
+ {note.title}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return (
+ (
+ No user with the username "{params.username}" exists
+ ),
+ }}
+ />
+ )
+}
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/users+/index.tsx b/exercises/02.authentication/01.problem.basic/app/routes/users+/index.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/users+/index.tsx
rename to exercises/02.authentication/01.problem.basic/app/routes/users+/index.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/styles/tailwind.css b/exercises/02.authentication/01.problem.basic/app/styles/tailwind.css
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/styles/tailwind.css
rename to exercises/02.authentication/01.problem.basic/app/styles/tailwind.css
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/auth.server.test.ts b/exercises/02.authentication/01.problem.basic/app/utils/auth.server.test.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/auth.server.test.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/auth.server.test.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/auth.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/auth.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/auth.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/auth.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/cache.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/cache.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/cache.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/cache.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/client-hints.tsx b/exercises/02.authentication/01.problem.basic/app/utils/client-hints.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/client-hints.tsx
rename to exercises/02.authentication/01.problem.basic/app/utils/client-hints.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/connections.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/connections.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/connections.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/connections.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/connections.tsx b/exercises/02.authentication/01.problem.basic/app/utils/connections.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/connections.tsx
rename to exercises/02.authentication/01.problem.basic/app/utils/connections.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/db.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/db.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/db.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/db.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/email.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/email.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/email.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/email.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/env.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/env.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/env.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/env.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/headers.server.test.ts b/exercises/02.authentication/01.problem.basic/app/utils/headers.server.test.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/headers.server.test.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/headers.server.test.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/headers.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/headers.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/headers.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/headers.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/honeypot.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/honeypot.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/honeypot.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/honeypot.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/litefs.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/litefs.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/litefs.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/litefs.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/misc.error-message.test.ts b/exercises/02.authentication/01.problem.basic/app/utils/misc.error-message.test.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/misc.error-message.test.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/misc.error-message.test.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/misc.tsx b/exercises/02.authentication/01.problem.basic/app/utils/misc.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/misc.tsx
rename to exercises/02.authentication/01.problem.basic/app/utils/misc.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/misc.use-double-check.test.tsx b/exercises/02.authentication/01.problem.basic/app/utils/misc.use-double-check.test.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/misc.use-double-check.test.tsx
rename to exercises/02.authentication/01.problem.basic/app/utils/misc.use-double-check.test.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/monitoring.client.tsx b/exercises/02.authentication/01.problem.basic/app/utils/monitoring.client.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/monitoring.client.tsx
rename to exercises/02.authentication/01.problem.basic/app/utils/monitoring.client.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/nonce-provider.ts b/exercises/02.authentication/01.problem.basic/app/utils/nonce-provider.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/nonce-provider.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/nonce-provider.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/permissions.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/permissions.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/permissions.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/permissions.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/providers/constants.ts b/exercises/02.authentication/01.problem.basic/app/utils/providers/constants.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/providers/constants.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/providers/constants.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/providers/github.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/providers/github.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/providers/github.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/providers/github.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/providers/provider.ts b/exercises/02.authentication/01.problem.basic/app/utils/providers/provider.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/providers/provider.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/providers/provider.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/redirect-cookie.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/redirect-cookie.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/redirect-cookie.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/redirect-cookie.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/request-info.ts b/exercises/02.authentication/01.problem.basic/app/utils/request-info.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/request-info.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/request-info.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/session.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/session.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/session.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/session.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/storage.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/storage.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/storage.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/storage.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/theme.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/theme.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/theme.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/theme.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/timing.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/timing.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/timing.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/timing.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/toast.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/toast.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/toast.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/toast.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/totp.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/totp.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/totp.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/totp.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/user-validation.ts b/exercises/02.authentication/01.problem.basic/app/utils/user-validation.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/user-validation.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/user-validation.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/user.ts b/exercises/02.authentication/01.problem.basic/app/utils/user.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/user.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/user.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/app/utils/verification.server.ts b/exercises/02.authentication/01.problem.basic/app/utils/verification.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/utils/verification.server.ts
rename to exercises/02.authentication/01.problem.basic/app/utils/verification.server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/components.json b/exercises/02.authentication/01.problem.basic/components.json
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/components.json
rename to exercises/02.authentication/01.problem.basic/components.json
diff --git a/exercises/02.test-setup/03.problem.authentication/eslint.config.js b/exercises/02.authentication/01.problem.basic/eslint.config.js
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/eslint.config.js
rename to exercises/02.authentication/01.problem.basic/eslint.config.js
diff --git a/exercises/02.test-setup/03.problem.authentication/fly.toml b/exercises/02.authentication/01.problem.basic/fly.toml
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/fly.toml
rename to exercises/02.authentication/01.problem.basic/fly.toml
diff --git a/exercises/02.test-setup/03.problem.authentication/index.js b/exercises/02.authentication/01.problem.basic/index.js
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/index.js
rename to exercises/02.authentication/01.problem.basic/index.js
diff --git a/exercises/02.test-setup/03.problem.authentication/other/Dockerfile b/exercises/02.authentication/01.problem.basic/other/Dockerfile
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/Dockerfile
rename to exercises/02.authentication/01.problem.basic/other/Dockerfile
diff --git a/exercises/02.test-setup/03.problem.authentication/other/Dockerfile.dockerignore b/exercises/02.authentication/01.problem.basic/other/Dockerfile.dockerignore
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/Dockerfile.dockerignore
rename to exercises/02.authentication/01.problem.basic/other/Dockerfile.dockerignore
diff --git a/exercises/02.test-setup/03.problem.authentication/other/README.md b/exercises/02.authentication/01.problem.basic/other/README.md
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/README.md
rename to exercises/02.authentication/01.problem.basic/other/README.md
diff --git a/exercises/02.test-setup/03.problem.authentication/other/build-server.ts b/exercises/02.authentication/01.problem.basic/other/build-server.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/build-server.ts
rename to exercises/02.authentication/01.problem.basic/other/build-server.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/other/litefs.yml b/exercises/02.authentication/01.problem.basic/other/litefs.yml
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/litefs.yml
rename to exercises/02.authentication/01.problem.basic/other/litefs.yml
diff --git a/exercises/02.test-setup/03.problem.authentication/other/sly/sly.json b/exercises/02.authentication/01.problem.basic/other/sly/sly.json
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/sly/sly.json
rename to exercises/02.authentication/01.problem.basic/other/sly/sly.json
diff --git a/exercises/02.test-setup/03.problem.authentication/other/sly/transform-icon.ts b/exercises/02.authentication/01.problem.basic/other/sly/transform-icon.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/sly/transform-icon.ts
rename to exercises/02.authentication/01.problem.basic/other/sly/transform-icon.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/README.md b/exercises/02.authentication/01.problem.basic/other/svg-icons/README.md
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/README.md
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/README.md
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/arrow-left.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/arrow-left.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/arrow-left.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/arrow-left.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/arrow-right.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/arrow-right.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/arrow-right.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/arrow-right.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/avatar.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/avatar.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/avatar.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/avatar.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/camera.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/camera.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/camera.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/camera.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/check.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/check.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/check.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/check.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/clock.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/clock.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/clock.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/clock.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/cross-1.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/cross-1.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/cross-1.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/cross-1.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/dots-horizontal.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/dots-horizontal.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/dots-horizontal.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/dots-horizontal.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/download.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/download.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/download.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/download.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/envelope-closed.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/envelope-closed.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/envelope-closed.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/envelope-closed.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/exit.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/exit.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/exit.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/exit.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/file-text.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/file-text.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/file-text.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/file-text.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/github-logo.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/github-logo.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/github-logo.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/github-logo.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/laptop.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/laptop.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/laptop.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/laptop.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/link-2.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/link-2.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/link-2.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/link-2.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/lock-closed.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/lock-closed.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/lock-closed.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/lock-closed.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/lock-open-1.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/lock-open-1.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/lock-open-1.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/lock-open-1.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/magnifying-glass.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/magnifying-glass.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/magnifying-glass.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/magnifying-glass.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/moon.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/moon.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/moon.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/moon.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/passkey.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/passkey.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/passkey.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/passkey.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/pencil-1.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/pencil-1.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/pencil-1.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/pencil-1.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/pencil-2.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/pencil-2.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/pencil-2.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/pencil-2.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/plus.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/plus.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/plus.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/plus.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/question-mark-circled.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/question-mark-circled.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/question-mark-circled.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/question-mark-circled.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/reset.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/reset.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/reset.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/reset.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/sun.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/sun.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/sun.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/sun.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/trash.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/trash.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/trash.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/trash.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/other/svg-icons/update.svg b/exercises/02.authentication/01.problem.basic/other/svg-icons/update.svg
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/other/svg-icons/update.svg
rename to exercises/02.authentication/01.problem.basic/other/svg-icons/update.svg
diff --git a/exercises/02.test-setup/03.problem.authentication/package-lock.json b/exercises/02.authentication/01.problem.basic/package-lock.json
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/package-lock.json
rename to exercises/02.authentication/01.problem.basic/package-lock.json
diff --git a/exercises/02.test-setup/05.problem.test-data/package.json b/exercises/02.authentication/01.problem.basic/package.json
similarity index 97%
rename from exercises/02.test-setup/05.problem.test-data/package.json
rename to exercises/02.authentication/01.problem.basic/package.json
index b441ff8..3b609c5 100644
--- a/exercises/02.test-setup/05.problem.test-data/package.json
+++ b/exercises/02.authentication/01.problem.basic/package.json
@@ -1,5 +1,5 @@
{
- "name": "exercises_02.test-setup_05.problem.test-data",
+ "name": "exercises_02.authentication_01.problem.basic",
"private": true,
"sideEffects": false,
"type": "module",
@@ -115,7 +115,7 @@
"devDependencies": {
"@epic-web/config": "^1.20.1",
"@faker-js/faker": "^9.7.0",
- "@playwright/test": "^1.52.0",
+ "@playwright/test": "^1.57.0",
"@react-router/dev": "^7.5.3",
"@sly-cli/sly": "^2.1.1",
"@testing-library/dom": "^10.4.0",
@@ -145,7 +145,7 @@
"jsdom": "^25.0.1",
"msw": "^2.7.6",
"npm-run-all": "^4.1.5",
- "playwright-persona": "^0.2.5",
+ "playwright-persona": "^0.2.8",
"prettier": "^3.5.3",
"prettier-plugin-sql": "^0.19.0",
"prettier-plugin-tailwindcss": "^0.6.11",
diff --git a/exercises/02.test-setup/03.problem.authentication/playwright.config.ts b/exercises/02.authentication/01.problem.basic/playwright.config.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/playwright.config.ts
rename to exercises/02.authentication/01.problem.basic/playwright.config.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/prisma/migrations/20250221233640_init/migration.sql b/exercises/02.authentication/01.problem.basic/prisma/migrations/20250221233640_init/migration.sql
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/prisma/migrations/20250221233640_init/migration.sql
rename to exercises/02.authentication/01.problem.basic/prisma/migrations/20250221233640_init/migration.sql
diff --git a/exercises/02.test-setup/03.problem.authentication/prisma/migrations/migration_lock.toml b/exercises/02.authentication/01.problem.basic/prisma/migrations/migration_lock.toml
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/prisma/migrations/migration_lock.toml
rename to exercises/02.authentication/01.problem.basic/prisma/migrations/migration_lock.toml
diff --git a/exercises/02.test-setup/03.problem.authentication/prisma/schema.prisma b/exercises/02.authentication/01.problem.basic/prisma/schema.prisma
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/prisma/schema.prisma
rename to exercises/02.authentication/01.problem.basic/prisma/schema.prisma
diff --git a/exercises/03.guides/02.solution.test-annotations/prisma/seed.ts b/exercises/02.authentication/01.problem.basic/prisma/seed.ts
similarity index 99%
rename from exercises/03.guides/02.solution.test-annotations/prisma/seed.ts
rename to exercises/02.authentication/01.problem.basic/prisma/seed.ts
index 8454353..521e1d5 100644
--- a/exercises/03.guides/02.solution.test-annotations/prisma/seed.ts
+++ b/exercises/02.authentication/01.problem.basic/prisma/seed.ts
@@ -3,7 +3,7 @@ import { prisma } from '#app/utils/db.server.ts'
import { MOCK_CODE_GITHUB } from '#app/utils/providers/constants'
import {
createPassword,
- createUser,
+ generateUserInfo,
getNoteImages,
getUserImages,
} from '#tests/db-utils.ts'
@@ -19,7 +19,7 @@ async function seed() {
const userImages = await getUserImages()
for (let index = 0; index < totalUsers; index++) {
- const userData = createUser()
+ const userData = generateUserInfo()
const user = await prisma.user.create({
select: { id: true },
data: {
diff --git a/exercises/02.test-setup/03.problem.authentication/prisma/sql/searchUsers.sql b/exercises/02.authentication/01.problem.basic/prisma/sql/searchUsers.sql
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/prisma/sql/searchUsers.sql
rename to exercises/02.authentication/01.problem.basic/prisma/sql/searchUsers.sql
diff --git a/exercises/02.test-setup/03.problem.authentication/public/favicon.ico b/exercises/02.authentication/01.problem.basic/public/favicon.ico
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/public/favicon.ico
rename to exercises/02.authentication/01.problem.basic/public/favicon.ico
diff --git a/exercises/02.test-setup/03.problem.authentication/public/favicons/README.md b/exercises/02.authentication/01.problem.basic/public/favicons/README.md
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/public/favicons/README.md
rename to exercises/02.authentication/01.problem.basic/public/favicons/README.md
diff --git a/exercises/02.test-setup/03.problem.authentication/public/favicons/android-chrome-192x192.png b/exercises/02.authentication/01.problem.basic/public/favicons/android-chrome-192x192.png
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/public/favicons/android-chrome-192x192.png
rename to exercises/02.authentication/01.problem.basic/public/favicons/android-chrome-192x192.png
diff --git a/exercises/02.test-setup/03.problem.authentication/public/favicons/android-chrome-512x512.png b/exercises/02.authentication/01.problem.basic/public/favicons/android-chrome-512x512.png
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/public/favicons/android-chrome-512x512.png
rename to exercises/02.authentication/01.problem.basic/public/favicons/android-chrome-512x512.png
diff --git a/exercises/02.test-setup/03.problem.authentication/public/img/user.png b/exercises/02.authentication/01.problem.basic/public/img/user.png
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/public/img/user.png
rename to exercises/02.authentication/01.problem.basic/public/img/user.png
diff --git a/exercises/02.test-setup/03.problem.authentication/public/site.webmanifest b/exercises/02.authentication/01.problem.basic/public/site.webmanifest
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/public/site.webmanifest
rename to exercises/02.authentication/01.problem.basic/public/site.webmanifest
diff --git a/exercises/02.test-setup/03.problem.authentication/react-router.config.ts b/exercises/02.authentication/01.problem.basic/react-router.config.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/react-router.config.ts
rename to exercises/02.authentication/01.problem.basic/react-router.config.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/server/dev-server.js b/exercises/02.authentication/01.problem.basic/server/dev-server.js
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/server/dev-server.js
rename to exercises/02.authentication/01.problem.basic/server/dev-server.js
diff --git a/exercises/02.test-setup/03.problem.authentication/server/index.ts b/exercises/02.authentication/01.problem.basic/server/index.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/server/index.ts
rename to exercises/02.authentication/01.problem.basic/server/index.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/server/utils/monitoring.ts b/exercises/02.authentication/01.problem.basic/server/utils/monitoring.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/server/utils/monitoring.ts
rename to exercises/02.authentication/01.problem.basic/server/utils/monitoring.ts
diff --git a/exercises/02.authentication/01.problem.basic/tests/db-utils.ts b/exercises/02.authentication/01.problem.basic/tests/db-utils.ts
new file mode 100644
index 0000000..d95aaf2
--- /dev/null
+++ b/exercises/02.authentication/01.problem.basic/tests/db-utils.ts
@@ -0,0 +1,165 @@
+import { faker } from '@faker-js/faker'
+import bcrypt from 'bcryptjs'
+import { UniqueEnforcer } from 'enforce-unique'
+import { getPasswordHash } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+
+const uniqueUsernameEnforcer = new UniqueEnforcer()
+
+export function generateUserInfo() {
+ const firstName = faker.person.firstName()
+ const lastName = faker.person.lastName()
+
+ const username = uniqueUsernameEnforcer
+ .enforce(() => {
+ return (
+ faker.string.alphanumeric({ length: 2 }) +
+ '_' +
+ faker.internet.username({
+ firstName: firstName.toLowerCase(),
+ lastName: lastName.toLowerCase(),
+ })
+ )
+ })
+ .slice(0, 20)
+ .toLowerCase()
+ .replace(/[^a-z0-9_]/g, '_')
+
+ return {
+ username,
+ name: `${firstName} ${lastName}`,
+ email: `${username}@example.com`,
+ }
+}
+
+// 🐨 Implement the `createUser` function to help you create
+// a test user and clean up after it's no longer needed.
+export async function createUser() {
+ // 🐨 Generate user information by calling the `generateUserInfo` function
+ // and assigning its result into a `userInfo` variable.
+ // 💰 const userInfo = fn()
+ //
+ // 🐨 Declare a `password` variable and assign it a test password.
+ // 💰 const password = 'supersecret'
+ //
+ // 🐨 Create a user record in the database, using Prisma.
+ // 💰 const user = await prisma.user.create({
+ // data: {
+ // ...userInfo,
+ // password: { create: { hash: await getPasswordHash(password) } },
+ // }
+ // })
+ //
+ // 🐨 Return the `user` object, combining it with the `password`.
+ // 💰 return { ...user, password }
+ //
+ // 🐨 In the returned object, add a property `[Symbol.asyncDispose]`.
+ // As the value, provide an asynchronous function to it that deletes
+ // the test user you've created.
+ // 🦉 Use the `.deleteMany()` method in Prisma for graceful deletions.
+ // 💰 async [Symbol.asyncDispose]() {
+ // await prisma.user.deleteMany({ where: { id: user.id } })
+ // }
+}
+
+export async function createPasskey(input: {
+ id: string
+ userId: string
+ aaguid: string
+ publicKey: Uint8Array
+ counter?: number
+}) {
+ const passkey = await prisma.passkey.create({
+ data: {
+ id: input.id,
+ aaguid: input.aaguid,
+ userId: input.userId,
+ publicKey: input.publicKey,
+ backedUp: false,
+ webauthnUserId: input.userId,
+ deviceType: 'singleDevice',
+ counter: input.counter || 0,
+ },
+ })
+
+ return {
+ async [Symbol.asyncDispose]() {
+ await prisma.passkey.deleteMany({
+ where: {
+ id: passkey.id,
+ },
+ })
+ },
+ ...passkey,
+ }
+}
+
+export function createPassword(password: string = faker.internet.password()) {
+ return {
+ hash: bcrypt.hashSync(password, 10),
+ }
+}
+
+let noteImages: Array<{ altText: string; objectKey: string }> | undefined
+export async function getNoteImages() {
+ if (noteImages) return noteImages
+
+ noteImages = await Promise.all([
+ {
+ altText: 'a nice country house',
+ objectKey: 'notes/0.png',
+ },
+ {
+ altText: 'a city scape',
+ objectKey: 'notes/1.png',
+ },
+ {
+ altText: 'a sunrise',
+ objectKey: 'notes/2.png',
+ },
+ {
+ altText: 'a group of friends',
+ objectKey: 'notes/3.png',
+ },
+ {
+ altText: 'friends being inclusive of someone who looks lonely',
+ objectKey: 'notes/4.png',
+ },
+ {
+ altText: 'an illustration of a hot air balloon',
+ objectKey: 'notes/5.png',
+ },
+ {
+ altText:
+ 'an office full of laptops and other office equipment that look like it was abandoned in a rush out of the building in an emergency years ago.',
+ objectKey: 'notes/6.png',
+ },
+ {
+ altText: 'a rusty lock',
+ objectKey: 'notes/7.png',
+ },
+ {
+ altText: 'something very happy in nature',
+ objectKey: 'notes/8.png',
+ },
+ {
+ altText: `someone at the end of a cry session who's starting to feel a little better.`,
+ objectKey: 'notes/9.png',
+ },
+ ])
+
+ return noteImages
+}
+
+let userImages: Array<{ objectKey: string }> | undefined
+export async function getUserImages() {
+ if (userImages) return userImages
+
+ userImages = await Promise.all(
+ Array.from({ length: 10 }, (_, index) => ({
+ objectKey: `user/${index}.jpg`,
+ })),
+ )
+
+ return userImages
+}
diff --git a/exercises/02.authentication/01.problem.basic/tests/e2e/authentication-basic.test.ts b/exercises/02.authentication/01.problem.basic/tests/e2e/authentication-basic.test.ts
new file mode 100644
index 0000000..7f75d88
--- /dev/null
+++ b/exercises/02.authentication/01.problem.basic/tests/e2e/authentication-basic.test.ts
@@ -0,0 +1,43 @@
+import { createUser } from '#tests/db-utils.ts'
+import { test, expect } from '#tests/test-extend.ts'
+
+test('authenticates using an email and a password', async ({
+ navigate,
+ page,
+}) => {
+ // 🐨 Create a test user via the `createUser` utility you've prepared earlier.
+ // Note that `createUser` returns a Promise that resolves to a disposable object.
+ // Disposing of the user object is also asynchronous, so declare it appropriately.
+ // 💰 await using name = await util()
+ //
+ // 🐨 Go to the login page.
+ // 💰 await navigate(route)
+ //
+ // 🐨 Fill in the login form.
+ // Locate the form fields by their labels: "Username" and "Password".
+ // Fill in the respective test user's information into those fields.
+ // 💰 await page.getByLabel(label).fill(value)
+ //
+ // 🐨 Submit the login form.
+ // 💰 await page.getByRole('button', { name: accessibleName }).click()
+ //
+ // 🐨 Add an assertion that a link element with the `user.name` text is visible on the page.
+ // 💰 await expect(locator).toBeVisible()
+})
+
+test('displays an error message when authenticating with invalid credentials', async ({
+ navigate,
+ page,
+}) => {
+ // 🐨 Go to the login page.
+ //
+ // 🐨 Fill in the login form with intentionally invalid data.
+ // 💰 await page.getByLabel(label).fill(value)
+ //
+ // 🐨 Submit the login form.
+ // 💰 await page.getByRole('button', { name: accessibleName }).click()
+ //
+ // 🐨 Add an assertion that an element with the "alert" role and text "Invalid username or password"
+ // is visible to the user.
+ // 💰 await expect(locator).toBeVisible()
+})
diff --git a/exercises/02.test-setup/03.problem.authentication/tests/setup/custom-matchers.ts b/exercises/02.authentication/01.problem.basic/tests/setup/custom-matchers.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/tests/setup/custom-matchers.ts
rename to exercises/02.authentication/01.problem.basic/tests/setup/custom-matchers.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/tests/setup/db-setup.ts b/exercises/02.authentication/01.problem.basic/tests/setup/db-setup.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/tests/setup/db-setup.ts
rename to exercises/02.authentication/01.problem.basic/tests/setup/db-setup.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/tests/setup/global-setup.ts b/exercises/02.authentication/01.problem.basic/tests/setup/global-setup.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/tests/setup/global-setup.ts
rename to exercises/02.authentication/01.problem.basic/tests/setup/global-setup.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/tests/setup/setup-test-env.ts b/exercises/02.authentication/01.problem.basic/tests/setup/setup-test-env.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/tests/setup/setup-test-env.ts
rename to exercises/02.authentication/01.problem.basic/tests/setup/setup-test-env.ts
diff --git a/exercises/03.guides/03.solution.blocking-unneeded-requests/tests/test-extend.ts b/exercises/02.authentication/01.problem.basic/tests/test-extend.ts
similarity index 80%
rename from exercises/03.guides/03.solution.blocking-unneeded-requests/tests/test-extend.ts
rename to exercises/02.authentication/01.problem.basic/tests/test-extend.ts
index ce8099b..a51b50d 100644
--- a/exercises/03.guides/03.solution.blocking-unneeded-requests/tests/test-extend.ts
+++ b/exercises/02.authentication/01.problem.basic/tests/test-extend.ts
@@ -1,6 +1,4 @@
-import { createNetworkFixture, type NetworkFixture } from '@msw/playwright'
import { test as testBase, expect } from '@playwright/test'
-import { http, HttpResponse } from 'msw'
import {
definePersona,
combinePersonas,
@@ -9,21 +7,20 @@ import {
import { href, type Register } from 'react-router'
import { getPasswordHash } from '#app/utils/auth.server.ts'
import { prisma } from '#app/utils/db.server.ts'
-import { createUser } from '#tests/db-utils'
+import { generateUserInfo } from '#tests/db-utils'
interface Fixtures {
navigate: (
...args: Parameters>
) => Promise
authenticate: AuthenticateFunction<[typeof user]>
- network: NetworkFixture
}
const user = definePersona('user', {
async createSession({ page }) {
const user = await prisma.user.create({
data: {
- ...createUser(),
+ ...generateUserInfo(),
roles: { connect: { name: 'user' } },
password: { create: { hash: await getPasswordHash('supersecret') } },
},
@@ -55,13 +52,6 @@ export const test = testBase.extend({
})
},
authenticate: combinePersonas(user),
- network: createNetworkFixture({
- initialHandlers: [
- http.get('https://assets.onedollarstats.com/*', () => {
- return new HttpResponse()
- }),
- ],
- }),
})
export { expect }
diff --git a/exercises/02.test-setup/03.problem.authentication/tests/utils.ts b/exercises/02.authentication/01.problem.basic/tests/utils.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/tests/utils.ts
rename to exercises/02.authentication/01.problem.basic/tests/utils.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/tsconfig.json b/exercises/02.authentication/01.problem.basic/tsconfig.json
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/tsconfig.json
rename to exercises/02.authentication/01.problem.basic/tsconfig.json
diff --git a/exercises/02.test-setup/03.problem.authentication/types/deps.d.ts b/exercises/02.authentication/01.problem.basic/types/deps.d.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/types/deps.d.ts
rename to exercises/02.authentication/01.problem.basic/types/deps.d.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/types/env.env.d.ts b/exercises/02.authentication/01.problem.basic/types/env.env.d.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/types/env.env.d.ts
rename to exercises/02.authentication/01.problem.basic/types/env.env.d.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/types/icon-name.d.ts b/exercises/02.authentication/01.problem.basic/types/icon-name.d.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/types/icon-name.d.ts
rename to exercises/02.authentication/01.problem.basic/types/icon-name.d.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/types/reset.d.ts b/exercises/02.authentication/01.problem.basic/types/reset.d.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/types/reset.d.ts
rename to exercises/02.authentication/01.problem.basic/types/reset.d.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/vite.config.ts b/exercises/02.authentication/01.problem.basic/vite.config.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/vite.config.ts
rename to exercises/02.authentication/01.problem.basic/vite.config.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/.env b/exercises/02.authentication/01.solution.basic/.env
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/.env
rename to exercises/02.authentication/01.solution.basic/.env
diff --git a/exercises/02.test-setup/03.solution.authentication/.env.example b/exercises/02.authentication/01.solution.basic/.env.example
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/.env.example
rename to exercises/02.authentication/01.solution.basic/.env.example
diff --git a/exercises/02.test-setup/03.solution.authentication/.gitignore b/exercises/02.authentication/01.solution.basic/.gitignore
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/.gitignore
rename to exercises/02.authentication/01.solution.basic/.gitignore
diff --git a/exercises/02.test-setup/03.solution.authentication/.npmrc b/exercises/02.authentication/01.solution.basic/.npmrc
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/.npmrc
rename to exercises/02.authentication/01.solution.basic/.npmrc
diff --git a/exercises/02.test-setup/03.solution.authentication/.prettierignore b/exercises/02.authentication/01.solution.basic/.prettierignore
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/.prettierignore
rename to exercises/02.authentication/01.solution.basic/.prettierignore
diff --git a/exercises/02.test-setup/03.solution.authentication/.vscode/extensions.json b/exercises/02.authentication/01.solution.basic/.vscode/extensions.json
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/.vscode/extensions.json
rename to exercises/02.authentication/01.solution.basic/.vscode/extensions.json
diff --git a/exercises/02.test-setup/03.solution.authentication/.vscode/remix.code-snippets b/exercises/02.authentication/01.solution.basic/.vscode/remix.code-snippets
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/.vscode/remix.code-snippets
rename to exercises/02.authentication/01.solution.basic/.vscode/remix.code-snippets
diff --git a/exercises/02.test-setup/03.solution.authentication/.vscode/settings.json b/exercises/02.authentication/01.solution.basic/.vscode/settings.json
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/.vscode/settings.json
rename to exercises/02.authentication/01.solution.basic/.vscode/settings.json
diff --git a/exercises/02.authentication/01.solution.basic/README.mdx b/exercises/02.authentication/01.solution.basic/README.mdx
new file mode 100644
index 0000000..5d01ec7
--- /dev/null
+++ b/exercises/02.authentication/01.solution.basic/README.mdx
@@ -0,0 +1,18 @@
+# Basic
+
+- Testing a basic (email+password) authentication.
+
+## Solution
+
+- Explain why `timeout` here is necessary:
+
+```ts
+ async verifySession({ page, session }) {
+ await page.goto('/')
+ await expect(page.getByText(session.user.name!)).toBeVisible({
+ timeout: 100,
+ })
+ },
+```
+
+Without it, the default Playwright timeout of 5s will make auth extremely slow.
diff --git a/exercises/02.test-setup/03.solution.authentication/app/assets/favicons/apple-touch-icon.png b/exercises/02.authentication/01.solution.basic/app/assets/favicons/apple-touch-icon.png
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/assets/favicons/apple-touch-icon.png
rename to exercises/02.authentication/01.solution.basic/app/assets/favicons/apple-touch-icon.png
diff --git a/exercises/02.test-setup/03.solution.authentication/app/assets/favicons/favicon.svg b/exercises/02.authentication/01.solution.basic/app/assets/favicons/favicon.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/assets/favicons/favicon.svg
rename to exercises/02.authentication/01.solution.basic/app/assets/favicons/favicon.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/error-boundary.tsx b/exercises/02.authentication/01.solution.basic/app/components/error-boundary.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/error-boundary.tsx
rename to exercises/02.authentication/01.solution.basic/app/components/error-boundary.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/floating-toolbar.tsx b/exercises/02.authentication/01.solution.basic/app/components/floating-toolbar.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/floating-toolbar.tsx
rename to exercises/02.authentication/01.solution.basic/app/components/floating-toolbar.tsx
diff --git a/exercises/02.authentication/01.solution.basic/app/components/forms.tsx b/exercises/02.authentication/01.solution.basic/app/components/forms.tsx
new file mode 100644
index 0000000..8d3c72a
--- /dev/null
+++ b/exercises/02.authentication/01.solution.basic/app/components/forms.tsx
@@ -0,0 +1,202 @@
+import { useInputControl } from '@conform-to/react'
+import { REGEXP_ONLY_DIGITS_AND_CHARS, type OTPInputProps } from 'input-otp'
+import React, { useId } from 'react'
+import { Checkbox, type CheckboxProps } from './ui/checkbox.tsx'
+import {
+ InputOTP,
+ InputOTPGroup,
+ InputOTPSeparator,
+ InputOTPSlot,
+} from './ui/input-otp.tsx'
+import { Input } from './ui/input.tsx'
+import { Label } from './ui/label.tsx'
+import { Textarea } from './ui/textarea.tsx'
+
+export type ListOfErrors = Array | null | undefined
+
+export function ErrorList({
+ id,
+ errors,
+}: {
+ errors?: ListOfErrors
+ id?: string
+}) {
+ const errorsToRender = errors?.filter(Boolean)
+ if (!errorsToRender?.length) return null
+ return (
+
+ {errorsToRender.map((e) => (
+
+ {e}
+
+ ))}
+
+ )
+}
+
+export function Field({
+ labelProps,
+ inputProps,
+ errors,
+ className,
+}: {
+ labelProps: React.LabelHTMLAttributes
+ inputProps: React.InputHTMLAttributes
+ errors?: ListOfErrors
+ className?: string
+}) {
+ const fallbackId = useId()
+ const id = inputProps.id ?? fallbackId
+ const errorId = errors?.length ? `${id}-error` : undefined
+ return (
+
+
+
+
+ {errorId ? : null}
+
+
+ )
+}
+
+export function OTPField({
+ labelProps,
+ inputProps,
+ errors,
+ className,
+}: {
+ labelProps: React.LabelHTMLAttributes
+ inputProps: Partial
+ errors?: ListOfErrors
+ className?: string
+}) {
+ const fallbackId = useId()
+ const id = inputProps.id ?? fallbackId
+ const errorId = errors?.length ? `${id}-error` : undefined
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {errorId ? : null}
+
+
+ )
+}
+
+export function TextareaField({
+ labelProps,
+ textareaProps,
+ errors,
+ className,
+}: {
+ labelProps: React.LabelHTMLAttributes
+ textareaProps: React.TextareaHTMLAttributes
+ errors?: ListOfErrors
+ className?: string
+}) {
+ const fallbackId = useId()
+ const id = textareaProps.id ?? textareaProps.name ?? fallbackId
+ const errorId = errors?.length ? `${id}-error` : undefined
+ return (
+
+
+
+
+ {errorId ? : null}
+
+
+ )
+}
+
+export function CheckboxField({
+ labelProps,
+ buttonProps,
+ errors,
+ className,
+}: {
+ labelProps: React.ComponentProps<'label'>
+ buttonProps: CheckboxProps & {
+ name: string
+ form: string
+ value?: string
+ }
+ errors?: ListOfErrors
+ className?: string
+}) {
+ const { key, defaultChecked, ...checkboxProps } = buttonProps
+ const fallbackId = useId()
+ const checkedValue = buttonProps.value ?? 'on'
+ const input = useInputControl({
+ key,
+ name: buttonProps.name,
+ formId: buttonProps.form,
+ initialValue: defaultChecked ? checkedValue : undefined,
+ })
+ const id = buttonProps.id ?? fallbackId
+ const errorId = errors?.length ? `${id}-error` : undefined
+
+ return (
+
+
+ {
+ input.change(state.valueOf() ? checkedValue : '')
+ buttonProps.onCheckedChange?.(state)
+ }}
+ onFocus={(event) => {
+ input.focus()
+ buttonProps.onFocus?.(event)
+ }}
+ onBlur={(event) => {
+ input.blur()
+ buttonProps.onBlur?.(event)
+ }}
+ type="button"
+ />
+
+
+
+ {errorId ? : null}
+
+
+ )
+}
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/progress-bar.tsx b/exercises/02.authentication/01.solution.basic/app/components/progress-bar.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/progress-bar.tsx
rename to exercises/02.authentication/01.solution.basic/app/components/progress-bar.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/search-bar.tsx b/exercises/02.authentication/01.solution.basic/app/components/search-bar.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/search-bar.tsx
rename to exercises/02.authentication/01.solution.basic/app/components/search-bar.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/spacer.tsx b/exercises/02.authentication/01.solution.basic/app/components/spacer.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/spacer.tsx
rename to exercises/02.authentication/01.solution.basic/app/components/spacer.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/toaster.tsx b/exercises/02.authentication/01.solution.basic/app/components/toaster.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/toaster.tsx
rename to exercises/02.authentication/01.solution.basic/app/components/toaster.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/ui/README.md b/exercises/02.authentication/01.solution.basic/app/components/ui/README.md
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/ui/README.md
rename to exercises/02.authentication/01.solution.basic/app/components/ui/README.md
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/ui/button.tsx b/exercises/02.authentication/01.solution.basic/app/components/ui/button.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/ui/button.tsx
rename to exercises/02.authentication/01.solution.basic/app/components/ui/button.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/ui/checkbox.tsx b/exercises/02.authentication/01.solution.basic/app/components/ui/checkbox.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/ui/checkbox.tsx
rename to exercises/02.authentication/01.solution.basic/app/components/ui/checkbox.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/ui/dropdown-menu.tsx b/exercises/02.authentication/01.solution.basic/app/components/ui/dropdown-menu.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/ui/dropdown-menu.tsx
rename to exercises/02.authentication/01.solution.basic/app/components/ui/dropdown-menu.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/ui/icon.tsx b/exercises/02.authentication/01.solution.basic/app/components/ui/icon.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/ui/icon.tsx
rename to exercises/02.authentication/01.solution.basic/app/components/ui/icon.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/ui/input-otp.tsx b/exercises/02.authentication/01.solution.basic/app/components/ui/input-otp.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/ui/input-otp.tsx
rename to exercises/02.authentication/01.solution.basic/app/components/ui/input-otp.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/ui/input.tsx b/exercises/02.authentication/01.solution.basic/app/components/ui/input.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/ui/input.tsx
rename to exercises/02.authentication/01.solution.basic/app/components/ui/input.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/ui/label.tsx b/exercises/02.authentication/01.solution.basic/app/components/ui/label.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/ui/label.tsx
rename to exercises/02.authentication/01.solution.basic/app/components/ui/label.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/ui/sonner.tsx b/exercises/02.authentication/01.solution.basic/app/components/ui/sonner.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/ui/sonner.tsx
rename to exercises/02.authentication/01.solution.basic/app/components/ui/sonner.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/ui/status-button.tsx b/exercises/02.authentication/01.solution.basic/app/components/ui/status-button.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/ui/status-button.tsx
rename to exercises/02.authentication/01.solution.basic/app/components/ui/status-button.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/ui/textarea.tsx b/exercises/02.authentication/01.solution.basic/app/components/ui/textarea.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/ui/textarea.tsx
rename to exercises/02.authentication/01.solution.basic/app/components/ui/textarea.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/ui/tooltip.tsx b/exercises/02.authentication/01.solution.basic/app/components/ui/tooltip.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/ui/tooltip.tsx
rename to exercises/02.authentication/01.solution.basic/app/components/ui/tooltip.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/user-dropdown.tsx b/exercises/02.authentication/01.solution.basic/app/components/user-dropdown.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/user-dropdown.tsx
rename to exercises/02.authentication/01.solution.basic/app/components/user-dropdown.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/entry.client.tsx b/exercises/02.authentication/01.solution.basic/app/entry.client.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/entry.client.tsx
rename to exercises/02.authentication/01.solution.basic/app/entry.client.tsx
diff --git a/exercises/02.authentication/01.solution.basic/app/entry.server.tsx b/exercises/02.authentication/01.solution.basic/app/entry.server.tsx
new file mode 100644
index 0000000..8d8b1de
--- /dev/null
+++ b/exercises/02.authentication/01.solution.basic/app/entry.server.tsx
@@ -0,0 +1,143 @@
+import crypto from 'node:crypto'
+import { PassThrough } from 'node:stream'
+import { styleText } from 'node:util'
+import { contentSecurity } from '@nichtsam/helmet/content'
+import { createReadableStreamFromReadable } from '@react-router/node'
+import * as Sentry from '@sentry/react-router'
+import { isbot } from 'isbot'
+import { renderToPipeableStream } from 'react-dom/server'
+import {
+ ServerRouter,
+ type LoaderFunctionArgs,
+ type ActionFunctionArgs,
+ type HandleDocumentRequestFunction,
+} from 'react-router'
+import { getEnv, init } from './utils/env.server.ts'
+import { getInstanceInfo } from './utils/litefs.server.ts'
+import { NonceProvider } from './utils/nonce-provider.ts'
+import { makeTimings } from './utils/timing.server.ts'
+
+export const streamTimeout = 5000
+
+init()
+global.ENV = getEnv()
+
+const MODE = process.env.NODE_ENV ?? 'development'
+
+type DocRequestArgs = Parameters
+
+export default async function handleRequest(...args: DocRequestArgs) {
+ const [request, responseStatusCode, responseHeaders, reactRouterContext] =
+ args
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ responseHeaders.set('fly-primary-instance', primaryInstance)
+ responseHeaders.set('fly-instance', currentInstance)
+
+ if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
+ responseHeaders.append('Document-Policy', 'js-profiling')
+ }
+
+ const callbackName = isbot(request.headers.get('user-agent'))
+ ? 'onAllReady'
+ : 'onShellReady'
+
+ const nonce = crypto.randomBytes(16).toString('hex')
+ return new Promise(async (resolve, reject) => {
+ let didError = false
+ // NOTE: this timing will only include things that are rendered in the shell
+ // and will not include suspended components and deferred loaders
+ const timings = makeTimings('render', 'renderToPipeableStream')
+
+ const { pipe, abort } = renderToPipeableStream(
+
+
+ ,
+ {
+ [callbackName]: () => {
+ const body = new PassThrough()
+ responseHeaders.set('Content-Type', 'text/html')
+ responseHeaders.append('Server-Timing', timings.toString())
+
+ contentSecurity(responseHeaders, {
+ crossOriginEmbedderPolicy: false,
+ contentSecurityPolicy: {
+ // NOTE: Remove reportOnly when you're ready to enforce this CSP
+ reportOnly: true,
+ directives: {
+ fetch: {
+ 'connect-src': [
+ MODE === 'development' ? 'ws:' : undefined,
+ process.env.SENTRY_DSN ? '*.sentry.io' : undefined,
+ "'self'",
+ ],
+ 'font-src': ["'self'"],
+ 'frame-src': ["'self'"],
+ 'img-src': ["'self'", 'data:'],
+ 'script-src': [
+ "'strict-dynamic'",
+ "'self'",
+ `'nonce-${nonce}'`,
+ ],
+ 'script-src-attr': [`'nonce-${nonce}'`],
+ },
+ },
+ },
+ xFrameOptions: false,
+ })
+
+ resolve(
+ new Response(createReadableStreamFromReadable(body), {
+ headers: responseHeaders,
+ status: didError ? 500 : responseStatusCode,
+ }),
+ )
+ pipe(body)
+ },
+ onShellError: (err: unknown) => {
+ reject(err)
+ },
+ onError: () => {
+ didError = true
+ },
+ nonce,
+ },
+ )
+
+ setTimeout(abort, streamTimeout + 5000)
+ })
+}
+
+export async function handleDataRequest(response: Response) {
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ response.headers.set('fly-primary-instance', primaryInstance)
+ response.headers.set('fly-instance', currentInstance)
+
+ return response
+}
+
+export function handleError(
+ error: unknown,
+ { request }: LoaderFunctionArgs | ActionFunctionArgs,
+): void {
+ // Skip capturing if the request is aborted as Remix docs suggest
+ // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
+ if (request.signal.aborted) {
+ return
+ }
+
+ if (error instanceof Error) {
+ console.error(styleText('red', String(error.stack)))
+ } else {
+ console.error(error)
+ }
+
+ Sentry.captureException(error)
+}
diff --git a/exercises/02.test-setup/03.solution.authentication/app/root.tsx b/exercises/02.authentication/01.solution.basic/app/root.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/root.tsx
rename to exercises/02.authentication/01.solution.basic/app/root.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes.ts b/exercises/02.authentication/01.solution.basic/app/routes.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes.ts
rename to exercises/02.authentication/01.solution.basic/app/routes.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/$.tsx b/exercises/02.authentication/01.solution.basic/app/routes/$.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/$.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/$.tsx
diff --git a/exercises/03.guides/03.problem.blocking-unneeded-requests/app/routes/_auth+/auth.$provider.callback.test.ts b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/auth.$provider.callback.test.ts
similarity index 97%
rename from exercises/03.guides/03.problem.blocking-unneeded-requests/app/routes/_auth+/auth.$provider.callback.test.ts
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/auth.$provider.callback.test.ts
index 643e453..3765dd7 100644
--- a/exercises/03.guides/03.problem.blocking-unneeded-requests/app/routes/_auth+/auth.$provider.callback.test.ts
+++ b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/auth.$provider.callback.test.ts
@@ -9,7 +9,7 @@ import { GITHUB_PROVIDER_NAME } from '#app/utils/connections.tsx'
import { prisma } from '#app/utils/db.server.ts'
import { authSessionStorage } from '#app/utils/session.server.ts'
import { generateTOTP } from '#app/utils/totp.server.ts'
-import { createUser } from '#tests/db-utils.ts'
+import { generateUserInfo } from '#tests/db-utils.ts'
import { insertGitHubUser, deleteGitHubUsers } from '#tests/mocks/github.ts'
import { server } from '#tests/mocks/index.ts'
import { consoleError } from '#tests/setup/setup-test-env.ts'
@@ -109,7 +109,7 @@ test(`when a user is logged in and has already connected, it doesn't do anything
test('when a user exists with the same email, create connection and make session', async () => {
const githubUser = await insertGitHubUser()
const email = githubUser.primaryEmail.toLowerCase()
- const { userId } = await setupUser({ ...createUser(), email })
+ const { userId } = await setupUser({ ...generateUserInfo(), email })
const request = await setupRequest({ code: githubUser.code })
const response = await loader({ request, params: PARAMS, context: {} })
@@ -141,7 +141,7 @@ test('gives an error if the account is already connected to another user', async
const githubUser = await insertGitHubUser()
await prisma.user.create({
data: {
- ...createUser(),
+ ...generateUserInfo(),
connections: {
create: {
providerName: GITHUB_PROVIDER_NAME,
@@ -245,7 +245,7 @@ async function setupRequest({
return request
}
-async function setupUser(userData = createUser()) {
+async function setupUser(userData = generateUserInfo()) {
const session = await prisma.session.create({
data: {
expirationDate: getSessionExpirationDate(),
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/auth.$provider.callback.ts b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/auth.$provider.callback.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/auth.$provider.callback.ts
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/auth.$provider.callback.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/auth_.$provider.ts b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/auth_.$provider.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/auth_.$provider.ts
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/auth_.$provider.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/forgot-password.tsx b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/forgot-password.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/forgot-password.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/forgot-password.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/login.server.ts b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/login.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/login.server.ts
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/login.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/login.tsx b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/login.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/login.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/login.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/logout.tsx b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/logout.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/logout.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/logout.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/onboarding.server.ts b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/onboarding.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/onboarding.server.ts
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/onboarding.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/onboarding.tsx b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/onboarding.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/onboarding.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/onboarding.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/onboarding_.$provider.server.ts b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/onboarding_.$provider.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/onboarding_.$provider.server.ts
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/onboarding_.$provider.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/onboarding_.$provider.tsx b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/onboarding_.$provider.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/onboarding_.$provider.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/onboarding_.$provider.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/reset-password.server.ts b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/reset-password.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/reset-password.server.ts
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/reset-password.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/reset-password.tsx b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/reset-password.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/reset-password.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/reset-password.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/signup.tsx b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/signup.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/signup.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/signup.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/verify.server.ts b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/verify.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/verify.server.ts
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/verify.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/verify.tsx b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/verify.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/verify.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/verify.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/webauthn+/authentication.ts b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/webauthn+/authentication.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/webauthn+/authentication.ts
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/webauthn+/authentication.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/webauthn+/registration.ts b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/webauthn+/registration.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/webauthn+/registration.ts
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/webauthn+/registration.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/webauthn+/utils.server.ts b/exercises/02.authentication/01.solution.basic/app/routes/_auth+/webauthn+/utils.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_auth+/webauthn+/utils.server.ts
rename to exercises/02.authentication/01.solution.basic/app/routes/_auth+/webauthn+/utils.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/about.tsx b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/about.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/about.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/about.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/index.tsx b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/index.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/index.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/index.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/docker.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/docker.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/docker.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/docker.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/eslint.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/eslint.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/eslint.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/eslint.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/faker.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/faker.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/faker.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/faker.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/fly.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/fly.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/fly.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/fly.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/github.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/github.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/github.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/github.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/logos.ts b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/logos.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/logos.ts
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/logos.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/msw.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/msw.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/msw.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/msw.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/playwright.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/playwright.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/playwright.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/playwright.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/prettier.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/prettier.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/prettier.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/prettier.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/prisma.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/prisma.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/prisma.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/prisma.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/radix.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/radix.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/radix.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/radix.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/react-email.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/react-email.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/react-email.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/react-email.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/remix.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/remix.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/remix.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/remix.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/resend.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/resend.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/resend.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/resend.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/sentry.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/sentry.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/sentry.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/sentry.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/shadcn-ui.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/shadcn-ui.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/shadcn-ui.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/shadcn-ui.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/sqlite.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/sqlite.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/sqlite.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/sqlite.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/stars.jpg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/stars.jpg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/stars.jpg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/stars.jpg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/tailwind.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/tailwind.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/tailwind.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/tailwind.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/testing-library.png b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/testing-library.png
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/testing-library.png
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/testing-library.png
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/typescript.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/typescript.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/typescript.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/typescript.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/vitest.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/vitest.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/vitest.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/vitest.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/zod.svg b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/zod.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/logos/zod.svg
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/logos/zod.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/privacy.tsx b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/privacy.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/privacy.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/privacy.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/support.tsx b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/support.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/support.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/support.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/tos.tsx b/exercises/02.authentication/01.solution.basic/app/routes/_marketing+/tos.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_marketing+/tos.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/_marketing+/tos.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_seo+/robots[.]txt.ts b/exercises/02.authentication/01.solution.basic/app/routes/_seo+/robots[.]txt.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_seo+/robots[.]txt.ts
rename to exercises/02.authentication/01.solution.basic/app/routes/_seo+/robots[.]txt.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/_seo+/sitemap[.]xml.ts b/exercises/02.authentication/01.solution.basic/app/routes/_seo+/sitemap[.]xml.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/_seo+/sitemap[.]xml.ts
rename to exercises/02.authentication/01.solution.basic/app/routes/_seo+/sitemap[.]xml.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/admin+/cache.tsx b/exercises/02.authentication/01.solution.basic/app/routes/admin+/cache.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/admin+/cache.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/admin+/cache.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/admin+/cache_.lru.$cacheKey.ts b/exercises/02.authentication/01.solution.basic/app/routes/admin+/cache_.lru.$cacheKey.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/admin+/cache_.lru.$cacheKey.ts
rename to exercises/02.authentication/01.solution.basic/app/routes/admin+/cache_.lru.$cacheKey.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/admin+/cache_.sqlite.$cacheKey.ts b/exercises/02.authentication/01.solution.basic/app/routes/admin+/cache_.sqlite.$cacheKey.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/admin+/cache_.sqlite.$cacheKey.ts
rename to exercises/02.authentication/01.solution.basic/app/routes/admin+/cache_.sqlite.$cacheKey.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/admin+/cache_.sqlite.server.ts b/exercises/02.authentication/01.solution.basic/app/routes/admin+/cache_.sqlite.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/admin+/cache_.sqlite.server.ts
rename to exercises/02.authentication/01.solution.basic/app/routes/admin+/cache_.sqlite.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/admin+/cache_.sqlite.tsx b/exercises/02.authentication/01.solution.basic/app/routes/admin+/cache_.sqlite.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/admin+/cache_.sqlite.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/admin+/cache_.sqlite.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/me.tsx b/exercises/02.authentication/01.solution.basic/app/routes/me.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/me.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/me.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/resources+/download-user-data.tsx b/exercises/02.authentication/01.solution.basic/app/routes/resources+/download-user-data.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/resources+/download-user-data.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/resources+/download-user-data.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/resources+/healthcheck.tsx b/exercises/02.authentication/01.solution.basic/app/routes/resources+/healthcheck.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/resources+/healthcheck.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/resources+/healthcheck.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/resources+/images.tsx b/exercises/02.authentication/01.solution.basic/app/routes/resources+/images.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/resources+/images.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/resources+/images.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/resources+/theme-switch.tsx b/exercises/02.authentication/01.solution.basic/app/routes/resources+/theme-switch.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/resources+/theme-switch.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/resources+/theme-switch.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.change-email.server.tsx b/exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.change-email.server.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.change-email.server.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.change-email.server.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.change-email.tsx b/exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.change-email.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.change-email.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.change-email.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.connections.tsx b/exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.connections.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.connections.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.connections.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.index.tsx b/exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.index.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.index.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.index.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.passkeys.tsx b/exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.passkeys.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.passkeys.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.passkeys.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.password.tsx b/exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.password.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.password.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.password.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.password_.create.tsx b/exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.password_.create.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.password_.create.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.password_.create.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.photo.tsx b/exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.photo.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.photo.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.photo.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.tsx b/exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.two-factor.disable.tsx b/exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.two-factor.disable.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.two-factor.disable.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.two-factor.disable.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.two-factor.index.tsx b/exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.two-factor.index.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.two-factor.index.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.two-factor.index.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.two-factor.tsx b/exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.two-factor.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.two-factor.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.two-factor.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.two-factor.verify.tsx b/exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.two-factor.verify.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/settings+/profile.two-factor.verify.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/settings+/profile.two-factor.verify.tsx
diff --git a/exercises/03.guides/03.problem.blocking-unneeded-requests/app/routes/users+/$username.test.tsx b/exercises/02.authentication/01.solution.basic/app/routes/users+/$username.test.tsx
similarity index 93%
rename from exercises/03.guides/03.problem.blocking-unneeded-requests/app/routes/users+/$username.test.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/users+/$username.test.tsx
index b0784bd..0884a24 100644
--- a/exercises/03.guides/03.problem.blocking-unneeded-requests/app/routes/users+/$username.test.tsx
+++ b/exercises/02.authentication/01.solution.basic/app/routes/users+/$username.test.tsx
@@ -10,7 +10,7 @@ import { loader as rootLoader } from '#app/root.tsx'
import { getSessionExpirationDate, sessionKey } from '#app/utils/auth.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { authSessionStorage } from '#app/utils/session.server.ts'
-import { createUser, getUserImages } from '#tests/db-utils.ts'
+import { generateUserInfo, getUserImages } from '#tests/db-utils.ts'
import { default as UsernameRoute, loader } from './$username.tsx'
test('The user profile when not logged in as self', async () => {
@@ -19,7 +19,7 @@ test('The user profile when not logged in as self', async () => {
userImages[faker.number.int({ min: 0, max: userImages.length - 1 })]
const user = await prisma.user.create({
select: { id: true, username: true, name: true },
- data: { ...createUser(), image: { create: userImage } },
+ data: { ...generateUserInfo(), image: { create: userImage } },
})
const App = createRoutesStub([
{
@@ -44,7 +44,7 @@ test('The user profile when logged in as self', async () => {
userImages[faker.number.int({ min: 0, max: userImages.length - 1 })]
const user = await prisma.user.create({
select: { id: true, username: true, name: true },
- data: { ...createUser(), image: { create: userImage } },
+ data: { ...generateUserInfo(), image: { create: userImage } },
})
const session = await prisma.session.create({
select: { id: true },
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/users+/$username.tsx b/exercises/02.authentication/01.solution.basic/app/routes/users+/$username.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/users+/$username.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/users+/$username.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/users+/$username_+/__note-editor.server.tsx b/exercises/02.authentication/01.solution.basic/app/routes/users+/$username_+/__note-editor.server.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/users+/$username_+/__note-editor.server.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/users+/$username_+/__note-editor.server.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/users+/$username_+/__note-editor.tsx b/exercises/02.authentication/01.solution.basic/app/routes/users+/$username_+/__note-editor.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/users+/$username_+/__note-editor.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/users+/$username_+/__note-editor.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/users+/$username_+/notes.$noteId.tsx b/exercises/02.authentication/01.solution.basic/app/routes/users+/$username_+/notes.$noteId.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/users+/$username_+/notes.$noteId.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/users+/$username_+/notes.$noteId.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/users+/$username_+/notes.$noteId_.edit.tsx b/exercises/02.authentication/01.solution.basic/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/users+/$username_+/notes.index.tsx b/exercises/02.authentication/01.solution.basic/app/routes/users+/$username_+/notes.index.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/users+/$username_+/notes.index.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/users+/$username_+/notes.index.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/users+/$username_+/notes.new.tsx b/exercises/02.authentication/01.solution.basic/app/routes/users+/$username_+/notes.new.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/users+/$username_+/notes.new.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/users+/$username_+/notes.new.tsx
diff --git a/exercises/02.authentication/01.solution.basic/app/routes/users+/$username_+/notes.tsx b/exercises/02.authentication/01.solution.basic/app/routes/users+/$username_+/notes.tsx
new file mode 100644
index 0000000..ded41ca
--- /dev/null
+++ b/exercises/02.authentication/01.solution.basic/app/routes/users+/$username_+/notes.tsx
@@ -0,0 +1,105 @@
+import { invariantResponse } from '@epic-web/invariant'
+import { Img } from 'openimg/react'
+import { Link, NavLink, Outlet } from 'react-router'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { cn, getUserImgSrc } from '#app/utils/misc.tsx'
+import { useOptionalUser } from '#app/utils/user.ts'
+import { type Route } from './+types/notes.ts'
+
+export async function loader({ params }: Route.LoaderArgs) {
+ const owner = await prisma.user.findFirst({
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ image: { select: { objectKey: true } },
+ notes: { select: { id: true, title: true } },
+ },
+ where: { username: params.username },
+ })
+
+ invariantResponse(owner, 'Owner not found', { status: 404 })
+
+ return { owner }
+}
+
+export default function NotesRoute({ loaderData }: Route.ComponentProps) {
+ const user = useOptionalUser()
+ const isOwner = user?.id === loaderData.owner.id
+ const ownerDisplayName = loaderData.owner.name ?? loaderData.owner.username
+ const navLinkDefaultClassName =
+ 'line-clamp-2 block rounded-l-full py-2 pl-8 pr-6 text-base lg:text-xl'
+ return (
+
+
+
+
+
+
+
+ {ownerDisplayName}'s Notes
+
+
+
+ {isOwner ? (
+
+
+ cn(navLinkDefaultClassName, isActive && 'bg-accent')
+ }
+ >
+ New Note
+
+
+ ) : null}
+ {loaderData.owner.notes.map((note) => (
+
+
+ cn(navLinkDefaultClassName, isActive && 'bg-accent')
+ }
+ >
+ {note.title}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return (
+ (
+ No user with the username "{params.username}" exists
+ ),
+ }}
+ />
+ )
+}
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/users+/index.tsx b/exercises/02.authentication/01.solution.basic/app/routes/users+/index.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/routes/users+/index.tsx
rename to exercises/02.authentication/01.solution.basic/app/routes/users+/index.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/styles/tailwind.css b/exercises/02.authentication/01.solution.basic/app/styles/tailwind.css
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/styles/tailwind.css
rename to exercises/02.authentication/01.solution.basic/app/styles/tailwind.css
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/auth.server.test.ts b/exercises/02.authentication/01.solution.basic/app/utils/auth.server.test.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/auth.server.test.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/auth.server.test.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/auth.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/auth.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/auth.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/auth.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/cache.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/cache.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/cache.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/cache.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/client-hints.tsx b/exercises/02.authentication/01.solution.basic/app/utils/client-hints.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/client-hints.tsx
rename to exercises/02.authentication/01.solution.basic/app/utils/client-hints.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/connections.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/connections.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/connections.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/connections.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/connections.tsx b/exercises/02.authentication/01.solution.basic/app/utils/connections.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/connections.tsx
rename to exercises/02.authentication/01.solution.basic/app/utils/connections.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/db.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/db.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/db.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/db.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/email.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/email.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/email.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/email.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/env.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/env.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/env.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/env.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/headers.server.test.ts b/exercises/02.authentication/01.solution.basic/app/utils/headers.server.test.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/headers.server.test.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/headers.server.test.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/headers.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/headers.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/headers.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/headers.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/honeypot.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/honeypot.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/honeypot.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/honeypot.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/litefs.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/litefs.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/litefs.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/litefs.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/misc.error-message.test.ts b/exercises/02.authentication/01.solution.basic/app/utils/misc.error-message.test.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/misc.error-message.test.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/misc.error-message.test.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/misc.tsx b/exercises/02.authentication/01.solution.basic/app/utils/misc.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/misc.tsx
rename to exercises/02.authentication/01.solution.basic/app/utils/misc.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/misc.use-double-check.test.tsx b/exercises/02.authentication/01.solution.basic/app/utils/misc.use-double-check.test.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/misc.use-double-check.test.tsx
rename to exercises/02.authentication/01.solution.basic/app/utils/misc.use-double-check.test.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/monitoring.client.tsx b/exercises/02.authentication/01.solution.basic/app/utils/monitoring.client.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/monitoring.client.tsx
rename to exercises/02.authentication/01.solution.basic/app/utils/monitoring.client.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/nonce-provider.ts b/exercises/02.authentication/01.solution.basic/app/utils/nonce-provider.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/nonce-provider.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/nonce-provider.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/permissions.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/permissions.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/permissions.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/permissions.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/providers/constants.ts b/exercises/02.authentication/01.solution.basic/app/utils/providers/constants.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/providers/constants.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/providers/constants.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/providers/github.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/providers/github.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/providers/github.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/providers/github.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/providers/provider.ts b/exercises/02.authentication/01.solution.basic/app/utils/providers/provider.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/providers/provider.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/providers/provider.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/redirect-cookie.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/redirect-cookie.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/redirect-cookie.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/redirect-cookie.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/request-info.ts b/exercises/02.authentication/01.solution.basic/app/utils/request-info.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/request-info.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/request-info.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/session.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/session.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/session.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/session.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/storage.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/storage.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/storage.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/storage.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/theme.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/theme.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/theme.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/theme.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/timing.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/timing.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/timing.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/timing.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/toast.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/toast.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/toast.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/toast.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/totp.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/totp.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/totp.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/totp.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/user-validation.ts b/exercises/02.authentication/01.solution.basic/app/utils/user-validation.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/user-validation.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/user-validation.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/user.ts b/exercises/02.authentication/01.solution.basic/app/utils/user.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/user.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/user.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/app/utils/verification.server.ts b/exercises/02.authentication/01.solution.basic/app/utils/verification.server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/utils/verification.server.ts
rename to exercises/02.authentication/01.solution.basic/app/utils/verification.server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/components.json b/exercises/02.authentication/01.solution.basic/components.json
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/components.json
rename to exercises/02.authentication/01.solution.basic/components.json
diff --git a/exercises/02.test-setup/03.solution.authentication/eslint.config.js b/exercises/02.authentication/01.solution.basic/eslint.config.js
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/eslint.config.js
rename to exercises/02.authentication/01.solution.basic/eslint.config.js
diff --git a/exercises/02.test-setup/03.solution.authentication/fly.toml b/exercises/02.authentication/01.solution.basic/fly.toml
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/fly.toml
rename to exercises/02.authentication/01.solution.basic/fly.toml
diff --git a/exercises/02.test-setup/03.solution.authentication/index.js b/exercises/02.authentication/01.solution.basic/index.js
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/index.js
rename to exercises/02.authentication/01.solution.basic/index.js
diff --git a/exercises/02.test-setup/03.solution.authentication/other/Dockerfile b/exercises/02.authentication/01.solution.basic/other/Dockerfile
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/Dockerfile
rename to exercises/02.authentication/01.solution.basic/other/Dockerfile
diff --git a/exercises/02.test-setup/03.solution.authentication/other/Dockerfile.dockerignore b/exercises/02.authentication/01.solution.basic/other/Dockerfile.dockerignore
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/Dockerfile.dockerignore
rename to exercises/02.authentication/01.solution.basic/other/Dockerfile.dockerignore
diff --git a/exercises/02.test-setup/03.solution.authentication/other/README.md b/exercises/02.authentication/01.solution.basic/other/README.md
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/README.md
rename to exercises/02.authentication/01.solution.basic/other/README.md
diff --git a/exercises/02.test-setup/03.solution.authentication/other/build-server.ts b/exercises/02.authentication/01.solution.basic/other/build-server.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/build-server.ts
rename to exercises/02.authentication/01.solution.basic/other/build-server.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/other/litefs.yml b/exercises/02.authentication/01.solution.basic/other/litefs.yml
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/litefs.yml
rename to exercises/02.authentication/01.solution.basic/other/litefs.yml
diff --git a/exercises/02.test-setup/03.solution.authentication/other/sly/sly.json b/exercises/02.authentication/01.solution.basic/other/sly/sly.json
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/sly/sly.json
rename to exercises/02.authentication/01.solution.basic/other/sly/sly.json
diff --git a/exercises/02.test-setup/03.solution.authentication/other/sly/transform-icon.ts b/exercises/02.authentication/01.solution.basic/other/sly/transform-icon.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/sly/transform-icon.ts
rename to exercises/02.authentication/01.solution.basic/other/sly/transform-icon.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/README.md b/exercises/02.authentication/01.solution.basic/other/svg-icons/README.md
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/README.md
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/README.md
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/arrow-left.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/arrow-left.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/arrow-left.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/arrow-left.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/arrow-right.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/arrow-right.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/arrow-right.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/arrow-right.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/avatar.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/avatar.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/avatar.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/avatar.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/camera.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/camera.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/camera.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/camera.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/check.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/check.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/check.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/check.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/clock.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/clock.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/clock.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/clock.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/cross-1.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/cross-1.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/cross-1.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/cross-1.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/dots-horizontal.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/dots-horizontal.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/dots-horizontal.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/dots-horizontal.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/download.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/download.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/download.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/download.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/envelope-closed.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/envelope-closed.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/envelope-closed.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/envelope-closed.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/exit.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/exit.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/exit.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/exit.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/file-text.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/file-text.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/file-text.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/file-text.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/github-logo.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/github-logo.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/github-logo.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/github-logo.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/laptop.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/laptop.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/laptop.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/laptop.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/link-2.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/link-2.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/link-2.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/link-2.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/lock-closed.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/lock-closed.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/lock-closed.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/lock-closed.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/lock-open-1.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/lock-open-1.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/lock-open-1.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/lock-open-1.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/magnifying-glass.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/magnifying-glass.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/magnifying-glass.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/magnifying-glass.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/moon.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/moon.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/moon.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/moon.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/passkey.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/passkey.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/passkey.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/passkey.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/pencil-1.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/pencil-1.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/pencil-1.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/pencil-1.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/pencil-2.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/pencil-2.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/pencil-2.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/pencil-2.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/plus.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/plus.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/plus.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/plus.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/question-mark-circled.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/question-mark-circled.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/question-mark-circled.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/question-mark-circled.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/reset.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/reset.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/reset.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/reset.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/sun.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/sun.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/sun.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/sun.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/trash.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/trash.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/trash.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/trash.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/other/svg-icons/update.svg b/exercises/02.authentication/01.solution.basic/other/svg-icons/update.svg
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/other/svg-icons/update.svg
rename to exercises/02.authentication/01.solution.basic/other/svg-icons/update.svg
diff --git a/exercises/02.test-setup/03.solution.authentication/package-lock.json b/exercises/02.authentication/01.solution.basic/package-lock.json
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/package-lock.json
rename to exercises/02.authentication/01.solution.basic/package-lock.json
diff --git a/exercises/02.test-setup/05.solution.test-data/package.json b/exercises/02.authentication/01.solution.basic/package.json
similarity index 97%
rename from exercises/02.test-setup/05.solution.test-data/package.json
rename to exercises/02.authentication/01.solution.basic/package.json
index e5f4b2a..2285884 100644
--- a/exercises/02.test-setup/05.solution.test-data/package.json
+++ b/exercises/02.authentication/01.solution.basic/package.json
@@ -1,5 +1,5 @@
{
- "name": "exercises_02.test-setup_05.solution.test-data",
+ "name": "exercises_02.authentication_01.solution.basic",
"private": true,
"sideEffects": false,
"type": "module",
@@ -115,7 +115,7 @@
"devDependencies": {
"@epic-web/config": "^1.20.1",
"@faker-js/faker": "^9.7.0",
- "@playwright/test": "^1.52.0",
+ "@playwright/test": "^1.57.0",
"@react-router/dev": "^7.5.3",
"@sly-cli/sly": "^2.1.1",
"@testing-library/dom": "^10.4.0",
@@ -145,7 +145,7 @@
"jsdom": "^25.0.1",
"msw": "^2.7.6",
"npm-run-all": "^4.1.5",
- "playwright-persona": "^0.2.5",
+ "playwright-persona": "^0.2.8",
"prettier": "^3.5.3",
"prettier-plugin-sql": "^0.19.0",
"prettier-plugin-tailwindcss": "^0.6.11",
diff --git a/exercises/02.test-setup/03.solution.authentication/playwright.config.ts b/exercises/02.authentication/01.solution.basic/playwright.config.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/playwright.config.ts
rename to exercises/02.authentication/01.solution.basic/playwright.config.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/prisma/migrations/20250221233640_init/migration.sql b/exercises/02.authentication/01.solution.basic/prisma/migrations/20250221233640_init/migration.sql
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/prisma/migrations/20250221233640_init/migration.sql
rename to exercises/02.authentication/01.solution.basic/prisma/migrations/20250221233640_init/migration.sql
diff --git a/exercises/02.test-setup/03.solution.authentication/prisma/migrations/migration_lock.toml b/exercises/02.authentication/01.solution.basic/prisma/migrations/migration_lock.toml
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/prisma/migrations/migration_lock.toml
rename to exercises/02.authentication/01.solution.basic/prisma/migrations/migration_lock.toml
diff --git a/exercises/02.test-setup/03.solution.authentication/prisma/schema.prisma b/exercises/02.authentication/01.solution.basic/prisma/schema.prisma
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/prisma/schema.prisma
rename to exercises/02.authentication/01.solution.basic/prisma/schema.prisma
diff --git a/exercises/03.guides/03.problem.blocking-unneeded-requests/prisma/seed.ts b/exercises/02.authentication/01.solution.basic/prisma/seed.ts
similarity index 99%
rename from exercises/03.guides/03.problem.blocking-unneeded-requests/prisma/seed.ts
rename to exercises/02.authentication/01.solution.basic/prisma/seed.ts
index 8454353..521e1d5 100644
--- a/exercises/03.guides/03.problem.blocking-unneeded-requests/prisma/seed.ts
+++ b/exercises/02.authentication/01.solution.basic/prisma/seed.ts
@@ -3,7 +3,7 @@ import { prisma } from '#app/utils/db.server.ts'
import { MOCK_CODE_GITHUB } from '#app/utils/providers/constants'
import {
createPassword,
- createUser,
+ generateUserInfo,
getNoteImages,
getUserImages,
} from '#tests/db-utils.ts'
@@ -19,7 +19,7 @@ async function seed() {
const userImages = await getUserImages()
for (let index = 0; index < totalUsers; index++) {
- const userData = createUser()
+ const userData = generateUserInfo()
const user = await prisma.user.create({
select: { id: true },
data: {
diff --git a/exercises/02.test-setup/03.solution.authentication/prisma/sql/searchUsers.sql b/exercises/02.authentication/01.solution.basic/prisma/sql/searchUsers.sql
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/prisma/sql/searchUsers.sql
rename to exercises/02.authentication/01.solution.basic/prisma/sql/searchUsers.sql
diff --git a/exercises/02.test-setup/03.solution.authentication/public/favicon.ico b/exercises/02.authentication/01.solution.basic/public/favicon.ico
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/public/favicon.ico
rename to exercises/02.authentication/01.solution.basic/public/favicon.ico
diff --git a/exercises/02.test-setup/03.solution.authentication/public/favicons/README.md b/exercises/02.authentication/01.solution.basic/public/favicons/README.md
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/public/favicons/README.md
rename to exercises/02.authentication/01.solution.basic/public/favicons/README.md
diff --git a/exercises/02.test-setup/03.solution.authentication/public/favicons/android-chrome-192x192.png b/exercises/02.authentication/01.solution.basic/public/favicons/android-chrome-192x192.png
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/public/favicons/android-chrome-192x192.png
rename to exercises/02.authentication/01.solution.basic/public/favicons/android-chrome-192x192.png
diff --git a/exercises/02.test-setup/03.solution.authentication/public/favicons/android-chrome-512x512.png b/exercises/02.authentication/01.solution.basic/public/favicons/android-chrome-512x512.png
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/public/favicons/android-chrome-512x512.png
rename to exercises/02.authentication/01.solution.basic/public/favicons/android-chrome-512x512.png
diff --git a/exercises/02.test-setup/03.solution.authentication/public/img/user.png b/exercises/02.authentication/01.solution.basic/public/img/user.png
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/public/img/user.png
rename to exercises/02.authentication/01.solution.basic/public/img/user.png
diff --git a/exercises/02.test-setup/03.solution.authentication/public/site.webmanifest b/exercises/02.authentication/01.solution.basic/public/site.webmanifest
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/public/site.webmanifest
rename to exercises/02.authentication/01.solution.basic/public/site.webmanifest
diff --git a/exercises/02.test-setup/03.solution.authentication/react-router.config.ts b/exercises/02.authentication/01.solution.basic/react-router.config.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/react-router.config.ts
rename to exercises/02.authentication/01.solution.basic/react-router.config.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/server/dev-server.js b/exercises/02.authentication/01.solution.basic/server/dev-server.js
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/server/dev-server.js
rename to exercises/02.authentication/01.solution.basic/server/dev-server.js
diff --git a/exercises/02.test-setup/03.solution.authentication/server/index.ts b/exercises/02.authentication/01.solution.basic/server/index.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/server/index.ts
rename to exercises/02.authentication/01.solution.basic/server/index.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/server/utils/monitoring.ts b/exercises/02.authentication/01.solution.basic/server/utils/monitoring.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/server/utils/monitoring.ts
rename to exercises/02.authentication/01.solution.basic/server/utils/monitoring.ts
diff --git a/exercises/02.authentication/01.solution.basic/tests/db-utils.ts b/exercises/02.authentication/01.solution.basic/tests/db-utils.ts
new file mode 100644
index 0000000..62543fe
--- /dev/null
+++ b/exercises/02.authentication/01.solution.basic/tests/db-utils.ts
@@ -0,0 +1,156 @@
+import { faker } from '@faker-js/faker'
+import bcrypt from 'bcryptjs'
+import { UniqueEnforcer } from 'enforce-unique'
+import { getPasswordHash } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+
+const uniqueUsernameEnforcer = new UniqueEnforcer()
+
+export function generateUserInfo() {
+ const firstName = faker.person.firstName()
+ const lastName = faker.person.lastName()
+
+ const username = uniqueUsernameEnforcer
+ .enforce(() => {
+ return (
+ faker.string.alphanumeric({ length: 2 }) +
+ '_' +
+ faker.internet.username({
+ firstName: firstName.toLowerCase(),
+ lastName: lastName.toLowerCase(),
+ })
+ )
+ })
+ .slice(0, 20)
+ .toLowerCase()
+ .replace(/[^a-z0-9_]/g, '_')
+
+ return {
+ username,
+ name: `${firstName} ${lastName}`,
+ email: `${username}@example.com`,
+ }
+}
+
+export async function createUser() {
+ const userInfo = generateUserInfo()
+ const password = 'supersecret'
+ const user = await prisma.user.create({
+ data: {
+ ...userInfo,
+ password: { create: { hash: await getPasswordHash(password) } },
+ },
+ })
+
+ return {
+ async [Symbol.asyncDispose]() {
+ await prisma.user.deleteMany({
+ where: { id: user.id },
+ })
+ },
+ ...user,
+ password,
+ }
+}
+
+export async function createPasskey(input: {
+ id: string
+ userId: string
+ aaguid: string
+ publicKey: Uint8Array
+ counter?: number
+}) {
+ const passkey = await prisma.passkey.create({
+ data: {
+ id: input.id,
+ aaguid: input.aaguid,
+ userId: input.userId,
+ publicKey: input.publicKey,
+ backedUp: false,
+ webauthnUserId: input.userId,
+ deviceType: 'singleDevice',
+ counter: input.counter || 0,
+ },
+ })
+
+ return {
+ async [Symbol.asyncDispose]() {
+ await prisma.passkey.deleteMany({
+ where: {
+ id: passkey.id,
+ },
+ })
+ },
+ ...passkey,
+ }
+}
+
+export function createPassword(password: string = faker.internet.password()) {
+ return {
+ hash: bcrypt.hashSync(password, 10),
+ }
+}
+
+let noteImages: Array<{ altText: string; objectKey: string }> | undefined
+export async function getNoteImages() {
+ if (noteImages) return noteImages
+
+ noteImages = await Promise.all([
+ {
+ altText: 'a nice country house',
+ objectKey: 'notes/0.png',
+ },
+ {
+ altText: 'a city scape',
+ objectKey: 'notes/1.png',
+ },
+ {
+ altText: 'a sunrise',
+ objectKey: 'notes/2.png',
+ },
+ {
+ altText: 'a group of friends',
+ objectKey: 'notes/3.png',
+ },
+ {
+ altText: 'friends being inclusive of someone who looks lonely',
+ objectKey: 'notes/4.png',
+ },
+ {
+ altText: 'an illustration of a hot air balloon',
+ objectKey: 'notes/5.png',
+ },
+ {
+ altText:
+ 'an office full of laptops and other office equipment that look like it was abandoned in a rush out of the building in an emergency years ago.',
+ objectKey: 'notes/6.png',
+ },
+ {
+ altText: 'a rusty lock',
+ objectKey: 'notes/7.png',
+ },
+ {
+ altText: 'something very happy in nature',
+ objectKey: 'notes/8.png',
+ },
+ {
+ altText: `someone at the end of a cry session who's starting to feel a little better.`,
+ objectKey: 'notes/9.png',
+ },
+ ])
+
+ return noteImages
+}
+
+let userImages: Array<{ objectKey: string }> | undefined
+export async function getUserImages() {
+ if (userImages) return userImages
+
+ userImages = await Promise.all(
+ Array.from({ length: 10 }, (_, index) => ({
+ objectKey: `user/${index}.jpg`,
+ })),
+ )
+
+ return userImages
+}
diff --git a/exercises/02.authentication/01.solution.basic/tests/e2e/authentication-basic.test.ts b/exercises/02.authentication/01.solution.basic/tests/e2e/authentication-basic.test.ts
new file mode 100644
index 0000000..11b9741
--- /dev/null
+++ b/exercises/02.authentication/01.solution.basic/tests/e2e/authentication-basic.test.ts
@@ -0,0 +1,29 @@
+import { createUser } from '#tests/db-utils.ts'
+import { test, expect } from '#tests/test-extend.ts'
+
+test('authenticates using a email and password', async ({ navigate, page }) => {
+ await using user = await createUser()
+
+ await navigate('/login')
+
+ await page.getByLabel('Username').fill(user.username)
+ await page.getByLabel('Password').fill(user.password)
+ await page.getByRole('button', { name: 'Log in' }).click()
+
+ await expect(page.getByRole('link', { name: user.name! })).toBeVisible()
+})
+
+test('displays an error message when authenticating with invalid credentials', async ({
+ navigate,
+ page,
+}) => {
+ await navigate('/login')
+
+ await page.getByLabel('Username').fill('non_existing_user')
+ await page.getByLabel('Password').fill('non_existing_password')
+ await page.getByRole('button', { name: 'Log in' }).click()
+
+ await expect(
+ page.getByRole('alert').getByText('Invalid username or password'),
+ ).toBeVisible()
+})
diff --git a/exercises/02.test-setup/03.solution.authentication/tests/setup/custom-matchers.ts b/exercises/02.authentication/01.solution.basic/tests/setup/custom-matchers.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/tests/setup/custom-matchers.ts
rename to exercises/02.authentication/01.solution.basic/tests/setup/custom-matchers.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/tests/setup/db-setup.ts b/exercises/02.authentication/01.solution.basic/tests/setup/db-setup.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/tests/setup/db-setup.ts
rename to exercises/02.authentication/01.solution.basic/tests/setup/db-setup.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/tests/setup/global-setup.ts b/exercises/02.authentication/01.solution.basic/tests/setup/global-setup.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/tests/setup/global-setup.ts
rename to exercises/02.authentication/01.solution.basic/tests/setup/global-setup.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/tests/setup/setup-test-env.ts b/exercises/02.authentication/01.solution.basic/tests/setup/setup-test-env.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/tests/setup/setup-test-env.ts
rename to exercises/02.authentication/01.solution.basic/tests/setup/setup-test-env.ts
diff --git a/exercises/03.guides/03.problem.blocking-unneeded-requests/tests/test-extend.ts b/exercises/02.authentication/01.solution.basic/tests/test-extend.ts
similarity index 69%
rename from exercises/03.guides/03.problem.blocking-unneeded-requests/tests/test-extend.ts
rename to exercises/02.authentication/01.solution.basic/tests/test-extend.ts
index 597ae96..a51b50d 100644
--- a/exercises/03.guides/03.problem.blocking-unneeded-requests/tests/test-extend.ts
+++ b/exercises/02.authentication/01.solution.basic/tests/test-extend.ts
@@ -1,7 +1,4 @@
-import { createNetworkFixture, type NetworkFixture } from '@msw/playwright'
import { test as testBase, expect } from '@playwright/test'
-// 🐨 Imoprt the `http` object and the `HttpResponse` class from "msw".
-// 💰 import { this, that } from 'package'
import {
definePersona,
combinePersonas,
@@ -10,21 +7,20 @@ import {
import { href, type Register } from 'react-router'
import { getPasswordHash } from '#app/utils/auth.server.ts'
import { prisma } from '#app/utils/db.server.ts'
-import { createUser } from '#tests/db-utils'
+import { generateUserInfo } from '#tests/db-utils'
interface Fixtures {
navigate: (
...args: Parameters>
) => Promise
authenticate: AuthenticateFunction<[typeof user]>
- network: NetworkFixture
}
const user = definePersona('user', {
async createSession({ page }) {
const user = await prisma.user.create({
data: {
- ...createUser(),
+ ...generateUserInfo(),
roles: { connect: { name: 'user' } },
password: { create: { hash: await getPasswordHash('supersecret') } },
},
@@ -56,16 +52,6 @@ export const test = testBase.extend({
})
},
authenticate: combinePersonas(user),
- network: createNetworkFixture({
- initialHandlers: [
- // 🐨 Add a new handler for a GET request to the "https://assets.onedollarstats.com" URL.
- // Capture all the requests to that URL by using a wildcard "*".
- // 💰 http.get(path, resolver)
- //
- // 🐨 In the resolver for that handler, return an empty response.
- // 💰 new HttpResponse()
- ],
- }),
})
export { expect }
diff --git a/exercises/02.test-setup/03.solution.authentication/tests/utils.ts b/exercises/02.authentication/01.solution.basic/tests/utils.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/tests/utils.ts
rename to exercises/02.authentication/01.solution.basic/tests/utils.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/tsconfig.json b/exercises/02.authentication/01.solution.basic/tsconfig.json
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/tsconfig.json
rename to exercises/02.authentication/01.solution.basic/tsconfig.json
diff --git a/exercises/02.test-setup/03.solution.authentication/types/deps.d.ts b/exercises/02.authentication/01.solution.basic/types/deps.d.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/types/deps.d.ts
rename to exercises/02.authentication/01.solution.basic/types/deps.d.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/types/env.env.d.ts b/exercises/02.authentication/01.solution.basic/types/env.env.d.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/types/env.env.d.ts
rename to exercises/02.authentication/01.solution.basic/types/env.env.d.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/types/icon-name.d.ts b/exercises/02.authentication/01.solution.basic/types/icon-name.d.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/types/icon-name.d.ts
rename to exercises/02.authentication/01.solution.basic/types/icon-name.d.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/types/reset.d.ts b/exercises/02.authentication/01.solution.basic/types/reset.d.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/types/reset.d.ts
rename to exercises/02.authentication/01.solution.basic/types/reset.d.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/vite.config.ts b/exercises/02.authentication/01.solution.basic/vite.config.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/vite.config.ts
rename to exercises/02.authentication/01.solution.basic/vite.config.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/.env b/exercises/02.authentication/02.problem.2fa/.env
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/.env
rename to exercises/02.authentication/02.problem.2fa/.env
diff --git a/exercises/02.test-setup/04.problem.api-mocking/.env.example b/exercises/02.authentication/02.problem.2fa/.env.example
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/.env.example
rename to exercises/02.authentication/02.problem.2fa/.env.example
diff --git a/exercises/02.test-setup/04.problem.api-mocking/.gitignore b/exercises/02.authentication/02.problem.2fa/.gitignore
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/.gitignore
rename to exercises/02.authentication/02.problem.2fa/.gitignore
diff --git a/exercises/02.test-setup/04.problem.api-mocking/.npmrc b/exercises/02.authentication/02.problem.2fa/.npmrc
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/.npmrc
rename to exercises/02.authentication/02.problem.2fa/.npmrc
diff --git a/exercises/02.test-setup/04.problem.api-mocking/.prettierignore b/exercises/02.authentication/02.problem.2fa/.prettierignore
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/.prettierignore
rename to exercises/02.authentication/02.problem.2fa/.prettierignore
diff --git a/exercises/02.test-setup/04.problem.api-mocking/.vscode/extensions.json b/exercises/02.authentication/02.problem.2fa/.vscode/extensions.json
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/.vscode/extensions.json
rename to exercises/02.authentication/02.problem.2fa/.vscode/extensions.json
diff --git a/exercises/02.test-setup/04.problem.api-mocking/.vscode/remix.code-snippets b/exercises/02.authentication/02.problem.2fa/.vscode/remix.code-snippets
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/.vscode/remix.code-snippets
rename to exercises/02.authentication/02.problem.2fa/.vscode/remix.code-snippets
diff --git a/exercises/02.test-setup/04.problem.api-mocking/.vscode/settings.json b/exercises/02.authentication/02.problem.2fa/.vscode/settings.json
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/.vscode/settings.json
rename to exercises/02.authentication/02.problem.2fa/.vscode/settings.json
diff --git a/exercises/02.authentication/02.problem.2fa/README.mdx b/exercises/02.authentication/02.problem.2fa/README.mdx
new file mode 100644
index 0000000..8c6fb0b
--- /dev/null
+++ b/exercises/02.authentication/02.problem.2fa/README.mdx
@@ -0,0 +1,7 @@
+# Two-factor authentication
+
+## Your task
+
+- Write the `tests/e2e/authentication-2fa.test.ts`.
+- Implement a `createVerification()` utility.
+- Use `@epic-web/totp` to generate the OTP in the test case.
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/assets/favicons/apple-touch-icon.png b/exercises/02.authentication/02.problem.2fa/app/assets/favicons/apple-touch-icon.png
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/assets/favicons/apple-touch-icon.png
rename to exercises/02.authentication/02.problem.2fa/app/assets/favicons/apple-touch-icon.png
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/assets/favicons/favicon.svg b/exercises/02.authentication/02.problem.2fa/app/assets/favicons/favicon.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/assets/favicons/favicon.svg
rename to exercises/02.authentication/02.problem.2fa/app/assets/favicons/favicon.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/error-boundary.tsx b/exercises/02.authentication/02.problem.2fa/app/components/error-boundary.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/error-boundary.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/error-boundary.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/floating-toolbar.tsx b/exercises/02.authentication/02.problem.2fa/app/components/floating-toolbar.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/floating-toolbar.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/floating-toolbar.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/components/forms.tsx b/exercises/02.authentication/02.problem.2fa/app/components/forms.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/components/forms.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/forms.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/progress-bar.tsx b/exercises/02.authentication/02.problem.2fa/app/components/progress-bar.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/progress-bar.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/progress-bar.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/search-bar.tsx b/exercises/02.authentication/02.problem.2fa/app/components/search-bar.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/search-bar.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/search-bar.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/spacer.tsx b/exercises/02.authentication/02.problem.2fa/app/components/spacer.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/spacer.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/spacer.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/toaster.tsx b/exercises/02.authentication/02.problem.2fa/app/components/toaster.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/toaster.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/toaster.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/ui/README.md b/exercises/02.authentication/02.problem.2fa/app/components/ui/README.md
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/ui/README.md
rename to exercises/02.authentication/02.problem.2fa/app/components/ui/README.md
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/ui/button.tsx b/exercises/02.authentication/02.problem.2fa/app/components/ui/button.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/ui/button.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/ui/button.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/ui/checkbox.tsx b/exercises/02.authentication/02.problem.2fa/app/components/ui/checkbox.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/ui/checkbox.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/ui/checkbox.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/ui/dropdown-menu.tsx b/exercises/02.authentication/02.problem.2fa/app/components/ui/dropdown-menu.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/ui/dropdown-menu.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/ui/dropdown-menu.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/ui/icon.tsx b/exercises/02.authentication/02.problem.2fa/app/components/ui/icon.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/ui/icon.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/ui/icon.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/ui/input-otp.tsx b/exercises/02.authentication/02.problem.2fa/app/components/ui/input-otp.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/ui/input-otp.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/ui/input-otp.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/ui/input.tsx b/exercises/02.authentication/02.problem.2fa/app/components/ui/input.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/ui/input.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/ui/input.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/ui/label.tsx b/exercises/02.authentication/02.problem.2fa/app/components/ui/label.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/ui/label.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/ui/label.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/ui/sonner.tsx b/exercises/02.authentication/02.problem.2fa/app/components/ui/sonner.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/ui/sonner.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/ui/sonner.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/ui/status-button.tsx b/exercises/02.authentication/02.problem.2fa/app/components/ui/status-button.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/ui/status-button.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/ui/status-button.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/ui/textarea.tsx b/exercises/02.authentication/02.problem.2fa/app/components/ui/textarea.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/ui/textarea.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/ui/textarea.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/ui/tooltip.tsx b/exercises/02.authentication/02.problem.2fa/app/components/ui/tooltip.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/ui/tooltip.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/ui/tooltip.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/components/user-dropdown.tsx b/exercises/02.authentication/02.problem.2fa/app/components/user-dropdown.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/components/user-dropdown.tsx
rename to exercises/02.authentication/02.problem.2fa/app/components/user-dropdown.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/entry.client.tsx b/exercises/02.authentication/02.problem.2fa/app/entry.client.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/entry.client.tsx
rename to exercises/02.authentication/02.problem.2fa/app/entry.client.tsx
diff --git a/exercises/02.authentication/02.problem.2fa/app/entry.server.tsx b/exercises/02.authentication/02.problem.2fa/app/entry.server.tsx
new file mode 100644
index 0000000..8d8b1de
--- /dev/null
+++ b/exercises/02.authentication/02.problem.2fa/app/entry.server.tsx
@@ -0,0 +1,143 @@
+import crypto from 'node:crypto'
+import { PassThrough } from 'node:stream'
+import { styleText } from 'node:util'
+import { contentSecurity } from '@nichtsam/helmet/content'
+import { createReadableStreamFromReadable } from '@react-router/node'
+import * as Sentry from '@sentry/react-router'
+import { isbot } from 'isbot'
+import { renderToPipeableStream } from 'react-dom/server'
+import {
+ ServerRouter,
+ type LoaderFunctionArgs,
+ type ActionFunctionArgs,
+ type HandleDocumentRequestFunction,
+} from 'react-router'
+import { getEnv, init } from './utils/env.server.ts'
+import { getInstanceInfo } from './utils/litefs.server.ts'
+import { NonceProvider } from './utils/nonce-provider.ts'
+import { makeTimings } from './utils/timing.server.ts'
+
+export const streamTimeout = 5000
+
+init()
+global.ENV = getEnv()
+
+const MODE = process.env.NODE_ENV ?? 'development'
+
+type DocRequestArgs = Parameters
+
+export default async function handleRequest(...args: DocRequestArgs) {
+ const [request, responseStatusCode, responseHeaders, reactRouterContext] =
+ args
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ responseHeaders.set('fly-primary-instance', primaryInstance)
+ responseHeaders.set('fly-instance', currentInstance)
+
+ if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
+ responseHeaders.append('Document-Policy', 'js-profiling')
+ }
+
+ const callbackName = isbot(request.headers.get('user-agent'))
+ ? 'onAllReady'
+ : 'onShellReady'
+
+ const nonce = crypto.randomBytes(16).toString('hex')
+ return new Promise(async (resolve, reject) => {
+ let didError = false
+ // NOTE: this timing will only include things that are rendered in the shell
+ // and will not include suspended components and deferred loaders
+ const timings = makeTimings('render', 'renderToPipeableStream')
+
+ const { pipe, abort } = renderToPipeableStream(
+
+
+ ,
+ {
+ [callbackName]: () => {
+ const body = new PassThrough()
+ responseHeaders.set('Content-Type', 'text/html')
+ responseHeaders.append('Server-Timing', timings.toString())
+
+ contentSecurity(responseHeaders, {
+ crossOriginEmbedderPolicy: false,
+ contentSecurityPolicy: {
+ // NOTE: Remove reportOnly when you're ready to enforce this CSP
+ reportOnly: true,
+ directives: {
+ fetch: {
+ 'connect-src': [
+ MODE === 'development' ? 'ws:' : undefined,
+ process.env.SENTRY_DSN ? '*.sentry.io' : undefined,
+ "'self'",
+ ],
+ 'font-src': ["'self'"],
+ 'frame-src': ["'self'"],
+ 'img-src': ["'self'", 'data:'],
+ 'script-src': [
+ "'strict-dynamic'",
+ "'self'",
+ `'nonce-${nonce}'`,
+ ],
+ 'script-src-attr': [`'nonce-${nonce}'`],
+ },
+ },
+ },
+ xFrameOptions: false,
+ })
+
+ resolve(
+ new Response(createReadableStreamFromReadable(body), {
+ headers: responseHeaders,
+ status: didError ? 500 : responseStatusCode,
+ }),
+ )
+ pipe(body)
+ },
+ onShellError: (err: unknown) => {
+ reject(err)
+ },
+ onError: () => {
+ didError = true
+ },
+ nonce,
+ },
+ )
+
+ setTimeout(abort, streamTimeout + 5000)
+ })
+}
+
+export async function handleDataRequest(response: Response) {
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ response.headers.set('fly-primary-instance', primaryInstance)
+ response.headers.set('fly-instance', currentInstance)
+
+ return response
+}
+
+export function handleError(
+ error: unknown,
+ { request }: LoaderFunctionArgs | ActionFunctionArgs,
+): void {
+ // Skip capturing if the request is aborted as Remix docs suggest
+ // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
+ if (request.signal.aborted) {
+ return
+ }
+
+ if (error instanceof Error) {
+ console.error(styleText('red', String(error.stack)))
+ } else {
+ console.error(error)
+ }
+
+ Sentry.captureException(error)
+}
diff --git a/exercises/02.test-setup/05.problem.test-data/app/root.tsx b/exercises/02.authentication/02.problem.2fa/app/root.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/root.tsx
rename to exercises/02.authentication/02.problem.2fa/app/root.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes.ts b/exercises/02.authentication/02.problem.2fa/app/routes.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes.ts
rename to exercises/02.authentication/02.problem.2fa/app/routes.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/$.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/$.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/$.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/$.tsx
diff --git a/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/auth.$provider.callback.test.ts b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/auth.$provider.callback.test.ts
new file mode 100644
index 0000000..3765dd7
--- /dev/null
+++ b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/auth.$provider.callback.test.ts
@@ -0,0 +1,265 @@
+import { invariant } from '@epic-web/invariant'
+import { faker } from '@faker-js/faker'
+import { SetCookie } from '@mjackson/headers'
+import { http } from 'msw'
+import { afterEach, expect, test } from 'vitest'
+import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx'
+import { getSessionExpirationDate, sessionKey } from '#app/utils/auth.server.ts'
+import { GITHUB_PROVIDER_NAME } from '#app/utils/connections.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { generateTOTP } from '#app/utils/totp.server.ts'
+import { generateUserInfo } from '#tests/db-utils.ts'
+import { insertGitHubUser, deleteGitHubUsers } from '#tests/mocks/github.ts'
+import { server } from '#tests/mocks/index.ts'
+import { consoleError } from '#tests/setup/setup-test-env.ts'
+import { BASE_URL, convertSetCookieToCookie } from '#tests/utils.ts'
+import { loader } from './auth.$provider.callback.ts'
+
+const ROUTE_PATH = '/auth/github/callback'
+const PARAMS = { provider: 'github' }
+
+afterEach(async () => {
+ await deleteGitHubUsers()
+})
+
+test('a new user goes to onboarding', async () => {
+ const request = await setupRequest()
+ const response = await loader({ request, params: PARAMS, context: {} }).catch(
+ (e) => e,
+ )
+ expect(response).toHaveRedirect('/onboarding/github')
+})
+
+test('when auth fails, send the user to login with a toast', async () => {
+ consoleError.mockImplementation(() => {})
+ server.use(
+ http.post('https://github.com/login/oauth/access_token', async () => {
+ return new Response(null, { status: 400 })
+ }),
+ )
+ const request = await setupRequest()
+ const response = await loader({ request, params: PARAMS, context: {} }).catch(
+ (e) => e,
+ )
+ invariant(response instanceof Response, 'response should be a Response')
+ expect(response).toHaveRedirect('/login')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Auth Failed',
+ type: 'error',
+ }),
+ )
+ expect(consoleError).toHaveBeenCalledTimes(1)
+})
+
+test('when a user is logged in, it creates the connection', async () => {
+ const githubUser = await insertGitHubUser()
+ const session = await setupUser()
+ const request = await setupRequest({
+ sessionId: session.id,
+ code: githubUser.code,
+ })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/settings/profile/connections')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Connected',
+ type: 'success',
+ description: expect.stringContaining(githubUser.profile.login),
+ }),
+ )
+ const connection = await prisma.connection.findFirst({
+ select: { id: true },
+ where: {
+ userId: session.userId,
+ providerId: githubUser.profile.id.toString(),
+ },
+ })
+ expect(
+ connection,
+ 'the connection was not created in the database',
+ ).toBeTruthy()
+})
+
+test(`when a user is logged in and has already connected, it doesn't do anything and just redirects the user back to the connections page`, async () => {
+ const session = await setupUser()
+ const githubUser = await insertGitHubUser()
+ await prisma.connection.create({
+ data: {
+ providerName: GITHUB_PROVIDER_NAME,
+ userId: session.userId,
+ providerId: githubUser.profile.id.toString(),
+ },
+ })
+ const request = await setupRequest({
+ sessionId: session.id,
+ code: githubUser.code,
+ })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/settings/profile/connections')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Already Connected',
+ description: expect.stringContaining(githubUser.profile.login),
+ }),
+ )
+})
+
+test('when a user exists with the same email, create connection and make session', async () => {
+ const githubUser = await insertGitHubUser()
+ const email = githubUser.primaryEmail.toLowerCase()
+ const { userId } = await setupUser({ ...generateUserInfo(), email })
+ const request = await setupRequest({ code: githubUser.code })
+ const response = await loader({ request, params: PARAMS, context: {} })
+
+ expect(response).toHaveRedirect('/')
+
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ type: 'message',
+ description: expect.stringContaining(githubUser.profile.login),
+ }),
+ )
+
+ const connection = await prisma.connection.findFirst({
+ select: { id: true },
+ where: {
+ userId: userId,
+ providerId: githubUser.profile.id.toString(),
+ },
+ })
+ expect(
+ connection,
+ 'the connection was not created in the database',
+ ).toBeTruthy()
+
+ await expect(response).toHaveSessionForUser(userId)
+})
+
+test('gives an error if the account is already connected to another user', async () => {
+ const githubUser = await insertGitHubUser()
+ await prisma.user.create({
+ data: {
+ ...generateUserInfo(),
+ connections: {
+ create: {
+ providerName: GITHUB_PROVIDER_NAME,
+ providerId: githubUser.profile.id.toString(),
+ },
+ },
+ },
+ })
+ const session = await setupUser()
+ const request = await setupRequest({
+ sessionId: session.id,
+ code: githubUser.code,
+ })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/settings/profile/connections')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Already Connected',
+ description: expect.stringContaining(
+ 'already connected to another account',
+ ),
+ }),
+ )
+})
+
+test('if a user is not logged in, but the connection exists, make a session', async () => {
+ const githubUser = await insertGitHubUser()
+ const { userId } = await setupUser()
+ await prisma.connection.create({
+ data: {
+ providerName: GITHUB_PROVIDER_NAME,
+ providerId: githubUser.profile.id.toString(),
+ userId,
+ },
+ })
+ const request = await setupRequest({ code: githubUser.code })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/')
+ await expect(response).toHaveSessionForUser(userId)
+})
+
+test('if a user is not logged in, but the connection exists and they have enabled 2FA, send them to verify their 2FA and do not make a session', async () => {
+ const githubUser = await insertGitHubUser()
+ const { userId } = await setupUser()
+ await prisma.connection.create({
+ data: {
+ providerName: GITHUB_PROVIDER_NAME,
+ providerId: githubUser.profile.id.toString(),
+ userId,
+ },
+ })
+ const { otp: _otp, ...config } = await generateTOTP()
+ await prisma.verification.create({
+ data: {
+ type: twoFAVerificationType,
+ target: userId,
+ ...config,
+ },
+ })
+ const request = await setupRequest({ code: githubUser.code })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ const searchParams = new URLSearchParams({
+ type: twoFAVerificationType,
+ target: userId,
+ redirectTo: '/',
+ })
+ expect(response).toHaveRedirect(`/verify?${searchParams}`)
+})
+
+async function setupRequest({
+ sessionId,
+ code = faker.string.uuid(),
+}: { sessionId?: string; code?: string } = {}) {
+ const url = new URL(ROUTE_PATH, BASE_URL)
+ const state = faker.string.uuid()
+ url.searchParams.set('state', state)
+ url.searchParams.set('code', code)
+ const authSession = await authSessionStorage.getSession()
+ if (sessionId) authSession.set(sessionKey, sessionId)
+ const setSessionCookieHeader =
+ await authSessionStorage.commitSession(authSession)
+ const searchParams = new URLSearchParams({ code, state })
+ let authCookie = new SetCookie({
+ name: 'github',
+ value: searchParams.toString(),
+ path: '/',
+ sameSite: 'Lax',
+ httpOnly: true,
+ maxAge: 60 * 10,
+ secure: process.env.NODE_ENV === 'production' || undefined,
+ })
+ const request = new Request(url.toString(), {
+ method: 'GET',
+ headers: {
+ cookie: [
+ authCookie.toString(),
+ convertSetCookieToCookie(setSessionCookieHeader),
+ ].join('; '),
+ },
+ })
+ return request
+}
+
+async function setupUser(userData = generateUserInfo()) {
+ const session = await prisma.session.create({
+ data: {
+ expirationDate: getSessionExpirationDate(),
+ user: {
+ create: {
+ ...userData,
+ },
+ },
+ },
+ select: {
+ id: true,
+ userId: true,
+ },
+ })
+
+ return session
+}
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/auth.$provider.callback.ts b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/auth.$provider.callback.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/auth.$provider.callback.ts
rename to exercises/02.authentication/02.problem.2fa/app/routes/_auth+/auth.$provider.callback.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/auth_.$provider.ts b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/auth_.$provider.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/auth_.$provider.ts
rename to exercises/02.authentication/02.problem.2fa/app/routes/_auth+/auth_.$provider.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/forgot-password.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/forgot-password.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/forgot-password.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/_auth+/forgot-password.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/login.server.ts b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/login.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/login.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/routes/_auth+/login.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/login.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/login.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/login.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/_auth+/login.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/logout.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/logout.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/logout.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/_auth+/logout.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/onboarding.server.ts b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/onboarding.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/onboarding.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/routes/_auth+/onboarding.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/onboarding.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/onboarding.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/onboarding.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/_auth+/onboarding.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/onboarding_.$provider.server.ts b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/onboarding_.$provider.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/onboarding_.$provider.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/routes/_auth+/onboarding_.$provider.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/onboarding_.$provider.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/onboarding_.$provider.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/onboarding_.$provider.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/_auth+/onboarding_.$provider.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/reset-password.server.ts b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/reset-password.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/reset-password.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/routes/_auth+/reset-password.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/reset-password.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/reset-password.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/reset-password.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/_auth+/reset-password.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/signup.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/signup.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/signup.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/_auth+/signup.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/verify.server.ts b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/verify.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/verify.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/routes/_auth+/verify.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/verify.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/verify.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/verify.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/_auth+/verify.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/webauthn+/authentication.ts b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/webauthn+/authentication.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/webauthn+/authentication.ts
rename to exercises/02.authentication/02.problem.2fa/app/routes/_auth+/webauthn+/authentication.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/webauthn+/registration.ts b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/webauthn+/registration.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/webauthn+/registration.ts
rename to exercises/02.authentication/02.problem.2fa/app/routes/_auth+/webauthn+/registration.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/webauthn+/utils.server.ts b/exercises/02.authentication/02.problem.2fa/app/routes/_auth+/webauthn+/utils.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_auth+/webauthn+/utils.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/routes/_auth+/webauthn+/utils.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/about.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/about.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/about.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/about.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/index.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/index.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/index.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/index.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/docker.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/docker.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/docker.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/docker.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/eslint.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/eslint.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/eslint.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/eslint.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/faker.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/faker.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/faker.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/faker.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/fly.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/fly.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/fly.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/fly.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/github.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/github.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/github.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/github.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/logos.ts b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/logos.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/logos.ts
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/logos.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/msw.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/msw.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/msw.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/msw.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/playwright.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/playwright.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/playwright.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/playwright.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/prettier.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/prettier.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/prettier.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/prettier.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/prisma.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/prisma.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/prisma.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/prisma.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/radix.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/radix.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/radix.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/radix.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/react-email.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/react-email.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/react-email.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/react-email.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/remix.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/remix.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/remix.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/remix.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/resend.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/resend.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/resend.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/resend.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/sentry.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/sentry.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/sentry.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/sentry.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/shadcn-ui.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/shadcn-ui.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/shadcn-ui.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/shadcn-ui.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/sqlite.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/sqlite.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/sqlite.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/sqlite.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/stars.jpg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/stars.jpg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/stars.jpg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/stars.jpg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/tailwind.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/tailwind.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/tailwind.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/tailwind.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/testing-library.png b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/testing-library.png
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/testing-library.png
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/testing-library.png
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/typescript.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/typescript.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/typescript.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/typescript.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/vitest.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/vitest.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/vitest.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/vitest.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/zod.svg b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/zod.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/logos/zod.svg
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/logos/zod.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/privacy.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/privacy.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/privacy.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/privacy.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/support.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/support.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/support.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/support.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/tos.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/tos.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_marketing+/tos.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/_marketing+/tos.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_seo+/robots[.]txt.ts b/exercises/02.authentication/02.problem.2fa/app/routes/_seo+/robots[.]txt.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_seo+/robots[.]txt.ts
rename to exercises/02.authentication/02.problem.2fa/app/routes/_seo+/robots[.]txt.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/_seo+/sitemap[.]xml.ts b/exercises/02.authentication/02.problem.2fa/app/routes/_seo+/sitemap[.]xml.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/_seo+/sitemap[.]xml.ts
rename to exercises/02.authentication/02.problem.2fa/app/routes/_seo+/sitemap[.]xml.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/admin+/cache.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/admin+/cache.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/admin+/cache.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/admin+/cache.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/admin+/cache_.lru.$cacheKey.ts b/exercises/02.authentication/02.problem.2fa/app/routes/admin+/cache_.lru.$cacheKey.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/admin+/cache_.lru.$cacheKey.ts
rename to exercises/02.authentication/02.problem.2fa/app/routes/admin+/cache_.lru.$cacheKey.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/admin+/cache_.sqlite.$cacheKey.ts b/exercises/02.authentication/02.problem.2fa/app/routes/admin+/cache_.sqlite.$cacheKey.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/admin+/cache_.sqlite.$cacheKey.ts
rename to exercises/02.authentication/02.problem.2fa/app/routes/admin+/cache_.sqlite.$cacheKey.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/admin+/cache_.sqlite.server.ts b/exercises/02.authentication/02.problem.2fa/app/routes/admin+/cache_.sqlite.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/admin+/cache_.sqlite.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/routes/admin+/cache_.sqlite.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/admin+/cache_.sqlite.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/admin+/cache_.sqlite.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/admin+/cache_.sqlite.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/admin+/cache_.sqlite.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/me.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/me.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/me.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/me.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/resources+/download-user-data.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/resources+/download-user-data.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/resources+/download-user-data.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/resources+/download-user-data.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/resources+/healthcheck.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/resources+/healthcheck.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/resources+/healthcheck.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/resources+/healthcheck.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/resources+/images.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/resources+/images.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/resources+/images.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/resources+/images.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/resources+/theme-switch.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/resources+/theme-switch.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/resources+/theme-switch.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/resources+/theme-switch.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.change-email.server.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.change-email.server.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.change-email.server.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.change-email.server.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.change-email.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.change-email.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.change-email.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.change-email.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.connections.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.connections.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.connections.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.connections.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.index.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.index.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.index.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.index.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.passkeys.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.passkeys.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.passkeys.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.passkeys.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.password.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.password.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.password.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.password.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.password_.create.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.password_.create.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.password_.create.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.password_.create.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.photo.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.photo.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.photo.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.photo.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.two-factor.disable.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.two-factor.disable.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.two-factor.disable.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.two-factor.disable.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.two-factor.index.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.two-factor.index.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.two-factor.index.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.two-factor.index.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.two-factor.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.two-factor.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.two-factor.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.two-factor.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.two-factor.verify.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.two-factor.verify.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/settings+/profile.two-factor.verify.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/settings+/profile.two-factor.verify.tsx
diff --git a/exercises/02.authentication/02.problem.2fa/app/routes/users+/$username.test.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/users+/$username.test.tsx
new file mode 100644
index 0000000..0884a24
--- /dev/null
+++ b/exercises/02.authentication/02.problem.2fa/app/routes/users+/$username.test.tsx
@@ -0,0 +1,97 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { faker } from '@faker-js/faker'
+import { render, screen } from '@testing-library/react'
+import { createRoutesStub } from 'react-router'
+import setCookieParser from 'set-cookie-parser'
+import { test } from 'vitest'
+import { loader as rootLoader } from '#app/root.tsx'
+import { getSessionExpirationDate, sessionKey } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { generateUserInfo, getUserImages } from '#tests/db-utils.ts'
+import { default as UsernameRoute, loader } from './$username.tsx'
+
+test('The user profile when not logged in as self', async () => {
+ const userImages = await getUserImages()
+ const userImage =
+ userImages[faker.number.int({ min: 0, max: userImages.length - 1 })]
+ const user = await prisma.user.create({
+ select: { id: true, username: true, name: true },
+ data: { ...generateUserInfo(), image: { create: userImage } },
+ })
+ const App = createRoutesStub([
+ {
+ path: '/users/:username',
+ Component: UsernameRoute,
+ loader,
+ HydrateFallback: () => Loading...
,
+ },
+ ])
+
+ const routeUrl = `/users/${user.username}`
+ render( )
+
+ await screen.findByRole('heading', { level: 1, name: user.name! })
+ await screen.findByRole('img', { name: user.name! })
+ await screen.findByRole('link', { name: `${user.name}'s notes` })
+})
+
+test('The user profile when logged in as self', async () => {
+ const userImages = await getUserImages()
+ const userImage =
+ userImages[faker.number.int({ min: 0, max: userImages.length - 1 })]
+ const user = await prisma.user.create({
+ select: { id: true, username: true, name: true },
+ data: { ...generateUserInfo(), image: { create: userImage } },
+ })
+ const session = await prisma.session.create({
+ select: { id: true },
+ data: {
+ expirationDate: getSessionExpirationDate(),
+ userId: user.id,
+ },
+ })
+
+ const authSession = await authSessionStorage.getSession()
+ authSession.set(sessionKey, session.id)
+ const setCookieHeader = await authSessionStorage.commitSession(authSession)
+ const parsedCookie = setCookieParser.parseString(setCookieHeader)
+ const cookieHeader = new URLSearchParams({
+ [parsedCookie.name]: parsedCookie.value,
+ }).toString()
+
+ const App = createRoutesStub([
+ {
+ id: 'root',
+ path: '/',
+ loader: async (args) => {
+ // add the cookie header to the request
+ args.request.headers.set('cookie', cookieHeader)
+ return rootLoader({ ...args, context: args.context })
+ },
+ HydrateFallback: () => Loading...
,
+ children: [
+ {
+ path: 'users/:username',
+ Component: UsernameRoute,
+ loader: async (args) => {
+ // add the cookie header to the request
+ args.request.headers.set('cookie', cookieHeader)
+ return loader(args)
+ },
+ },
+ ],
+ },
+ ])
+
+ const routeUrl = `/users/${user.username}`
+ render( )
+
+ await screen.findByRole('heading', { level: 1, name: user.name! })
+ await screen.findByRole('img', { name: user.name! })
+ await screen.findByRole('button', { name: /logout/i })
+ await screen.findByRole('link', { name: /my notes/i })
+ await screen.findByRole('link', { name: /edit profile/i })
+})
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/users+/$username.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/users+/$username.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/users+/$username.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/users+/$username.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/users+/$username_+/__note-editor.server.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/users+/$username_+/__note-editor.server.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/users+/$username_+/__note-editor.server.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/users+/$username_+/__note-editor.server.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/users+/$username_+/__note-editor.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/users+/$username_+/__note-editor.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/users+/$username_+/__note-editor.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/users+/$username_+/__note-editor.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/users+/$username_+/notes.$noteId.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/users+/$username_+/notes.$noteId.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/users+/$username_+/notes.$noteId.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/users+/$username_+/notes.$noteId.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/users+/$username_+/notes.$noteId_.edit.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/users+/$username_+/notes.index.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/users+/$username_+/notes.index.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/users+/$username_+/notes.index.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/users+/$username_+/notes.index.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/users+/$username_+/notes.new.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/users+/$username_+/notes.new.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/users+/$username_+/notes.new.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/users+/$username_+/notes.new.tsx
diff --git a/exercises/02.authentication/02.problem.2fa/app/routes/users+/$username_+/notes.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/users+/$username_+/notes.tsx
new file mode 100644
index 0000000..ded41ca
--- /dev/null
+++ b/exercises/02.authentication/02.problem.2fa/app/routes/users+/$username_+/notes.tsx
@@ -0,0 +1,105 @@
+import { invariantResponse } from '@epic-web/invariant'
+import { Img } from 'openimg/react'
+import { Link, NavLink, Outlet } from 'react-router'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { cn, getUserImgSrc } from '#app/utils/misc.tsx'
+import { useOptionalUser } from '#app/utils/user.ts'
+import { type Route } from './+types/notes.ts'
+
+export async function loader({ params }: Route.LoaderArgs) {
+ const owner = await prisma.user.findFirst({
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ image: { select: { objectKey: true } },
+ notes: { select: { id: true, title: true } },
+ },
+ where: { username: params.username },
+ })
+
+ invariantResponse(owner, 'Owner not found', { status: 404 })
+
+ return { owner }
+}
+
+export default function NotesRoute({ loaderData }: Route.ComponentProps) {
+ const user = useOptionalUser()
+ const isOwner = user?.id === loaderData.owner.id
+ const ownerDisplayName = loaderData.owner.name ?? loaderData.owner.username
+ const navLinkDefaultClassName =
+ 'line-clamp-2 block rounded-l-full py-2 pl-8 pr-6 text-base lg:text-xl'
+ return (
+
+
+
+
+
+
+
+ {ownerDisplayName}'s Notes
+
+
+
+ {isOwner ? (
+
+
+ cn(navLinkDefaultClassName, isActive && 'bg-accent')
+ }
+ >
+ New Note
+
+
+ ) : null}
+ {loaderData.owner.notes.map((note) => (
+
+
+ cn(navLinkDefaultClassName, isActive && 'bg-accent')
+ }
+ >
+ {note.title}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return (
+ (
+ No user with the username "{params.username}" exists
+ ),
+ }}
+ />
+ )
+}
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/users+/index.tsx b/exercises/02.authentication/02.problem.2fa/app/routes/users+/index.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/routes/users+/index.tsx
rename to exercises/02.authentication/02.problem.2fa/app/routes/users+/index.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/styles/tailwind.css b/exercises/02.authentication/02.problem.2fa/app/styles/tailwind.css
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/styles/tailwind.css
rename to exercises/02.authentication/02.problem.2fa/app/styles/tailwind.css
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/auth.server.test.ts b/exercises/02.authentication/02.problem.2fa/app/utils/auth.server.test.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/auth.server.test.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/auth.server.test.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/auth.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/auth.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/auth.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/auth.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/cache.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/cache.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/cache.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/cache.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/client-hints.tsx b/exercises/02.authentication/02.problem.2fa/app/utils/client-hints.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/client-hints.tsx
rename to exercises/02.authentication/02.problem.2fa/app/utils/client-hints.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/connections.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/connections.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/connections.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/connections.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/connections.tsx b/exercises/02.authentication/02.problem.2fa/app/utils/connections.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/connections.tsx
rename to exercises/02.authentication/02.problem.2fa/app/utils/connections.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/db.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/db.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/db.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/db.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/email.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/email.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/email.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/email.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/env.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/env.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/env.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/env.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/headers.server.test.ts b/exercises/02.authentication/02.problem.2fa/app/utils/headers.server.test.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/headers.server.test.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/headers.server.test.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/headers.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/headers.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/headers.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/headers.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/honeypot.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/honeypot.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/honeypot.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/honeypot.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/litefs.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/litefs.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/litefs.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/litefs.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/misc.error-message.test.ts b/exercises/02.authentication/02.problem.2fa/app/utils/misc.error-message.test.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/misc.error-message.test.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/misc.error-message.test.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/misc.tsx b/exercises/02.authentication/02.problem.2fa/app/utils/misc.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/misc.tsx
rename to exercises/02.authentication/02.problem.2fa/app/utils/misc.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/misc.use-double-check.test.tsx b/exercises/02.authentication/02.problem.2fa/app/utils/misc.use-double-check.test.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/misc.use-double-check.test.tsx
rename to exercises/02.authentication/02.problem.2fa/app/utils/misc.use-double-check.test.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/monitoring.client.tsx b/exercises/02.authentication/02.problem.2fa/app/utils/monitoring.client.tsx
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/monitoring.client.tsx
rename to exercises/02.authentication/02.problem.2fa/app/utils/monitoring.client.tsx
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/nonce-provider.ts b/exercises/02.authentication/02.problem.2fa/app/utils/nonce-provider.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/nonce-provider.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/nonce-provider.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/permissions.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/permissions.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/permissions.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/permissions.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/providers/constants.ts b/exercises/02.authentication/02.problem.2fa/app/utils/providers/constants.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/providers/constants.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/providers/constants.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/providers/github.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/providers/github.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/providers/github.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/providers/github.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/providers/provider.ts b/exercises/02.authentication/02.problem.2fa/app/utils/providers/provider.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/providers/provider.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/providers/provider.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/redirect-cookie.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/redirect-cookie.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/redirect-cookie.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/redirect-cookie.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/request-info.ts b/exercises/02.authentication/02.problem.2fa/app/utils/request-info.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/request-info.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/request-info.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/session.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/session.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/session.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/session.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/storage.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/storage.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/storage.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/storage.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/theme.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/theme.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/theme.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/theme.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/timing.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/timing.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/timing.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/timing.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/toast.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/toast.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/toast.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/toast.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/totp.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/totp.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/totp.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/totp.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/user-validation.ts b/exercises/02.authentication/02.problem.2fa/app/utils/user-validation.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/user-validation.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/user-validation.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/user.ts b/exercises/02.authentication/02.problem.2fa/app/utils/user.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/user.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/user.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/utils/verification.server.ts b/exercises/02.authentication/02.problem.2fa/app/utils/verification.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/app/utils/verification.server.ts
rename to exercises/02.authentication/02.problem.2fa/app/utils/verification.server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/components.json b/exercises/02.authentication/02.problem.2fa/components.json
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/components.json
rename to exercises/02.authentication/02.problem.2fa/components.json
diff --git a/exercises/02.test-setup/04.problem.api-mocking/eslint.config.js b/exercises/02.authentication/02.problem.2fa/eslint.config.js
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/eslint.config.js
rename to exercises/02.authentication/02.problem.2fa/eslint.config.js
diff --git a/exercises/02.test-setup/04.problem.api-mocking/fly.toml b/exercises/02.authentication/02.problem.2fa/fly.toml
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/fly.toml
rename to exercises/02.authentication/02.problem.2fa/fly.toml
diff --git a/exercises/02.test-setup/04.problem.api-mocking/index.js b/exercises/02.authentication/02.problem.2fa/index.js
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/index.js
rename to exercises/02.authentication/02.problem.2fa/index.js
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/Dockerfile b/exercises/02.authentication/02.problem.2fa/other/Dockerfile
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/Dockerfile
rename to exercises/02.authentication/02.problem.2fa/other/Dockerfile
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/Dockerfile.dockerignore b/exercises/02.authentication/02.problem.2fa/other/Dockerfile.dockerignore
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/Dockerfile.dockerignore
rename to exercises/02.authentication/02.problem.2fa/other/Dockerfile.dockerignore
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/README.md b/exercises/02.authentication/02.problem.2fa/other/README.md
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/README.md
rename to exercises/02.authentication/02.problem.2fa/other/README.md
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/build-server.ts b/exercises/02.authentication/02.problem.2fa/other/build-server.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/build-server.ts
rename to exercises/02.authentication/02.problem.2fa/other/build-server.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/litefs.yml b/exercises/02.authentication/02.problem.2fa/other/litefs.yml
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/litefs.yml
rename to exercises/02.authentication/02.problem.2fa/other/litefs.yml
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/sly/sly.json b/exercises/02.authentication/02.problem.2fa/other/sly/sly.json
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/sly/sly.json
rename to exercises/02.authentication/02.problem.2fa/other/sly/sly.json
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/sly/transform-icon.ts b/exercises/02.authentication/02.problem.2fa/other/sly/transform-icon.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/sly/transform-icon.ts
rename to exercises/02.authentication/02.problem.2fa/other/sly/transform-icon.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/README.md b/exercises/02.authentication/02.problem.2fa/other/svg-icons/README.md
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/README.md
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/README.md
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/arrow-left.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/arrow-left.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/arrow-left.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/arrow-left.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/arrow-right.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/arrow-right.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/arrow-right.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/arrow-right.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/avatar.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/avatar.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/avatar.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/avatar.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/camera.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/camera.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/camera.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/camera.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/check.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/check.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/check.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/check.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/clock.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/clock.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/clock.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/clock.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/cross-1.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/cross-1.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/cross-1.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/cross-1.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/dots-horizontal.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/dots-horizontal.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/dots-horizontal.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/dots-horizontal.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/download.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/download.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/download.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/download.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/envelope-closed.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/envelope-closed.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/envelope-closed.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/envelope-closed.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/exit.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/exit.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/exit.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/exit.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/file-text.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/file-text.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/file-text.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/file-text.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/github-logo.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/github-logo.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/github-logo.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/github-logo.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/laptop.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/laptop.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/laptop.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/laptop.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/link-2.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/link-2.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/link-2.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/link-2.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/lock-closed.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/lock-closed.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/lock-closed.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/lock-closed.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/lock-open-1.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/lock-open-1.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/lock-open-1.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/lock-open-1.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/magnifying-glass.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/magnifying-glass.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/magnifying-glass.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/magnifying-glass.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/moon.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/moon.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/moon.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/moon.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/passkey.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/passkey.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/passkey.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/passkey.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/pencil-1.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/pencil-1.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/pencil-1.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/pencil-1.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/pencil-2.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/pencil-2.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/pencil-2.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/pencil-2.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/plus.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/plus.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/plus.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/plus.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/question-mark-circled.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/question-mark-circled.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/question-mark-circled.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/question-mark-circled.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/reset.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/reset.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/reset.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/reset.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/sun.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/sun.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/sun.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/sun.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/trash.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/trash.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/trash.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/trash.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/update.svg b/exercises/02.authentication/02.problem.2fa/other/svg-icons/update.svg
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/other/svg-icons/update.svg
rename to exercises/02.authentication/02.problem.2fa/other/svg-icons/update.svg
diff --git a/exercises/02.test-setup/04.problem.api-mocking/package-lock.json b/exercises/02.authentication/02.problem.2fa/package-lock.json
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/package-lock.json
rename to exercises/02.authentication/02.problem.2fa/package-lock.json
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/package.json b/exercises/02.authentication/02.problem.2fa/package.json
similarity index 97%
rename from exercises/02.test-setup/01.solution.custom-fixtures/package.json
rename to exercises/02.authentication/02.problem.2fa/package.json
index f6809af..6c7a6a9 100644
--- a/exercises/02.test-setup/01.solution.custom-fixtures/package.json
+++ b/exercises/02.authentication/02.problem.2fa/package.json
@@ -1,5 +1,5 @@
{
- "name": "exercises_02.test-setup_01.solution.custom-fixtures",
+ "name": "exercises_02.authentication_02.problem.2fa",
"private": true,
"sideEffects": false,
"type": "module",
@@ -115,7 +115,7 @@
"devDependencies": {
"@epic-web/config": "^1.20.1",
"@faker-js/faker": "^9.7.0",
- "@playwright/test": "^1.52.0",
+ "@playwright/test": "^1.57.0",
"@react-router/dev": "^7.5.3",
"@sly-cli/sly": "^2.1.1",
"@testing-library/dom": "^10.4.0",
@@ -145,12 +145,13 @@
"jsdom": "^25.0.1",
"msw": "^2.7.6",
"npm-run-all": "^4.1.5",
- "playwright-persona": "^0.2.5",
+ "playwright-persona": "^0.2.8",
"prettier": "^3.5.3",
"prettier-plugin-sql": "^0.19.0",
"prettier-plugin-tailwindcss": "^0.6.11",
"react-router-devtools": "^5.0.5",
"remix-flat-routes": "^0.8.5",
+ "test-passkey": "^1.0.1",
"tsx": "^4.19.4",
"tw-animate-css": "^1.2.9",
"typescript": "^5.8.3",
diff --git a/exercises/02.test-setup/04.problem.api-mocking/playwright.config.ts b/exercises/02.authentication/02.problem.2fa/playwright.config.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/playwright.config.ts
rename to exercises/02.authentication/02.problem.2fa/playwright.config.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/prisma/migrations/20250221233640_init/migration.sql b/exercises/02.authentication/02.problem.2fa/prisma/migrations/20250221233640_init/migration.sql
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/prisma/migrations/20250221233640_init/migration.sql
rename to exercises/02.authentication/02.problem.2fa/prisma/migrations/20250221233640_init/migration.sql
diff --git a/exercises/02.test-setup/04.problem.api-mocking/prisma/migrations/migration_lock.toml b/exercises/02.authentication/02.problem.2fa/prisma/migrations/migration_lock.toml
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/prisma/migrations/migration_lock.toml
rename to exercises/02.authentication/02.problem.2fa/prisma/migrations/migration_lock.toml
diff --git a/exercises/02.test-setup/04.problem.api-mocking/prisma/schema.prisma b/exercises/02.authentication/02.problem.2fa/prisma/schema.prisma
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/prisma/schema.prisma
rename to exercises/02.authentication/02.problem.2fa/prisma/schema.prisma
diff --git a/exercises/02.authentication/02.problem.2fa/prisma/seed.ts b/exercises/02.authentication/02.problem.2fa/prisma/seed.ts
new file mode 100644
index 0000000..521e1d5
--- /dev/null
+++ b/exercises/02.authentication/02.problem.2fa/prisma/seed.ts
@@ -0,0 +1,263 @@
+import { faker } from '@faker-js/faker'
+import { prisma } from '#app/utils/db.server.ts'
+import { MOCK_CODE_GITHUB } from '#app/utils/providers/constants'
+import {
+ createPassword,
+ generateUserInfo,
+ getNoteImages,
+ getUserImages,
+} from '#tests/db-utils.ts'
+import { insertGitHubUser } from '#tests/mocks/github.ts'
+
+async function seed() {
+ console.log('🌱 Seeding...')
+ console.time(`🌱 Database has been seeded`)
+
+ const totalUsers = 5
+ console.time(`👤 Created ${totalUsers} users...`)
+ const noteImages = await getNoteImages()
+ const userImages = await getUserImages()
+
+ for (let index = 0; index < totalUsers; index++) {
+ const userData = generateUserInfo()
+ const user = await prisma.user.create({
+ select: { id: true },
+ data: {
+ ...userData,
+ password: { create: createPassword(userData.username) },
+ roles: { connect: { name: 'user' } },
+ },
+ })
+
+ // Upload user profile image
+ const userImage = userImages[index % userImages.length]
+ if (userImage) {
+ await prisma.userImage.create({
+ data: {
+ userId: user.id,
+ objectKey: userImage.objectKey,
+ },
+ })
+ }
+
+ // Create notes with images
+ const notesCount = faker.number.int({ min: 1, max: 3 })
+ for (let noteIndex = 0; noteIndex < notesCount; noteIndex++) {
+ const note = await prisma.note.create({
+ select: { id: true },
+ data: {
+ title: faker.lorem.sentence(),
+ content: faker.lorem.paragraphs(),
+ ownerId: user.id,
+ },
+ })
+
+ // Add images to note
+ const noteImageCount = faker.number.int({ min: 1, max: 3 })
+ for (let imageIndex = 0; imageIndex < noteImageCount; imageIndex++) {
+ const imgNumber = faker.number.int({ min: 0, max: 9 })
+ const noteImage = noteImages[imgNumber]
+ if (noteImage) {
+ await prisma.noteImage.create({
+ data: {
+ noteId: note.id,
+ altText: noteImage.altText,
+ objectKey: noteImage.objectKey,
+ },
+ })
+ }
+ }
+ }
+ }
+ console.timeEnd(`👤 Created ${totalUsers} users...`)
+
+ console.time(`🐨 Created admin user "kody"`)
+
+ const kodyImages = {
+ kodyUser: { objectKey: 'user/kody.png' },
+ cuteKoala: {
+ altText: 'an adorable koala cartoon illustration',
+ objectKey: 'kody-notes/cute-koala.png',
+ },
+ koalaEating: {
+ altText: 'a cartoon illustration of a koala in a tree eating',
+ objectKey: 'kody-notes/koala-eating.png',
+ },
+ koalaCuddle: {
+ altText: 'a cartoon illustration of koalas cuddling',
+ objectKey: 'kody-notes/koala-cuddle.png',
+ },
+ mountain: {
+ altText: 'a beautiful mountain covered in snow',
+ objectKey: 'kody-notes/mountain.png',
+ },
+ koalaCoder: {
+ altText: 'a koala coding at the computer',
+ objectKey: 'kody-notes/koala-coder.png',
+ },
+ koalaMentor: {
+ altText:
+ 'a koala in a friendly and helpful posture. The Koala is standing next to and teaching a woman who is coding on a computer and shows positive signs of learning and understanding what is being explained.',
+ objectKey: 'kody-notes/koala-mentor.png',
+ },
+ koalaSoccer: {
+ altText: 'a cute cartoon koala kicking a soccer ball on a soccer field ',
+ objectKey: 'kody-notes/koala-soccer.png',
+ },
+ }
+
+ const githubUser = await insertGitHubUser(MOCK_CODE_GITHUB)
+
+ const kody = await prisma.user.create({
+ select: { id: true },
+ data: {
+ email: 'kody@kcd.dev',
+ username: 'kody',
+ name: 'Kody',
+ password: { create: createPassword('kodylovesyou') },
+ connections: {
+ create: {
+ providerName: 'github',
+ providerId: String(githubUser.profile.id),
+ },
+ },
+ roles: { connect: [{ name: 'admin' }, { name: 'user' }] },
+ },
+ })
+
+ await prisma.userImage.create({
+ data: {
+ userId: kody.id,
+ objectKey: kodyImages.kodyUser.objectKey,
+ },
+ })
+
+ // Create Kody's notes
+ const kodyNotes = [
+ {
+ id: 'd27a197e',
+ title: 'Basic Koala Facts',
+ content:
+ 'Koalas are found in the eucalyptus forests of eastern Australia. They have grey fur with a cream-coloured chest, and strong, clawed feet, perfect for living in the branches of trees!',
+ images: [kodyImages.cuteKoala, kodyImages.koalaEating],
+ },
+ {
+ id: '414f0c09',
+ title: 'Koalas like to cuddle',
+ content:
+ 'Cuddly critters, koalas measure about 60cm to 85cm long, and weigh about 14kg.',
+ images: [kodyImages.koalaCuddle],
+ },
+ {
+ id: '260366b1',
+ title: 'Not bears',
+ content:
+ "Although you may have heard people call them koala 'bears', these awesome animals aren't bears at all – they are in fact marsupials. A group of mammals, most marsupials have pouches where their newborns develop.",
+ images: [],
+ },
+ {
+ id: 'bb79cf45',
+ title: 'Snowboarding Adventure',
+ content:
+ "Today was an epic day on the slopes! Shredded fresh powder with my friends, caught some sick air, and even attempted a backflip. Can't wait for the next snowy adventure!",
+ images: [kodyImages.mountain],
+ },
+ {
+ id: '9f4308be',
+ title: 'Onewheel Tricks',
+ content:
+ "Mastered a new trick on my Onewheel today called '180 Spin'. It's exhilarating to carve through the streets while pulling off these rad moves. Time to level up and learn more!",
+ images: [],
+ },
+ {
+ id: '306021fb',
+ title: 'Coding Dilemma',
+ content:
+ "Stuck on a bug in my latest coding project. Need to figure out why my function isn't returning the expected output. Time to dig deep, debug, and conquer this challenge!",
+ images: [kodyImages.koalaCoder],
+ },
+ {
+ id: '16d4912a',
+ title: 'Coding Mentorship',
+ content:
+ "Had a fantastic coding mentoring session today with Sarah. Helped her understand the concept of recursion, and she made great progress. It's incredibly fulfilling to help others improve their coding skills.",
+ images: [kodyImages.koalaMentor],
+ },
+ {
+ id: '3199199e',
+ title: 'Koala Fun Facts',
+ content:
+ "Did you know that koalas sleep for up to 20 hours a day? It's because their diet of eucalyptus leaves doesn't provide much energy. But when I'm awake, I enjoy munching on leaves, chilling in trees, and being the cuddliest koala around!",
+ images: [],
+ },
+ {
+ id: '2030ffd3',
+ title: 'Skiing Adventure',
+ content:
+ 'Spent the day hitting the slopes on my skis. The fresh powder made for some incredible runs and breathtaking views. Skiing down the mountain at top speed is an adrenaline rush like no other!',
+ images: [kodyImages.mountain],
+ },
+ {
+ id: 'f375a804',
+ title: 'Code Jam Success',
+ content:
+ 'Participated in a coding competition today and secured the first place! The adrenaline, the challenging problems, and the satisfaction of finding optimal solutions—it was an amazing experience. Feeling proud and motivated to keep pushing my coding skills further!',
+ images: [kodyImages.koalaCoder],
+ },
+ {
+ id: '562c541b',
+ title: 'Koala Conservation Efforts',
+ content:
+ "Joined a local conservation group to protect koalas and their habitats. Together, we're planting more eucalyptus trees, raising awareness about their endangered status, and working towards a sustainable future for these adorable creatures. Every small step counts!",
+ images: [],
+ },
+ {
+ id: 'f67ca40b',
+ title: 'Game day',
+ content:
+ "Just got back from the most amazing game. I've been playing soccer for a long time, but I've not once scored a goal. Well, today all that changed! I finally scored my first ever goal.\n\nI'm in an indoor league, and my team's not the best, but we're pretty good and I have fun, that's all that really matters. Anyway, I found myself at the other end of the field with the ball. It was just me and the goalie. I normally just kick the ball and hope it goes in, but the ball was already rolling toward the goal. The goalie was about to get the ball, so I had to charge. I managed to get possession of the ball just before the goalie got it. I brought it around the goalie and had a perfect shot. I screamed so loud in excitement. After all these years playing, I finally scored a goal!\n\nI know it's not a lot for most folks, but it meant a lot to me. We did end up winning the game by one. It makes me feel great that I had a part to play in that.\n\nIn this team, I'm the captain. I'm constantly cheering my team on. Even after getting injured, I continued to come and watch from the side-lines. I enjoy yelling (encouragingly) at my team mates and helping them be the best they can. I'm definitely not the best player by a long stretch. But I really enjoy the game. It's a great way to get exercise and have good social interactions once a week.\n\nThat said, it can be hard to keep people coming and paying dues and stuff. If people don't show up it can be really hard to find subs. I have a list of people I can text, but sometimes I can't find anyone.\n\nBut yeah, today was awesome. I felt like more than just a player that gets in the way of the opposition, but an actual asset to the team. Really great feeling.\n\nAnyway, I'm rambling at this point and really this is just so we can have a note that's pretty long to test things out. I think it's long enough now... Cheers!",
+ images: [kodyImages.koalaSoccer],
+ },
+ ]
+
+ for (const noteData of kodyNotes) {
+ const note = await prisma.note.create({
+ select: { id: true },
+ data: {
+ id: noteData.id,
+ title: noteData.title,
+ content: noteData.content,
+ ownerId: kody.id,
+ },
+ })
+
+ for (const image of noteData.images) {
+ await prisma.noteImage.create({
+ data: {
+ noteId: note.id,
+ altText: image.altText,
+ objectKey: image.objectKey,
+ },
+ })
+ }
+ }
+
+ console.timeEnd(`🐨 Created admin user "kody"`)
+
+ console.timeEnd(`🌱 Database has been seeded`)
+}
+
+seed()
+ .catch((e) => {
+ console.error(e)
+ process.exit(1)
+ })
+ .finally(async () => {
+ await prisma.$disconnect()
+ })
+
+// we're ok to import from the test directory in this file
+/*
+eslint
+ no-restricted-imports: "off",
+*/
diff --git a/exercises/02.test-setup/04.problem.api-mocking/prisma/sql/searchUsers.sql b/exercises/02.authentication/02.problem.2fa/prisma/sql/searchUsers.sql
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/prisma/sql/searchUsers.sql
rename to exercises/02.authentication/02.problem.2fa/prisma/sql/searchUsers.sql
diff --git a/exercises/02.test-setup/04.problem.api-mocking/public/favicon.ico b/exercises/02.authentication/02.problem.2fa/public/favicon.ico
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/public/favicon.ico
rename to exercises/02.authentication/02.problem.2fa/public/favicon.ico
diff --git a/exercises/02.test-setup/04.problem.api-mocking/public/favicons/README.md b/exercises/02.authentication/02.problem.2fa/public/favicons/README.md
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/public/favicons/README.md
rename to exercises/02.authentication/02.problem.2fa/public/favicons/README.md
diff --git a/exercises/02.test-setup/04.problem.api-mocking/public/favicons/android-chrome-192x192.png b/exercises/02.authentication/02.problem.2fa/public/favicons/android-chrome-192x192.png
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/public/favicons/android-chrome-192x192.png
rename to exercises/02.authentication/02.problem.2fa/public/favicons/android-chrome-192x192.png
diff --git a/exercises/02.test-setup/04.problem.api-mocking/public/favicons/android-chrome-512x512.png b/exercises/02.authentication/02.problem.2fa/public/favicons/android-chrome-512x512.png
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/public/favicons/android-chrome-512x512.png
rename to exercises/02.authentication/02.problem.2fa/public/favicons/android-chrome-512x512.png
diff --git a/exercises/02.test-setup/04.problem.api-mocking/public/img/user.png b/exercises/02.authentication/02.problem.2fa/public/img/user.png
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/public/img/user.png
rename to exercises/02.authentication/02.problem.2fa/public/img/user.png
diff --git a/exercises/02.test-setup/04.problem.api-mocking/public/site.webmanifest b/exercises/02.authentication/02.problem.2fa/public/site.webmanifest
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/public/site.webmanifest
rename to exercises/02.authentication/02.problem.2fa/public/site.webmanifest
diff --git a/exercises/02.test-setup/04.problem.api-mocking/react-router.config.ts b/exercises/02.authentication/02.problem.2fa/react-router.config.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/react-router.config.ts
rename to exercises/02.authentication/02.problem.2fa/react-router.config.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/server/dev-server.js b/exercises/02.authentication/02.problem.2fa/server/dev-server.js
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/server/dev-server.js
rename to exercises/02.authentication/02.problem.2fa/server/dev-server.js
diff --git a/exercises/02.test-setup/04.problem.api-mocking/server/index.ts b/exercises/02.authentication/02.problem.2fa/server/index.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/server/index.ts
rename to exercises/02.authentication/02.problem.2fa/server/index.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/server/utils/monitoring.ts b/exercises/02.authentication/02.problem.2fa/server/utils/monitoring.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/server/utils/monitoring.ts
rename to exercises/02.authentication/02.problem.2fa/server/utils/monitoring.ts
diff --git a/exercises/03.guides/02.solution.test-annotations/tests/db-utils.ts b/exercises/02.authentication/02.problem.2fa/tests/db-utils.ts
similarity index 68%
rename from exercises/03.guides/02.solution.test-annotations/tests/db-utils.ts
rename to exercises/02.authentication/02.problem.2fa/tests/db-utils.ts
index f701a8a..3b58c39 100644
--- a/exercises/03.guides/02.solution.test-annotations/tests/db-utils.ts
+++ b/exercises/02.authentication/02.problem.2fa/tests/db-utils.ts
@@ -1,10 +1,13 @@
+import { type generateTOTP } from '@epic-web/totp'
import { faker } from '@faker-js/faker'
import bcrypt from 'bcryptjs'
import { UniqueEnforcer } from 'enforce-unique'
+import { getPasswordHash } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
const uniqueUsernameEnforcer = new UniqueEnforcer()
-export function createUser() {
+export function generateUserInfo() {
const firstName = faker.person.firstName()
const lastName = faker.person.lastName()
@@ -22,6 +25,7 @@ export function createUser() {
.slice(0, 20)
.toLowerCase()
.replace(/[^a-z0-9_]/g, '_')
+
return {
username,
name: `${firstName} ${lastName}`,
@@ -29,6 +33,50 @@ export function createUser() {
}
}
+export async function createUser() {
+ const userInfo = generateUserInfo()
+ const password = 'supersecret'
+ const user = await prisma.user.create({
+ data: {
+ ...userInfo,
+ password: { create: { hash: await getPasswordHash(password) } },
+ },
+ })
+
+ return {
+ async [Symbol.asyncDispose]() {
+ await prisma.user.deleteMany({
+ where: { id: user.id },
+ })
+ },
+ ...user,
+ password,
+ }
+}
+
+export async function createVerification(input: {
+ totp: Awaited>
+ userId: string
+}) {
+ const { otp, ...totpConfig } = input.totp
+ const verification = await prisma.verification.create({
+ data: {
+ ...totpConfig,
+ type: '2fa',
+ target: input.userId,
+ },
+ })
+
+ return {
+ async [Symbol.asyncDispose]() {
+ await prisma.verification.deleteMany({
+ where: { id: verification.id },
+ })
+ },
+ ...verification,
+ }
+}
+
export function createPassword(password: string = faker.internet.password()) {
return {
hash: bcrypt.hashSync(password, 10),
diff --git a/exercises/02.authentication/02.problem.2fa/tests/e2e/authentication-2fa.test.ts b/exercises/02.authentication/02.problem.2fa/tests/e2e/authentication-2fa.test.ts
new file mode 100644
index 0000000..88ac661
--- /dev/null
+++ b/exercises/02.authentication/02.problem.2fa/tests/e2e/authentication-2fa.test.ts
@@ -0,0 +1,10 @@
+import { generateTOTP } from '@epic-web/totp'
+import { createUser, createVerification } from '#tests/db-utils.ts'
+import { test, expect } from '#tests/test-extend.ts'
+
+test('authenticates using two-factor authentication', async ({
+ navigate,
+ page,
+}) => {
+ /** @todo Instructions */
+})
diff --git a/exercises/02.test-setup/04.problem.api-mocking/tests/setup/custom-matchers.ts b/exercises/02.authentication/02.problem.2fa/tests/setup/custom-matchers.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/tests/setup/custom-matchers.ts
rename to exercises/02.authentication/02.problem.2fa/tests/setup/custom-matchers.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/tests/setup/db-setup.ts b/exercises/02.authentication/02.problem.2fa/tests/setup/db-setup.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/tests/setup/db-setup.ts
rename to exercises/02.authentication/02.problem.2fa/tests/setup/db-setup.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/tests/setup/global-setup.ts b/exercises/02.authentication/02.problem.2fa/tests/setup/global-setup.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/tests/setup/global-setup.ts
rename to exercises/02.authentication/02.problem.2fa/tests/setup/global-setup.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/tests/setup/setup-test-env.ts b/exercises/02.authentication/02.problem.2fa/tests/setup/setup-test-env.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/tests/setup/setup-test-env.ts
rename to exercises/02.authentication/02.problem.2fa/tests/setup/setup-test-env.ts
diff --git a/exercises/02.authentication/02.problem.2fa/tests/test-extend.ts b/exercises/02.authentication/02.problem.2fa/tests/test-extend.ts
new file mode 100644
index 0000000..a51b50d
--- /dev/null
+++ b/exercises/02.authentication/02.problem.2fa/tests/test-extend.ts
@@ -0,0 +1,57 @@
+import { test as testBase, expect } from '@playwright/test'
+import {
+ definePersona,
+ combinePersonas,
+ type AuthenticateFunction,
+} from 'playwright-persona'
+import { href, type Register } from 'react-router'
+import { getPasswordHash } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { generateUserInfo } from '#tests/db-utils'
+
+interface Fixtures {
+ navigate: (
+ ...args: Parameters>
+ ) => Promise
+ authenticate: AuthenticateFunction<[typeof user]>
+}
+
+const user = definePersona('user', {
+ async createSession({ page }) {
+ const user = await prisma.user.create({
+ data: {
+ ...generateUserInfo(),
+ roles: { connect: { name: 'user' } },
+ password: { create: { hash: await getPasswordHash('supersecret') } },
+ },
+ })
+
+ await page.goto('/login')
+ await page.getByLabel('Username').fill(user.username)
+ await page.getByLabel('Password').fill('supersecret')
+ await page.getByRole('button', { name: 'Log in' }).click()
+ await page.getByText(user.name!).waitFor({ state: 'visible' })
+
+ return { user }
+ },
+ async verifySession({ page, session }) {
+ await page.goto('/')
+ await expect(page.getByText(session.user.name!)).toBeVisible({
+ timeout: 100,
+ })
+ },
+ async destroySession({ session }) {
+ await prisma.user.deleteMany({ where: { id: session.user.id } })
+ },
+})
+
+export const test = testBase.extend({
+ async navigate({ page }, use) {
+ await use(async (...args) => {
+ await page.goto(href(...args))
+ })
+ },
+ authenticate: combinePersonas(user),
+})
+
+export { expect }
diff --git a/exercises/02.test-setup/04.problem.api-mocking/tests/utils.ts b/exercises/02.authentication/02.problem.2fa/tests/utils.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/tests/utils.ts
rename to exercises/02.authentication/02.problem.2fa/tests/utils.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/tsconfig.json b/exercises/02.authentication/02.problem.2fa/tsconfig.json
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/tsconfig.json
rename to exercises/02.authentication/02.problem.2fa/tsconfig.json
diff --git a/exercises/02.test-setup/04.problem.api-mocking/types/deps.d.ts b/exercises/02.authentication/02.problem.2fa/types/deps.d.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/types/deps.d.ts
rename to exercises/02.authentication/02.problem.2fa/types/deps.d.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/types/env.env.d.ts b/exercises/02.authentication/02.problem.2fa/types/env.env.d.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/types/env.env.d.ts
rename to exercises/02.authentication/02.problem.2fa/types/env.env.d.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/types/icon-name.d.ts b/exercises/02.authentication/02.problem.2fa/types/icon-name.d.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/types/icon-name.d.ts
rename to exercises/02.authentication/02.problem.2fa/types/icon-name.d.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/types/reset.d.ts b/exercises/02.authentication/02.problem.2fa/types/reset.d.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/types/reset.d.ts
rename to exercises/02.authentication/02.problem.2fa/types/reset.d.ts
diff --git a/exercises/02.test-setup/04.problem.api-mocking/vite.config.ts b/exercises/02.authentication/02.problem.2fa/vite.config.ts
similarity index 100%
rename from exercises/02.test-setup/04.problem.api-mocking/vite.config.ts
rename to exercises/02.authentication/02.problem.2fa/vite.config.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/.env b/exercises/02.authentication/02.solution.2fa/.env
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/.env
rename to exercises/02.authentication/02.solution.2fa/.env
diff --git a/exercises/02.test-setup/04.solution.api-mocking/.env.example b/exercises/02.authentication/02.solution.2fa/.env.example
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/.env.example
rename to exercises/02.authentication/02.solution.2fa/.env.example
diff --git a/exercises/02.test-setup/04.solution.api-mocking/.gitignore b/exercises/02.authentication/02.solution.2fa/.gitignore
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/.gitignore
rename to exercises/02.authentication/02.solution.2fa/.gitignore
diff --git a/exercises/02.test-setup/04.solution.api-mocking/.npmrc b/exercises/02.authentication/02.solution.2fa/.npmrc
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/.npmrc
rename to exercises/02.authentication/02.solution.2fa/.npmrc
diff --git a/exercises/02.test-setup/04.solution.api-mocking/.prettierignore b/exercises/02.authentication/02.solution.2fa/.prettierignore
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/.prettierignore
rename to exercises/02.authentication/02.solution.2fa/.prettierignore
diff --git a/exercises/02.test-setup/04.solution.api-mocking/.vscode/extensions.json b/exercises/02.authentication/02.solution.2fa/.vscode/extensions.json
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/.vscode/extensions.json
rename to exercises/02.authentication/02.solution.2fa/.vscode/extensions.json
diff --git a/exercises/02.test-setup/04.solution.api-mocking/.vscode/remix.code-snippets b/exercises/02.authentication/02.solution.2fa/.vscode/remix.code-snippets
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/.vscode/remix.code-snippets
rename to exercises/02.authentication/02.solution.2fa/.vscode/remix.code-snippets
diff --git a/exercises/02.test-setup/04.solution.api-mocking/.vscode/settings.json b/exercises/02.authentication/02.solution.2fa/.vscode/settings.json
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/.vscode/settings.json
rename to exercises/02.authentication/02.solution.2fa/.vscode/settings.json
diff --git a/exercises/02.authentication/02.solution.2fa/README.mdx b/exercises/02.authentication/02.solution.2fa/README.mdx
new file mode 100644
index 0000000..30d2c6d
--- /dev/null
+++ b/exercises/02.authentication/02.solution.2fa/README.mdx
@@ -0,0 +1 @@
+# Two-factor authentication
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/assets/favicons/apple-touch-icon.png b/exercises/02.authentication/02.solution.2fa/app/assets/favicons/apple-touch-icon.png
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/assets/favicons/apple-touch-icon.png
rename to exercises/02.authentication/02.solution.2fa/app/assets/favicons/apple-touch-icon.png
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/assets/favicons/favicon.svg b/exercises/02.authentication/02.solution.2fa/app/assets/favicons/favicon.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/assets/favicons/favicon.svg
rename to exercises/02.authentication/02.solution.2fa/app/assets/favicons/favicon.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/error-boundary.tsx b/exercises/02.authentication/02.solution.2fa/app/components/error-boundary.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/error-boundary.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/error-boundary.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/floating-toolbar.tsx b/exercises/02.authentication/02.solution.2fa/app/components/floating-toolbar.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/floating-toolbar.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/floating-toolbar.tsx
diff --git a/exercises/02.test-setup/03.solution.authentication/app/components/forms.tsx b/exercises/02.authentication/02.solution.2fa/app/components/forms.tsx
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/app/components/forms.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/forms.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/progress-bar.tsx b/exercises/02.authentication/02.solution.2fa/app/components/progress-bar.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/progress-bar.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/progress-bar.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/search-bar.tsx b/exercises/02.authentication/02.solution.2fa/app/components/search-bar.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/search-bar.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/search-bar.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/spacer.tsx b/exercises/02.authentication/02.solution.2fa/app/components/spacer.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/spacer.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/spacer.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/toaster.tsx b/exercises/02.authentication/02.solution.2fa/app/components/toaster.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/toaster.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/toaster.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/ui/README.md b/exercises/02.authentication/02.solution.2fa/app/components/ui/README.md
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/ui/README.md
rename to exercises/02.authentication/02.solution.2fa/app/components/ui/README.md
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/ui/button.tsx b/exercises/02.authentication/02.solution.2fa/app/components/ui/button.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/ui/button.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/ui/button.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/ui/checkbox.tsx b/exercises/02.authentication/02.solution.2fa/app/components/ui/checkbox.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/ui/checkbox.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/ui/checkbox.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/ui/dropdown-menu.tsx b/exercises/02.authentication/02.solution.2fa/app/components/ui/dropdown-menu.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/ui/dropdown-menu.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/ui/dropdown-menu.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/ui/icon.tsx b/exercises/02.authentication/02.solution.2fa/app/components/ui/icon.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/ui/icon.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/ui/icon.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/ui/input-otp.tsx b/exercises/02.authentication/02.solution.2fa/app/components/ui/input-otp.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/ui/input-otp.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/ui/input-otp.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/ui/input.tsx b/exercises/02.authentication/02.solution.2fa/app/components/ui/input.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/ui/input.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/ui/input.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/ui/label.tsx b/exercises/02.authentication/02.solution.2fa/app/components/ui/label.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/ui/label.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/ui/label.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/ui/sonner.tsx b/exercises/02.authentication/02.solution.2fa/app/components/ui/sonner.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/ui/sonner.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/ui/sonner.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/ui/status-button.tsx b/exercises/02.authentication/02.solution.2fa/app/components/ui/status-button.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/ui/status-button.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/ui/status-button.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/ui/textarea.tsx b/exercises/02.authentication/02.solution.2fa/app/components/ui/textarea.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/ui/textarea.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/ui/textarea.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/ui/tooltip.tsx b/exercises/02.authentication/02.solution.2fa/app/components/ui/tooltip.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/ui/tooltip.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/ui/tooltip.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/components/user-dropdown.tsx b/exercises/02.authentication/02.solution.2fa/app/components/user-dropdown.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/components/user-dropdown.tsx
rename to exercises/02.authentication/02.solution.2fa/app/components/user-dropdown.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/entry.client.tsx b/exercises/02.authentication/02.solution.2fa/app/entry.client.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/entry.client.tsx
rename to exercises/02.authentication/02.solution.2fa/app/entry.client.tsx
diff --git a/exercises/02.authentication/02.solution.2fa/app/entry.server.tsx b/exercises/02.authentication/02.solution.2fa/app/entry.server.tsx
new file mode 100644
index 0000000..8d8b1de
--- /dev/null
+++ b/exercises/02.authentication/02.solution.2fa/app/entry.server.tsx
@@ -0,0 +1,143 @@
+import crypto from 'node:crypto'
+import { PassThrough } from 'node:stream'
+import { styleText } from 'node:util'
+import { contentSecurity } from '@nichtsam/helmet/content'
+import { createReadableStreamFromReadable } from '@react-router/node'
+import * as Sentry from '@sentry/react-router'
+import { isbot } from 'isbot'
+import { renderToPipeableStream } from 'react-dom/server'
+import {
+ ServerRouter,
+ type LoaderFunctionArgs,
+ type ActionFunctionArgs,
+ type HandleDocumentRequestFunction,
+} from 'react-router'
+import { getEnv, init } from './utils/env.server.ts'
+import { getInstanceInfo } from './utils/litefs.server.ts'
+import { NonceProvider } from './utils/nonce-provider.ts'
+import { makeTimings } from './utils/timing.server.ts'
+
+export const streamTimeout = 5000
+
+init()
+global.ENV = getEnv()
+
+const MODE = process.env.NODE_ENV ?? 'development'
+
+type DocRequestArgs = Parameters
+
+export default async function handleRequest(...args: DocRequestArgs) {
+ const [request, responseStatusCode, responseHeaders, reactRouterContext] =
+ args
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ responseHeaders.set('fly-primary-instance', primaryInstance)
+ responseHeaders.set('fly-instance', currentInstance)
+
+ if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
+ responseHeaders.append('Document-Policy', 'js-profiling')
+ }
+
+ const callbackName = isbot(request.headers.get('user-agent'))
+ ? 'onAllReady'
+ : 'onShellReady'
+
+ const nonce = crypto.randomBytes(16).toString('hex')
+ return new Promise(async (resolve, reject) => {
+ let didError = false
+ // NOTE: this timing will only include things that are rendered in the shell
+ // and will not include suspended components and deferred loaders
+ const timings = makeTimings('render', 'renderToPipeableStream')
+
+ const { pipe, abort } = renderToPipeableStream(
+
+
+ ,
+ {
+ [callbackName]: () => {
+ const body = new PassThrough()
+ responseHeaders.set('Content-Type', 'text/html')
+ responseHeaders.append('Server-Timing', timings.toString())
+
+ contentSecurity(responseHeaders, {
+ crossOriginEmbedderPolicy: false,
+ contentSecurityPolicy: {
+ // NOTE: Remove reportOnly when you're ready to enforce this CSP
+ reportOnly: true,
+ directives: {
+ fetch: {
+ 'connect-src': [
+ MODE === 'development' ? 'ws:' : undefined,
+ process.env.SENTRY_DSN ? '*.sentry.io' : undefined,
+ "'self'",
+ ],
+ 'font-src': ["'self'"],
+ 'frame-src': ["'self'"],
+ 'img-src': ["'self'", 'data:'],
+ 'script-src': [
+ "'strict-dynamic'",
+ "'self'",
+ `'nonce-${nonce}'`,
+ ],
+ 'script-src-attr': [`'nonce-${nonce}'`],
+ },
+ },
+ },
+ xFrameOptions: false,
+ })
+
+ resolve(
+ new Response(createReadableStreamFromReadable(body), {
+ headers: responseHeaders,
+ status: didError ? 500 : responseStatusCode,
+ }),
+ )
+ pipe(body)
+ },
+ onShellError: (err: unknown) => {
+ reject(err)
+ },
+ onError: () => {
+ didError = true
+ },
+ nonce,
+ },
+ )
+
+ setTimeout(abort, streamTimeout + 5000)
+ })
+}
+
+export async function handleDataRequest(response: Response) {
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ response.headers.set('fly-primary-instance', primaryInstance)
+ response.headers.set('fly-instance', currentInstance)
+
+ return response
+}
+
+export function handleError(
+ error: unknown,
+ { request }: LoaderFunctionArgs | ActionFunctionArgs,
+): void {
+ // Skip capturing if the request is aborted as Remix docs suggest
+ // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
+ if (request.signal.aborted) {
+ return
+ }
+
+ if (error instanceof Error) {
+ console.error(styleText('red', String(error.stack)))
+ } else {
+ console.error(error)
+ }
+
+ Sentry.captureException(error)
+}
diff --git a/exercises/02.test-setup/05.solution.test-data/app/root.tsx b/exercises/02.authentication/02.solution.2fa/app/root.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/root.tsx
rename to exercises/02.authentication/02.solution.2fa/app/root.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes.ts b/exercises/02.authentication/02.solution.2fa/app/routes.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes.ts
rename to exercises/02.authentication/02.solution.2fa/app/routes.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/$.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/$.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/$.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/$.tsx
diff --git a/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/auth.$provider.callback.test.ts b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/auth.$provider.callback.test.ts
new file mode 100644
index 0000000..3765dd7
--- /dev/null
+++ b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/auth.$provider.callback.test.ts
@@ -0,0 +1,265 @@
+import { invariant } from '@epic-web/invariant'
+import { faker } from '@faker-js/faker'
+import { SetCookie } from '@mjackson/headers'
+import { http } from 'msw'
+import { afterEach, expect, test } from 'vitest'
+import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx'
+import { getSessionExpirationDate, sessionKey } from '#app/utils/auth.server.ts'
+import { GITHUB_PROVIDER_NAME } from '#app/utils/connections.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { generateTOTP } from '#app/utils/totp.server.ts'
+import { generateUserInfo } from '#tests/db-utils.ts'
+import { insertGitHubUser, deleteGitHubUsers } from '#tests/mocks/github.ts'
+import { server } from '#tests/mocks/index.ts'
+import { consoleError } from '#tests/setup/setup-test-env.ts'
+import { BASE_URL, convertSetCookieToCookie } from '#tests/utils.ts'
+import { loader } from './auth.$provider.callback.ts'
+
+const ROUTE_PATH = '/auth/github/callback'
+const PARAMS = { provider: 'github' }
+
+afterEach(async () => {
+ await deleteGitHubUsers()
+})
+
+test('a new user goes to onboarding', async () => {
+ const request = await setupRequest()
+ const response = await loader({ request, params: PARAMS, context: {} }).catch(
+ (e) => e,
+ )
+ expect(response).toHaveRedirect('/onboarding/github')
+})
+
+test('when auth fails, send the user to login with a toast', async () => {
+ consoleError.mockImplementation(() => {})
+ server.use(
+ http.post('https://github.com/login/oauth/access_token', async () => {
+ return new Response(null, { status: 400 })
+ }),
+ )
+ const request = await setupRequest()
+ const response = await loader({ request, params: PARAMS, context: {} }).catch(
+ (e) => e,
+ )
+ invariant(response instanceof Response, 'response should be a Response')
+ expect(response).toHaveRedirect('/login')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Auth Failed',
+ type: 'error',
+ }),
+ )
+ expect(consoleError).toHaveBeenCalledTimes(1)
+})
+
+test('when a user is logged in, it creates the connection', async () => {
+ const githubUser = await insertGitHubUser()
+ const session = await setupUser()
+ const request = await setupRequest({
+ sessionId: session.id,
+ code: githubUser.code,
+ })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/settings/profile/connections')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Connected',
+ type: 'success',
+ description: expect.stringContaining(githubUser.profile.login),
+ }),
+ )
+ const connection = await prisma.connection.findFirst({
+ select: { id: true },
+ where: {
+ userId: session.userId,
+ providerId: githubUser.profile.id.toString(),
+ },
+ })
+ expect(
+ connection,
+ 'the connection was not created in the database',
+ ).toBeTruthy()
+})
+
+test(`when a user is logged in and has already connected, it doesn't do anything and just redirects the user back to the connections page`, async () => {
+ const session = await setupUser()
+ const githubUser = await insertGitHubUser()
+ await prisma.connection.create({
+ data: {
+ providerName: GITHUB_PROVIDER_NAME,
+ userId: session.userId,
+ providerId: githubUser.profile.id.toString(),
+ },
+ })
+ const request = await setupRequest({
+ sessionId: session.id,
+ code: githubUser.code,
+ })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/settings/profile/connections')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Already Connected',
+ description: expect.stringContaining(githubUser.profile.login),
+ }),
+ )
+})
+
+test('when a user exists with the same email, create connection and make session', async () => {
+ const githubUser = await insertGitHubUser()
+ const email = githubUser.primaryEmail.toLowerCase()
+ const { userId } = await setupUser({ ...generateUserInfo(), email })
+ const request = await setupRequest({ code: githubUser.code })
+ const response = await loader({ request, params: PARAMS, context: {} })
+
+ expect(response).toHaveRedirect('/')
+
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ type: 'message',
+ description: expect.stringContaining(githubUser.profile.login),
+ }),
+ )
+
+ const connection = await prisma.connection.findFirst({
+ select: { id: true },
+ where: {
+ userId: userId,
+ providerId: githubUser.profile.id.toString(),
+ },
+ })
+ expect(
+ connection,
+ 'the connection was not created in the database',
+ ).toBeTruthy()
+
+ await expect(response).toHaveSessionForUser(userId)
+})
+
+test('gives an error if the account is already connected to another user', async () => {
+ const githubUser = await insertGitHubUser()
+ await prisma.user.create({
+ data: {
+ ...generateUserInfo(),
+ connections: {
+ create: {
+ providerName: GITHUB_PROVIDER_NAME,
+ providerId: githubUser.profile.id.toString(),
+ },
+ },
+ },
+ })
+ const session = await setupUser()
+ const request = await setupRequest({
+ sessionId: session.id,
+ code: githubUser.code,
+ })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/settings/profile/connections')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Already Connected',
+ description: expect.stringContaining(
+ 'already connected to another account',
+ ),
+ }),
+ )
+})
+
+test('if a user is not logged in, but the connection exists, make a session', async () => {
+ const githubUser = await insertGitHubUser()
+ const { userId } = await setupUser()
+ await prisma.connection.create({
+ data: {
+ providerName: GITHUB_PROVIDER_NAME,
+ providerId: githubUser.profile.id.toString(),
+ userId,
+ },
+ })
+ const request = await setupRequest({ code: githubUser.code })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/')
+ await expect(response).toHaveSessionForUser(userId)
+})
+
+test('if a user is not logged in, but the connection exists and they have enabled 2FA, send them to verify their 2FA and do not make a session', async () => {
+ const githubUser = await insertGitHubUser()
+ const { userId } = await setupUser()
+ await prisma.connection.create({
+ data: {
+ providerName: GITHUB_PROVIDER_NAME,
+ providerId: githubUser.profile.id.toString(),
+ userId,
+ },
+ })
+ const { otp: _otp, ...config } = await generateTOTP()
+ await prisma.verification.create({
+ data: {
+ type: twoFAVerificationType,
+ target: userId,
+ ...config,
+ },
+ })
+ const request = await setupRequest({ code: githubUser.code })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ const searchParams = new URLSearchParams({
+ type: twoFAVerificationType,
+ target: userId,
+ redirectTo: '/',
+ })
+ expect(response).toHaveRedirect(`/verify?${searchParams}`)
+})
+
+async function setupRequest({
+ sessionId,
+ code = faker.string.uuid(),
+}: { sessionId?: string; code?: string } = {}) {
+ const url = new URL(ROUTE_PATH, BASE_URL)
+ const state = faker.string.uuid()
+ url.searchParams.set('state', state)
+ url.searchParams.set('code', code)
+ const authSession = await authSessionStorage.getSession()
+ if (sessionId) authSession.set(sessionKey, sessionId)
+ const setSessionCookieHeader =
+ await authSessionStorage.commitSession(authSession)
+ const searchParams = new URLSearchParams({ code, state })
+ let authCookie = new SetCookie({
+ name: 'github',
+ value: searchParams.toString(),
+ path: '/',
+ sameSite: 'Lax',
+ httpOnly: true,
+ maxAge: 60 * 10,
+ secure: process.env.NODE_ENV === 'production' || undefined,
+ })
+ const request = new Request(url.toString(), {
+ method: 'GET',
+ headers: {
+ cookie: [
+ authCookie.toString(),
+ convertSetCookieToCookie(setSessionCookieHeader),
+ ].join('; '),
+ },
+ })
+ return request
+}
+
+async function setupUser(userData = generateUserInfo()) {
+ const session = await prisma.session.create({
+ data: {
+ expirationDate: getSessionExpirationDate(),
+ user: {
+ create: {
+ ...userData,
+ },
+ },
+ },
+ select: {
+ id: true,
+ userId: true,
+ },
+ })
+
+ return session
+}
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/auth.$provider.callback.ts b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/auth.$provider.callback.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/auth.$provider.callback.ts
rename to exercises/02.authentication/02.solution.2fa/app/routes/_auth+/auth.$provider.callback.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/auth_.$provider.ts b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/auth_.$provider.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/auth_.$provider.ts
rename to exercises/02.authentication/02.solution.2fa/app/routes/_auth+/auth_.$provider.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/forgot-password.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/forgot-password.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/forgot-password.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/_auth+/forgot-password.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/login.server.ts b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/login.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/login.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/routes/_auth+/login.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/login.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/login.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/login.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/_auth+/login.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/logout.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/logout.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/logout.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/_auth+/logout.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/onboarding.server.ts b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/onboarding.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/onboarding.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/routes/_auth+/onboarding.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/onboarding.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/onboarding.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/onboarding.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/_auth+/onboarding.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/onboarding_.$provider.server.ts b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/onboarding_.$provider.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/onboarding_.$provider.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/routes/_auth+/onboarding_.$provider.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/onboarding_.$provider.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/onboarding_.$provider.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/onboarding_.$provider.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/_auth+/onboarding_.$provider.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/reset-password.server.ts b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/reset-password.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/reset-password.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/routes/_auth+/reset-password.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/reset-password.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/reset-password.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/reset-password.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/_auth+/reset-password.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/signup.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/signup.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/signup.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/_auth+/signup.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/verify.server.ts b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/verify.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/verify.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/routes/_auth+/verify.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/verify.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/verify.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/verify.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/_auth+/verify.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/webauthn+/authentication.ts b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/webauthn+/authentication.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/webauthn+/authentication.ts
rename to exercises/02.authentication/02.solution.2fa/app/routes/_auth+/webauthn+/authentication.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/webauthn+/registration.ts b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/webauthn+/registration.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/webauthn+/registration.ts
rename to exercises/02.authentication/02.solution.2fa/app/routes/_auth+/webauthn+/registration.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/webauthn+/utils.server.ts b/exercises/02.authentication/02.solution.2fa/app/routes/_auth+/webauthn+/utils.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_auth+/webauthn+/utils.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/routes/_auth+/webauthn+/utils.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/about.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/about.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/about.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/about.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/index.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/index.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/index.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/index.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/docker.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/docker.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/docker.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/docker.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/eslint.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/eslint.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/eslint.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/eslint.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/faker.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/faker.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/faker.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/faker.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/fly.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/fly.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/fly.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/fly.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/github.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/github.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/github.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/github.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/logos.ts b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/logos.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/logos.ts
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/logos.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/msw.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/msw.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/msw.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/msw.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/playwright.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/playwright.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/playwright.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/playwright.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/prettier.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/prettier.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/prettier.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/prettier.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/prisma.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/prisma.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/prisma.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/prisma.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/radix.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/radix.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/radix.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/radix.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/react-email.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/react-email.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/react-email.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/react-email.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/remix.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/remix.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/remix.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/remix.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/resend.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/resend.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/resend.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/resend.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/sentry.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/sentry.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/sentry.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/sentry.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/shadcn-ui.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/shadcn-ui.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/shadcn-ui.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/shadcn-ui.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/sqlite.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/sqlite.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/sqlite.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/sqlite.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/stars.jpg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/stars.jpg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/stars.jpg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/stars.jpg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/tailwind.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/tailwind.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/tailwind.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/tailwind.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/testing-library.png b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/testing-library.png
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/testing-library.png
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/testing-library.png
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/typescript.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/typescript.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/typescript.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/typescript.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/vitest.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/vitest.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/vitest.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/vitest.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/zod.svg b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/zod.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/logos/zod.svg
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/logos/zod.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/privacy.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/privacy.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/privacy.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/privacy.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/support.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/support.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/support.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/support.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/tos.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/tos.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_marketing+/tos.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/_marketing+/tos.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_seo+/robots[.]txt.ts b/exercises/02.authentication/02.solution.2fa/app/routes/_seo+/robots[.]txt.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_seo+/robots[.]txt.ts
rename to exercises/02.authentication/02.solution.2fa/app/routes/_seo+/robots[.]txt.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/_seo+/sitemap[.]xml.ts b/exercises/02.authentication/02.solution.2fa/app/routes/_seo+/sitemap[.]xml.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/_seo+/sitemap[.]xml.ts
rename to exercises/02.authentication/02.solution.2fa/app/routes/_seo+/sitemap[.]xml.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/admin+/cache.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/admin+/cache.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/admin+/cache.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/admin+/cache.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/admin+/cache_.lru.$cacheKey.ts b/exercises/02.authentication/02.solution.2fa/app/routes/admin+/cache_.lru.$cacheKey.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/admin+/cache_.lru.$cacheKey.ts
rename to exercises/02.authentication/02.solution.2fa/app/routes/admin+/cache_.lru.$cacheKey.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/admin+/cache_.sqlite.$cacheKey.ts b/exercises/02.authentication/02.solution.2fa/app/routes/admin+/cache_.sqlite.$cacheKey.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/admin+/cache_.sqlite.$cacheKey.ts
rename to exercises/02.authentication/02.solution.2fa/app/routes/admin+/cache_.sqlite.$cacheKey.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/admin+/cache_.sqlite.server.ts b/exercises/02.authentication/02.solution.2fa/app/routes/admin+/cache_.sqlite.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/admin+/cache_.sqlite.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/routes/admin+/cache_.sqlite.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/admin+/cache_.sqlite.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/admin+/cache_.sqlite.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/admin+/cache_.sqlite.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/admin+/cache_.sqlite.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/me.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/me.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/me.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/me.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/resources+/download-user-data.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/resources+/download-user-data.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/resources+/download-user-data.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/resources+/download-user-data.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/resources+/healthcheck.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/resources+/healthcheck.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/resources+/healthcheck.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/resources+/healthcheck.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/resources+/images.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/resources+/images.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/resources+/images.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/resources+/images.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/resources+/theme-switch.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/resources+/theme-switch.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/resources+/theme-switch.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/resources+/theme-switch.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.change-email.server.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.change-email.server.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.change-email.server.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.change-email.server.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.change-email.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.change-email.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.change-email.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.change-email.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.connections.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.connections.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.connections.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.connections.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.index.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.index.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.index.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.index.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.passkeys.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.passkeys.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.passkeys.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.passkeys.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.password.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.password.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.password.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.password.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.password_.create.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.password_.create.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.password_.create.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.password_.create.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.photo.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.photo.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.photo.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.photo.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.two-factor.disable.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.two-factor.disable.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.two-factor.disable.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.two-factor.disable.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.two-factor.index.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.two-factor.index.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.two-factor.index.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.two-factor.index.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.two-factor.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.two-factor.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.two-factor.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.two-factor.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.two-factor.verify.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.two-factor.verify.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/settings+/profile.two-factor.verify.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/settings+/profile.two-factor.verify.tsx
diff --git a/exercises/02.authentication/02.solution.2fa/app/routes/users+/$username.test.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/users+/$username.test.tsx
new file mode 100644
index 0000000..0884a24
--- /dev/null
+++ b/exercises/02.authentication/02.solution.2fa/app/routes/users+/$username.test.tsx
@@ -0,0 +1,97 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { faker } from '@faker-js/faker'
+import { render, screen } from '@testing-library/react'
+import { createRoutesStub } from 'react-router'
+import setCookieParser from 'set-cookie-parser'
+import { test } from 'vitest'
+import { loader as rootLoader } from '#app/root.tsx'
+import { getSessionExpirationDate, sessionKey } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { generateUserInfo, getUserImages } from '#tests/db-utils.ts'
+import { default as UsernameRoute, loader } from './$username.tsx'
+
+test('The user profile when not logged in as self', async () => {
+ const userImages = await getUserImages()
+ const userImage =
+ userImages[faker.number.int({ min: 0, max: userImages.length - 1 })]
+ const user = await prisma.user.create({
+ select: { id: true, username: true, name: true },
+ data: { ...generateUserInfo(), image: { create: userImage } },
+ })
+ const App = createRoutesStub([
+ {
+ path: '/users/:username',
+ Component: UsernameRoute,
+ loader,
+ HydrateFallback: () => Loading...
,
+ },
+ ])
+
+ const routeUrl = `/users/${user.username}`
+ render( )
+
+ await screen.findByRole('heading', { level: 1, name: user.name! })
+ await screen.findByRole('img', { name: user.name! })
+ await screen.findByRole('link', { name: `${user.name}'s notes` })
+})
+
+test('The user profile when logged in as self', async () => {
+ const userImages = await getUserImages()
+ const userImage =
+ userImages[faker.number.int({ min: 0, max: userImages.length - 1 })]
+ const user = await prisma.user.create({
+ select: { id: true, username: true, name: true },
+ data: { ...generateUserInfo(), image: { create: userImage } },
+ })
+ const session = await prisma.session.create({
+ select: { id: true },
+ data: {
+ expirationDate: getSessionExpirationDate(),
+ userId: user.id,
+ },
+ })
+
+ const authSession = await authSessionStorage.getSession()
+ authSession.set(sessionKey, session.id)
+ const setCookieHeader = await authSessionStorage.commitSession(authSession)
+ const parsedCookie = setCookieParser.parseString(setCookieHeader)
+ const cookieHeader = new URLSearchParams({
+ [parsedCookie.name]: parsedCookie.value,
+ }).toString()
+
+ const App = createRoutesStub([
+ {
+ id: 'root',
+ path: '/',
+ loader: async (args) => {
+ // add the cookie header to the request
+ args.request.headers.set('cookie', cookieHeader)
+ return rootLoader({ ...args, context: args.context })
+ },
+ HydrateFallback: () => Loading...
,
+ children: [
+ {
+ path: 'users/:username',
+ Component: UsernameRoute,
+ loader: async (args) => {
+ // add the cookie header to the request
+ args.request.headers.set('cookie', cookieHeader)
+ return loader(args)
+ },
+ },
+ ],
+ },
+ ])
+
+ const routeUrl = `/users/${user.username}`
+ render( )
+
+ await screen.findByRole('heading', { level: 1, name: user.name! })
+ await screen.findByRole('img', { name: user.name! })
+ await screen.findByRole('button', { name: /logout/i })
+ await screen.findByRole('link', { name: /my notes/i })
+ await screen.findByRole('link', { name: /edit profile/i })
+})
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/users+/$username.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/users+/$username.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/users+/$username.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/users+/$username.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/users+/$username_+/__note-editor.server.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/users+/$username_+/__note-editor.server.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/users+/$username_+/__note-editor.server.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/users+/$username_+/__note-editor.server.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/users+/$username_+/__note-editor.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/users+/$username_+/__note-editor.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/users+/$username_+/__note-editor.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/users+/$username_+/__note-editor.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/users+/$username_+/notes.$noteId.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/users+/$username_+/notes.$noteId.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/users+/$username_+/notes.$noteId.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/users+/$username_+/notes.$noteId.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/users+/$username_+/notes.$noteId_.edit.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/users+/$username_+/notes.index.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/users+/$username_+/notes.index.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/users+/$username_+/notes.index.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/users+/$username_+/notes.index.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/users+/$username_+/notes.new.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/users+/$username_+/notes.new.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/users+/$username_+/notes.new.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/users+/$username_+/notes.new.tsx
diff --git a/exercises/02.authentication/02.solution.2fa/app/routes/users+/$username_+/notes.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/users+/$username_+/notes.tsx
new file mode 100644
index 0000000..ded41ca
--- /dev/null
+++ b/exercises/02.authentication/02.solution.2fa/app/routes/users+/$username_+/notes.tsx
@@ -0,0 +1,105 @@
+import { invariantResponse } from '@epic-web/invariant'
+import { Img } from 'openimg/react'
+import { Link, NavLink, Outlet } from 'react-router'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { cn, getUserImgSrc } from '#app/utils/misc.tsx'
+import { useOptionalUser } from '#app/utils/user.ts'
+import { type Route } from './+types/notes.ts'
+
+export async function loader({ params }: Route.LoaderArgs) {
+ const owner = await prisma.user.findFirst({
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ image: { select: { objectKey: true } },
+ notes: { select: { id: true, title: true } },
+ },
+ where: { username: params.username },
+ })
+
+ invariantResponse(owner, 'Owner not found', { status: 404 })
+
+ return { owner }
+}
+
+export default function NotesRoute({ loaderData }: Route.ComponentProps) {
+ const user = useOptionalUser()
+ const isOwner = user?.id === loaderData.owner.id
+ const ownerDisplayName = loaderData.owner.name ?? loaderData.owner.username
+ const navLinkDefaultClassName =
+ 'line-clamp-2 block rounded-l-full py-2 pl-8 pr-6 text-base lg:text-xl'
+ return (
+
+
+
+
+
+
+
+ {ownerDisplayName}'s Notes
+
+
+
+ {isOwner ? (
+
+
+ cn(navLinkDefaultClassName, isActive && 'bg-accent')
+ }
+ >
+ New Note
+
+
+ ) : null}
+ {loaderData.owner.notes.map((note) => (
+
+
+ cn(navLinkDefaultClassName, isActive && 'bg-accent')
+ }
+ >
+ {note.title}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return (
+ (
+ No user with the username "{params.username}" exists
+ ),
+ }}
+ />
+ )
+}
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/users+/index.tsx b/exercises/02.authentication/02.solution.2fa/app/routes/users+/index.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/routes/users+/index.tsx
rename to exercises/02.authentication/02.solution.2fa/app/routes/users+/index.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/styles/tailwind.css b/exercises/02.authentication/02.solution.2fa/app/styles/tailwind.css
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/styles/tailwind.css
rename to exercises/02.authentication/02.solution.2fa/app/styles/tailwind.css
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/auth.server.test.ts b/exercises/02.authentication/02.solution.2fa/app/utils/auth.server.test.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/auth.server.test.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/auth.server.test.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/auth.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/auth.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/auth.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/auth.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/cache.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/cache.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/cache.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/cache.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/client-hints.tsx b/exercises/02.authentication/02.solution.2fa/app/utils/client-hints.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/client-hints.tsx
rename to exercises/02.authentication/02.solution.2fa/app/utils/client-hints.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/connections.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/connections.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/connections.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/connections.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/connections.tsx b/exercises/02.authentication/02.solution.2fa/app/utils/connections.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/connections.tsx
rename to exercises/02.authentication/02.solution.2fa/app/utils/connections.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/db.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/db.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/db.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/db.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/email.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/email.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/email.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/email.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/env.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/env.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/env.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/env.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/headers.server.test.ts b/exercises/02.authentication/02.solution.2fa/app/utils/headers.server.test.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/headers.server.test.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/headers.server.test.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/headers.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/headers.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/headers.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/headers.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/honeypot.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/honeypot.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/honeypot.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/honeypot.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/litefs.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/litefs.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/litefs.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/litefs.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/misc.error-message.test.ts b/exercises/02.authentication/02.solution.2fa/app/utils/misc.error-message.test.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/misc.error-message.test.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/misc.error-message.test.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/misc.tsx b/exercises/02.authentication/02.solution.2fa/app/utils/misc.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/misc.tsx
rename to exercises/02.authentication/02.solution.2fa/app/utils/misc.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/misc.use-double-check.test.tsx b/exercises/02.authentication/02.solution.2fa/app/utils/misc.use-double-check.test.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/misc.use-double-check.test.tsx
rename to exercises/02.authentication/02.solution.2fa/app/utils/misc.use-double-check.test.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/monitoring.client.tsx b/exercises/02.authentication/02.solution.2fa/app/utils/monitoring.client.tsx
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/monitoring.client.tsx
rename to exercises/02.authentication/02.solution.2fa/app/utils/monitoring.client.tsx
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/nonce-provider.ts b/exercises/02.authentication/02.solution.2fa/app/utils/nonce-provider.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/nonce-provider.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/nonce-provider.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/permissions.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/permissions.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/permissions.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/permissions.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/providers/constants.ts b/exercises/02.authentication/02.solution.2fa/app/utils/providers/constants.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/providers/constants.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/providers/constants.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/providers/github.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/providers/github.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/providers/github.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/providers/github.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/providers/provider.ts b/exercises/02.authentication/02.solution.2fa/app/utils/providers/provider.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/providers/provider.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/providers/provider.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/redirect-cookie.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/redirect-cookie.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/redirect-cookie.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/redirect-cookie.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/request-info.ts b/exercises/02.authentication/02.solution.2fa/app/utils/request-info.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/request-info.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/request-info.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/session.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/session.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/session.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/session.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/storage.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/storage.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/storage.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/storage.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/theme.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/theme.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/theme.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/theme.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/timing.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/timing.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/timing.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/timing.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/toast.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/toast.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/toast.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/toast.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/totp.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/totp.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/totp.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/totp.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/user-validation.ts b/exercises/02.authentication/02.solution.2fa/app/utils/user-validation.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/user-validation.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/user-validation.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/user.ts b/exercises/02.authentication/02.solution.2fa/app/utils/user.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/user.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/user.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/utils/verification.server.ts b/exercises/02.authentication/02.solution.2fa/app/utils/verification.server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/app/utils/verification.server.ts
rename to exercises/02.authentication/02.solution.2fa/app/utils/verification.server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/components.json b/exercises/02.authentication/02.solution.2fa/components.json
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/components.json
rename to exercises/02.authentication/02.solution.2fa/components.json
diff --git a/exercises/02.test-setup/04.solution.api-mocking/eslint.config.js b/exercises/02.authentication/02.solution.2fa/eslint.config.js
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/eslint.config.js
rename to exercises/02.authentication/02.solution.2fa/eslint.config.js
diff --git a/exercises/02.test-setup/04.solution.api-mocking/fly.toml b/exercises/02.authentication/02.solution.2fa/fly.toml
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/fly.toml
rename to exercises/02.authentication/02.solution.2fa/fly.toml
diff --git a/exercises/02.test-setup/04.solution.api-mocking/index.js b/exercises/02.authentication/02.solution.2fa/index.js
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/index.js
rename to exercises/02.authentication/02.solution.2fa/index.js
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/Dockerfile b/exercises/02.authentication/02.solution.2fa/other/Dockerfile
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/Dockerfile
rename to exercises/02.authentication/02.solution.2fa/other/Dockerfile
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/Dockerfile.dockerignore b/exercises/02.authentication/02.solution.2fa/other/Dockerfile.dockerignore
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/Dockerfile.dockerignore
rename to exercises/02.authentication/02.solution.2fa/other/Dockerfile.dockerignore
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/README.md b/exercises/02.authentication/02.solution.2fa/other/README.md
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/README.md
rename to exercises/02.authentication/02.solution.2fa/other/README.md
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/build-server.ts b/exercises/02.authentication/02.solution.2fa/other/build-server.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/build-server.ts
rename to exercises/02.authentication/02.solution.2fa/other/build-server.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/litefs.yml b/exercises/02.authentication/02.solution.2fa/other/litefs.yml
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/litefs.yml
rename to exercises/02.authentication/02.solution.2fa/other/litefs.yml
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/sly/sly.json b/exercises/02.authentication/02.solution.2fa/other/sly/sly.json
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/sly/sly.json
rename to exercises/02.authentication/02.solution.2fa/other/sly/sly.json
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/sly/transform-icon.ts b/exercises/02.authentication/02.solution.2fa/other/sly/transform-icon.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/sly/transform-icon.ts
rename to exercises/02.authentication/02.solution.2fa/other/sly/transform-icon.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/README.md b/exercises/02.authentication/02.solution.2fa/other/svg-icons/README.md
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/README.md
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/README.md
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/arrow-left.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/arrow-left.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/arrow-left.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/arrow-left.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/arrow-right.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/arrow-right.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/arrow-right.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/arrow-right.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/avatar.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/avatar.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/avatar.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/avatar.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/camera.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/camera.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/camera.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/camera.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/check.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/check.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/check.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/check.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/clock.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/clock.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/clock.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/clock.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/cross-1.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/cross-1.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/cross-1.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/cross-1.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/dots-horizontal.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/dots-horizontal.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/dots-horizontal.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/dots-horizontal.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/download.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/download.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/download.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/download.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/envelope-closed.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/envelope-closed.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/envelope-closed.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/envelope-closed.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/exit.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/exit.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/exit.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/exit.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/file-text.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/file-text.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/file-text.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/file-text.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/github-logo.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/github-logo.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/github-logo.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/github-logo.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/laptop.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/laptop.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/laptop.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/laptop.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/link-2.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/link-2.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/link-2.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/link-2.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/lock-closed.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/lock-closed.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/lock-closed.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/lock-closed.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/lock-open-1.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/lock-open-1.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/lock-open-1.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/lock-open-1.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/magnifying-glass.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/magnifying-glass.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/magnifying-glass.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/magnifying-glass.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/moon.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/moon.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/moon.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/moon.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/passkey.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/passkey.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/passkey.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/passkey.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/pencil-1.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/pencil-1.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/pencil-1.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/pencil-1.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/pencil-2.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/pencil-2.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/pencil-2.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/pencil-2.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/plus.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/plus.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/plus.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/plus.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/question-mark-circled.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/question-mark-circled.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/question-mark-circled.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/question-mark-circled.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/reset.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/reset.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/reset.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/reset.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/sun.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/sun.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/sun.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/sun.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/trash.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/trash.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/trash.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/trash.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/update.svg b/exercises/02.authentication/02.solution.2fa/other/svg-icons/update.svg
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/other/svg-icons/update.svg
rename to exercises/02.authentication/02.solution.2fa/other/svg-icons/update.svg
diff --git a/exercises/02.test-setup/04.solution.api-mocking/package-lock.json b/exercises/02.authentication/02.solution.2fa/package-lock.json
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/package-lock.json
rename to exercises/02.authentication/02.solution.2fa/package-lock.json
diff --git a/exercises/02.authentication/02.solution.2fa/package.json b/exercises/02.authentication/02.solution.2fa/package.json
new file mode 100644
index 0000000..afe799b
--- /dev/null
+++ b/exercises/02.authentication/02.solution.2fa/package.json
@@ -0,0 +1,172 @@
+{
+ "name": "exercises_02.authentication_02.solution.2fa",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "imports": {
+ "#app/*": "./app/*",
+ "#tests/*": "./tests/*"
+ },
+ "scripts": {
+ "build": "run-s build:*",
+ "build:remix": "react-router build",
+ "build:server": "tsx ./other/build-server.ts",
+ "dev": "cross-env NODE_ENV=development MOCKS=true node ./server/dev-server.js",
+ "dev:no-mocks": "cross-env NODE_ENV=development node ./server/dev-server.js",
+ "format": "prettier --write .",
+ "lint": "eslint .",
+ "setup": "npm run build && prisma migrate deploy && prisma generate --sql && playwright install",
+ "start": "cross-env NODE_ENV=production node .",
+ "start:mocks": "cross-env NODE_ENV=production MOCKS=true tsx .",
+ "test": "npx playwright test",
+ "coverage": "vitest run --coverage",
+ "test:e2e": "npm run test:e2e:dev --silent",
+ "test:e2e:dev": "npx playwright test --ui",
+ "pretest:e2e:run": "npm run build",
+ "test:e2e:run": "npx cross-env CI=true npx playwright test",
+ "test:e2e:install": "npx playwright install --with-deps chromium",
+ "typecheck": "react-router typegen && tsc",
+ "validate": "run-p \"test -- --run\" lint typecheck test:e2e:run",
+ "post:playground": "npx react-router typegen && prisma migrate deploy && prisma generate --sql"
+ },
+ "prettier": "@epic-web/config/prettier",
+ "eslintIgnore": [
+ "/node_modules",
+ "/build",
+ "/public/build",
+ "/playwright-report",
+ "/server-build"
+ ],
+ "dependencies": {
+ "@conform-to/react": "^1.5.0",
+ "@conform-to/zod": "^1.5.0",
+ "@epic-web/cachified": "^5.5.2",
+ "@epic-web/client-hints": "^1.3.5",
+ "@epic-web/invariant": "^1.0.0",
+ "@epic-web/remember": "^1.1.0",
+ "@epic-web/totp": "^4.0.1",
+ "@mjackson/form-data-parser": "^0.7.0",
+ "@mjackson/headers": "^0.10.0",
+ "@nasa-gcn/remix-seo": "^2.0.1",
+ "@nichtsam/helmet": "^0.3.1",
+ "@oslojs/crypto": "^1.0.1",
+ "@oslojs/encoding": "^1.1.0",
+ "@paralleldrive/cuid2": "^2.2.2",
+ "@prisma/client": "^6.7.0",
+ "@prisma/instrumentation": "^6.7.0",
+ "@radix-ui/react-checkbox": "^1.2.3",
+ "@radix-ui/react-dropdown-menu": "^2.1.12",
+ "@radix-ui/react-label": "^2.1.4",
+ "@radix-ui/react-slot": "^1.2.0",
+ "@radix-ui/react-toast": "^1.2.11",
+ "@radix-ui/react-tooltip": "^1.2.4",
+ "@react-email/components": "0.0.38",
+ "@react-router/express": "^7.5.3",
+ "@react-router/node": "^7.5.3",
+ "@react-router/remix-routes-option-adapter": "^7.5.3",
+ "@remix-run/server-runtime": "^2.16.5",
+ "@sentry/profiling-node": "^9.32.0",
+ "@sentry/react-router": "^9.32.0",
+ "@simplewebauthn/browser": "^13.1.0",
+ "@simplewebauthn/server": "^13.1.1",
+ "@tailwindcss/vite": "^4.1.5",
+ "@tusbar/cache-control": "1.0.2",
+ "address": "^2.0.3",
+ "bcryptjs": "^3.0.2",
+ "class-variance-authority": "^0.7.1",
+ "close-with-grace": "^2.2.0",
+ "clsx": "^2.1.1",
+ "compression": "^1.8.0",
+ "cookie": "^1.0.2",
+ "cross-env": "^7.0.3",
+ "date-fns": "^4.1.0",
+ "dotenv": "^16.5.0",
+ "execa": "^9.5.2",
+ "express": "^4.21.2",
+ "express-rate-limit": "^7.5.0",
+ "get-port": "^7.1.0",
+ "glob": "^11.0.2",
+ "input-otp": "^1.4.2",
+ "intl-parse-accept-language": "^1.0.0",
+ "isbot": "^5.1.27",
+ "litefs-js": "^2.0.2",
+ "lru-cache": "^11.1.0",
+ "mime-types": "^3.0.1",
+ "morgan": "^1.10.0",
+ "openimg": "^1.1.0",
+ "prisma": "^6.7.0",
+ "qrcode": "^1.5.4",
+ "react": "^19.1.0",
+ "react-dom": "^19.1.0",
+ "react-router": "^7.5.3",
+ "remix-auth": "^4.2.0",
+ "remix-auth-github": "^3.0.2",
+ "remix-utils": "^8.5.0",
+ "set-cookie-parser": "^2.7.1",
+ "sharp": "^0.34.2",
+ "sonner": "^2.0.3",
+ "source-map-support": "^0.5.21",
+ "spin-delay": "^2.0.1",
+ "tailwind-merge": "^3.2.0",
+ "tailwindcss": "^4.1.5",
+ "vite-env-only": "^3.0.3",
+ "zod": "^3.24.4"
+ },
+ "devDependencies": {
+ "@epic-web/config": "^1.20.1",
+ "@faker-js/faker": "^9.7.0",
+ "@playwright/test": "^1.57.0",
+ "@react-router/dev": "^7.5.3",
+ "@sly-cli/sly": "^2.1.1",
+ "@testing-library/dom": "^10.4.0",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.3.0",
+ "@testing-library/user-event": "^14.6.1",
+ "@total-typescript/ts-reset": "^0.6.1",
+ "@types/compression": "^1.7.5",
+ "@types/eslint": "^9.6.1",
+ "@types/express": "^4.17.21",
+ "@types/fs-extra": "^11.0.4",
+ "@types/glob": "^8.1.0",
+ "@types/mime-types": "^2.1.4",
+ "@types/morgan": "^1.9.9",
+ "@types/node": "^22.15.3",
+ "@types/qrcode": "^1.5.5",
+ "@types/react": "^19.1.2",
+ "@types/react-dom": "^19.1.3",
+ "@types/set-cookie-parser": "^2.4.10",
+ "@types/source-map-support": "^0.5.10",
+ "@vitejs/plugin-react": "^4.4.1",
+ "@vitest/coverage-v8": "^3.1.3",
+ "enforce-unique": "^1.3.0",
+ "esbuild": "^0.25.3",
+ "eslint": "^9.26.0",
+ "fs-extra": "^11.3.0",
+ "jsdom": "^25.0.1",
+ "msw": "^2.7.6",
+ "npm-run-all": "^4.1.5",
+ "playwright-persona": "^0.2.8",
+ "prettier": "^3.5.3",
+ "prettier-plugin-sql": "^0.19.0",
+ "prettier-plugin-tailwindcss": "^0.6.11",
+ "react-router-devtools": "^5.0.5",
+ "remix-flat-routes": "^0.8.5",
+ "test-passkey": "^1.0.1",
+ "tsx": "^4.19.4",
+ "tw-animate-css": "^1.2.9",
+ "typescript": "^5.8.3",
+ "vite": "^6.3.5",
+ "vite-plugin-icons-spritesheet": "^3.0.1",
+ "vitest": "^3.1.3"
+ },
+ "engines": {
+ "node": "22.14.0"
+ },
+ "prisma": {
+ "seed": "tsx prisma/seed.ts"
+ },
+ "epic-stack": {
+ "head": "4e6532d08219cef41299615ad7556b205aa0b77d",
+ "date": "2025-07-02T03:11:12Z"
+ }
+}
diff --git a/exercises/02.test-setup/04.solution.api-mocking/playwright.config.ts b/exercises/02.authentication/02.solution.2fa/playwright.config.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/playwright.config.ts
rename to exercises/02.authentication/02.solution.2fa/playwright.config.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/prisma/migrations/20250221233640_init/migration.sql b/exercises/02.authentication/02.solution.2fa/prisma/migrations/20250221233640_init/migration.sql
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/prisma/migrations/20250221233640_init/migration.sql
rename to exercises/02.authentication/02.solution.2fa/prisma/migrations/20250221233640_init/migration.sql
diff --git a/exercises/02.test-setup/04.solution.api-mocking/prisma/migrations/migration_lock.toml b/exercises/02.authentication/02.solution.2fa/prisma/migrations/migration_lock.toml
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/prisma/migrations/migration_lock.toml
rename to exercises/02.authentication/02.solution.2fa/prisma/migrations/migration_lock.toml
diff --git a/exercises/02.test-setup/04.solution.api-mocking/prisma/schema.prisma b/exercises/02.authentication/02.solution.2fa/prisma/schema.prisma
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/prisma/schema.prisma
rename to exercises/02.authentication/02.solution.2fa/prisma/schema.prisma
diff --git a/exercises/02.authentication/02.solution.2fa/prisma/seed.ts b/exercises/02.authentication/02.solution.2fa/prisma/seed.ts
new file mode 100644
index 0000000..521e1d5
--- /dev/null
+++ b/exercises/02.authentication/02.solution.2fa/prisma/seed.ts
@@ -0,0 +1,263 @@
+import { faker } from '@faker-js/faker'
+import { prisma } from '#app/utils/db.server.ts'
+import { MOCK_CODE_GITHUB } from '#app/utils/providers/constants'
+import {
+ createPassword,
+ generateUserInfo,
+ getNoteImages,
+ getUserImages,
+} from '#tests/db-utils.ts'
+import { insertGitHubUser } from '#tests/mocks/github.ts'
+
+async function seed() {
+ console.log('🌱 Seeding...')
+ console.time(`🌱 Database has been seeded`)
+
+ const totalUsers = 5
+ console.time(`👤 Created ${totalUsers} users...`)
+ const noteImages = await getNoteImages()
+ const userImages = await getUserImages()
+
+ for (let index = 0; index < totalUsers; index++) {
+ const userData = generateUserInfo()
+ const user = await prisma.user.create({
+ select: { id: true },
+ data: {
+ ...userData,
+ password: { create: createPassword(userData.username) },
+ roles: { connect: { name: 'user' } },
+ },
+ })
+
+ // Upload user profile image
+ const userImage = userImages[index % userImages.length]
+ if (userImage) {
+ await prisma.userImage.create({
+ data: {
+ userId: user.id,
+ objectKey: userImage.objectKey,
+ },
+ })
+ }
+
+ // Create notes with images
+ const notesCount = faker.number.int({ min: 1, max: 3 })
+ for (let noteIndex = 0; noteIndex < notesCount; noteIndex++) {
+ const note = await prisma.note.create({
+ select: { id: true },
+ data: {
+ title: faker.lorem.sentence(),
+ content: faker.lorem.paragraphs(),
+ ownerId: user.id,
+ },
+ })
+
+ // Add images to note
+ const noteImageCount = faker.number.int({ min: 1, max: 3 })
+ for (let imageIndex = 0; imageIndex < noteImageCount; imageIndex++) {
+ const imgNumber = faker.number.int({ min: 0, max: 9 })
+ const noteImage = noteImages[imgNumber]
+ if (noteImage) {
+ await prisma.noteImage.create({
+ data: {
+ noteId: note.id,
+ altText: noteImage.altText,
+ objectKey: noteImage.objectKey,
+ },
+ })
+ }
+ }
+ }
+ }
+ console.timeEnd(`👤 Created ${totalUsers} users...`)
+
+ console.time(`🐨 Created admin user "kody"`)
+
+ const kodyImages = {
+ kodyUser: { objectKey: 'user/kody.png' },
+ cuteKoala: {
+ altText: 'an adorable koala cartoon illustration',
+ objectKey: 'kody-notes/cute-koala.png',
+ },
+ koalaEating: {
+ altText: 'a cartoon illustration of a koala in a tree eating',
+ objectKey: 'kody-notes/koala-eating.png',
+ },
+ koalaCuddle: {
+ altText: 'a cartoon illustration of koalas cuddling',
+ objectKey: 'kody-notes/koala-cuddle.png',
+ },
+ mountain: {
+ altText: 'a beautiful mountain covered in snow',
+ objectKey: 'kody-notes/mountain.png',
+ },
+ koalaCoder: {
+ altText: 'a koala coding at the computer',
+ objectKey: 'kody-notes/koala-coder.png',
+ },
+ koalaMentor: {
+ altText:
+ 'a koala in a friendly and helpful posture. The Koala is standing next to and teaching a woman who is coding on a computer and shows positive signs of learning and understanding what is being explained.',
+ objectKey: 'kody-notes/koala-mentor.png',
+ },
+ koalaSoccer: {
+ altText: 'a cute cartoon koala kicking a soccer ball on a soccer field ',
+ objectKey: 'kody-notes/koala-soccer.png',
+ },
+ }
+
+ const githubUser = await insertGitHubUser(MOCK_CODE_GITHUB)
+
+ const kody = await prisma.user.create({
+ select: { id: true },
+ data: {
+ email: 'kody@kcd.dev',
+ username: 'kody',
+ name: 'Kody',
+ password: { create: createPassword('kodylovesyou') },
+ connections: {
+ create: {
+ providerName: 'github',
+ providerId: String(githubUser.profile.id),
+ },
+ },
+ roles: { connect: [{ name: 'admin' }, { name: 'user' }] },
+ },
+ })
+
+ await prisma.userImage.create({
+ data: {
+ userId: kody.id,
+ objectKey: kodyImages.kodyUser.objectKey,
+ },
+ })
+
+ // Create Kody's notes
+ const kodyNotes = [
+ {
+ id: 'd27a197e',
+ title: 'Basic Koala Facts',
+ content:
+ 'Koalas are found in the eucalyptus forests of eastern Australia. They have grey fur with a cream-coloured chest, and strong, clawed feet, perfect for living in the branches of trees!',
+ images: [kodyImages.cuteKoala, kodyImages.koalaEating],
+ },
+ {
+ id: '414f0c09',
+ title: 'Koalas like to cuddle',
+ content:
+ 'Cuddly critters, koalas measure about 60cm to 85cm long, and weigh about 14kg.',
+ images: [kodyImages.koalaCuddle],
+ },
+ {
+ id: '260366b1',
+ title: 'Not bears',
+ content:
+ "Although you may have heard people call them koala 'bears', these awesome animals aren't bears at all – they are in fact marsupials. A group of mammals, most marsupials have pouches where their newborns develop.",
+ images: [],
+ },
+ {
+ id: 'bb79cf45',
+ title: 'Snowboarding Adventure',
+ content:
+ "Today was an epic day on the slopes! Shredded fresh powder with my friends, caught some sick air, and even attempted a backflip. Can't wait for the next snowy adventure!",
+ images: [kodyImages.mountain],
+ },
+ {
+ id: '9f4308be',
+ title: 'Onewheel Tricks',
+ content:
+ "Mastered a new trick on my Onewheel today called '180 Spin'. It's exhilarating to carve through the streets while pulling off these rad moves. Time to level up and learn more!",
+ images: [],
+ },
+ {
+ id: '306021fb',
+ title: 'Coding Dilemma',
+ content:
+ "Stuck on a bug in my latest coding project. Need to figure out why my function isn't returning the expected output. Time to dig deep, debug, and conquer this challenge!",
+ images: [kodyImages.koalaCoder],
+ },
+ {
+ id: '16d4912a',
+ title: 'Coding Mentorship',
+ content:
+ "Had a fantastic coding mentoring session today with Sarah. Helped her understand the concept of recursion, and she made great progress. It's incredibly fulfilling to help others improve their coding skills.",
+ images: [kodyImages.koalaMentor],
+ },
+ {
+ id: '3199199e',
+ title: 'Koala Fun Facts',
+ content:
+ "Did you know that koalas sleep for up to 20 hours a day? It's because their diet of eucalyptus leaves doesn't provide much energy. But when I'm awake, I enjoy munching on leaves, chilling in trees, and being the cuddliest koala around!",
+ images: [],
+ },
+ {
+ id: '2030ffd3',
+ title: 'Skiing Adventure',
+ content:
+ 'Spent the day hitting the slopes on my skis. The fresh powder made for some incredible runs and breathtaking views. Skiing down the mountain at top speed is an adrenaline rush like no other!',
+ images: [kodyImages.mountain],
+ },
+ {
+ id: 'f375a804',
+ title: 'Code Jam Success',
+ content:
+ 'Participated in a coding competition today and secured the first place! The adrenaline, the challenging problems, and the satisfaction of finding optimal solutions—it was an amazing experience. Feeling proud and motivated to keep pushing my coding skills further!',
+ images: [kodyImages.koalaCoder],
+ },
+ {
+ id: '562c541b',
+ title: 'Koala Conservation Efforts',
+ content:
+ "Joined a local conservation group to protect koalas and their habitats. Together, we're planting more eucalyptus trees, raising awareness about their endangered status, and working towards a sustainable future for these adorable creatures. Every small step counts!",
+ images: [],
+ },
+ {
+ id: 'f67ca40b',
+ title: 'Game day',
+ content:
+ "Just got back from the most amazing game. I've been playing soccer for a long time, but I've not once scored a goal. Well, today all that changed! I finally scored my first ever goal.\n\nI'm in an indoor league, and my team's not the best, but we're pretty good and I have fun, that's all that really matters. Anyway, I found myself at the other end of the field with the ball. It was just me and the goalie. I normally just kick the ball and hope it goes in, but the ball was already rolling toward the goal. The goalie was about to get the ball, so I had to charge. I managed to get possession of the ball just before the goalie got it. I brought it around the goalie and had a perfect shot. I screamed so loud in excitement. After all these years playing, I finally scored a goal!\n\nI know it's not a lot for most folks, but it meant a lot to me. We did end up winning the game by one. It makes me feel great that I had a part to play in that.\n\nIn this team, I'm the captain. I'm constantly cheering my team on. Even after getting injured, I continued to come and watch from the side-lines. I enjoy yelling (encouragingly) at my team mates and helping them be the best they can. I'm definitely not the best player by a long stretch. But I really enjoy the game. It's a great way to get exercise and have good social interactions once a week.\n\nThat said, it can be hard to keep people coming and paying dues and stuff. If people don't show up it can be really hard to find subs. I have a list of people I can text, but sometimes I can't find anyone.\n\nBut yeah, today was awesome. I felt like more than just a player that gets in the way of the opposition, but an actual asset to the team. Really great feeling.\n\nAnyway, I'm rambling at this point and really this is just so we can have a note that's pretty long to test things out. I think it's long enough now... Cheers!",
+ images: [kodyImages.koalaSoccer],
+ },
+ ]
+
+ for (const noteData of kodyNotes) {
+ const note = await prisma.note.create({
+ select: { id: true },
+ data: {
+ id: noteData.id,
+ title: noteData.title,
+ content: noteData.content,
+ ownerId: kody.id,
+ },
+ })
+
+ for (const image of noteData.images) {
+ await prisma.noteImage.create({
+ data: {
+ noteId: note.id,
+ altText: image.altText,
+ objectKey: image.objectKey,
+ },
+ })
+ }
+ }
+
+ console.timeEnd(`🐨 Created admin user "kody"`)
+
+ console.timeEnd(`🌱 Database has been seeded`)
+}
+
+seed()
+ .catch((e) => {
+ console.error(e)
+ process.exit(1)
+ })
+ .finally(async () => {
+ await prisma.$disconnect()
+ })
+
+// we're ok to import from the test directory in this file
+/*
+eslint
+ no-restricted-imports: "off",
+*/
diff --git a/exercises/02.test-setup/04.solution.api-mocking/prisma/sql/searchUsers.sql b/exercises/02.authentication/02.solution.2fa/prisma/sql/searchUsers.sql
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/prisma/sql/searchUsers.sql
rename to exercises/02.authentication/02.solution.2fa/prisma/sql/searchUsers.sql
diff --git a/exercises/02.test-setup/04.solution.api-mocking/public/favicon.ico b/exercises/02.authentication/02.solution.2fa/public/favicon.ico
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/public/favicon.ico
rename to exercises/02.authentication/02.solution.2fa/public/favicon.ico
diff --git a/exercises/02.test-setup/04.solution.api-mocking/public/favicons/README.md b/exercises/02.authentication/02.solution.2fa/public/favicons/README.md
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/public/favicons/README.md
rename to exercises/02.authentication/02.solution.2fa/public/favicons/README.md
diff --git a/exercises/02.test-setup/04.solution.api-mocking/public/favicons/android-chrome-192x192.png b/exercises/02.authentication/02.solution.2fa/public/favicons/android-chrome-192x192.png
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/public/favicons/android-chrome-192x192.png
rename to exercises/02.authentication/02.solution.2fa/public/favicons/android-chrome-192x192.png
diff --git a/exercises/02.test-setup/04.solution.api-mocking/public/favicons/android-chrome-512x512.png b/exercises/02.authentication/02.solution.2fa/public/favicons/android-chrome-512x512.png
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/public/favicons/android-chrome-512x512.png
rename to exercises/02.authentication/02.solution.2fa/public/favicons/android-chrome-512x512.png
diff --git a/exercises/02.test-setup/04.solution.api-mocking/public/img/user.png b/exercises/02.authentication/02.solution.2fa/public/img/user.png
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/public/img/user.png
rename to exercises/02.authentication/02.solution.2fa/public/img/user.png
diff --git a/exercises/02.test-setup/04.solution.api-mocking/public/site.webmanifest b/exercises/02.authentication/02.solution.2fa/public/site.webmanifest
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/public/site.webmanifest
rename to exercises/02.authentication/02.solution.2fa/public/site.webmanifest
diff --git a/exercises/02.test-setup/04.solution.api-mocking/react-router.config.ts b/exercises/02.authentication/02.solution.2fa/react-router.config.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/react-router.config.ts
rename to exercises/02.authentication/02.solution.2fa/react-router.config.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/server/dev-server.js b/exercises/02.authentication/02.solution.2fa/server/dev-server.js
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/server/dev-server.js
rename to exercises/02.authentication/02.solution.2fa/server/dev-server.js
diff --git a/exercises/02.test-setup/04.solution.api-mocking/server/index.ts b/exercises/02.authentication/02.solution.2fa/server/index.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/server/index.ts
rename to exercises/02.authentication/02.solution.2fa/server/index.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/server/utils/monitoring.ts b/exercises/02.authentication/02.solution.2fa/server/utils/monitoring.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/server/utils/monitoring.ts
rename to exercises/02.authentication/02.solution.2fa/server/utils/monitoring.ts
diff --git a/exercises/03.guides/03.problem.blocking-unneeded-requests/tests/db-utils.ts b/exercises/02.authentication/02.solution.2fa/tests/db-utils.ts
similarity index 68%
rename from exercises/03.guides/03.problem.blocking-unneeded-requests/tests/db-utils.ts
rename to exercises/02.authentication/02.solution.2fa/tests/db-utils.ts
index f701a8a..3b58c39 100644
--- a/exercises/03.guides/03.problem.blocking-unneeded-requests/tests/db-utils.ts
+++ b/exercises/02.authentication/02.solution.2fa/tests/db-utils.ts
@@ -1,10 +1,13 @@
+import { type generateTOTP } from '@epic-web/totp'
import { faker } from '@faker-js/faker'
import bcrypt from 'bcryptjs'
import { UniqueEnforcer } from 'enforce-unique'
+import { getPasswordHash } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
const uniqueUsernameEnforcer = new UniqueEnforcer()
-export function createUser() {
+export function generateUserInfo() {
const firstName = faker.person.firstName()
const lastName = faker.person.lastName()
@@ -22,6 +25,7 @@ export function createUser() {
.slice(0, 20)
.toLowerCase()
.replace(/[^a-z0-9_]/g, '_')
+
return {
username,
name: `${firstName} ${lastName}`,
@@ -29,6 +33,50 @@ export function createUser() {
}
}
+export async function createUser() {
+ const userInfo = generateUserInfo()
+ const password = 'supersecret'
+ const user = await prisma.user.create({
+ data: {
+ ...userInfo,
+ password: { create: { hash: await getPasswordHash(password) } },
+ },
+ })
+
+ return {
+ async [Symbol.asyncDispose]() {
+ await prisma.user.deleteMany({
+ where: { id: user.id },
+ })
+ },
+ ...user,
+ password,
+ }
+}
+
+export async function createVerification(input: {
+ totp: Awaited>
+ userId: string
+}) {
+ const { otp, ...totpConfig } = input.totp
+ const verification = await prisma.verification.create({
+ data: {
+ ...totpConfig,
+ type: '2fa',
+ target: input.userId,
+ },
+ })
+
+ return {
+ async [Symbol.asyncDispose]() {
+ await prisma.verification.deleteMany({
+ where: { id: verification.id },
+ })
+ },
+ ...verification,
+ }
+}
+
export function createPassword(password: string = faker.internet.password()) {
return {
hash: bcrypt.hashSync(password, 10),
diff --git a/exercises/02.authentication/02.solution.2fa/tests/e2e/authentication-2fa.test.ts b/exercises/02.authentication/02.solution.2fa/tests/e2e/authentication-2fa.test.ts
new file mode 100644
index 0000000..3ea630a
--- /dev/null
+++ b/exercises/02.authentication/02.solution.2fa/tests/e2e/authentication-2fa.test.ts
@@ -0,0 +1,35 @@
+import { generateTOTP } from '@epic-web/totp'
+import { createUser, createVerification } from '#tests/db-utils.ts'
+import { test, expect } from '#tests/test-extend.ts'
+
+test('authenticates using two-factor authentication', async ({
+ navigate,
+ page,
+}) => {
+ // Create a test user and enable 2FA for them directly in the database.
+ await using user = await createUser()
+ const totp = await generateTOTP()
+ await using _ = await createVerification({
+ totp,
+ userId: user.id,
+ })
+
+ // Log in as the created user.
+ await navigate('/login')
+
+ await page.getByLabel('Username').fill(user.username)
+ await page.getByLabel('Password').fill(user.password)
+ await page.getByRole('button', { name: 'Log in' }).click()
+
+ await expect(
+ page.getByRole('heading', { name: 'Check your 2FA app' }),
+ ).toBeVisible()
+
+ await page
+ .getByRole('textbox', { name: /code/i })
+ .fill((await generateTOTP(totp)).otp)
+
+ await page.getByRole('button', { name: 'Submit' }).click()
+
+ await expect(page.getByRole('link', { name: user.name! })).toBeVisible()
+})
diff --git a/exercises/02.test-setup/04.solution.api-mocking/tests/setup/custom-matchers.ts b/exercises/02.authentication/02.solution.2fa/tests/setup/custom-matchers.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/tests/setup/custom-matchers.ts
rename to exercises/02.authentication/02.solution.2fa/tests/setup/custom-matchers.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/tests/setup/db-setup.ts b/exercises/02.authentication/02.solution.2fa/tests/setup/db-setup.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/tests/setup/db-setup.ts
rename to exercises/02.authentication/02.solution.2fa/tests/setup/db-setup.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/tests/setup/global-setup.ts b/exercises/02.authentication/02.solution.2fa/tests/setup/global-setup.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/tests/setup/global-setup.ts
rename to exercises/02.authentication/02.solution.2fa/tests/setup/global-setup.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/tests/setup/setup-test-env.ts b/exercises/02.authentication/02.solution.2fa/tests/setup/setup-test-env.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/tests/setup/setup-test-env.ts
rename to exercises/02.authentication/02.solution.2fa/tests/setup/setup-test-env.ts
diff --git a/exercises/02.authentication/02.solution.2fa/tests/test-extend.ts b/exercises/02.authentication/02.solution.2fa/tests/test-extend.ts
new file mode 100644
index 0000000..a51b50d
--- /dev/null
+++ b/exercises/02.authentication/02.solution.2fa/tests/test-extend.ts
@@ -0,0 +1,57 @@
+import { test as testBase, expect } from '@playwright/test'
+import {
+ definePersona,
+ combinePersonas,
+ type AuthenticateFunction,
+} from 'playwright-persona'
+import { href, type Register } from 'react-router'
+import { getPasswordHash } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { generateUserInfo } from '#tests/db-utils'
+
+interface Fixtures {
+ navigate: (
+ ...args: Parameters>
+ ) => Promise
+ authenticate: AuthenticateFunction<[typeof user]>
+}
+
+const user = definePersona('user', {
+ async createSession({ page }) {
+ const user = await prisma.user.create({
+ data: {
+ ...generateUserInfo(),
+ roles: { connect: { name: 'user' } },
+ password: { create: { hash: await getPasswordHash('supersecret') } },
+ },
+ })
+
+ await page.goto('/login')
+ await page.getByLabel('Username').fill(user.username)
+ await page.getByLabel('Password').fill('supersecret')
+ await page.getByRole('button', { name: 'Log in' }).click()
+ await page.getByText(user.name!).waitFor({ state: 'visible' })
+
+ return { user }
+ },
+ async verifySession({ page, session }) {
+ await page.goto('/')
+ await expect(page.getByText(session.user.name!)).toBeVisible({
+ timeout: 100,
+ })
+ },
+ async destroySession({ session }) {
+ await prisma.user.deleteMany({ where: { id: session.user.id } })
+ },
+})
+
+export const test = testBase.extend({
+ async navigate({ page }, use) {
+ await use(async (...args) => {
+ await page.goto(href(...args))
+ })
+ },
+ authenticate: combinePersonas(user),
+})
+
+export { expect }
diff --git a/exercises/02.test-setup/04.solution.api-mocking/tests/utils.ts b/exercises/02.authentication/02.solution.2fa/tests/utils.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/tests/utils.ts
rename to exercises/02.authentication/02.solution.2fa/tests/utils.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/tsconfig.json b/exercises/02.authentication/02.solution.2fa/tsconfig.json
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/tsconfig.json
rename to exercises/02.authentication/02.solution.2fa/tsconfig.json
diff --git a/exercises/02.test-setup/04.solution.api-mocking/types/deps.d.ts b/exercises/02.authentication/02.solution.2fa/types/deps.d.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/types/deps.d.ts
rename to exercises/02.authentication/02.solution.2fa/types/deps.d.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/types/env.env.d.ts b/exercises/02.authentication/02.solution.2fa/types/env.env.d.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/types/env.env.d.ts
rename to exercises/02.authentication/02.solution.2fa/types/env.env.d.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/types/icon-name.d.ts b/exercises/02.authentication/02.solution.2fa/types/icon-name.d.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/types/icon-name.d.ts
rename to exercises/02.authentication/02.solution.2fa/types/icon-name.d.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/types/reset.d.ts b/exercises/02.authentication/02.solution.2fa/types/reset.d.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/types/reset.d.ts
rename to exercises/02.authentication/02.solution.2fa/types/reset.d.ts
diff --git a/exercises/02.test-setup/04.solution.api-mocking/vite.config.ts b/exercises/02.authentication/02.solution.2fa/vite.config.ts
similarity index 100%
rename from exercises/02.test-setup/04.solution.api-mocking/vite.config.ts
rename to exercises/02.authentication/02.solution.2fa/vite.config.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/.env b/exercises/02.authentication/03.problem.passkeys/.env
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/.env
rename to exercises/02.authentication/03.problem.passkeys/.env
diff --git a/exercises/02.test-setup/05.problem.test-data/.env.example b/exercises/02.authentication/03.problem.passkeys/.env.example
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/.env.example
rename to exercises/02.authentication/03.problem.passkeys/.env.example
diff --git a/exercises/02.test-setup/05.problem.test-data/.gitignore b/exercises/02.authentication/03.problem.passkeys/.gitignore
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/.gitignore
rename to exercises/02.authentication/03.problem.passkeys/.gitignore
diff --git a/exercises/02.test-setup/05.problem.test-data/.npmrc b/exercises/02.authentication/03.problem.passkeys/.npmrc
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/.npmrc
rename to exercises/02.authentication/03.problem.passkeys/.npmrc
diff --git a/exercises/02.test-setup/05.problem.test-data/.prettierignore b/exercises/02.authentication/03.problem.passkeys/.prettierignore
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/.prettierignore
rename to exercises/02.authentication/03.problem.passkeys/.prettierignore
diff --git a/exercises/02.test-setup/05.problem.test-data/.vscode/extensions.json b/exercises/02.authentication/03.problem.passkeys/.vscode/extensions.json
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/.vscode/extensions.json
rename to exercises/02.authentication/03.problem.passkeys/.vscode/extensions.json
diff --git a/exercises/02.test-setup/05.problem.test-data/.vscode/remix.code-snippets b/exercises/02.authentication/03.problem.passkeys/.vscode/remix.code-snippets
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/.vscode/remix.code-snippets
rename to exercises/02.authentication/03.problem.passkeys/.vscode/remix.code-snippets
diff --git a/exercises/02.test-setup/05.problem.test-data/.vscode/settings.json b/exercises/02.authentication/03.problem.passkeys/.vscode/settings.json
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/.vscode/settings.json
rename to exercises/02.authentication/03.problem.passkeys/.vscode/settings.json
diff --git a/exercises/02.authentication/03.problem.passkeys/README.mdx b/exercises/02.authentication/03.problem.passkeys/README.mdx
new file mode 100644
index 0000000..57936c8
--- /dev/null
+++ b/exercises/02.authentication/03.problem.passkeys/README.mdx
@@ -0,0 +1,9 @@
+# Passkeys
+
+## Your task
+
+- Write the `tests/e2e/authentication-passkey.test.ts` test suite.
+- Create a `createWebAuthnClient()` utility.
+- Install `test-passkey` as a dependency.
+- Create a `createPasskey()` utility.
+- Describe two test cases: successful and unsuccessul auth with passkeys.
diff --git a/exercises/02.test-setup/05.problem.test-data/app/assets/favicons/apple-touch-icon.png b/exercises/02.authentication/03.problem.passkeys/app/assets/favicons/apple-touch-icon.png
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/assets/favicons/apple-touch-icon.png
rename to exercises/02.authentication/03.problem.passkeys/app/assets/favicons/apple-touch-icon.png
diff --git a/exercises/02.test-setup/05.problem.test-data/app/assets/favicons/favicon.svg b/exercises/02.authentication/03.problem.passkeys/app/assets/favicons/favicon.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/assets/favicons/favicon.svg
rename to exercises/02.authentication/03.problem.passkeys/app/assets/favicons/favicon.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/error-boundary.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/error-boundary.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/error-boundary.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/error-boundary.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/floating-toolbar.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/floating-toolbar.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/floating-toolbar.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/floating-toolbar.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/forms.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/forms.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/forms.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/forms.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/progress-bar.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/progress-bar.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/progress-bar.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/progress-bar.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/search-bar.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/search-bar.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/search-bar.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/search-bar.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/spacer.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/spacer.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/spacer.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/spacer.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/toaster.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/toaster.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/toaster.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/toaster.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/ui/README.md b/exercises/02.authentication/03.problem.passkeys/app/components/ui/README.md
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/ui/README.md
rename to exercises/02.authentication/03.problem.passkeys/app/components/ui/README.md
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/ui/button.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/ui/button.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/ui/button.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/ui/button.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/ui/checkbox.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/ui/checkbox.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/ui/checkbox.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/ui/checkbox.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/ui/dropdown-menu.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/ui/dropdown-menu.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/ui/dropdown-menu.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/ui/dropdown-menu.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/ui/icon.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/ui/icon.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/ui/icon.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/ui/icon.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/ui/input-otp.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/ui/input-otp.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/ui/input-otp.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/ui/input-otp.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/ui/input.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/ui/input.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/ui/input.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/ui/input.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/ui/label.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/ui/label.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/ui/label.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/ui/label.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/ui/sonner.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/ui/sonner.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/ui/sonner.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/ui/sonner.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/ui/status-button.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/ui/status-button.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/ui/status-button.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/ui/status-button.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/ui/textarea.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/ui/textarea.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/ui/textarea.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/ui/textarea.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/ui/tooltip.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/ui/tooltip.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/ui/tooltip.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/ui/tooltip.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/components/user-dropdown.tsx b/exercises/02.authentication/03.problem.passkeys/app/components/user-dropdown.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/components/user-dropdown.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/components/user-dropdown.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/entry.client.tsx b/exercises/02.authentication/03.problem.passkeys/app/entry.client.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/entry.client.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/entry.client.tsx
diff --git a/exercises/02.authentication/03.problem.passkeys/app/entry.server.tsx b/exercises/02.authentication/03.problem.passkeys/app/entry.server.tsx
new file mode 100644
index 0000000..02c0651
--- /dev/null
+++ b/exercises/02.authentication/03.problem.passkeys/app/entry.server.tsx
@@ -0,0 +1,142 @@
+import crypto from 'node:crypto'
+import { PassThrough } from 'node:stream'
+import { styleText } from 'node:util'
+import { contentSecurity } from '@nichtsam/helmet/content'
+import { createReadableStreamFromReadable } from '@react-router/node'
+import * as Sentry from '@sentry/react-router'
+import { isbot } from 'isbot'
+import { renderToPipeableStream } from 'react-dom/server'
+import {
+ ServerRouter,
+ type LoaderFunctionArgs,
+ type ActionFunctionArgs,
+ type HandleDocumentRequestFunction,
+} from 'react-router'
+import { getEnv, init } from './utils/env.server.ts'
+import { getInstanceInfo } from './utils/litefs.server.ts'
+import { NonceProvider } from './utils/nonce-provider.ts'
+import { makeTimings } from './utils/timing.server.ts'
+
+export const streamTimeout = 5000
+
+init()
+global.ENV = getEnv()
+
+const MODE = process.env.NODE_ENV ?? 'development'
+
+type DocRequestArgs = Parameters
+
+export default async function handleRequest(...args: DocRequestArgs) {
+ const [request, responseStatusCode, responseHeaders, reactRouterContext] =
+ args
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ responseHeaders.set('fly-primary-instance', primaryInstance)
+ responseHeaders.set('fly-instance', currentInstance)
+
+ if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
+ responseHeaders.append('Document-Policy', 'js-profiling')
+ }
+
+ const callbackName = isbot(request.headers.get('user-agent'))
+ ? 'onAllReady'
+ : 'onShellReady'
+
+ const nonce = crypto.randomBytes(16).toString('hex')
+ return new Promise(async (resolve, reject) => {
+ let didError = false
+ // NOTE: this timing will only include things that are rendered in the shell
+ // and will not include suspended components and deferred loaders
+ const timings = makeTimings('render', 'renderToPipeableStream')
+
+ const { pipe, abort } = renderToPipeableStream(
+
+
+ ,
+ {
+ [callbackName]: () => {
+ const body = new PassThrough()
+ responseHeaders.set('Content-Type', 'text/html')
+ responseHeaders.append('Server-Timing', timings.toString())
+
+ contentSecurity(responseHeaders, {
+ crossOriginEmbedderPolicy: false,
+ contentSecurityPolicy: {
+ // NOTE: Remove reportOnly when you're ready to enforce this CSP
+ reportOnly: true,
+ directives: {
+ fetch: {
+ 'connect-src': [
+ MODE === 'development' ? 'ws:' : undefined,
+ process.env.SENTRY_DSN ? '*.sentry.io' : undefined,
+ "'self'",
+ ],
+ 'font-src': ["'self'"],
+ 'frame-src': ["'self'"],
+ 'img-src': ["'self'", 'data:'],
+ 'script-src': [
+ "'strict-dynamic'",
+ "'self'",
+ `'nonce-${nonce}'`,
+ ],
+ 'script-src-attr': [`'nonce-${nonce}'`],
+ },
+ },
+ },
+ xFrameOptions: false,
+ })
+ resolve(
+ new Response(createReadableStreamFromReadable(body), {
+ headers: responseHeaders,
+ status: didError ? 500 : responseStatusCode,
+ }),
+ )
+ pipe(body)
+ },
+ onShellError: (err: unknown) => {
+ reject(err)
+ },
+ onError: () => {
+ didError = true
+ },
+ nonce,
+ },
+ )
+
+ setTimeout(abort, streamTimeout + 5000)
+ })
+}
+
+export async function handleDataRequest(response: Response) {
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ response.headers.set('fly-primary-instance', primaryInstance)
+ response.headers.set('fly-instance', currentInstance)
+
+ return response
+}
+
+export function handleError(
+ error: unknown,
+ { request }: LoaderFunctionArgs | ActionFunctionArgs,
+): void {
+ // Skip capturing if the request is aborted as Remix docs suggest
+ // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
+ if (request.signal.aborted) {
+ return
+ }
+
+ if (error instanceof Error) {
+ console.error(styleText('red', String(error.stack)))
+ } else {
+ console.error(error)
+ }
+
+ Sentry.captureException(error)
+}
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/root.tsx b/exercises/02.authentication/03.problem.passkeys/app/root.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/root.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/root.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes.ts b/exercises/02.authentication/03.problem.passkeys/app/routes.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes.ts
rename to exercises/02.authentication/03.problem.passkeys/app/routes.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/$.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/$.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/$.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/$.tsx
diff --git a/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/auth.$provider.callback.test.ts b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/auth.$provider.callback.test.ts
new file mode 100644
index 0000000..3765dd7
--- /dev/null
+++ b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/auth.$provider.callback.test.ts
@@ -0,0 +1,265 @@
+import { invariant } from '@epic-web/invariant'
+import { faker } from '@faker-js/faker'
+import { SetCookie } from '@mjackson/headers'
+import { http } from 'msw'
+import { afterEach, expect, test } from 'vitest'
+import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx'
+import { getSessionExpirationDate, sessionKey } from '#app/utils/auth.server.ts'
+import { GITHUB_PROVIDER_NAME } from '#app/utils/connections.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { generateTOTP } from '#app/utils/totp.server.ts'
+import { generateUserInfo } from '#tests/db-utils.ts'
+import { insertGitHubUser, deleteGitHubUsers } from '#tests/mocks/github.ts'
+import { server } from '#tests/mocks/index.ts'
+import { consoleError } from '#tests/setup/setup-test-env.ts'
+import { BASE_URL, convertSetCookieToCookie } from '#tests/utils.ts'
+import { loader } from './auth.$provider.callback.ts'
+
+const ROUTE_PATH = '/auth/github/callback'
+const PARAMS = { provider: 'github' }
+
+afterEach(async () => {
+ await deleteGitHubUsers()
+})
+
+test('a new user goes to onboarding', async () => {
+ const request = await setupRequest()
+ const response = await loader({ request, params: PARAMS, context: {} }).catch(
+ (e) => e,
+ )
+ expect(response).toHaveRedirect('/onboarding/github')
+})
+
+test('when auth fails, send the user to login with a toast', async () => {
+ consoleError.mockImplementation(() => {})
+ server.use(
+ http.post('https://github.com/login/oauth/access_token', async () => {
+ return new Response(null, { status: 400 })
+ }),
+ )
+ const request = await setupRequest()
+ const response = await loader({ request, params: PARAMS, context: {} }).catch(
+ (e) => e,
+ )
+ invariant(response instanceof Response, 'response should be a Response')
+ expect(response).toHaveRedirect('/login')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Auth Failed',
+ type: 'error',
+ }),
+ )
+ expect(consoleError).toHaveBeenCalledTimes(1)
+})
+
+test('when a user is logged in, it creates the connection', async () => {
+ const githubUser = await insertGitHubUser()
+ const session = await setupUser()
+ const request = await setupRequest({
+ sessionId: session.id,
+ code: githubUser.code,
+ })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/settings/profile/connections')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Connected',
+ type: 'success',
+ description: expect.stringContaining(githubUser.profile.login),
+ }),
+ )
+ const connection = await prisma.connection.findFirst({
+ select: { id: true },
+ where: {
+ userId: session.userId,
+ providerId: githubUser.profile.id.toString(),
+ },
+ })
+ expect(
+ connection,
+ 'the connection was not created in the database',
+ ).toBeTruthy()
+})
+
+test(`when a user is logged in and has already connected, it doesn't do anything and just redirects the user back to the connections page`, async () => {
+ const session = await setupUser()
+ const githubUser = await insertGitHubUser()
+ await prisma.connection.create({
+ data: {
+ providerName: GITHUB_PROVIDER_NAME,
+ userId: session.userId,
+ providerId: githubUser.profile.id.toString(),
+ },
+ })
+ const request = await setupRequest({
+ sessionId: session.id,
+ code: githubUser.code,
+ })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/settings/profile/connections')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Already Connected',
+ description: expect.stringContaining(githubUser.profile.login),
+ }),
+ )
+})
+
+test('when a user exists with the same email, create connection and make session', async () => {
+ const githubUser = await insertGitHubUser()
+ const email = githubUser.primaryEmail.toLowerCase()
+ const { userId } = await setupUser({ ...generateUserInfo(), email })
+ const request = await setupRequest({ code: githubUser.code })
+ const response = await loader({ request, params: PARAMS, context: {} })
+
+ expect(response).toHaveRedirect('/')
+
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ type: 'message',
+ description: expect.stringContaining(githubUser.profile.login),
+ }),
+ )
+
+ const connection = await prisma.connection.findFirst({
+ select: { id: true },
+ where: {
+ userId: userId,
+ providerId: githubUser.profile.id.toString(),
+ },
+ })
+ expect(
+ connection,
+ 'the connection was not created in the database',
+ ).toBeTruthy()
+
+ await expect(response).toHaveSessionForUser(userId)
+})
+
+test('gives an error if the account is already connected to another user', async () => {
+ const githubUser = await insertGitHubUser()
+ await prisma.user.create({
+ data: {
+ ...generateUserInfo(),
+ connections: {
+ create: {
+ providerName: GITHUB_PROVIDER_NAME,
+ providerId: githubUser.profile.id.toString(),
+ },
+ },
+ },
+ })
+ const session = await setupUser()
+ const request = await setupRequest({
+ sessionId: session.id,
+ code: githubUser.code,
+ })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/settings/profile/connections')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Already Connected',
+ description: expect.stringContaining(
+ 'already connected to another account',
+ ),
+ }),
+ )
+})
+
+test('if a user is not logged in, but the connection exists, make a session', async () => {
+ const githubUser = await insertGitHubUser()
+ const { userId } = await setupUser()
+ await prisma.connection.create({
+ data: {
+ providerName: GITHUB_PROVIDER_NAME,
+ providerId: githubUser.profile.id.toString(),
+ userId,
+ },
+ })
+ const request = await setupRequest({ code: githubUser.code })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/')
+ await expect(response).toHaveSessionForUser(userId)
+})
+
+test('if a user is not logged in, but the connection exists and they have enabled 2FA, send them to verify their 2FA and do not make a session', async () => {
+ const githubUser = await insertGitHubUser()
+ const { userId } = await setupUser()
+ await prisma.connection.create({
+ data: {
+ providerName: GITHUB_PROVIDER_NAME,
+ providerId: githubUser.profile.id.toString(),
+ userId,
+ },
+ })
+ const { otp: _otp, ...config } = await generateTOTP()
+ await prisma.verification.create({
+ data: {
+ type: twoFAVerificationType,
+ target: userId,
+ ...config,
+ },
+ })
+ const request = await setupRequest({ code: githubUser.code })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ const searchParams = new URLSearchParams({
+ type: twoFAVerificationType,
+ target: userId,
+ redirectTo: '/',
+ })
+ expect(response).toHaveRedirect(`/verify?${searchParams}`)
+})
+
+async function setupRequest({
+ sessionId,
+ code = faker.string.uuid(),
+}: { sessionId?: string; code?: string } = {}) {
+ const url = new URL(ROUTE_PATH, BASE_URL)
+ const state = faker.string.uuid()
+ url.searchParams.set('state', state)
+ url.searchParams.set('code', code)
+ const authSession = await authSessionStorage.getSession()
+ if (sessionId) authSession.set(sessionKey, sessionId)
+ const setSessionCookieHeader =
+ await authSessionStorage.commitSession(authSession)
+ const searchParams = new URLSearchParams({ code, state })
+ let authCookie = new SetCookie({
+ name: 'github',
+ value: searchParams.toString(),
+ path: '/',
+ sameSite: 'Lax',
+ httpOnly: true,
+ maxAge: 60 * 10,
+ secure: process.env.NODE_ENV === 'production' || undefined,
+ })
+ const request = new Request(url.toString(), {
+ method: 'GET',
+ headers: {
+ cookie: [
+ authCookie.toString(),
+ convertSetCookieToCookie(setSessionCookieHeader),
+ ].join('; '),
+ },
+ })
+ return request
+}
+
+async function setupUser(userData = generateUserInfo()) {
+ const session = await prisma.session.create({
+ data: {
+ expirationDate: getSessionExpirationDate(),
+ user: {
+ create: {
+ ...userData,
+ },
+ },
+ },
+ select: {
+ id: true,
+ userId: true,
+ },
+ })
+
+ return session
+}
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/auth.$provider.callback.ts b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/auth.$provider.callback.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/auth.$provider.callback.ts
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/auth.$provider.callback.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/auth_.$provider.ts b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/auth_.$provider.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/auth_.$provider.ts
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/auth_.$provider.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/forgot-password.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/forgot-password.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/forgot-password.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/forgot-password.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/login.server.ts b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/login.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/login.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/login.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/login.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/login.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/login.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/login.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/logout.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/logout.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/logout.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/logout.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/onboarding.server.ts b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/onboarding.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/onboarding.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/onboarding.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/onboarding.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/onboarding.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/onboarding.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/onboarding.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/onboarding_.$provider.server.ts b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/onboarding_.$provider.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/onboarding_.$provider.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/onboarding_.$provider.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/onboarding_.$provider.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/onboarding_.$provider.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/onboarding_.$provider.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/onboarding_.$provider.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/reset-password.server.ts b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/reset-password.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/reset-password.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/reset-password.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/reset-password.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/reset-password.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/reset-password.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/reset-password.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/signup.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/signup.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/signup.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/signup.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/verify.server.ts b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/verify.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/verify.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/verify.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/verify.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/verify.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/verify.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/verify.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/webauthn+/authentication.ts b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/webauthn+/authentication.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/webauthn+/authentication.ts
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/webauthn+/authentication.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/webauthn+/registration.ts b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/webauthn+/registration.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/webauthn+/registration.ts
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/webauthn+/registration.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/webauthn+/utils.server.ts b/exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/webauthn+/utils.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_auth+/webauthn+/utils.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_auth+/webauthn+/utils.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/about.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/about.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/about.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/about.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/index.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/index.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/index.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/index.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/docker.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/docker.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/docker.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/docker.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/eslint.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/eslint.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/eslint.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/eslint.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/faker.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/faker.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/faker.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/faker.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/fly.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/fly.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/fly.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/fly.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/github.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/github.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/github.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/github.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/logos.ts b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/logos.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/logos.ts
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/logos.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/msw.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/msw.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/msw.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/msw.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/playwright.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/playwright.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/playwright.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/playwright.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/prettier.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/prettier.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/prettier.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/prettier.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/prisma.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/prisma.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/prisma.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/prisma.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/radix.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/radix.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/radix.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/radix.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/react-email.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/react-email.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/react-email.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/react-email.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/remix.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/remix.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/remix.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/remix.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/resend.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/resend.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/resend.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/resend.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/sentry.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/sentry.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/sentry.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/sentry.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/shadcn-ui.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/shadcn-ui.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/shadcn-ui.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/shadcn-ui.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/sqlite.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/sqlite.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/sqlite.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/sqlite.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/stars.jpg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/stars.jpg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/stars.jpg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/stars.jpg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/tailwind.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/tailwind.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/tailwind.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/tailwind.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/testing-library.png b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/testing-library.png
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/testing-library.png
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/testing-library.png
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/typescript.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/typescript.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/typescript.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/typescript.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/vitest.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/vitest.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/vitest.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/vitest.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/zod.svg b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/zod.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/logos/zod.svg
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/logos/zod.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/privacy.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/privacy.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/privacy.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/privacy.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/support.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/support.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/support.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/support.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/tos.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/tos.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_marketing+/tos.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_marketing+/tos.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_seo+/robots[.]txt.ts b/exercises/02.authentication/03.problem.passkeys/app/routes/_seo+/robots[.]txt.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_seo+/robots[.]txt.ts
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_seo+/robots[.]txt.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/_seo+/sitemap[.]xml.ts b/exercises/02.authentication/03.problem.passkeys/app/routes/_seo+/sitemap[.]xml.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/_seo+/sitemap[.]xml.ts
rename to exercises/02.authentication/03.problem.passkeys/app/routes/_seo+/sitemap[.]xml.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/admin+/cache.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/admin+/cache.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/admin+/cache.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/admin+/cache.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/admin+/cache_.lru.$cacheKey.ts b/exercises/02.authentication/03.problem.passkeys/app/routes/admin+/cache_.lru.$cacheKey.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/admin+/cache_.lru.$cacheKey.ts
rename to exercises/02.authentication/03.problem.passkeys/app/routes/admin+/cache_.lru.$cacheKey.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/admin+/cache_.sqlite.$cacheKey.ts b/exercises/02.authentication/03.problem.passkeys/app/routes/admin+/cache_.sqlite.$cacheKey.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/admin+/cache_.sqlite.$cacheKey.ts
rename to exercises/02.authentication/03.problem.passkeys/app/routes/admin+/cache_.sqlite.$cacheKey.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/admin+/cache_.sqlite.server.ts b/exercises/02.authentication/03.problem.passkeys/app/routes/admin+/cache_.sqlite.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/admin+/cache_.sqlite.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/routes/admin+/cache_.sqlite.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/admin+/cache_.sqlite.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/admin+/cache_.sqlite.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/admin+/cache_.sqlite.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/admin+/cache_.sqlite.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/me.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/me.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/me.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/me.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/resources+/download-user-data.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/resources+/download-user-data.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/resources+/download-user-data.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/resources+/download-user-data.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/resources+/healthcheck.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/resources+/healthcheck.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/resources+/healthcheck.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/resources+/healthcheck.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/resources+/images.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/resources+/images.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/resources+/images.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/resources+/images.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/resources+/theme-switch.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/resources+/theme-switch.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/resources+/theme-switch.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/resources+/theme-switch.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.change-email.server.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.change-email.server.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.change-email.server.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.change-email.server.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.change-email.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.change-email.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.change-email.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.change-email.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.connections.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.connections.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.connections.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.connections.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.index.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.index.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.index.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.index.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.passkeys.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.passkeys.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.passkeys.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.passkeys.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.password.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.password.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.password.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.password.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.password_.create.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.password_.create.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.password_.create.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.password_.create.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.photo.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.photo.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.photo.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.photo.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.two-factor.disable.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.two-factor.disable.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.two-factor.disable.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.two-factor.disable.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.two-factor.index.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.two-factor.index.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.two-factor.index.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.two-factor.index.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.two-factor.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.two-factor.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.two-factor.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.two-factor.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.two-factor.verify.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.two-factor.verify.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/settings+/profile.two-factor.verify.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/settings+/profile.two-factor.verify.tsx
diff --git a/exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username.test.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username.test.tsx
new file mode 100644
index 0000000..0884a24
--- /dev/null
+++ b/exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username.test.tsx
@@ -0,0 +1,97 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { faker } from '@faker-js/faker'
+import { render, screen } from '@testing-library/react'
+import { createRoutesStub } from 'react-router'
+import setCookieParser from 'set-cookie-parser'
+import { test } from 'vitest'
+import { loader as rootLoader } from '#app/root.tsx'
+import { getSessionExpirationDate, sessionKey } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { generateUserInfo, getUserImages } from '#tests/db-utils.ts'
+import { default as UsernameRoute, loader } from './$username.tsx'
+
+test('The user profile when not logged in as self', async () => {
+ const userImages = await getUserImages()
+ const userImage =
+ userImages[faker.number.int({ min: 0, max: userImages.length - 1 })]
+ const user = await prisma.user.create({
+ select: { id: true, username: true, name: true },
+ data: { ...generateUserInfo(), image: { create: userImage } },
+ })
+ const App = createRoutesStub([
+ {
+ path: '/users/:username',
+ Component: UsernameRoute,
+ loader,
+ HydrateFallback: () => Loading...
,
+ },
+ ])
+
+ const routeUrl = `/users/${user.username}`
+ render( )
+
+ await screen.findByRole('heading', { level: 1, name: user.name! })
+ await screen.findByRole('img', { name: user.name! })
+ await screen.findByRole('link', { name: `${user.name}'s notes` })
+})
+
+test('The user profile when logged in as self', async () => {
+ const userImages = await getUserImages()
+ const userImage =
+ userImages[faker.number.int({ min: 0, max: userImages.length - 1 })]
+ const user = await prisma.user.create({
+ select: { id: true, username: true, name: true },
+ data: { ...generateUserInfo(), image: { create: userImage } },
+ })
+ const session = await prisma.session.create({
+ select: { id: true },
+ data: {
+ expirationDate: getSessionExpirationDate(),
+ userId: user.id,
+ },
+ })
+
+ const authSession = await authSessionStorage.getSession()
+ authSession.set(sessionKey, session.id)
+ const setCookieHeader = await authSessionStorage.commitSession(authSession)
+ const parsedCookie = setCookieParser.parseString(setCookieHeader)
+ const cookieHeader = new URLSearchParams({
+ [parsedCookie.name]: parsedCookie.value,
+ }).toString()
+
+ const App = createRoutesStub([
+ {
+ id: 'root',
+ path: '/',
+ loader: async (args) => {
+ // add the cookie header to the request
+ args.request.headers.set('cookie', cookieHeader)
+ return rootLoader({ ...args, context: args.context })
+ },
+ HydrateFallback: () => Loading...
,
+ children: [
+ {
+ path: 'users/:username',
+ Component: UsernameRoute,
+ loader: async (args) => {
+ // add the cookie header to the request
+ args.request.headers.set('cookie', cookieHeader)
+ return loader(args)
+ },
+ },
+ ],
+ },
+ ])
+
+ const routeUrl = `/users/${user.username}`
+ render( )
+
+ await screen.findByRole('heading', { level: 1, name: user.name! })
+ await screen.findByRole('img', { name: user.name! })
+ await screen.findByRole('button', { name: /logout/i })
+ await screen.findByRole('link', { name: /my notes/i })
+ await screen.findByRole('link', { name: /edit profile/i })
+})
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/users+/$username.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/users+/$username.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/users+/$username_+/__note-editor.server.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username_+/__note-editor.server.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/users+/$username_+/__note-editor.server.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username_+/__note-editor.server.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/users+/$username_+/__note-editor.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username_+/__note-editor.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/users+/$username_+/__note-editor.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username_+/__note-editor.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/users+/$username_+/notes.$noteId.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username_+/notes.$noteId.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/users+/$username_+/notes.$noteId.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username_+/notes.$noteId.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/users+/$username_+/notes.$noteId_.edit.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/users+/$username_+/notes.index.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username_+/notes.index.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/users+/$username_+/notes.index.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username_+/notes.index.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/users+/$username_+/notes.new.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username_+/notes.new.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/users+/$username_+/notes.new.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username_+/notes.new.tsx
diff --git a/exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username_+/notes.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username_+/notes.tsx
new file mode 100644
index 0000000..ded41ca
--- /dev/null
+++ b/exercises/02.authentication/03.problem.passkeys/app/routes/users+/$username_+/notes.tsx
@@ -0,0 +1,105 @@
+import { invariantResponse } from '@epic-web/invariant'
+import { Img } from 'openimg/react'
+import { Link, NavLink, Outlet } from 'react-router'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { cn, getUserImgSrc } from '#app/utils/misc.tsx'
+import { useOptionalUser } from '#app/utils/user.ts'
+import { type Route } from './+types/notes.ts'
+
+export async function loader({ params }: Route.LoaderArgs) {
+ const owner = await prisma.user.findFirst({
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ image: { select: { objectKey: true } },
+ notes: { select: { id: true, title: true } },
+ },
+ where: { username: params.username },
+ })
+
+ invariantResponse(owner, 'Owner not found', { status: 404 })
+
+ return { owner }
+}
+
+export default function NotesRoute({ loaderData }: Route.ComponentProps) {
+ const user = useOptionalUser()
+ const isOwner = user?.id === loaderData.owner.id
+ const ownerDisplayName = loaderData.owner.name ?? loaderData.owner.username
+ const navLinkDefaultClassName =
+ 'line-clamp-2 block rounded-l-full py-2 pl-8 pr-6 text-base lg:text-xl'
+ return (
+
+
+
+
+
+
+
+ {ownerDisplayName}'s Notes
+
+
+
+ {isOwner ? (
+
+
+ cn(navLinkDefaultClassName, isActive && 'bg-accent')
+ }
+ >
+ New Note
+
+
+ ) : null}
+ {loaderData.owner.notes.map((note) => (
+
+
+ cn(navLinkDefaultClassName, isActive && 'bg-accent')
+ }
+ >
+ {note.title}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return (
+ (
+ No user with the username "{params.username}" exists
+ ),
+ }}
+ />
+ )
+}
diff --git a/exercises/02.test-setup/05.problem.test-data/app/routes/users+/index.tsx b/exercises/02.authentication/03.problem.passkeys/app/routes/users+/index.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/routes/users+/index.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/routes/users+/index.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/styles/tailwind.css b/exercises/02.authentication/03.problem.passkeys/app/styles/tailwind.css
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/styles/tailwind.css
rename to exercises/02.authentication/03.problem.passkeys/app/styles/tailwind.css
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/auth.server.test.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/auth.server.test.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/auth.server.test.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/auth.server.test.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/auth.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/auth.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/auth.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/auth.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/cache.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/cache.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/cache.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/cache.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/client-hints.tsx b/exercises/02.authentication/03.problem.passkeys/app/utils/client-hints.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/client-hints.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/utils/client-hints.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/connections.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/connections.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/connections.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/connections.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/connections.tsx b/exercises/02.authentication/03.problem.passkeys/app/utils/connections.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/connections.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/utils/connections.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/db.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/db.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/db.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/db.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/email.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/email.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/email.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/email.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/env.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/env.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/env.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/env.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/headers.server.test.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/headers.server.test.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/headers.server.test.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/headers.server.test.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/headers.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/headers.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/headers.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/headers.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/honeypot.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/honeypot.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/honeypot.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/honeypot.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/litefs.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/litefs.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/litefs.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/litefs.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/misc.error-message.test.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/misc.error-message.test.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/misc.error-message.test.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/misc.error-message.test.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/misc.tsx b/exercises/02.authentication/03.problem.passkeys/app/utils/misc.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/misc.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/utils/misc.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/misc.use-double-check.test.tsx b/exercises/02.authentication/03.problem.passkeys/app/utils/misc.use-double-check.test.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/misc.use-double-check.test.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/utils/misc.use-double-check.test.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/monitoring.client.tsx b/exercises/02.authentication/03.problem.passkeys/app/utils/monitoring.client.tsx
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/monitoring.client.tsx
rename to exercises/02.authentication/03.problem.passkeys/app/utils/monitoring.client.tsx
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/nonce-provider.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/nonce-provider.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/nonce-provider.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/nonce-provider.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/permissions.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/permissions.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/permissions.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/permissions.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/providers/constants.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/providers/constants.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/providers/constants.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/providers/constants.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/providers/github.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/providers/github.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/providers/github.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/providers/github.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/providers/provider.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/providers/provider.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/providers/provider.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/providers/provider.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/redirect-cookie.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/redirect-cookie.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/redirect-cookie.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/redirect-cookie.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/request-info.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/request-info.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/request-info.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/request-info.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/session.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/session.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/session.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/session.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/storage.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/storage.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/storage.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/storage.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/theme.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/theme.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/theme.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/theme.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/timing.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/timing.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/timing.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/timing.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/toast.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/toast.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/toast.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/toast.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/totp.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/totp.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/totp.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/totp.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/user-validation.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/user-validation.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/user-validation.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/user-validation.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/user.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/user.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/user.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/user.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/app/utils/verification.server.ts b/exercises/02.authentication/03.problem.passkeys/app/utils/verification.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/app/utils/verification.server.ts
rename to exercises/02.authentication/03.problem.passkeys/app/utils/verification.server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/components.json b/exercises/02.authentication/03.problem.passkeys/components.json
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/components.json
rename to exercises/02.authentication/03.problem.passkeys/components.json
diff --git a/exercises/02.test-setup/05.problem.test-data/eslint.config.js b/exercises/02.authentication/03.problem.passkeys/eslint.config.js
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/eslint.config.js
rename to exercises/02.authentication/03.problem.passkeys/eslint.config.js
diff --git a/exercises/02.test-setup/05.problem.test-data/fly.toml b/exercises/02.authentication/03.problem.passkeys/fly.toml
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/fly.toml
rename to exercises/02.authentication/03.problem.passkeys/fly.toml
diff --git a/exercises/02.test-setup/05.problem.test-data/index.js b/exercises/02.authentication/03.problem.passkeys/index.js
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/index.js
rename to exercises/02.authentication/03.problem.passkeys/index.js
diff --git a/exercises/02.test-setup/05.problem.test-data/other/Dockerfile b/exercises/02.authentication/03.problem.passkeys/other/Dockerfile
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/Dockerfile
rename to exercises/02.authentication/03.problem.passkeys/other/Dockerfile
diff --git a/exercises/02.test-setup/05.problem.test-data/other/Dockerfile.dockerignore b/exercises/02.authentication/03.problem.passkeys/other/Dockerfile.dockerignore
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/Dockerfile.dockerignore
rename to exercises/02.authentication/03.problem.passkeys/other/Dockerfile.dockerignore
diff --git a/exercises/02.test-setup/05.problem.test-data/other/README.md b/exercises/02.authentication/03.problem.passkeys/other/README.md
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/README.md
rename to exercises/02.authentication/03.problem.passkeys/other/README.md
diff --git a/exercises/02.test-setup/05.problem.test-data/other/build-server.ts b/exercises/02.authentication/03.problem.passkeys/other/build-server.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/build-server.ts
rename to exercises/02.authentication/03.problem.passkeys/other/build-server.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/other/litefs.yml b/exercises/02.authentication/03.problem.passkeys/other/litefs.yml
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/litefs.yml
rename to exercises/02.authentication/03.problem.passkeys/other/litefs.yml
diff --git a/exercises/02.test-setup/05.problem.test-data/other/sly/sly.json b/exercises/02.authentication/03.problem.passkeys/other/sly/sly.json
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/sly/sly.json
rename to exercises/02.authentication/03.problem.passkeys/other/sly/sly.json
diff --git a/exercises/02.test-setup/05.problem.test-data/other/sly/transform-icon.ts b/exercises/02.authentication/03.problem.passkeys/other/sly/transform-icon.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/sly/transform-icon.ts
rename to exercises/02.authentication/03.problem.passkeys/other/sly/transform-icon.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/README.md b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/README.md
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/README.md
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/README.md
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/arrow-left.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/arrow-left.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/arrow-left.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/arrow-left.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/arrow-right.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/arrow-right.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/arrow-right.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/arrow-right.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/avatar.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/avatar.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/avatar.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/avatar.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/camera.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/camera.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/camera.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/camera.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/check.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/check.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/check.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/check.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/clock.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/clock.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/clock.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/clock.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/cross-1.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/cross-1.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/cross-1.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/cross-1.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/dots-horizontal.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/dots-horizontal.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/dots-horizontal.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/dots-horizontal.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/download.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/download.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/download.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/download.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/envelope-closed.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/envelope-closed.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/envelope-closed.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/envelope-closed.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/exit.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/exit.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/exit.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/exit.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/file-text.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/file-text.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/file-text.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/file-text.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/github-logo.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/github-logo.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/github-logo.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/github-logo.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/laptop.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/laptop.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/laptop.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/laptop.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/link-2.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/link-2.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/link-2.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/link-2.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/lock-closed.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/lock-closed.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/lock-closed.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/lock-closed.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/lock-open-1.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/lock-open-1.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/lock-open-1.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/lock-open-1.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/magnifying-glass.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/magnifying-glass.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/magnifying-glass.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/magnifying-glass.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/moon.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/moon.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/moon.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/moon.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/passkey.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/passkey.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/passkey.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/passkey.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/pencil-1.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/pencil-1.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/pencil-1.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/pencil-1.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/pencil-2.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/pencil-2.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/pencil-2.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/pencil-2.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/plus.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/plus.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/plus.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/plus.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/question-mark-circled.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/question-mark-circled.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/question-mark-circled.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/question-mark-circled.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/reset.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/reset.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/reset.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/reset.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/sun.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/sun.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/sun.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/sun.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/trash.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/trash.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/trash.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/trash.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/other/svg-icons/update.svg b/exercises/02.authentication/03.problem.passkeys/other/svg-icons/update.svg
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/other/svg-icons/update.svg
rename to exercises/02.authentication/03.problem.passkeys/other/svg-icons/update.svg
diff --git a/exercises/02.test-setup/05.problem.test-data/package-lock.json b/exercises/02.authentication/03.problem.passkeys/package-lock.json
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/package-lock.json
rename to exercises/02.authentication/03.problem.passkeys/package-lock.json
diff --git a/exercises/02.authentication/03.problem.passkeys/package.json b/exercises/02.authentication/03.problem.passkeys/package.json
new file mode 100644
index 0000000..ff197b9
--- /dev/null
+++ b/exercises/02.authentication/03.problem.passkeys/package.json
@@ -0,0 +1,172 @@
+{
+ "name": "exercises_02.authentication_03.problem.passkeys",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "imports": {
+ "#app/*": "./app/*",
+ "#tests/*": "./tests/*"
+ },
+ "scripts": {
+ "build": "run-s build:*",
+ "build:remix": "react-router build",
+ "build:server": "tsx ./other/build-server.ts",
+ "dev": "cross-env NODE_ENV=development MOCKS=true node ./server/dev-server.js",
+ "dev:no-mocks": "cross-env NODE_ENV=development node ./server/dev-server.js",
+ "format": "prettier --write .",
+ "lint": "eslint .",
+ "setup": "npm run build && prisma migrate deploy && prisma generate --sql && playwright install",
+ "start": "cross-env NODE_ENV=production node .",
+ "start:mocks": "cross-env NODE_ENV=production MOCKS=true tsx .",
+ "test": "npx playwright test",
+ "coverage": "vitest run --coverage",
+ "test:e2e": "npm run test:e2e:dev --silent",
+ "test:e2e:dev": "npx playwright test --ui",
+ "pretest:e2e:run": "npm run build",
+ "test:e2e:run": "npx cross-env CI=true npx playwright test",
+ "test:e2e:install": "npx playwright install --with-deps chromium",
+ "typecheck": "react-router typegen && tsc",
+ "validate": "run-p \"test -- --run\" lint typecheck test:e2e:run",
+ "post:playground": "npx react-router typegen && prisma migrate deploy && prisma generate --sql"
+ },
+ "prettier": "@epic-web/config/prettier",
+ "eslintIgnore": [
+ "/node_modules",
+ "/build",
+ "/public/build",
+ "/playwright-report",
+ "/server-build"
+ ],
+ "dependencies": {
+ "@conform-to/react": "^1.5.0",
+ "@conform-to/zod": "^1.5.0",
+ "@epic-web/cachified": "^5.5.2",
+ "@epic-web/client-hints": "^1.3.5",
+ "@epic-web/invariant": "^1.0.0",
+ "@epic-web/remember": "^1.1.0",
+ "@epic-web/totp": "^4.0.1",
+ "@mjackson/form-data-parser": "^0.7.0",
+ "@mjackson/headers": "^0.10.0",
+ "@nasa-gcn/remix-seo": "^2.0.1",
+ "@nichtsam/helmet": "^0.3.1",
+ "@oslojs/crypto": "^1.0.1",
+ "@oslojs/encoding": "^1.1.0",
+ "@paralleldrive/cuid2": "^2.2.2",
+ "@prisma/client": "^6.7.0",
+ "@prisma/instrumentation": "^6.7.0",
+ "@radix-ui/react-checkbox": "^1.2.3",
+ "@radix-ui/react-dropdown-menu": "^2.1.12",
+ "@radix-ui/react-label": "^2.1.4",
+ "@radix-ui/react-slot": "^1.2.0",
+ "@radix-ui/react-toast": "^1.2.11",
+ "@radix-ui/react-tooltip": "^1.2.4",
+ "@react-email/components": "0.0.38",
+ "@react-router/express": "^7.5.3",
+ "@react-router/node": "^7.5.3",
+ "@react-router/remix-routes-option-adapter": "^7.5.3",
+ "@remix-run/server-runtime": "^2.16.5",
+ "@sentry/profiling-node": "^9.32.0",
+ "@sentry/react-router": "^9.32.0",
+ "@simplewebauthn/browser": "^13.1.0",
+ "@simplewebauthn/server": "^13.1.1",
+ "@tailwindcss/vite": "^4.1.5",
+ "@tusbar/cache-control": "1.0.2",
+ "address": "^2.0.3",
+ "bcryptjs": "^3.0.2",
+ "class-variance-authority": "^0.7.1",
+ "close-with-grace": "^2.2.0",
+ "clsx": "^2.1.1",
+ "compression": "^1.8.0",
+ "cookie": "^1.0.2",
+ "cross-env": "^7.0.3",
+ "date-fns": "^4.1.0",
+ "dotenv": "^16.5.0",
+ "execa": "^9.5.2",
+ "express": "^4.21.2",
+ "express-rate-limit": "^7.5.0",
+ "get-port": "^7.1.0",
+ "glob": "^11.0.2",
+ "input-otp": "^1.4.2",
+ "intl-parse-accept-language": "^1.0.0",
+ "isbot": "^5.1.27",
+ "litefs-js": "^2.0.2",
+ "lru-cache": "^11.1.0",
+ "mime-types": "^3.0.1",
+ "morgan": "^1.10.0",
+ "openimg": "^1.1.0",
+ "prisma": "^6.7.0",
+ "qrcode": "^1.5.4",
+ "react": "^19.1.0",
+ "react-dom": "^19.1.0",
+ "react-router": "^7.5.3",
+ "remix-auth": "^4.2.0",
+ "remix-auth-github": "^3.0.2",
+ "remix-utils": "^8.5.0",
+ "set-cookie-parser": "^2.7.1",
+ "sharp": "^0.34.2",
+ "sonner": "^2.0.3",
+ "source-map-support": "^0.5.21",
+ "spin-delay": "^2.0.1",
+ "tailwind-merge": "^3.2.0",
+ "tailwindcss": "^4.1.5",
+ "vite-env-only": "^3.0.3",
+ "zod": "^3.24.4"
+ },
+ "devDependencies": {
+ "@epic-web/config": "^1.20.1",
+ "@faker-js/faker": "^9.7.0",
+ "@playwright/test": "^1.57.0",
+ "@react-router/dev": "^7.5.3",
+ "@sly-cli/sly": "^2.1.1",
+ "@testing-library/dom": "^10.4.0",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.3.0",
+ "@testing-library/user-event": "^14.6.1",
+ "@total-typescript/ts-reset": "^0.6.1",
+ "@types/compression": "^1.7.5",
+ "@types/eslint": "^9.6.1",
+ "@types/express": "^4.17.21",
+ "@types/fs-extra": "^11.0.4",
+ "@types/glob": "^8.1.0",
+ "@types/mime-types": "^2.1.4",
+ "@types/morgan": "^1.9.9",
+ "@types/node": "^22.15.3",
+ "@types/qrcode": "^1.5.5",
+ "@types/react": "^19.1.2",
+ "@types/react-dom": "^19.1.3",
+ "@types/set-cookie-parser": "^2.4.10",
+ "@types/source-map-support": "^0.5.10",
+ "@vitejs/plugin-react": "^4.4.1",
+ "@vitest/coverage-v8": "^3.1.3",
+ "enforce-unique": "^1.3.0",
+ "esbuild": "^0.25.3",
+ "eslint": "^9.26.0",
+ "fs-extra": "^11.3.0",
+ "jsdom": "^25.0.1",
+ "msw": "^2.7.6",
+ "npm-run-all": "^4.1.5",
+ "playwright-persona": "^0.2.8",
+ "prettier": "^3.5.3",
+ "prettier-plugin-sql": "^0.19.0",
+ "prettier-plugin-tailwindcss": "^0.6.11",
+ "react-router-devtools": "^5.0.5",
+ "remix-flat-routes": "^0.8.5",
+ "test-passkey": "^1.0.2",
+ "tsx": "^4.19.4",
+ "tw-animate-css": "^1.2.9",
+ "typescript": "^5.8.3",
+ "vite": "^6.3.5",
+ "vite-plugin-icons-spritesheet": "^3.0.1",
+ "vitest": "^3.1.3"
+ },
+ "engines": {
+ "node": "22.14.0"
+ },
+ "prisma": {
+ "seed": "tsx prisma/seed.ts"
+ },
+ "epic-stack": {
+ "head": "4e6532d08219cef41299615ad7556b205aa0b77d",
+ "date": "2025-07-02T03:11:12Z"
+ }
+}
diff --git a/exercises/02.test-setup/05.problem.test-data/playwright.config.ts b/exercises/02.authentication/03.problem.passkeys/playwright.config.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/playwright.config.ts
rename to exercises/02.authentication/03.problem.passkeys/playwright.config.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/prisma/migrations/20250221233640_init/migration.sql b/exercises/02.authentication/03.problem.passkeys/prisma/migrations/20250221233640_init/migration.sql
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/prisma/migrations/20250221233640_init/migration.sql
rename to exercises/02.authentication/03.problem.passkeys/prisma/migrations/20250221233640_init/migration.sql
diff --git a/exercises/02.test-setup/05.problem.test-data/prisma/migrations/migration_lock.toml b/exercises/02.authentication/03.problem.passkeys/prisma/migrations/migration_lock.toml
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/prisma/migrations/migration_lock.toml
rename to exercises/02.authentication/03.problem.passkeys/prisma/migrations/migration_lock.toml
diff --git a/exercises/02.test-setup/05.problem.test-data/prisma/schema.prisma b/exercises/02.authentication/03.problem.passkeys/prisma/schema.prisma
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/prisma/schema.prisma
rename to exercises/02.authentication/03.problem.passkeys/prisma/schema.prisma
diff --git a/exercises/02.authentication/03.problem.passkeys/prisma/seed.ts b/exercises/02.authentication/03.problem.passkeys/prisma/seed.ts
new file mode 100644
index 0000000..521e1d5
--- /dev/null
+++ b/exercises/02.authentication/03.problem.passkeys/prisma/seed.ts
@@ -0,0 +1,263 @@
+import { faker } from '@faker-js/faker'
+import { prisma } from '#app/utils/db.server.ts'
+import { MOCK_CODE_GITHUB } from '#app/utils/providers/constants'
+import {
+ createPassword,
+ generateUserInfo,
+ getNoteImages,
+ getUserImages,
+} from '#tests/db-utils.ts'
+import { insertGitHubUser } from '#tests/mocks/github.ts'
+
+async function seed() {
+ console.log('🌱 Seeding...')
+ console.time(`🌱 Database has been seeded`)
+
+ const totalUsers = 5
+ console.time(`👤 Created ${totalUsers} users...`)
+ const noteImages = await getNoteImages()
+ const userImages = await getUserImages()
+
+ for (let index = 0; index < totalUsers; index++) {
+ const userData = generateUserInfo()
+ const user = await prisma.user.create({
+ select: { id: true },
+ data: {
+ ...userData,
+ password: { create: createPassword(userData.username) },
+ roles: { connect: { name: 'user' } },
+ },
+ })
+
+ // Upload user profile image
+ const userImage = userImages[index % userImages.length]
+ if (userImage) {
+ await prisma.userImage.create({
+ data: {
+ userId: user.id,
+ objectKey: userImage.objectKey,
+ },
+ })
+ }
+
+ // Create notes with images
+ const notesCount = faker.number.int({ min: 1, max: 3 })
+ for (let noteIndex = 0; noteIndex < notesCount; noteIndex++) {
+ const note = await prisma.note.create({
+ select: { id: true },
+ data: {
+ title: faker.lorem.sentence(),
+ content: faker.lorem.paragraphs(),
+ ownerId: user.id,
+ },
+ })
+
+ // Add images to note
+ const noteImageCount = faker.number.int({ min: 1, max: 3 })
+ for (let imageIndex = 0; imageIndex < noteImageCount; imageIndex++) {
+ const imgNumber = faker.number.int({ min: 0, max: 9 })
+ const noteImage = noteImages[imgNumber]
+ if (noteImage) {
+ await prisma.noteImage.create({
+ data: {
+ noteId: note.id,
+ altText: noteImage.altText,
+ objectKey: noteImage.objectKey,
+ },
+ })
+ }
+ }
+ }
+ }
+ console.timeEnd(`👤 Created ${totalUsers} users...`)
+
+ console.time(`🐨 Created admin user "kody"`)
+
+ const kodyImages = {
+ kodyUser: { objectKey: 'user/kody.png' },
+ cuteKoala: {
+ altText: 'an adorable koala cartoon illustration',
+ objectKey: 'kody-notes/cute-koala.png',
+ },
+ koalaEating: {
+ altText: 'a cartoon illustration of a koala in a tree eating',
+ objectKey: 'kody-notes/koala-eating.png',
+ },
+ koalaCuddle: {
+ altText: 'a cartoon illustration of koalas cuddling',
+ objectKey: 'kody-notes/koala-cuddle.png',
+ },
+ mountain: {
+ altText: 'a beautiful mountain covered in snow',
+ objectKey: 'kody-notes/mountain.png',
+ },
+ koalaCoder: {
+ altText: 'a koala coding at the computer',
+ objectKey: 'kody-notes/koala-coder.png',
+ },
+ koalaMentor: {
+ altText:
+ 'a koala in a friendly and helpful posture. The Koala is standing next to and teaching a woman who is coding on a computer and shows positive signs of learning and understanding what is being explained.',
+ objectKey: 'kody-notes/koala-mentor.png',
+ },
+ koalaSoccer: {
+ altText: 'a cute cartoon koala kicking a soccer ball on a soccer field ',
+ objectKey: 'kody-notes/koala-soccer.png',
+ },
+ }
+
+ const githubUser = await insertGitHubUser(MOCK_CODE_GITHUB)
+
+ const kody = await prisma.user.create({
+ select: { id: true },
+ data: {
+ email: 'kody@kcd.dev',
+ username: 'kody',
+ name: 'Kody',
+ password: { create: createPassword('kodylovesyou') },
+ connections: {
+ create: {
+ providerName: 'github',
+ providerId: String(githubUser.profile.id),
+ },
+ },
+ roles: { connect: [{ name: 'admin' }, { name: 'user' }] },
+ },
+ })
+
+ await prisma.userImage.create({
+ data: {
+ userId: kody.id,
+ objectKey: kodyImages.kodyUser.objectKey,
+ },
+ })
+
+ // Create Kody's notes
+ const kodyNotes = [
+ {
+ id: 'd27a197e',
+ title: 'Basic Koala Facts',
+ content:
+ 'Koalas are found in the eucalyptus forests of eastern Australia. They have grey fur with a cream-coloured chest, and strong, clawed feet, perfect for living in the branches of trees!',
+ images: [kodyImages.cuteKoala, kodyImages.koalaEating],
+ },
+ {
+ id: '414f0c09',
+ title: 'Koalas like to cuddle',
+ content:
+ 'Cuddly critters, koalas measure about 60cm to 85cm long, and weigh about 14kg.',
+ images: [kodyImages.koalaCuddle],
+ },
+ {
+ id: '260366b1',
+ title: 'Not bears',
+ content:
+ "Although you may have heard people call them koala 'bears', these awesome animals aren't bears at all – they are in fact marsupials. A group of mammals, most marsupials have pouches where their newborns develop.",
+ images: [],
+ },
+ {
+ id: 'bb79cf45',
+ title: 'Snowboarding Adventure',
+ content:
+ "Today was an epic day on the slopes! Shredded fresh powder with my friends, caught some sick air, and even attempted a backflip. Can't wait for the next snowy adventure!",
+ images: [kodyImages.mountain],
+ },
+ {
+ id: '9f4308be',
+ title: 'Onewheel Tricks',
+ content:
+ "Mastered a new trick on my Onewheel today called '180 Spin'. It's exhilarating to carve through the streets while pulling off these rad moves. Time to level up and learn more!",
+ images: [],
+ },
+ {
+ id: '306021fb',
+ title: 'Coding Dilemma',
+ content:
+ "Stuck on a bug in my latest coding project. Need to figure out why my function isn't returning the expected output. Time to dig deep, debug, and conquer this challenge!",
+ images: [kodyImages.koalaCoder],
+ },
+ {
+ id: '16d4912a',
+ title: 'Coding Mentorship',
+ content:
+ "Had a fantastic coding mentoring session today with Sarah. Helped her understand the concept of recursion, and she made great progress. It's incredibly fulfilling to help others improve their coding skills.",
+ images: [kodyImages.koalaMentor],
+ },
+ {
+ id: '3199199e',
+ title: 'Koala Fun Facts',
+ content:
+ "Did you know that koalas sleep for up to 20 hours a day? It's because their diet of eucalyptus leaves doesn't provide much energy. But when I'm awake, I enjoy munching on leaves, chilling in trees, and being the cuddliest koala around!",
+ images: [],
+ },
+ {
+ id: '2030ffd3',
+ title: 'Skiing Adventure',
+ content:
+ 'Spent the day hitting the slopes on my skis. The fresh powder made for some incredible runs and breathtaking views. Skiing down the mountain at top speed is an adrenaline rush like no other!',
+ images: [kodyImages.mountain],
+ },
+ {
+ id: 'f375a804',
+ title: 'Code Jam Success',
+ content:
+ 'Participated in a coding competition today and secured the first place! The adrenaline, the challenging problems, and the satisfaction of finding optimal solutions—it was an amazing experience. Feeling proud and motivated to keep pushing my coding skills further!',
+ images: [kodyImages.koalaCoder],
+ },
+ {
+ id: '562c541b',
+ title: 'Koala Conservation Efforts',
+ content:
+ "Joined a local conservation group to protect koalas and their habitats. Together, we're planting more eucalyptus trees, raising awareness about their endangered status, and working towards a sustainable future for these adorable creatures. Every small step counts!",
+ images: [],
+ },
+ {
+ id: 'f67ca40b',
+ title: 'Game day',
+ content:
+ "Just got back from the most amazing game. I've been playing soccer for a long time, but I've not once scored a goal. Well, today all that changed! I finally scored my first ever goal.\n\nI'm in an indoor league, and my team's not the best, but we're pretty good and I have fun, that's all that really matters. Anyway, I found myself at the other end of the field with the ball. It was just me and the goalie. I normally just kick the ball and hope it goes in, but the ball was already rolling toward the goal. The goalie was about to get the ball, so I had to charge. I managed to get possession of the ball just before the goalie got it. I brought it around the goalie and had a perfect shot. I screamed so loud in excitement. After all these years playing, I finally scored a goal!\n\nI know it's not a lot for most folks, but it meant a lot to me. We did end up winning the game by one. It makes me feel great that I had a part to play in that.\n\nIn this team, I'm the captain. I'm constantly cheering my team on. Even after getting injured, I continued to come and watch from the side-lines. I enjoy yelling (encouragingly) at my team mates and helping them be the best they can. I'm definitely not the best player by a long stretch. But I really enjoy the game. It's a great way to get exercise and have good social interactions once a week.\n\nThat said, it can be hard to keep people coming and paying dues and stuff. If people don't show up it can be really hard to find subs. I have a list of people I can text, but sometimes I can't find anyone.\n\nBut yeah, today was awesome. I felt like more than just a player that gets in the way of the opposition, but an actual asset to the team. Really great feeling.\n\nAnyway, I'm rambling at this point and really this is just so we can have a note that's pretty long to test things out. I think it's long enough now... Cheers!",
+ images: [kodyImages.koalaSoccer],
+ },
+ ]
+
+ for (const noteData of kodyNotes) {
+ const note = await prisma.note.create({
+ select: { id: true },
+ data: {
+ id: noteData.id,
+ title: noteData.title,
+ content: noteData.content,
+ ownerId: kody.id,
+ },
+ })
+
+ for (const image of noteData.images) {
+ await prisma.noteImage.create({
+ data: {
+ noteId: note.id,
+ altText: image.altText,
+ objectKey: image.objectKey,
+ },
+ })
+ }
+ }
+
+ console.timeEnd(`🐨 Created admin user "kody"`)
+
+ console.timeEnd(`🌱 Database has been seeded`)
+}
+
+seed()
+ .catch((e) => {
+ console.error(e)
+ process.exit(1)
+ })
+ .finally(async () => {
+ await prisma.$disconnect()
+ })
+
+// we're ok to import from the test directory in this file
+/*
+eslint
+ no-restricted-imports: "off",
+*/
diff --git a/exercises/02.test-setup/05.problem.test-data/prisma/sql/searchUsers.sql b/exercises/02.authentication/03.problem.passkeys/prisma/sql/searchUsers.sql
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/prisma/sql/searchUsers.sql
rename to exercises/02.authentication/03.problem.passkeys/prisma/sql/searchUsers.sql
diff --git a/exercises/02.test-setup/05.problem.test-data/public/favicon.ico b/exercises/02.authentication/03.problem.passkeys/public/favicon.ico
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/public/favicon.ico
rename to exercises/02.authentication/03.problem.passkeys/public/favicon.ico
diff --git a/exercises/02.test-setup/05.problem.test-data/public/favicons/README.md b/exercises/02.authentication/03.problem.passkeys/public/favicons/README.md
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/public/favicons/README.md
rename to exercises/02.authentication/03.problem.passkeys/public/favicons/README.md
diff --git a/exercises/02.test-setup/05.problem.test-data/public/favicons/android-chrome-192x192.png b/exercises/02.authentication/03.problem.passkeys/public/favicons/android-chrome-192x192.png
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/public/favicons/android-chrome-192x192.png
rename to exercises/02.authentication/03.problem.passkeys/public/favicons/android-chrome-192x192.png
diff --git a/exercises/02.test-setup/05.problem.test-data/public/favicons/android-chrome-512x512.png b/exercises/02.authentication/03.problem.passkeys/public/favicons/android-chrome-512x512.png
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/public/favicons/android-chrome-512x512.png
rename to exercises/02.authentication/03.problem.passkeys/public/favicons/android-chrome-512x512.png
diff --git a/exercises/02.test-setup/05.problem.test-data/public/img/user.png b/exercises/02.authentication/03.problem.passkeys/public/img/user.png
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/public/img/user.png
rename to exercises/02.authentication/03.problem.passkeys/public/img/user.png
diff --git a/exercises/02.test-setup/05.problem.test-data/public/site.webmanifest b/exercises/02.authentication/03.problem.passkeys/public/site.webmanifest
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/public/site.webmanifest
rename to exercises/02.authentication/03.problem.passkeys/public/site.webmanifest
diff --git a/exercises/02.test-setup/05.problem.test-data/react-router.config.ts b/exercises/02.authentication/03.problem.passkeys/react-router.config.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/react-router.config.ts
rename to exercises/02.authentication/03.problem.passkeys/react-router.config.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/server/dev-server.js b/exercises/02.authentication/03.problem.passkeys/server/dev-server.js
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/server/dev-server.js
rename to exercises/02.authentication/03.problem.passkeys/server/dev-server.js
diff --git a/exercises/02.test-setup/05.problem.test-data/server/index.ts b/exercises/02.authentication/03.problem.passkeys/server/index.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/server/index.ts
rename to exercises/02.authentication/03.problem.passkeys/server/index.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/server/utils/monitoring.ts b/exercises/02.authentication/03.problem.passkeys/server/utils/monitoring.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/server/utils/monitoring.ts
rename to exercises/02.authentication/03.problem.passkeys/server/utils/monitoring.ts
diff --git a/exercises/02.authentication/03.problem.passkeys/tests/db-utils.ts b/exercises/02.authentication/03.problem.passkeys/tests/db-utils.ts
new file mode 100644
index 0000000..22980ca
--- /dev/null
+++ b/exercises/02.authentication/03.problem.passkeys/tests/db-utils.ts
@@ -0,0 +1,156 @@
+import { faker } from '@faker-js/faker'
+import bcrypt from 'bcryptjs'
+import { UniqueEnforcer } from 'enforce-unique'
+import { getPasswordHash } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+
+const uniqueUsernameEnforcer = new UniqueEnforcer()
+
+export function generateUserInfo() {
+ const firstName = faker.person.firstName()
+ const lastName = faker.person.lastName()
+
+ const username = uniqueUsernameEnforcer
+ .enforce(() => {
+ return (
+ faker.string.alphanumeric({ length: 2 }) +
+ '_' +
+ faker.internet.username({
+ firstName: firstName.toLowerCase(),
+ lastName: lastName.toLowerCase(),
+ })
+ )
+ })
+ .slice(0, 20)
+ .toLowerCase()
+ .replace(/[^a-z0-9_]/g, '_')
+
+ return {
+ username,
+ name: `${firstName} ${lastName}`,
+ email: `${username}@example.com`,
+ }
+}
+
+export async function createUser() {
+ const userInfo = generateUserInfo()
+ const password = 'supersecret'
+ const user = await prisma.user.create({
+ data: {
+ ...userInfo,
+ password: { create: { hash: await getPasswordHash(password) } },
+ },
+ })
+
+ return {
+ async [Symbol.asyncDispose]() {
+ await prisma.user.deleteMany({
+ where: { id: user.id },
+ })
+ },
+ ...user,
+ password,
+ }
+}
+
+export async function createPasskey(input: {
+ id: string
+ userId: string
+ aaguid: string
+ publicKey: Uint8Array
+ counter?: number
+}) {
+ const passkey = await prisma.passkey.create({
+ data: {
+ id: input.id,
+ aaguid: input.aaguid,
+ userId: input.userId,
+ publicKey: input.publicKey,
+ backedUp: false,
+ webauthnUserId: input.userId,
+ deviceType: 'singleDevice',
+ counter: input.counter || 0,
+ },
+ })
+
+ return {
+ async [Symbol.asyncDispose]() {
+ await prisma.passkey.deleteMany({
+ where: {
+ id: passkey.id,
+ },
+ })
+ },
+ ...passkey,
+ }
+}
+
+export function createPassword(password: string = faker.internet.password()) {
+ return {
+ hash: bcrypt.hashSync(password, 10),
+ }
+}
+
+let noteImages: Array<{ altText: string; objectKey: string }> | undefined
+export async function getNoteImages() {
+ if (noteImages) return noteImages
+
+ noteImages = await Promise.all([
+ {
+ altText: 'a nice country house',
+ objectKey: 'notes/0.png',
+ },
+ {
+ altText: 'a city scape',
+ objectKey: 'notes/1.png',
+ },
+ {
+ altText: 'a sunrise',
+ objectKey: 'notes/2.png',
+ },
+ {
+ altText: 'a group of friends',
+ objectKey: 'notes/3.png',
+ },
+ {
+ altText: 'friends being inclusive of someone who looks lonely',
+ objectKey: 'notes/4.png',
+ },
+ {
+ altText: 'an illustration of a hot air balloon',
+ objectKey: 'notes/5.png',
+ },
+ {
+ altText:
+ 'an office full of laptops and other office equipment that look like it was abandoned in a rush out of the building in an emergency years ago.',
+ objectKey: 'notes/6.png',
+ },
+ {
+ altText: 'a rusty lock',
+ objectKey: 'notes/7.png',
+ },
+ {
+ altText: 'something very happy in nature',
+ objectKey: 'notes/8.png',
+ },
+ {
+ altText: `someone at the end of a cry session who's starting to feel a little better.`,
+ objectKey: 'notes/9.png',
+ },
+ ])
+
+ return noteImages
+}
+
+let userImages: Array<{ objectKey: string }> | undefined
+export async function getUserImages() {
+ if (userImages) return userImages
+
+ userImages = await Promise.all(
+ Array.from({ length: 10 }, (_, index) => ({
+ objectKey: `user/${index}.jpg`,
+ })),
+ )
+
+ return userImages
+}
diff --git a/exercises/02.authentication/03.problem.passkeys/tests/e2e/authentication-passkeys.test.ts b/exercises/02.authentication/03.problem.passkeys/tests/e2e/authentication-passkeys.test.ts
new file mode 100644
index 0000000..57b95cc
--- /dev/null
+++ b/exercises/02.authentication/03.problem.passkeys/tests/e2e/authentication-passkeys.test.ts
@@ -0,0 +1,23 @@
+import { type Page } from '@playwright/test'
+import { createTestPasskey } from 'test-passkey'
+import { createPasskey, createUser } from '#tests/db-utils.ts'
+import { test, expect } from '#tests/test-extend.ts'
+
+async function createWebAuthnClient(page: Page) {
+ /** @todo */
+}
+
+test('authenticates using an existing passkey', async ({ navigate, page }) => {
+ await navigate('/login')
+
+ /** @todo */
+})
+
+test('displays an error when authenticating via a passkey fails', async ({
+ navigate,
+ page,
+}) => {
+ await navigate('/login')
+
+ /** @todo */
+})
diff --git a/exercises/02.test-setup/05.problem.test-data/tests/setup/custom-matchers.ts b/exercises/02.authentication/03.problem.passkeys/tests/setup/custom-matchers.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/tests/setup/custom-matchers.ts
rename to exercises/02.authentication/03.problem.passkeys/tests/setup/custom-matchers.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/tests/setup/db-setup.ts b/exercises/02.authentication/03.problem.passkeys/tests/setup/db-setup.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/tests/setup/db-setup.ts
rename to exercises/02.authentication/03.problem.passkeys/tests/setup/db-setup.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/tests/setup/global-setup.ts b/exercises/02.authentication/03.problem.passkeys/tests/setup/global-setup.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/tests/setup/global-setup.ts
rename to exercises/02.authentication/03.problem.passkeys/tests/setup/global-setup.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/tests/setup/setup-test-env.ts b/exercises/02.authentication/03.problem.passkeys/tests/setup/setup-test-env.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/tests/setup/setup-test-env.ts
rename to exercises/02.authentication/03.problem.passkeys/tests/setup/setup-test-env.ts
diff --git a/exercises/02.authentication/03.problem.passkeys/tests/test-extend.ts b/exercises/02.authentication/03.problem.passkeys/tests/test-extend.ts
new file mode 100644
index 0000000..a51b50d
--- /dev/null
+++ b/exercises/02.authentication/03.problem.passkeys/tests/test-extend.ts
@@ -0,0 +1,57 @@
+import { test as testBase, expect } from '@playwright/test'
+import {
+ definePersona,
+ combinePersonas,
+ type AuthenticateFunction,
+} from 'playwright-persona'
+import { href, type Register } from 'react-router'
+import { getPasswordHash } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { generateUserInfo } from '#tests/db-utils'
+
+interface Fixtures {
+ navigate: (
+ ...args: Parameters>
+ ) => Promise
+ authenticate: AuthenticateFunction<[typeof user]>
+}
+
+const user = definePersona('user', {
+ async createSession({ page }) {
+ const user = await prisma.user.create({
+ data: {
+ ...generateUserInfo(),
+ roles: { connect: { name: 'user' } },
+ password: { create: { hash: await getPasswordHash('supersecret') } },
+ },
+ })
+
+ await page.goto('/login')
+ await page.getByLabel('Username').fill(user.username)
+ await page.getByLabel('Password').fill('supersecret')
+ await page.getByRole('button', { name: 'Log in' }).click()
+ await page.getByText(user.name!).waitFor({ state: 'visible' })
+
+ return { user }
+ },
+ async verifySession({ page, session }) {
+ await page.goto('/')
+ await expect(page.getByText(session.user.name!)).toBeVisible({
+ timeout: 100,
+ })
+ },
+ async destroySession({ session }) {
+ await prisma.user.deleteMany({ where: { id: session.user.id } })
+ },
+})
+
+export const test = testBase.extend({
+ async navigate({ page }, use) {
+ await use(async (...args) => {
+ await page.goto(href(...args))
+ })
+ },
+ authenticate: combinePersonas(user),
+})
+
+export { expect }
diff --git a/exercises/02.test-setup/05.problem.test-data/tests/utils.ts b/exercises/02.authentication/03.problem.passkeys/tests/utils.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/tests/utils.ts
rename to exercises/02.authentication/03.problem.passkeys/tests/utils.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/tsconfig.json b/exercises/02.authentication/03.problem.passkeys/tsconfig.json
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/tsconfig.json
rename to exercises/02.authentication/03.problem.passkeys/tsconfig.json
diff --git a/exercises/02.test-setup/05.problem.test-data/types/deps.d.ts b/exercises/02.authentication/03.problem.passkeys/types/deps.d.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/types/deps.d.ts
rename to exercises/02.authentication/03.problem.passkeys/types/deps.d.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/types/env.env.d.ts b/exercises/02.authentication/03.problem.passkeys/types/env.env.d.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/types/env.env.d.ts
rename to exercises/02.authentication/03.problem.passkeys/types/env.env.d.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/types/icon-name.d.ts b/exercises/02.authentication/03.problem.passkeys/types/icon-name.d.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/types/icon-name.d.ts
rename to exercises/02.authentication/03.problem.passkeys/types/icon-name.d.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/types/reset.d.ts b/exercises/02.authentication/03.problem.passkeys/types/reset.d.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/types/reset.d.ts
rename to exercises/02.authentication/03.problem.passkeys/types/reset.d.ts
diff --git a/exercises/02.test-setup/05.problem.test-data/vite.config.ts b/exercises/02.authentication/03.problem.passkeys/vite.config.ts
similarity index 100%
rename from exercises/02.test-setup/05.problem.test-data/vite.config.ts
rename to exercises/02.authentication/03.problem.passkeys/vite.config.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/.env b/exercises/02.authentication/03.solution.passkeys/.env
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/.env
rename to exercises/02.authentication/03.solution.passkeys/.env
diff --git a/exercises/02.test-setup/05.solution.test-data/.env.example b/exercises/02.authentication/03.solution.passkeys/.env.example
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/.env.example
rename to exercises/02.authentication/03.solution.passkeys/.env.example
diff --git a/exercises/02.test-setup/05.solution.test-data/.gitignore b/exercises/02.authentication/03.solution.passkeys/.gitignore
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/.gitignore
rename to exercises/02.authentication/03.solution.passkeys/.gitignore
diff --git a/exercises/02.test-setup/05.solution.test-data/.npmrc b/exercises/02.authentication/03.solution.passkeys/.npmrc
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/.npmrc
rename to exercises/02.authentication/03.solution.passkeys/.npmrc
diff --git a/exercises/02.test-setup/05.solution.test-data/.prettierignore b/exercises/02.authentication/03.solution.passkeys/.prettierignore
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/.prettierignore
rename to exercises/02.authentication/03.solution.passkeys/.prettierignore
diff --git a/exercises/02.test-setup/05.solution.test-data/.vscode/extensions.json b/exercises/02.authentication/03.solution.passkeys/.vscode/extensions.json
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/.vscode/extensions.json
rename to exercises/02.authentication/03.solution.passkeys/.vscode/extensions.json
diff --git a/exercises/02.test-setup/05.solution.test-data/.vscode/remix.code-snippets b/exercises/02.authentication/03.solution.passkeys/.vscode/remix.code-snippets
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/.vscode/remix.code-snippets
rename to exercises/02.authentication/03.solution.passkeys/.vscode/remix.code-snippets
diff --git a/exercises/02.test-setup/05.solution.test-data/.vscode/settings.json b/exercises/02.authentication/03.solution.passkeys/.vscode/settings.json
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/.vscode/settings.json
rename to exercises/02.authentication/03.solution.passkeys/.vscode/settings.json
diff --git a/exercises/02.authentication/03.solution.passkeys/README.mdx b/exercises/02.authentication/03.solution.passkeys/README.mdx
new file mode 100644
index 0000000..a13a75f
--- /dev/null
+++ b/exercises/02.authentication/03.solution.passkeys/README.mdx
@@ -0,0 +1 @@
+# Passkeys
\ No newline at end of file
diff --git a/exercises/02.test-setup/05.solution.test-data/app/assets/favicons/apple-touch-icon.png b/exercises/02.authentication/03.solution.passkeys/app/assets/favicons/apple-touch-icon.png
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/assets/favicons/apple-touch-icon.png
rename to exercises/02.authentication/03.solution.passkeys/app/assets/favicons/apple-touch-icon.png
diff --git a/exercises/02.test-setup/05.solution.test-data/app/assets/favicons/favicon.svg b/exercises/02.authentication/03.solution.passkeys/app/assets/favicons/favicon.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/assets/favicons/favicon.svg
rename to exercises/02.authentication/03.solution.passkeys/app/assets/favicons/favicon.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/error-boundary.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/error-boundary.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/error-boundary.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/error-boundary.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/floating-toolbar.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/floating-toolbar.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/floating-toolbar.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/floating-toolbar.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/forms.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/forms.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/forms.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/forms.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/progress-bar.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/progress-bar.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/progress-bar.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/progress-bar.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/search-bar.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/search-bar.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/search-bar.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/search-bar.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/spacer.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/spacer.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/spacer.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/spacer.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/toaster.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/toaster.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/toaster.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/toaster.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/ui/README.md b/exercises/02.authentication/03.solution.passkeys/app/components/ui/README.md
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/ui/README.md
rename to exercises/02.authentication/03.solution.passkeys/app/components/ui/README.md
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/ui/button.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/ui/button.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/ui/button.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/ui/button.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/ui/checkbox.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/ui/checkbox.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/ui/checkbox.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/ui/checkbox.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/ui/dropdown-menu.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/ui/dropdown-menu.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/ui/dropdown-menu.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/ui/dropdown-menu.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/ui/icon.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/ui/icon.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/ui/icon.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/ui/icon.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/ui/input-otp.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/ui/input-otp.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/ui/input-otp.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/ui/input-otp.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/ui/input.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/ui/input.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/ui/input.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/ui/input.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/ui/label.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/ui/label.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/ui/label.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/ui/label.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/ui/sonner.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/ui/sonner.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/ui/sonner.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/ui/sonner.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/ui/status-button.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/ui/status-button.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/ui/status-button.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/ui/status-button.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/ui/textarea.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/ui/textarea.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/ui/textarea.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/ui/textarea.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/ui/tooltip.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/ui/tooltip.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/ui/tooltip.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/ui/tooltip.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/components/user-dropdown.tsx b/exercises/02.authentication/03.solution.passkeys/app/components/user-dropdown.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/components/user-dropdown.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/components/user-dropdown.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/entry.client.tsx b/exercises/02.authentication/03.solution.passkeys/app/entry.client.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/entry.client.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/entry.client.tsx
diff --git a/exercises/02.authentication/03.solution.passkeys/app/entry.server.tsx b/exercises/02.authentication/03.solution.passkeys/app/entry.server.tsx
new file mode 100644
index 0000000..02c0651
--- /dev/null
+++ b/exercises/02.authentication/03.solution.passkeys/app/entry.server.tsx
@@ -0,0 +1,142 @@
+import crypto from 'node:crypto'
+import { PassThrough } from 'node:stream'
+import { styleText } from 'node:util'
+import { contentSecurity } from '@nichtsam/helmet/content'
+import { createReadableStreamFromReadable } from '@react-router/node'
+import * as Sentry from '@sentry/react-router'
+import { isbot } from 'isbot'
+import { renderToPipeableStream } from 'react-dom/server'
+import {
+ ServerRouter,
+ type LoaderFunctionArgs,
+ type ActionFunctionArgs,
+ type HandleDocumentRequestFunction,
+} from 'react-router'
+import { getEnv, init } from './utils/env.server.ts'
+import { getInstanceInfo } from './utils/litefs.server.ts'
+import { NonceProvider } from './utils/nonce-provider.ts'
+import { makeTimings } from './utils/timing.server.ts'
+
+export const streamTimeout = 5000
+
+init()
+global.ENV = getEnv()
+
+const MODE = process.env.NODE_ENV ?? 'development'
+
+type DocRequestArgs = Parameters
+
+export default async function handleRequest(...args: DocRequestArgs) {
+ const [request, responseStatusCode, responseHeaders, reactRouterContext] =
+ args
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ responseHeaders.set('fly-primary-instance', primaryInstance)
+ responseHeaders.set('fly-instance', currentInstance)
+
+ if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
+ responseHeaders.append('Document-Policy', 'js-profiling')
+ }
+
+ const callbackName = isbot(request.headers.get('user-agent'))
+ ? 'onAllReady'
+ : 'onShellReady'
+
+ const nonce = crypto.randomBytes(16).toString('hex')
+ return new Promise(async (resolve, reject) => {
+ let didError = false
+ // NOTE: this timing will only include things that are rendered in the shell
+ // and will not include suspended components and deferred loaders
+ const timings = makeTimings('render', 'renderToPipeableStream')
+
+ const { pipe, abort } = renderToPipeableStream(
+
+
+ ,
+ {
+ [callbackName]: () => {
+ const body = new PassThrough()
+ responseHeaders.set('Content-Type', 'text/html')
+ responseHeaders.append('Server-Timing', timings.toString())
+
+ contentSecurity(responseHeaders, {
+ crossOriginEmbedderPolicy: false,
+ contentSecurityPolicy: {
+ // NOTE: Remove reportOnly when you're ready to enforce this CSP
+ reportOnly: true,
+ directives: {
+ fetch: {
+ 'connect-src': [
+ MODE === 'development' ? 'ws:' : undefined,
+ process.env.SENTRY_DSN ? '*.sentry.io' : undefined,
+ "'self'",
+ ],
+ 'font-src': ["'self'"],
+ 'frame-src': ["'self'"],
+ 'img-src': ["'self'", 'data:'],
+ 'script-src': [
+ "'strict-dynamic'",
+ "'self'",
+ `'nonce-${nonce}'`,
+ ],
+ 'script-src-attr': [`'nonce-${nonce}'`],
+ },
+ },
+ },
+ xFrameOptions: false,
+ })
+ resolve(
+ new Response(createReadableStreamFromReadable(body), {
+ headers: responseHeaders,
+ status: didError ? 500 : responseStatusCode,
+ }),
+ )
+ pipe(body)
+ },
+ onShellError: (err: unknown) => {
+ reject(err)
+ },
+ onError: () => {
+ didError = true
+ },
+ nonce,
+ },
+ )
+
+ setTimeout(abort, streamTimeout + 5000)
+ })
+}
+
+export async function handleDataRequest(response: Response) {
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ response.headers.set('fly-primary-instance', primaryInstance)
+ response.headers.set('fly-instance', currentInstance)
+
+ return response
+}
+
+export function handleError(
+ error: unknown,
+ { request }: LoaderFunctionArgs | ActionFunctionArgs,
+): void {
+ // Skip capturing if the request is aborted as Remix docs suggest
+ // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
+ if (request.signal.aborted) {
+ return
+ }
+
+ if (error instanceof Error) {
+ console.error(styleText('red', String(error.stack)))
+ } else {
+ console.error(error)
+ }
+
+ Sentry.captureException(error)
+}
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/root.tsx b/exercises/02.authentication/03.solution.passkeys/app/root.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/root.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/root.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes.ts b/exercises/02.authentication/03.solution.passkeys/app/routes.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes.ts
rename to exercises/02.authentication/03.solution.passkeys/app/routes.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/$.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/$.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/$.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/$.tsx
diff --git a/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/auth.$provider.callback.test.ts b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/auth.$provider.callback.test.ts
new file mode 100644
index 0000000..3765dd7
--- /dev/null
+++ b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/auth.$provider.callback.test.ts
@@ -0,0 +1,265 @@
+import { invariant } from '@epic-web/invariant'
+import { faker } from '@faker-js/faker'
+import { SetCookie } from '@mjackson/headers'
+import { http } from 'msw'
+import { afterEach, expect, test } from 'vitest'
+import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx'
+import { getSessionExpirationDate, sessionKey } from '#app/utils/auth.server.ts'
+import { GITHUB_PROVIDER_NAME } from '#app/utils/connections.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { generateTOTP } from '#app/utils/totp.server.ts'
+import { generateUserInfo } from '#tests/db-utils.ts'
+import { insertGitHubUser, deleteGitHubUsers } from '#tests/mocks/github.ts'
+import { server } from '#tests/mocks/index.ts'
+import { consoleError } from '#tests/setup/setup-test-env.ts'
+import { BASE_URL, convertSetCookieToCookie } from '#tests/utils.ts'
+import { loader } from './auth.$provider.callback.ts'
+
+const ROUTE_PATH = '/auth/github/callback'
+const PARAMS = { provider: 'github' }
+
+afterEach(async () => {
+ await deleteGitHubUsers()
+})
+
+test('a new user goes to onboarding', async () => {
+ const request = await setupRequest()
+ const response = await loader({ request, params: PARAMS, context: {} }).catch(
+ (e) => e,
+ )
+ expect(response).toHaveRedirect('/onboarding/github')
+})
+
+test('when auth fails, send the user to login with a toast', async () => {
+ consoleError.mockImplementation(() => {})
+ server.use(
+ http.post('https://github.com/login/oauth/access_token', async () => {
+ return new Response(null, { status: 400 })
+ }),
+ )
+ const request = await setupRequest()
+ const response = await loader({ request, params: PARAMS, context: {} }).catch(
+ (e) => e,
+ )
+ invariant(response instanceof Response, 'response should be a Response')
+ expect(response).toHaveRedirect('/login')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Auth Failed',
+ type: 'error',
+ }),
+ )
+ expect(consoleError).toHaveBeenCalledTimes(1)
+})
+
+test('when a user is logged in, it creates the connection', async () => {
+ const githubUser = await insertGitHubUser()
+ const session = await setupUser()
+ const request = await setupRequest({
+ sessionId: session.id,
+ code: githubUser.code,
+ })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/settings/profile/connections')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Connected',
+ type: 'success',
+ description: expect.stringContaining(githubUser.profile.login),
+ }),
+ )
+ const connection = await prisma.connection.findFirst({
+ select: { id: true },
+ where: {
+ userId: session.userId,
+ providerId: githubUser.profile.id.toString(),
+ },
+ })
+ expect(
+ connection,
+ 'the connection was not created in the database',
+ ).toBeTruthy()
+})
+
+test(`when a user is logged in and has already connected, it doesn't do anything and just redirects the user back to the connections page`, async () => {
+ const session = await setupUser()
+ const githubUser = await insertGitHubUser()
+ await prisma.connection.create({
+ data: {
+ providerName: GITHUB_PROVIDER_NAME,
+ userId: session.userId,
+ providerId: githubUser.profile.id.toString(),
+ },
+ })
+ const request = await setupRequest({
+ sessionId: session.id,
+ code: githubUser.code,
+ })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/settings/profile/connections')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Already Connected',
+ description: expect.stringContaining(githubUser.profile.login),
+ }),
+ )
+})
+
+test('when a user exists with the same email, create connection and make session', async () => {
+ const githubUser = await insertGitHubUser()
+ const email = githubUser.primaryEmail.toLowerCase()
+ const { userId } = await setupUser({ ...generateUserInfo(), email })
+ const request = await setupRequest({ code: githubUser.code })
+ const response = await loader({ request, params: PARAMS, context: {} })
+
+ expect(response).toHaveRedirect('/')
+
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ type: 'message',
+ description: expect.stringContaining(githubUser.profile.login),
+ }),
+ )
+
+ const connection = await prisma.connection.findFirst({
+ select: { id: true },
+ where: {
+ userId: userId,
+ providerId: githubUser.profile.id.toString(),
+ },
+ })
+ expect(
+ connection,
+ 'the connection was not created in the database',
+ ).toBeTruthy()
+
+ await expect(response).toHaveSessionForUser(userId)
+})
+
+test('gives an error if the account is already connected to another user', async () => {
+ const githubUser = await insertGitHubUser()
+ await prisma.user.create({
+ data: {
+ ...generateUserInfo(),
+ connections: {
+ create: {
+ providerName: GITHUB_PROVIDER_NAME,
+ providerId: githubUser.profile.id.toString(),
+ },
+ },
+ },
+ })
+ const session = await setupUser()
+ const request = await setupRequest({
+ sessionId: session.id,
+ code: githubUser.code,
+ })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/settings/profile/connections')
+ await expect(response).toSendToast(
+ expect.objectContaining({
+ title: 'Already Connected',
+ description: expect.stringContaining(
+ 'already connected to another account',
+ ),
+ }),
+ )
+})
+
+test('if a user is not logged in, but the connection exists, make a session', async () => {
+ const githubUser = await insertGitHubUser()
+ const { userId } = await setupUser()
+ await prisma.connection.create({
+ data: {
+ providerName: GITHUB_PROVIDER_NAME,
+ providerId: githubUser.profile.id.toString(),
+ userId,
+ },
+ })
+ const request = await setupRequest({ code: githubUser.code })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ expect(response).toHaveRedirect('/')
+ await expect(response).toHaveSessionForUser(userId)
+})
+
+test('if a user is not logged in, but the connection exists and they have enabled 2FA, send them to verify their 2FA and do not make a session', async () => {
+ const githubUser = await insertGitHubUser()
+ const { userId } = await setupUser()
+ await prisma.connection.create({
+ data: {
+ providerName: GITHUB_PROVIDER_NAME,
+ providerId: githubUser.profile.id.toString(),
+ userId,
+ },
+ })
+ const { otp: _otp, ...config } = await generateTOTP()
+ await prisma.verification.create({
+ data: {
+ type: twoFAVerificationType,
+ target: userId,
+ ...config,
+ },
+ })
+ const request = await setupRequest({ code: githubUser.code })
+ const response = await loader({ request, params: PARAMS, context: {} })
+ const searchParams = new URLSearchParams({
+ type: twoFAVerificationType,
+ target: userId,
+ redirectTo: '/',
+ })
+ expect(response).toHaveRedirect(`/verify?${searchParams}`)
+})
+
+async function setupRequest({
+ sessionId,
+ code = faker.string.uuid(),
+}: { sessionId?: string; code?: string } = {}) {
+ const url = new URL(ROUTE_PATH, BASE_URL)
+ const state = faker.string.uuid()
+ url.searchParams.set('state', state)
+ url.searchParams.set('code', code)
+ const authSession = await authSessionStorage.getSession()
+ if (sessionId) authSession.set(sessionKey, sessionId)
+ const setSessionCookieHeader =
+ await authSessionStorage.commitSession(authSession)
+ const searchParams = new URLSearchParams({ code, state })
+ let authCookie = new SetCookie({
+ name: 'github',
+ value: searchParams.toString(),
+ path: '/',
+ sameSite: 'Lax',
+ httpOnly: true,
+ maxAge: 60 * 10,
+ secure: process.env.NODE_ENV === 'production' || undefined,
+ })
+ const request = new Request(url.toString(), {
+ method: 'GET',
+ headers: {
+ cookie: [
+ authCookie.toString(),
+ convertSetCookieToCookie(setSessionCookieHeader),
+ ].join('; '),
+ },
+ })
+ return request
+}
+
+async function setupUser(userData = generateUserInfo()) {
+ const session = await prisma.session.create({
+ data: {
+ expirationDate: getSessionExpirationDate(),
+ user: {
+ create: {
+ ...userData,
+ },
+ },
+ },
+ select: {
+ id: true,
+ userId: true,
+ },
+ })
+
+ return session
+}
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/auth.$provider.callback.ts b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/auth.$provider.callback.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/auth.$provider.callback.ts
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/auth.$provider.callback.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/auth_.$provider.ts b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/auth_.$provider.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/auth_.$provider.ts
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/auth_.$provider.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/forgot-password.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/forgot-password.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/forgot-password.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/forgot-password.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/login.server.ts b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/login.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/login.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/login.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/login.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/login.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/login.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/login.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/logout.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/logout.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/logout.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/logout.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/onboarding.server.ts b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/onboarding.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/onboarding.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/onboarding.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/onboarding.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/onboarding.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/onboarding.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/onboarding.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/onboarding_.$provider.server.ts b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/onboarding_.$provider.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/onboarding_.$provider.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/onboarding_.$provider.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/onboarding_.$provider.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/onboarding_.$provider.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/onboarding_.$provider.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/onboarding_.$provider.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/reset-password.server.ts b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/reset-password.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/reset-password.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/reset-password.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/reset-password.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/reset-password.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/reset-password.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/reset-password.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/signup.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/signup.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/signup.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/signup.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/verify.server.ts b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/verify.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/verify.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/verify.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/verify.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/verify.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/verify.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/verify.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/webauthn+/authentication.ts b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/webauthn+/authentication.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/webauthn+/authentication.ts
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/webauthn+/authentication.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/webauthn+/registration.ts b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/webauthn+/registration.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/webauthn+/registration.ts
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/webauthn+/registration.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/webauthn+/utils.server.ts b/exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/webauthn+/utils.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_auth+/webauthn+/utils.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_auth+/webauthn+/utils.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/about.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/about.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/about.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/about.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/index.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/index.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/index.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/index.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/docker.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/docker.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/docker.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/docker.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/eslint.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/eslint.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/eslint.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/eslint.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/faker.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/faker.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/faker.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/faker.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/fly.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/fly.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/fly.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/fly.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/github.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/github.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/github.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/github.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/logos.ts b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/logos.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/logos.ts
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/logos.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/msw.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/msw.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/msw.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/msw.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/playwright.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/playwright.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/playwright.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/playwright.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/prettier.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/prettier.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/prettier.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/prettier.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/prisma.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/prisma.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/prisma.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/prisma.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/radix.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/radix.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/radix.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/radix.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/react-email.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/react-email.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/react-email.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/react-email.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/remix.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/remix.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/remix.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/remix.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/resend.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/resend.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/resend.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/resend.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/sentry.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/sentry.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/sentry.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/sentry.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/shadcn-ui.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/shadcn-ui.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/shadcn-ui.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/shadcn-ui.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/sqlite.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/sqlite.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/sqlite.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/sqlite.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/stars.jpg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/stars.jpg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/stars.jpg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/stars.jpg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/tailwind.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/tailwind.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/tailwind.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/tailwind.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/testing-library.png b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/testing-library.png
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/testing-library.png
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/testing-library.png
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/typescript.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/typescript.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/typescript.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/typescript.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/vitest.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/vitest.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/vitest.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/vitest.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/zod.svg b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/zod.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/logos/zod.svg
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/logos/zod.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/privacy.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/privacy.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/privacy.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/privacy.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/support.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/support.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/support.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/support.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/tos.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/tos.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_marketing+/tos.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_marketing+/tos.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_seo+/robots[.]txt.ts b/exercises/02.authentication/03.solution.passkeys/app/routes/_seo+/robots[.]txt.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_seo+/robots[.]txt.ts
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_seo+/robots[.]txt.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/_seo+/sitemap[.]xml.ts b/exercises/02.authentication/03.solution.passkeys/app/routes/_seo+/sitemap[.]xml.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/_seo+/sitemap[.]xml.ts
rename to exercises/02.authentication/03.solution.passkeys/app/routes/_seo+/sitemap[.]xml.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/admin+/cache.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/admin+/cache.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/admin+/cache.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/admin+/cache.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/admin+/cache_.lru.$cacheKey.ts b/exercises/02.authentication/03.solution.passkeys/app/routes/admin+/cache_.lru.$cacheKey.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/admin+/cache_.lru.$cacheKey.ts
rename to exercises/02.authentication/03.solution.passkeys/app/routes/admin+/cache_.lru.$cacheKey.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/admin+/cache_.sqlite.$cacheKey.ts b/exercises/02.authentication/03.solution.passkeys/app/routes/admin+/cache_.sqlite.$cacheKey.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/admin+/cache_.sqlite.$cacheKey.ts
rename to exercises/02.authentication/03.solution.passkeys/app/routes/admin+/cache_.sqlite.$cacheKey.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/admin+/cache_.sqlite.server.ts b/exercises/02.authentication/03.solution.passkeys/app/routes/admin+/cache_.sqlite.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/admin+/cache_.sqlite.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/routes/admin+/cache_.sqlite.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/admin+/cache_.sqlite.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/admin+/cache_.sqlite.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/admin+/cache_.sqlite.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/admin+/cache_.sqlite.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/me.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/me.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/me.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/me.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/resources+/download-user-data.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/resources+/download-user-data.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/resources+/download-user-data.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/resources+/download-user-data.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/resources+/healthcheck.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/resources+/healthcheck.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/resources+/healthcheck.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/resources+/healthcheck.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/resources+/images.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/resources+/images.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/resources+/images.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/resources+/images.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/resources+/theme-switch.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/resources+/theme-switch.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/resources+/theme-switch.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/resources+/theme-switch.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.change-email.server.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.change-email.server.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.change-email.server.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.change-email.server.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.change-email.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.change-email.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.change-email.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.change-email.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.connections.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.connections.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.connections.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.connections.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.index.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.index.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.index.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.index.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.passkeys.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.passkeys.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.passkeys.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.passkeys.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.password.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.password.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.password.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.password.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.password_.create.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.password_.create.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.password_.create.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.password_.create.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.photo.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.photo.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.photo.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.photo.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.two-factor.disable.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.two-factor.disable.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.two-factor.disable.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.two-factor.disable.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.two-factor.index.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.two-factor.index.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.two-factor.index.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.two-factor.index.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.two-factor.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.two-factor.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.two-factor.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.two-factor.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.two-factor.verify.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.two-factor.verify.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/settings+/profile.two-factor.verify.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/settings+/profile.two-factor.verify.tsx
diff --git a/exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username.test.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username.test.tsx
new file mode 100644
index 0000000..0884a24
--- /dev/null
+++ b/exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username.test.tsx
@@ -0,0 +1,97 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { faker } from '@faker-js/faker'
+import { render, screen } from '@testing-library/react'
+import { createRoutesStub } from 'react-router'
+import setCookieParser from 'set-cookie-parser'
+import { test } from 'vitest'
+import { loader as rootLoader } from '#app/root.tsx'
+import { getSessionExpirationDate, sessionKey } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { authSessionStorage } from '#app/utils/session.server.ts'
+import { generateUserInfo, getUserImages } from '#tests/db-utils.ts'
+import { default as UsernameRoute, loader } from './$username.tsx'
+
+test('The user profile when not logged in as self', async () => {
+ const userImages = await getUserImages()
+ const userImage =
+ userImages[faker.number.int({ min: 0, max: userImages.length - 1 })]
+ const user = await prisma.user.create({
+ select: { id: true, username: true, name: true },
+ data: { ...generateUserInfo(), image: { create: userImage } },
+ })
+ const App = createRoutesStub([
+ {
+ path: '/users/:username',
+ Component: UsernameRoute,
+ loader,
+ HydrateFallback: () => Loading...
,
+ },
+ ])
+
+ const routeUrl = `/users/${user.username}`
+ render( )
+
+ await screen.findByRole('heading', { level: 1, name: user.name! })
+ await screen.findByRole('img', { name: user.name! })
+ await screen.findByRole('link', { name: `${user.name}'s notes` })
+})
+
+test('The user profile when logged in as self', async () => {
+ const userImages = await getUserImages()
+ const userImage =
+ userImages[faker.number.int({ min: 0, max: userImages.length - 1 })]
+ const user = await prisma.user.create({
+ select: { id: true, username: true, name: true },
+ data: { ...generateUserInfo(), image: { create: userImage } },
+ })
+ const session = await prisma.session.create({
+ select: { id: true },
+ data: {
+ expirationDate: getSessionExpirationDate(),
+ userId: user.id,
+ },
+ })
+
+ const authSession = await authSessionStorage.getSession()
+ authSession.set(sessionKey, session.id)
+ const setCookieHeader = await authSessionStorage.commitSession(authSession)
+ const parsedCookie = setCookieParser.parseString(setCookieHeader)
+ const cookieHeader = new URLSearchParams({
+ [parsedCookie.name]: parsedCookie.value,
+ }).toString()
+
+ const App = createRoutesStub([
+ {
+ id: 'root',
+ path: '/',
+ loader: async (args) => {
+ // add the cookie header to the request
+ args.request.headers.set('cookie', cookieHeader)
+ return rootLoader({ ...args, context: args.context })
+ },
+ HydrateFallback: () => Loading...
,
+ children: [
+ {
+ path: 'users/:username',
+ Component: UsernameRoute,
+ loader: async (args) => {
+ // add the cookie header to the request
+ args.request.headers.set('cookie', cookieHeader)
+ return loader(args)
+ },
+ },
+ ],
+ },
+ ])
+
+ const routeUrl = `/users/${user.username}`
+ render( )
+
+ await screen.findByRole('heading', { level: 1, name: user.name! })
+ await screen.findByRole('img', { name: user.name! })
+ await screen.findByRole('button', { name: /logout/i })
+ await screen.findByRole('link', { name: /my notes/i })
+ await screen.findByRole('link', { name: /edit profile/i })
+})
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/users+/$username.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/users+/$username.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/users+/$username_+/__note-editor.server.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username_+/__note-editor.server.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/users+/$username_+/__note-editor.server.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username_+/__note-editor.server.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/users+/$username_+/__note-editor.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username_+/__note-editor.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/users+/$username_+/__note-editor.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username_+/__note-editor.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/users+/$username_+/notes.$noteId.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username_+/notes.$noteId.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/users+/$username_+/notes.$noteId.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username_+/notes.$noteId.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/users+/$username_+/notes.$noteId_.edit.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/users+/$username_+/notes.index.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username_+/notes.index.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/users+/$username_+/notes.index.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username_+/notes.index.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/users+/$username_+/notes.new.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username_+/notes.new.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/users+/$username_+/notes.new.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username_+/notes.new.tsx
diff --git a/exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username_+/notes.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username_+/notes.tsx
new file mode 100644
index 0000000..ded41ca
--- /dev/null
+++ b/exercises/02.authentication/03.solution.passkeys/app/routes/users+/$username_+/notes.tsx
@@ -0,0 +1,105 @@
+import { invariantResponse } from '@epic-web/invariant'
+import { Img } from 'openimg/react'
+import { Link, NavLink, Outlet } from 'react-router'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { cn, getUserImgSrc } from '#app/utils/misc.tsx'
+import { useOptionalUser } from '#app/utils/user.ts'
+import { type Route } from './+types/notes.ts'
+
+export async function loader({ params }: Route.LoaderArgs) {
+ const owner = await prisma.user.findFirst({
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ image: { select: { objectKey: true } },
+ notes: { select: { id: true, title: true } },
+ },
+ where: { username: params.username },
+ })
+
+ invariantResponse(owner, 'Owner not found', { status: 404 })
+
+ return { owner }
+}
+
+export default function NotesRoute({ loaderData }: Route.ComponentProps) {
+ const user = useOptionalUser()
+ const isOwner = user?.id === loaderData.owner.id
+ const ownerDisplayName = loaderData.owner.name ?? loaderData.owner.username
+ const navLinkDefaultClassName =
+ 'line-clamp-2 block rounded-l-full py-2 pl-8 pr-6 text-base lg:text-xl'
+ return (
+
+
+
+
+
+
+
+ {ownerDisplayName}'s Notes
+
+
+
+ {isOwner ? (
+
+
+ cn(navLinkDefaultClassName, isActive && 'bg-accent')
+ }
+ >
+ New Note
+
+
+ ) : null}
+ {loaderData.owner.notes.map((note) => (
+
+
+ cn(navLinkDefaultClassName, isActive && 'bg-accent')
+ }
+ >
+ {note.title}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return (
+ (
+ No user with the username "{params.username}" exists
+ ),
+ }}
+ />
+ )
+}
diff --git a/exercises/02.test-setup/05.solution.test-data/app/routes/users+/index.tsx b/exercises/02.authentication/03.solution.passkeys/app/routes/users+/index.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/routes/users+/index.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/routes/users+/index.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/styles/tailwind.css b/exercises/02.authentication/03.solution.passkeys/app/styles/tailwind.css
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/styles/tailwind.css
rename to exercises/02.authentication/03.solution.passkeys/app/styles/tailwind.css
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/auth.server.test.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/auth.server.test.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/auth.server.test.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/auth.server.test.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/auth.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/auth.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/auth.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/auth.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/cache.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/cache.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/cache.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/cache.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/client-hints.tsx b/exercises/02.authentication/03.solution.passkeys/app/utils/client-hints.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/client-hints.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/utils/client-hints.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/connections.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/connections.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/connections.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/connections.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/connections.tsx b/exercises/02.authentication/03.solution.passkeys/app/utils/connections.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/connections.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/utils/connections.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/db.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/db.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/db.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/db.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/email.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/email.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/email.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/email.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/env.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/env.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/env.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/env.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/headers.server.test.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/headers.server.test.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/headers.server.test.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/headers.server.test.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/headers.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/headers.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/headers.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/headers.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/honeypot.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/honeypot.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/honeypot.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/honeypot.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/litefs.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/litefs.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/litefs.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/litefs.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/misc.error-message.test.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/misc.error-message.test.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/misc.error-message.test.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/misc.error-message.test.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/misc.tsx b/exercises/02.authentication/03.solution.passkeys/app/utils/misc.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/misc.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/utils/misc.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/misc.use-double-check.test.tsx b/exercises/02.authentication/03.solution.passkeys/app/utils/misc.use-double-check.test.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/misc.use-double-check.test.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/utils/misc.use-double-check.test.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/monitoring.client.tsx b/exercises/02.authentication/03.solution.passkeys/app/utils/monitoring.client.tsx
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/monitoring.client.tsx
rename to exercises/02.authentication/03.solution.passkeys/app/utils/monitoring.client.tsx
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/nonce-provider.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/nonce-provider.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/nonce-provider.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/nonce-provider.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/permissions.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/permissions.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/permissions.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/permissions.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/providers/constants.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/providers/constants.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/providers/constants.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/providers/constants.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/providers/github.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/providers/github.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/providers/github.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/providers/github.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/providers/provider.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/providers/provider.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/providers/provider.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/providers/provider.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/redirect-cookie.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/redirect-cookie.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/redirect-cookie.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/redirect-cookie.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/request-info.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/request-info.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/request-info.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/request-info.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/session.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/session.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/session.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/session.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/storage.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/storage.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/storage.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/storage.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/theme.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/theme.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/theme.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/theme.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/timing.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/timing.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/timing.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/timing.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/toast.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/toast.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/toast.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/toast.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/totp.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/totp.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/totp.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/totp.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/user-validation.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/user-validation.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/user-validation.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/user-validation.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/user.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/user.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/user.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/user.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/app/utils/verification.server.ts b/exercises/02.authentication/03.solution.passkeys/app/utils/verification.server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/app/utils/verification.server.ts
rename to exercises/02.authentication/03.solution.passkeys/app/utils/verification.server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/components.json b/exercises/02.authentication/03.solution.passkeys/components.json
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/components.json
rename to exercises/02.authentication/03.solution.passkeys/components.json
diff --git a/exercises/02.test-setup/05.solution.test-data/eslint.config.js b/exercises/02.authentication/03.solution.passkeys/eslint.config.js
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/eslint.config.js
rename to exercises/02.authentication/03.solution.passkeys/eslint.config.js
diff --git a/exercises/02.test-setup/05.solution.test-data/fly.toml b/exercises/02.authentication/03.solution.passkeys/fly.toml
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/fly.toml
rename to exercises/02.authentication/03.solution.passkeys/fly.toml
diff --git a/exercises/02.test-setup/05.solution.test-data/index.js b/exercises/02.authentication/03.solution.passkeys/index.js
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/index.js
rename to exercises/02.authentication/03.solution.passkeys/index.js
diff --git a/exercises/02.test-setup/05.solution.test-data/other/Dockerfile b/exercises/02.authentication/03.solution.passkeys/other/Dockerfile
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/Dockerfile
rename to exercises/02.authentication/03.solution.passkeys/other/Dockerfile
diff --git a/exercises/02.test-setup/05.solution.test-data/other/Dockerfile.dockerignore b/exercises/02.authentication/03.solution.passkeys/other/Dockerfile.dockerignore
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/Dockerfile.dockerignore
rename to exercises/02.authentication/03.solution.passkeys/other/Dockerfile.dockerignore
diff --git a/exercises/02.test-setup/05.solution.test-data/other/README.md b/exercises/02.authentication/03.solution.passkeys/other/README.md
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/README.md
rename to exercises/02.authentication/03.solution.passkeys/other/README.md
diff --git a/exercises/02.test-setup/05.solution.test-data/other/build-server.ts b/exercises/02.authentication/03.solution.passkeys/other/build-server.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/build-server.ts
rename to exercises/02.authentication/03.solution.passkeys/other/build-server.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/other/litefs.yml b/exercises/02.authentication/03.solution.passkeys/other/litefs.yml
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/litefs.yml
rename to exercises/02.authentication/03.solution.passkeys/other/litefs.yml
diff --git a/exercises/02.test-setup/05.solution.test-data/other/sly/sly.json b/exercises/02.authentication/03.solution.passkeys/other/sly/sly.json
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/sly/sly.json
rename to exercises/02.authentication/03.solution.passkeys/other/sly/sly.json
diff --git a/exercises/02.test-setup/05.solution.test-data/other/sly/transform-icon.ts b/exercises/02.authentication/03.solution.passkeys/other/sly/transform-icon.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/sly/transform-icon.ts
rename to exercises/02.authentication/03.solution.passkeys/other/sly/transform-icon.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/README.md b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/README.md
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/README.md
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/README.md
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/arrow-left.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/arrow-left.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/arrow-left.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/arrow-left.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/arrow-right.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/arrow-right.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/arrow-right.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/arrow-right.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/avatar.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/avatar.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/avatar.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/avatar.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/camera.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/camera.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/camera.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/camera.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/check.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/check.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/check.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/check.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/clock.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/clock.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/clock.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/clock.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/cross-1.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/cross-1.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/cross-1.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/cross-1.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/dots-horizontal.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/dots-horizontal.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/dots-horizontal.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/dots-horizontal.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/download.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/download.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/download.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/download.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/envelope-closed.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/envelope-closed.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/envelope-closed.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/envelope-closed.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/exit.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/exit.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/exit.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/exit.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/file-text.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/file-text.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/file-text.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/file-text.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/github-logo.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/github-logo.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/github-logo.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/github-logo.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/laptop.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/laptop.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/laptop.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/laptop.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/link-2.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/link-2.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/link-2.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/link-2.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/lock-closed.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/lock-closed.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/lock-closed.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/lock-closed.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/lock-open-1.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/lock-open-1.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/lock-open-1.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/lock-open-1.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/magnifying-glass.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/magnifying-glass.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/magnifying-glass.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/magnifying-glass.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/moon.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/moon.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/moon.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/moon.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/passkey.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/passkey.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/passkey.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/passkey.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/pencil-1.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/pencil-1.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/pencil-1.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/pencil-1.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/pencil-2.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/pencil-2.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/pencil-2.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/pencil-2.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/plus.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/plus.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/plus.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/plus.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/question-mark-circled.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/question-mark-circled.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/question-mark-circled.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/question-mark-circled.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/reset.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/reset.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/reset.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/reset.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/sun.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/sun.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/sun.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/sun.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/trash.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/trash.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/trash.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/trash.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/other/svg-icons/update.svg b/exercises/02.authentication/03.solution.passkeys/other/svg-icons/update.svg
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/other/svg-icons/update.svg
rename to exercises/02.authentication/03.solution.passkeys/other/svg-icons/update.svg
diff --git a/exercises/02.test-setup/05.solution.test-data/package-lock.json b/exercises/02.authentication/03.solution.passkeys/package-lock.json
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/package-lock.json
rename to exercises/02.authentication/03.solution.passkeys/package-lock.json
diff --git a/exercises/02.authentication/03.solution.passkeys/package.json b/exercises/02.authentication/03.solution.passkeys/package.json
new file mode 100644
index 0000000..a0c693d
--- /dev/null
+++ b/exercises/02.authentication/03.solution.passkeys/package.json
@@ -0,0 +1,172 @@
+{
+ "name": "exercises_02.authentication_03.solution.passkeys",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "imports": {
+ "#app/*": "./app/*",
+ "#tests/*": "./tests/*"
+ },
+ "scripts": {
+ "build": "run-s build:*",
+ "build:remix": "react-router build",
+ "build:server": "tsx ./other/build-server.ts",
+ "dev": "cross-env NODE_ENV=development MOCKS=true node ./server/dev-server.js",
+ "dev:no-mocks": "cross-env NODE_ENV=development node ./server/dev-server.js",
+ "format": "prettier --write .",
+ "lint": "eslint .",
+ "setup": "npm run build && prisma migrate deploy && prisma generate --sql && playwright install",
+ "start": "cross-env NODE_ENV=production node .",
+ "start:mocks": "cross-env NODE_ENV=production MOCKS=true tsx .",
+ "test": "npx playwright test",
+ "coverage": "vitest run --coverage",
+ "test:e2e": "npm run test:e2e:dev --silent",
+ "test:e2e:dev": "npx playwright test --ui",
+ "pretest:e2e:run": "npm run build",
+ "test:e2e:run": "npx cross-env CI=true npx playwright test",
+ "test:e2e:install": "npx playwright install --with-deps chromium",
+ "typecheck": "react-router typegen && tsc",
+ "validate": "run-p \"test -- --run\" lint typecheck test:e2e:run",
+ "post:playground": "npx react-router typegen && prisma migrate deploy && prisma generate --sql"
+ },
+ "prettier": "@epic-web/config/prettier",
+ "eslintIgnore": [
+ "/node_modules",
+ "/build",
+ "/public/build",
+ "/playwright-report",
+ "/server-build"
+ ],
+ "dependencies": {
+ "@conform-to/react": "^1.5.0",
+ "@conform-to/zod": "^1.5.0",
+ "@epic-web/cachified": "^5.5.2",
+ "@epic-web/client-hints": "^1.3.5",
+ "@epic-web/invariant": "^1.0.0",
+ "@epic-web/remember": "^1.1.0",
+ "@epic-web/totp": "^4.0.1",
+ "@mjackson/form-data-parser": "^0.7.0",
+ "@mjackson/headers": "^0.10.0",
+ "@nasa-gcn/remix-seo": "^2.0.1",
+ "@nichtsam/helmet": "^0.3.1",
+ "@oslojs/crypto": "^1.0.1",
+ "@oslojs/encoding": "^1.1.0",
+ "@paralleldrive/cuid2": "^2.2.2",
+ "@prisma/client": "^6.7.0",
+ "@prisma/instrumentation": "^6.7.0",
+ "@radix-ui/react-checkbox": "^1.2.3",
+ "@radix-ui/react-dropdown-menu": "^2.1.12",
+ "@radix-ui/react-label": "^2.1.4",
+ "@radix-ui/react-slot": "^1.2.0",
+ "@radix-ui/react-toast": "^1.2.11",
+ "@radix-ui/react-tooltip": "^1.2.4",
+ "@react-email/components": "0.0.38",
+ "@react-router/express": "^7.5.3",
+ "@react-router/node": "^7.5.3",
+ "@react-router/remix-routes-option-adapter": "^7.5.3",
+ "@remix-run/server-runtime": "^2.16.5",
+ "@sentry/profiling-node": "^9.32.0",
+ "@sentry/react-router": "^9.32.0",
+ "@simplewebauthn/browser": "^13.1.0",
+ "@simplewebauthn/server": "^13.1.1",
+ "@tailwindcss/vite": "^4.1.5",
+ "@tusbar/cache-control": "1.0.2",
+ "address": "^2.0.3",
+ "bcryptjs": "^3.0.2",
+ "class-variance-authority": "^0.7.1",
+ "close-with-grace": "^2.2.0",
+ "clsx": "^2.1.1",
+ "compression": "^1.8.0",
+ "cookie": "^1.0.2",
+ "cross-env": "^7.0.3",
+ "date-fns": "^4.1.0",
+ "dotenv": "^16.5.0",
+ "execa": "^9.5.2",
+ "express": "^4.21.2",
+ "express-rate-limit": "^7.5.0",
+ "get-port": "^7.1.0",
+ "glob": "^11.0.2",
+ "input-otp": "^1.4.2",
+ "intl-parse-accept-language": "^1.0.0",
+ "isbot": "^5.1.27",
+ "litefs-js": "^2.0.2",
+ "lru-cache": "^11.1.0",
+ "mime-types": "^3.0.1",
+ "morgan": "^1.10.0",
+ "openimg": "^1.1.0",
+ "prisma": "^6.7.0",
+ "qrcode": "^1.5.4",
+ "react": "^19.1.0",
+ "react-dom": "^19.1.0",
+ "react-router": "^7.5.3",
+ "remix-auth": "^4.2.0",
+ "remix-auth-github": "^3.0.2",
+ "remix-utils": "^8.5.0",
+ "set-cookie-parser": "^2.7.1",
+ "sharp": "^0.34.2",
+ "sonner": "^2.0.3",
+ "source-map-support": "^0.5.21",
+ "spin-delay": "^2.0.1",
+ "tailwind-merge": "^3.2.0",
+ "tailwindcss": "^4.1.5",
+ "vite-env-only": "^3.0.3",
+ "zod": "^3.24.4"
+ },
+ "devDependencies": {
+ "@epic-web/config": "^1.20.1",
+ "@faker-js/faker": "^9.7.0",
+ "@playwright/test": "^1.57.0",
+ "@react-router/dev": "^7.5.3",
+ "@sly-cli/sly": "^2.1.1",
+ "@testing-library/dom": "^10.4.0",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.3.0",
+ "@testing-library/user-event": "^14.6.1",
+ "@total-typescript/ts-reset": "^0.6.1",
+ "@types/compression": "^1.7.5",
+ "@types/eslint": "^9.6.1",
+ "@types/express": "^4.17.21",
+ "@types/fs-extra": "^11.0.4",
+ "@types/glob": "^8.1.0",
+ "@types/mime-types": "^2.1.4",
+ "@types/morgan": "^1.9.9",
+ "@types/node": "^22.15.3",
+ "@types/qrcode": "^1.5.5",
+ "@types/react": "^19.1.2",
+ "@types/react-dom": "^19.1.3",
+ "@types/set-cookie-parser": "^2.4.10",
+ "@types/source-map-support": "^0.5.10",
+ "@vitejs/plugin-react": "^4.4.1",
+ "@vitest/coverage-v8": "^3.1.3",
+ "enforce-unique": "^1.3.0",
+ "esbuild": "^0.25.3",
+ "eslint": "^9.26.0",
+ "fs-extra": "^11.3.0",
+ "jsdom": "^25.0.1",
+ "msw": "^2.7.6",
+ "npm-run-all": "^4.1.5",
+ "playwright-persona": "^0.2.8",
+ "prettier": "^3.5.3",
+ "prettier-plugin-sql": "^0.19.0",
+ "prettier-plugin-tailwindcss": "^0.6.11",
+ "react-router-devtools": "^5.0.5",
+ "remix-flat-routes": "^0.8.5",
+ "test-passkey": "^1.0.2",
+ "tsx": "^4.19.4",
+ "tw-animate-css": "^1.2.9",
+ "typescript": "^5.8.3",
+ "vite": "^6.3.5",
+ "vite-plugin-icons-spritesheet": "^3.0.1",
+ "vitest": "^3.1.3"
+ },
+ "engines": {
+ "node": "22.14.0"
+ },
+ "prisma": {
+ "seed": "tsx prisma/seed.ts"
+ },
+ "epic-stack": {
+ "head": "4e6532d08219cef41299615ad7556b205aa0b77d",
+ "date": "2025-07-02T03:11:12Z"
+ }
+}
diff --git a/exercises/02.test-setup/05.solution.test-data/playwright.config.ts b/exercises/02.authentication/03.solution.passkeys/playwright.config.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/playwright.config.ts
rename to exercises/02.authentication/03.solution.passkeys/playwright.config.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/prisma/migrations/20250221233640_init/migration.sql b/exercises/02.authentication/03.solution.passkeys/prisma/migrations/20250221233640_init/migration.sql
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/prisma/migrations/20250221233640_init/migration.sql
rename to exercises/02.authentication/03.solution.passkeys/prisma/migrations/20250221233640_init/migration.sql
diff --git a/exercises/02.test-setup/05.solution.test-data/prisma/migrations/migration_lock.toml b/exercises/02.authentication/03.solution.passkeys/prisma/migrations/migration_lock.toml
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/prisma/migrations/migration_lock.toml
rename to exercises/02.authentication/03.solution.passkeys/prisma/migrations/migration_lock.toml
diff --git a/exercises/02.test-setup/05.solution.test-data/prisma/schema.prisma b/exercises/02.authentication/03.solution.passkeys/prisma/schema.prisma
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/prisma/schema.prisma
rename to exercises/02.authentication/03.solution.passkeys/prisma/schema.prisma
diff --git a/exercises/02.authentication/03.solution.passkeys/prisma/seed.ts b/exercises/02.authentication/03.solution.passkeys/prisma/seed.ts
new file mode 100644
index 0000000..521e1d5
--- /dev/null
+++ b/exercises/02.authentication/03.solution.passkeys/prisma/seed.ts
@@ -0,0 +1,263 @@
+import { faker } from '@faker-js/faker'
+import { prisma } from '#app/utils/db.server.ts'
+import { MOCK_CODE_GITHUB } from '#app/utils/providers/constants'
+import {
+ createPassword,
+ generateUserInfo,
+ getNoteImages,
+ getUserImages,
+} from '#tests/db-utils.ts'
+import { insertGitHubUser } from '#tests/mocks/github.ts'
+
+async function seed() {
+ console.log('🌱 Seeding...')
+ console.time(`🌱 Database has been seeded`)
+
+ const totalUsers = 5
+ console.time(`👤 Created ${totalUsers} users...`)
+ const noteImages = await getNoteImages()
+ const userImages = await getUserImages()
+
+ for (let index = 0; index < totalUsers; index++) {
+ const userData = generateUserInfo()
+ const user = await prisma.user.create({
+ select: { id: true },
+ data: {
+ ...userData,
+ password: { create: createPassword(userData.username) },
+ roles: { connect: { name: 'user' } },
+ },
+ })
+
+ // Upload user profile image
+ const userImage = userImages[index % userImages.length]
+ if (userImage) {
+ await prisma.userImage.create({
+ data: {
+ userId: user.id,
+ objectKey: userImage.objectKey,
+ },
+ })
+ }
+
+ // Create notes with images
+ const notesCount = faker.number.int({ min: 1, max: 3 })
+ for (let noteIndex = 0; noteIndex < notesCount; noteIndex++) {
+ const note = await prisma.note.create({
+ select: { id: true },
+ data: {
+ title: faker.lorem.sentence(),
+ content: faker.lorem.paragraphs(),
+ ownerId: user.id,
+ },
+ })
+
+ // Add images to note
+ const noteImageCount = faker.number.int({ min: 1, max: 3 })
+ for (let imageIndex = 0; imageIndex < noteImageCount; imageIndex++) {
+ const imgNumber = faker.number.int({ min: 0, max: 9 })
+ const noteImage = noteImages[imgNumber]
+ if (noteImage) {
+ await prisma.noteImage.create({
+ data: {
+ noteId: note.id,
+ altText: noteImage.altText,
+ objectKey: noteImage.objectKey,
+ },
+ })
+ }
+ }
+ }
+ }
+ console.timeEnd(`👤 Created ${totalUsers} users...`)
+
+ console.time(`🐨 Created admin user "kody"`)
+
+ const kodyImages = {
+ kodyUser: { objectKey: 'user/kody.png' },
+ cuteKoala: {
+ altText: 'an adorable koala cartoon illustration',
+ objectKey: 'kody-notes/cute-koala.png',
+ },
+ koalaEating: {
+ altText: 'a cartoon illustration of a koala in a tree eating',
+ objectKey: 'kody-notes/koala-eating.png',
+ },
+ koalaCuddle: {
+ altText: 'a cartoon illustration of koalas cuddling',
+ objectKey: 'kody-notes/koala-cuddle.png',
+ },
+ mountain: {
+ altText: 'a beautiful mountain covered in snow',
+ objectKey: 'kody-notes/mountain.png',
+ },
+ koalaCoder: {
+ altText: 'a koala coding at the computer',
+ objectKey: 'kody-notes/koala-coder.png',
+ },
+ koalaMentor: {
+ altText:
+ 'a koala in a friendly and helpful posture. The Koala is standing next to and teaching a woman who is coding on a computer and shows positive signs of learning and understanding what is being explained.',
+ objectKey: 'kody-notes/koala-mentor.png',
+ },
+ koalaSoccer: {
+ altText: 'a cute cartoon koala kicking a soccer ball on a soccer field ',
+ objectKey: 'kody-notes/koala-soccer.png',
+ },
+ }
+
+ const githubUser = await insertGitHubUser(MOCK_CODE_GITHUB)
+
+ const kody = await prisma.user.create({
+ select: { id: true },
+ data: {
+ email: 'kody@kcd.dev',
+ username: 'kody',
+ name: 'Kody',
+ password: { create: createPassword('kodylovesyou') },
+ connections: {
+ create: {
+ providerName: 'github',
+ providerId: String(githubUser.profile.id),
+ },
+ },
+ roles: { connect: [{ name: 'admin' }, { name: 'user' }] },
+ },
+ })
+
+ await prisma.userImage.create({
+ data: {
+ userId: kody.id,
+ objectKey: kodyImages.kodyUser.objectKey,
+ },
+ })
+
+ // Create Kody's notes
+ const kodyNotes = [
+ {
+ id: 'd27a197e',
+ title: 'Basic Koala Facts',
+ content:
+ 'Koalas are found in the eucalyptus forests of eastern Australia. They have grey fur with a cream-coloured chest, and strong, clawed feet, perfect for living in the branches of trees!',
+ images: [kodyImages.cuteKoala, kodyImages.koalaEating],
+ },
+ {
+ id: '414f0c09',
+ title: 'Koalas like to cuddle',
+ content:
+ 'Cuddly critters, koalas measure about 60cm to 85cm long, and weigh about 14kg.',
+ images: [kodyImages.koalaCuddle],
+ },
+ {
+ id: '260366b1',
+ title: 'Not bears',
+ content:
+ "Although you may have heard people call them koala 'bears', these awesome animals aren't bears at all – they are in fact marsupials. A group of mammals, most marsupials have pouches where their newborns develop.",
+ images: [],
+ },
+ {
+ id: 'bb79cf45',
+ title: 'Snowboarding Adventure',
+ content:
+ "Today was an epic day on the slopes! Shredded fresh powder with my friends, caught some sick air, and even attempted a backflip. Can't wait for the next snowy adventure!",
+ images: [kodyImages.mountain],
+ },
+ {
+ id: '9f4308be',
+ title: 'Onewheel Tricks',
+ content:
+ "Mastered a new trick on my Onewheel today called '180 Spin'. It's exhilarating to carve through the streets while pulling off these rad moves. Time to level up and learn more!",
+ images: [],
+ },
+ {
+ id: '306021fb',
+ title: 'Coding Dilemma',
+ content:
+ "Stuck on a bug in my latest coding project. Need to figure out why my function isn't returning the expected output. Time to dig deep, debug, and conquer this challenge!",
+ images: [kodyImages.koalaCoder],
+ },
+ {
+ id: '16d4912a',
+ title: 'Coding Mentorship',
+ content:
+ "Had a fantastic coding mentoring session today with Sarah. Helped her understand the concept of recursion, and she made great progress. It's incredibly fulfilling to help others improve their coding skills.",
+ images: [kodyImages.koalaMentor],
+ },
+ {
+ id: '3199199e',
+ title: 'Koala Fun Facts',
+ content:
+ "Did you know that koalas sleep for up to 20 hours a day? It's because their diet of eucalyptus leaves doesn't provide much energy. But when I'm awake, I enjoy munching on leaves, chilling in trees, and being the cuddliest koala around!",
+ images: [],
+ },
+ {
+ id: '2030ffd3',
+ title: 'Skiing Adventure',
+ content:
+ 'Spent the day hitting the slopes on my skis. The fresh powder made for some incredible runs and breathtaking views. Skiing down the mountain at top speed is an adrenaline rush like no other!',
+ images: [kodyImages.mountain],
+ },
+ {
+ id: 'f375a804',
+ title: 'Code Jam Success',
+ content:
+ 'Participated in a coding competition today and secured the first place! The adrenaline, the challenging problems, and the satisfaction of finding optimal solutions—it was an amazing experience. Feeling proud and motivated to keep pushing my coding skills further!',
+ images: [kodyImages.koalaCoder],
+ },
+ {
+ id: '562c541b',
+ title: 'Koala Conservation Efforts',
+ content:
+ "Joined a local conservation group to protect koalas and their habitats. Together, we're planting more eucalyptus trees, raising awareness about their endangered status, and working towards a sustainable future for these adorable creatures. Every small step counts!",
+ images: [],
+ },
+ {
+ id: 'f67ca40b',
+ title: 'Game day',
+ content:
+ "Just got back from the most amazing game. I've been playing soccer for a long time, but I've not once scored a goal. Well, today all that changed! I finally scored my first ever goal.\n\nI'm in an indoor league, and my team's not the best, but we're pretty good and I have fun, that's all that really matters. Anyway, I found myself at the other end of the field with the ball. It was just me and the goalie. I normally just kick the ball and hope it goes in, but the ball was already rolling toward the goal. The goalie was about to get the ball, so I had to charge. I managed to get possession of the ball just before the goalie got it. I brought it around the goalie and had a perfect shot. I screamed so loud in excitement. After all these years playing, I finally scored a goal!\n\nI know it's not a lot for most folks, but it meant a lot to me. We did end up winning the game by one. It makes me feel great that I had a part to play in that.\n\nIn this team, I'm the captain. I'm constantly cheering my team on. Even after getting injured, I continued to come and watch from the side-lines. I enjoy yelling (encouragingly) at my team mates and helping them be the best they can. I'm definitely not the best player by a long stretch. But I really enjoy the game. It's a great way to get exercise and have good social interactions once a week.\n\nThat said, it can be hard to keep people coming and paying dues and stuff. If people don't show up it can be really hard to find subs. I have a list of people I can text, but sometimes I can't find anyone.\n\nBut yeah, today was awesome. I felt like more than just a player that gets in the way of the opposition, but an actual asset to the team. Really great feeling.\n\nAnyway, I'm rambling at this point and really this is just so we can have a note that's pretty long to test things out. I think it's long enough now... Cheers!",
+ images: [kodyImages.koalaSoccer],
+ },
+ ]
+
+ for (const noteData of kodyNotes) {
+ const note = await prisma.note.create({
+ select: { id: true },
+ data: {
+ id: noteData.id,
+ title: noteData.title,
+ content: noteData.content,
+ ownerId: kody.id,
+ },
+ })
+
+ for (const image of noteData.images) {
+ await prisma.noteImage.create({
+ data: {
+ noteId: note.id,
+ altText: image.altText,
+ objectKey: image.objectKey,
+ },
+ })
+ }
+ }
+
+ console.timeEnd(`🐨 Created admin user "kody"`)
+
+ console.timeEnd(`🌱 Database has been seeded`)
+}
+
+seed()
+ .catch((e) => {
+ console.error(e)
+ process.exit(1)
+ })
+ .finally(async () => {
+ await prisma.$disconnect()
+ })
+
+// we're ok to import from the test directory in this file
+/*
+eslint
+ no-restricted-imports: "off",
+*/
diff --git a/exercises/02.test-setup/05.solution.test-data/prisma/sql/searchUsers.sql b/exercises/02.authentication/03.solution.passkeys/prisma/sql/searchUsers.sql
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/prisma/sql/searchUsers.sql
rename to exercises/02.authentication/03.solution.passkeys/prisma/sql/searchUsers.sql
diff --git a/exercises/02.test-setup/05.solution.test-data/public/favicon.ico b/exercises/02.authentication/03.solution.passkeys/public/favicon.ico
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/public/favicon.ico
rename to exercises/02.authentication/03.solution.passkeys/public/favicon.ico
diff --git a/exercises/02.test-setup/05.solution.test-data/public/favicons/README.md b/exercises/02.authentication/03.solution.passkeys/public/favicons/README.md
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/public/favicons/README.md
rename to exercises/02.authentication/03.solution.passkeys/public/favicons/README.md
diff --git a/exercises/02.test-setup/05.solution.test-data/public/favicons/android-chrome-192x192.png b/exercises/02.authentication/03.solution.passkeys/public/favicons/android-chrome-192x192.png
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/public/favicons/android-chrome-192x192.png
rename to exercises/02.authentication/03.solution.passkeys/public/favicons/android-chrome-192x192.png
diff --git a/exercises/02.test-setup/05.solution.test-data/public/favicons/android-chrome-512x512.png b/exercises/02.authentication/03.solution.passkeys/public/favicons/android-chrome-512x512.png
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/public/favicons/android-chrome-512x512.png
rename to exercises/02.authentication/03.solution.passkeys/public/favicons/android-chrome-512x512.png
diff --git a/exercises/02.test-setup/05.solution.test-data/public/img/user.png b/exercises/02.authentication/03.solution.passkeys/public/img/user.png
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/public/img/user.png
rename to exercises/02.authentication/03.solution.passkeys/public/img/user.png
diff --git a/exercises/02.test-setup/05.solution.test-data/public/site.webmanifest b/exercises/02.authentication/03.solution.passkeys/public/site.webmanifest
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/public/site.webmanifest
rename to exercises/02.authentication/03.solution.passkeys/public/site.webmanifest
diff --git a/exercises/02.test-setup/05.solution.test-data/react-router.config.ts b/exercises/02.authentication/03.solution.passkeys/react-router.config.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/react-router.config.ts
rename to exercises/02.authentication/03.solution.passkeys/react-router.config.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/server/dev-server.js b/exercises/02.authentication/03.solution.passkeys/server/dev-server.js
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/server/dev-server.js
rename to exercises/02.authentication/03.solution.passkeys/server/dev-server.js
diff --git a/exercises/02.test-setup/05.solution.test-data/server/index.ts b/exercises/02.authentication/03.solution.passkeys/server/index.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/server/index.ts
rename to exercises/02.authentication/03.solution.passkeys/server/index.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/server/utils/monitoring.ts b/exercises/02.authentication/03.solution.passkeys/server/utils/monitoring.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/server/utils/monitoring.ts
rename to exercises/02.authentication/03.solution.passkeys/server/utils/monitoring.ts
diff --git a/exercises/02.authentication/03.solution.passkeys/tests/db-utils.ts b/exercises/02.authentication/03.solution.passkeys/tests/db-utils.ts
new file mode 100644
index 0000000..22980ca
--- /dev/null
+++ b/exercises/02.authentication/03.solution.passkeys/tests/db-utils.ts
@@ -0,0 +1,156 @@
+import { faker } from '@faker-js/faker'
+import bcrypt from 'bcryptjs'
+import { UniqueEnforcer } from 'enforce-unique'
+import { getPasswordHash } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+
+const uniqueUsernameEnforcer = new UniqueEnforcer()
+
+export function generateUserInfo() {
+ const firstName = faker.person.firstName()
+ const lastName = faker.person.lastName()
+
+ const username = uniqueUsernameEnforcer
+ .enforce(() => {
+ return (
+ faker.string.alphanumeric({ length: 2 }) +
+ '_' +
+ faker.internet.username({
+ firstName: firstName.toLowerCase(),
+ lastName: lastName.toLowerCase(),
+ })
+ )
+ })
+ .slice(0, 20)
+ .toLowerCase()
+ .replace(/[^a-z0-9_]/g, '_')
+
+ return {
+ username,
+ name: `${firstName} ${lastName}`,
+ email: `${username}@example.com`,
+ }
+}
+
+export async function createUser() {
+ const userInfo = generateUserInfo()
+ const password = 'supersecret'
+ const user = await prisma.user.create({
+ data: {
+ ...userInfo,
+ password: { create: { hash: await getPasswordHash(password) } },
+ },
+ })
+
+ return {
+ async [Symbol.asyncDispose]() {
+ await prisma.user.deleteMany({
+ where: { id: user.id },
+ })
+ },
+ ...user,
+ password,
+ }
+}
+
+export async function createPasskey(input: {
+ id: string
+ userId: string
+ aaguid: string
+ publicKey: Uint8Array
+ counter?: number
+}) {
+ const passkey = await prisma.passkey.create({
+ data: {
+ id: input.id,
+ aaguid: input.aaguid,
+ userId: input.userId,
+ publicKey: input.publicKey,
+ backedUp: false,
+ webauthnUserId: input.userId,
+ deviceType: 'singleDevice',
+ counter: input.counter || 0,
+ },
+ })
+
+ return {
+ async [Symbol.asyncDispose]() {
+ await prisma.passkey.deleteMany({
+ where: {
+ id: passkey.id,
+ },
+ })
+ },
+ ...passkey,
+ }
+}
+
+export function createPassword(password: string = faker.internet.password()) {
+ return {
+ hash: bcrypt.hashSync(password, 10),
+ }
+}
+
+let noteImages: Array<{ altText: string; objectKey: string }> | undefined
+export async function getNoteImages() {
+ if (noteImages) return noteImages
+
+ noteImages = await Promise.all([
+ {
+ altText: 'a nice country house',
+ objectKey: 'notes/0.png',
+ },
+ {
+ altText: 'a city scape',
+ objectKey: 'notes/1.png',
+ },
+ {
+ altText: 'a sunrise',
+ objectKey: 'notes/2.png',
+ },
+ {
+ altText: 'a group of friends',
+ objectKey: 'notes/3.png',
+ },
+ {
+ altText: 'friends being inclusive of someone who looks lonely',
+ objectKey: 'notes/4.png',
+ },
+ {
+ altText: 'an illustration of a hot air balloon',
+ objectKey: 'notes/5.png',
+ },
+ {
+ altText:
+ 'an office full of laptops and other office equipment that look like it was abandoned in a rush out of the building in an emergency years ago.',
+ objectKey: 'notes/6.png',
+ },
+ {
+ altText: 'a rusty lock',
+ objectKey: 'notes/7.png',
+ },
+ {
+ altText: 'something very happy in nature',
+ objectKey: 'notes/8.png',
+ },
+ {
+ altText: `someone at the end of a cry session who's starting to feel a little better.`,
+ objectKey: 'notes/9.png',
+ },
+ ])
+
+ return noteImages
+}
+
+let userImages: Array<{ objectKey: string }> | undefined
+export async function getUserImages() {
+ if (userImages) return userImages
+
+ userImages = await Promise.all(
+ Array.from({ length: 10 }, (_, index) => ({
+ objectKey: `user/${index}.jpg`,
+ })),
+ )
+
+ return userImages
+}
diff --git a/exercises/02.authentication/03.solution.passkeys/tests/e2e/authentication-passkeys.test.ts b/exercises/02.authentication/03.solution.passkeys/tests/e2e/authentication-passkeys.test.ts
new file mode 100644
index 0000000..d9ff8ca
--- /dev/null
+++ b/exercises/02.authentication/03.solution.passkeys/tests/e2e/authentication-passkeys.test.ts
@@ -0,0 +1,83 @@
+import { type Page } from '@playwright/test'
+import { createTestPasskey } from 'test-passkey'
+import { createPasskey, createUser } from '#tests/db-utils.ts'
+import { test, expect } from '#tests/test-extend.ts'
+
+async function createWebAuthnClient(page: Page) {
+ const client = await page.context().newCDPSession(page)
+ await client.send('WebAuthn.enable')
+
+ const result = await client.send('WebAuthn.addVirtualAuthenticator', {
+ options: {
+ protocol: 'ctap2',
+ transport: 'internal',
+ hasResidentKey: true,
+ hasUserVerification: true,
+ isUserVerified: true,
+ // Authenticator will automatically respond to the next prompt in the browser.
+ automaticPresenceSimulation: true,
+ },
+ })
+
+ return {
+ client,
+ authenticatorId: result.authenticatorId,
+ }
+}
+
+test('authenticates using an existing passkey', async ({ navigate, page }) => {
+ await navigate('/login')
+
+ // Create a test passkey.
+ const passkey = createTestPasskey({
+ rpId: new URL(page.url()).hostname,
+ })
+
+ // Add the passkey to the server.
+ await using user = await createUser()
+ await using _ = await createPasskey({
+ id: passkey.credential.credentialId,
+ aaguid: passkey.credential.aaguid || '',
+ publicKey: passkey.publicKey,
+ userId: user.id,
+ counter: passkey.credential.signCount,
+ })
+
+ // Add the passkey to the browser.
+ const { client, authenticatorId } = await createWebAuthnClient(page)
+ await client.send('WebAuthn.addCredential', {
+ authenticatorId,
+ credential: {
+ ...passkey.credential,
+ isResidentCredential: true,
+ userName: user.username,
+ userHandle: btoa(user.id),
+ userDisplayName: user.name ?? user.email,
+ },
+ })
+
+ await page.getByRole('button', { name: 'Login with a passkey' }).click()
+
+ await expect(page.getByText(user.name!)).toBeVisible()
+})
+
+test('displays an error when authenticating via a passkey fails', async ({
+ navigate,
+ page,
+}) => {
+ await navigate('/login')
+
+ const { client, authenticatorId } = await createWebAuthnClient(page)
+ await client.send('WebAuthn.setUserVerified', {
+ authenticatorId,
+ isUserVerified: false,
+ })
+
+ await page.getByRole('button', { name: 'Login with a passkey' }).click()
+
+ await expect(
+ page.getByText(
+ 'Failed to authenticate with passkey: The operation either timed out or was not allowed',
+ ),
+ ).toBeVisible()
+})
diff --git a/exercises/02.test-setup/05.solution.test-data/tests/setup/custom-matchers.ts b/exercises/02.authentication/03.solution.passkeys/tests/setup/custom-matchers.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/tests/setup/custom-matchers.ts
rename to exercises/02.authentication/03.solution.passkeys/tests/setup/custom-matchers.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/tests/setup/db-setup.ts b/exercises/02.authentication/03.solution.passkeys/tests/setup/db-setup.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/tests/setup/db-setup.ts
rename to exercises/02.authentication/03.solution.passkeys/tests/setup/db-setup.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/tests/setup/global-setup.ts b/exercises/02.authentication/03.solution.passkeys/tests/setup/global-setup.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/tests/setup/global-setup.ts
rename to exercises/02.authentication/03.solution.passkeys/tests/setup/global-setup.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/tests/setup/setup-test-env.ts b/exercises/02.authentication/03.solution.passkeys/tests/setup/setup-test-env.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/tests/setup/setup-test-env.ts
rename to exercises/02.authentication/03.solution.passkeys/tests/setup/setup-test-env.ts
diff --git a/exercises/02.authentication/03.solution.passkeys/tests/test-extend.ts b/exercises/02.authentication/03.solution.passkeys/tests/test-extend.ts
new file mode 100644
index 0000000..a51b50d
--- /dev/null
+++ b/exercises/02.authentication/03.solution.passkeys/tests/test-extend.ts
@@ -0,0 +1,57 @@
+import { test as testBase, expect } from '@playwright/test'
+import {
+ definePersona,
+ combinePersonas,
+ type AuthenticateFunction,
+} from 'playwright-persona'
+import { href, type Register } from 'react-router'
+import { getPasswordHash } from '#app/utils/auth.server.ts'
+import { prisma } from '#app/utils/db.server.ts'
+import { generateUserInfo } from '#tests/db-utils'
+
+interface Fixtures {
+ navigate: (
+ ...args: Parameters>
+ ) => Promise
+ authenticate: AuthenticateFunction<[typeof user]>
+}
+
+const user = definePersona('user', {
+ async createSession({ page }) {
+ const user = await prisma.user.create({
+ data: {
+ ...generateUserInfo(),
+ roles: { connect: { name: 'user' } },
+ password: { create: { hash: await getPasswordHash('supersecret') } },
+ },
+ })
+
+ await page.goto('/login')
+ await page.getByLabel('Username').fill(user.username)
+ await page.getByLabel('Password').fill('supersecret')
+ await page.getByRole('button', { name: 'Log in' }).click()
+ await page.getByText(user.name!).waitFor({ state: 'visible' })
+
+ return { user }
+ },
+ async verifySession({ page, session }) {
+ await page.goto('/')
+ await expect(page.getByText(session.user.name!)).toBeVisible({
+ timeout: 100,
+ })
+ },
+ async destroySession({ session }) {
+ await prisma.user.deleteMany({ where: { id: session.user.id } })
+ },
+})
+
+export const test = testBase.extend({
+ async navigate({ page }, use) {
+ await use(async (...args) => {
+ await page.goto(href(...args))
+ })
+ },
+ authenticate: combinePersonas(user),
+})
+
+export { expect }
diff --git a/exercises/02.test-setup/05.solution.test-data/tests/utils.ts b/exercises/02.authentication/03.solution.passkeys/tests/utils.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/tests/utils.ts
rename to exercises/02.authentication/03.solution.passkeys/tests/utils.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/tsconfig.json b/exercises/02.authentication/03.solution.passkeys/tsconfig.json
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/tsconfig.json
rename to exercises/02.authentication/03.solution.passkeys/tsconfig.json
diff --git a/exercises/02.test-setup/05.solution.test-data/types/deps.d.ts b/exercises/02.authentication/03.solution.passkeys/types/deps.d.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/types/deps.d.ts
rename to exercises/02.authentication/03.solution.passkeys/types/deps.d.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/types/env.env.d.ts b/exercises/02.authentication/03.solution.passkeys/types/env.env.d.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/types/env.env.d.ts
rename to exercises/02.authentication/03.solution.passkeys/types/env.env.d.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/types/icon-name.d.ts b/exercises/02.authentication/03.solution.passkeys/types/icon-name.d.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/types/icon-name.d.ts
rename to exercises/02.authentication/03.solution.passkeys/types/icon-name.d.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/types/reset.d.ts b/exercises/02.authentication/03.solution.passkeys/types/reset.d.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/types/reset.d.ts
rename to exercises/02.authentication/03.solution.passkeys/types/reset.d.ts
diff --git a/exercises/02.test-setup/05.solution.test-data/vite.config.ts b/exercises/02.authentication/03.solution.passkeys/vite.config.ts
similarity index 100%
rename from exercises/02.test-setup/05.solution.test-data/vite.config.ts
rename to exercises/02.authentication/03.solution.passkeys/vite.config.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/.cursor/rules/avoid-use-effect.mdc b/exercises/02.authentication/04.problem.protected-logic/.cursor/rules/avoid-use-effect.mdc
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/.cursor/rules/avoid-use-effect.mdc
rename to exercises/02.authentication/04.problem.protected-logic/.cursor/rules/avoid-use-effect.mdc
diff --git a/exercises/03.guides/01.problem.recording-interactions/.env b/exercises/02.authentication/04.problem.protected-logic/.env
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/.env
rename to exercises/02.authentication/04.problem.protected-logic/.env
diff --git a/exercises/03.guides/01.problem.recording-interactions/.env.example b/exercises/02.authentication/04.problem.protected-logic/.env.example
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/.env.example
rename to exercises/02.authentication/04.problem.protected-logic/.env.example
diff --git a/exercises/03.guides/01.problem.recording-interactions/.gitignore b/exercises/02.authentication/04.problem.protected-logic/.gitignore
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/.gitignore
rename to exercises/02.authentication/04.problem.protected-logic/.gitignore
diff --git a/exercises/03.guides/01.problem.recording-interactions/.npmrc b/exercises/02.authentication/04.problem.protected-logic/.npmrc
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/.npmrc
rename to exercises/02.authentication/04.problem.protected-logic/.npmrc
diff --git a/exercises/03.guides/01.problem.recording-interactions/.prettierignore b/exercises/02.authentication/04.problem.protected-logic/.prettierignore
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/.prettierignore
rename to exercises/02.authentication/04.problem.protected-logic/.prettierignore
diff --git a/exercises/03.guides/02.problem.test-annotations/.vscode/extensions.json b/exercises/02.authentication/04.problem.protected-logic/.vscode/extensions.json
similarity index 75%
rename from exercises/03.guides/02.problem.test-annotations/.vscode/extensions.json
rename to exercises/02.authentication/04.problem.protected-logic/.vscode/extensions.json
index f724eea..3c0a690 100644
--- a/exercises/03.guides/02.problem.test-annotations/.vscode/extensions.json
+++ b/exercises/02.authentication/04.problem.protected-logic/.vscode/extensions.json
@@ -6,7 +6,6 @@
"prisma.prisma",
"qwtel.sqlite-viewer",
"yoavbls.pretty-ts-errors",
- "github.vscode-github-actions",
- "ms-playwright.playwright"
+ "github.vscode-github-actions"
]
}
diff --git a/exercises/03.guides/01.problem.recording-interactions/.vscode/remix.code-snippets b/exercises/02.authentication/04.problem.protected-logic/.vscode/remix.code-snippets
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/.vscode/remix.code-snippets
rename to exercises/02.authentication/04.problem.protected-logic/.vscode/remix.code-snippets
diff --git a/exercises/03.guides/01.problem.recording-interactions/.vscode/settings.json b/exercises/02.authentication/04.problem.protected-logic/.vscode/settings.json
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/.vscode/settings.json
rename to exercises/02.authentication/04.problem.protected-logic/.vscode/settings.json
diff --git a/exercises/02.test-setup/03.problem.authentication/README.mdx b/exercises/02.authentication/04.problem.protected-logic/README.mdx
similarity index 90%
rename from exercises/02.test-setup/03.problem.authentication/README.mdx
rename to exercises/02.authentication/04.problem.protected-logic/README.mdx
index d28b90b..ec549ef 100644
--- a/exercises/02.test-setup/03.problem.authentication/README.mdx
+++ b/exercises/02.authentication/04.problem.protected-logic/README.mdx
@@ -1,4 +1,6 @@
-# Authentication
+# Protected logic
+
+- Testing the logic behind authentication (e.g. user creating notes).
## Your task
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/assets/favicons/apple-touch-icon.png b/exercises/02.authentication/04.problem.protected-logic/app/assets/favicons/apple-touch-icon.png
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/assets/favicons/apple-touch-icon.png
rename to exercises/02.authentication/04.problem.protected-logic/app/assets/favicons/apple-touch-icon.png
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/assets/favicons/favicon.svg b/exercises/02.authentication/04.problem.protected-logic/app/assets/favicons/favicon.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/assets/favicons/favicon.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/assets/favicons/favicon.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/error-boundary.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/error-boundary.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/error-boundary.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/error-boundary.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/floating-toolbar.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/floating-toolbar.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/floating-toolbar.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/floating-toolbar.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/forms.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/forms.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/forms.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/forms.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/progress-bar.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/progress-bar.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/progress-bar.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/progress-bar.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/search-bar.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/search-bar.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/search-bar.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/search-bar.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/spacer.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/spacer.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/spacer.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/spacer.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/toaster.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/toaster.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/toaster.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/toaster.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/ui/README.md b/exercises/02.authentication/04.problem.protected-logic/app/components/ui/README.md
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/ui/README.md
rename to exercises/02.authentication/04.problem.protected-logic/app/components/ui/README.md
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/ui/button.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/ui/button.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/ui/button.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/ui/button.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/ui/checkbox.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/ui/checkbox.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/ui/checkbox.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/ui/checkbox.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/ui/dropdown-menu.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/ui/dropdown-menu.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/ui/dropdown-menu.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/ui/dropdown-menu.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/ui/icon.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/ui/icon.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/ui/icon.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/ui/icon.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/ui/input-otp.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/ui/input-otp.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/ui/input-otp.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/ui/input-otp.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/ui/input.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/ui/input.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/ui/input.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/ui/input.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/ui/label.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/ui/label.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/ui/label.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/ui/label.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/ui/sonner.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/ui/sonner.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/ui/sonner.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/ui/sonner.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/ui/status-button.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/ui/status-button.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/ui/status-button.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/ui/status-button.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/ui/textarea.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/ui/textarea.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/ui/textarea.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/ui/textarea.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/ui/tooltip.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/ui/tooltip.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/ui/tooltip.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/ui/tooltip.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/components/user-dropdown.tsx b/exercises/02.authentication/04.problem.protected-logic/app/components/user-dropdown.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/components/user-dropdown.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/components/user-dropdown.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/entry.client.tsx b/exercises/02.authentication/04.problem.protected-logic/app/entry.client.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/entry.client.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/entry.client.tsx
diff --git a/exercises/02.authentication/04.problem.protected-logic/app/entry.server.tsx b/exercises/02.authentication/04.problem.protected-logic/app/entry.server.tsx
new file mode 100644
index 0000000..8d8b1de
--- /dev/null
+++ b/exercises/02.authentication/04.problem.protected-logic/app/entry.server.tsx
@@ -0,0 +1,143 @@
+import crypto from 'node:crypto'
+import { PassThrough } from 'node:stream'
+import { styleText } from 'node:util'
+import { contentSecurity } from '@nichtsam/helmet/content'
+import { createReadableStreamFromReadable } from '@react-router/node'
+import * as Sentry from '@sentry/react-router'
+import { isbot } from 'isbot'
+import { renderToPipeableStream } from 'react-dom/server'
+import {
+ ServerRouter,
+ type LoaderFunctionArgs,
+ type ActionFunctionArgs,
+ type HandleDocumentRequestFunction,
+} from 'react-router'
+import { getEnv, init } from './utils/env.server.ts'
+import { getInstanceInfo } from './utils/litefs.server.ts'
+import { NonceProvider } from './utils/nonce-provider.ts'
+import { makeTimings } from './utils/timing.server.ts'
+
+export const streamTimeout = 5000
+
+init()
+global.ENV = getEnv()
+
+const MODE = process.env.NODE_ENV ?? 'development'
+
+type DocRequestArgs = Parameters
+
+export default async function handleRequest(...args: DocRequestArgs) {
+ const [request, responseStatusCode, responseHeaders, reactRouterContext] =
+ args
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ responseHeaders.set('fly-primary-instance', primaryInstance)
+ responseHeaders.set('fly-instance', currentInstance)
+
+ if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
+ responseHeaders.append('Document-Policy', 'js-profiling')
+ }
+
+ const callbackName = isbot(request.headers.get('user-agent'))
+ ? 'onAllReady'
+ : 'onShellReady'
+
+ const nonce = crypto.randomBytes(16).toString('hex')
+ return new Promise(async (resolve, reject) => {
+ let didError = false
+ // NOTE: this timing will only include things that are rendered in the shell
+ // and will not include suspended components and deferred loaders
+ const timings = makeTimings('render', 'renderToPipeableStream')
+
+ const { pipe, abort } = renderToPipeableStream(
+
+
+ ,
+ {
+ [callbackName]: () => {
+ const body = new PassThrough()
+ responseHeaders.set('Content-Type', 'text/html')
+ responseHeaders.append('Server-Timing', timings.toString())
+
+ contentSecurity(responseHeaders, {
+ crossOriginEmbedderPolicy: false,
+ contentSecurityPolicy: {
+ // NOTE: Remove reportOnly when you're ready to enforce this CSP
+ reportOnly: true,
+ directives: {
+ fetch: {
+ 'connect-src': [
+ MODE === 'development' ? 'ws:' : undefined,
+ process.env.SENTRY_DSN ? '*.sentry.io' : undefined,
+ "'self'",
+ ],
+ 'font-src': ["'self'"],
+ 'frame-src': ["'self'"],
+ 'img-src': ["'self'", 'data:'],
+ 'script-src': [
+ "'strict-dynamic'",
+ "'self'",
+ `'nonce-${nonce}'`,
+ ],
+ 'script-src-attr': [`'nonce-${nonce}'`],
+ },
+ },
+ },
+ xFrameOptions: false,
+ })
+
+ resolve(
+ new Response(createReadableStreamFromReadable(body), {
+ headers: responseHeaders,
+ status: didError ? 500 : responseStatusCode,
+ }),
+ )
+ pipe(body)
+ },
+ onShellError: (err: unknown) => {
+ reject(err)
+ },
+ onError: () => {
+ didError = true
+ },
+ nonce,
+ },
+ )
+
+ setTimeout(abort, streamTimeout + 5000)
+ })
+}
+
+export async function handleDataRequest(response: Response) {
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ response.headers.set('fly-primary-instance', primaryInstance)
+ response.headers.set('fly-instance', currentInstance)
+
+ return response
+}
+
+export function handleError(
+ error: unknown,
+ { request }: LoaderFunctionArgs | ActionFunctionArgs,
+): void {
+ // Skip capturing if the request is aborted as Remix docs suggest
+ // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
+ if (request.signal.aborted) {
+ return
+ }
+
+ if (error instanceof Error) {
+ console.error(styleText('red', String(error.stack)))
+ } else {
+ console.error(error)
+ }
+
+ Sentry.captureException(error)
+}
diff --git a/exercises/03.guides/02.problem.test-annotations/app/root.tsx b/exercises/02.authentication/04.problem.protected-logic/app/root.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/root.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/root.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes.ts b/exercises/02.authentication/04.problem.protected-logic/app/routes.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/routes.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/$.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/$.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/$.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/$.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/auth.$provider.callback.test.ts b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/auth.$provider.callback.test.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/_auth+/auth.$provider.callback.test.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/auth.$provider.callback.test.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/auth.$provider.callback.ts b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/auth.$provider.callback.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/auth.$provider.callback.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/auth.$provider.callback.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/auth_.$provider.ts b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/auth_.$provider.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/auth_.$provider.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/auth_.$provider.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/forgot-password.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/forgot-password.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/forgot-password.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/forgot-password.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/login.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/login.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/login.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/login.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/login.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/login.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/login.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/login.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/logout.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/logout.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/logout.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/logout.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/onboarding.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/onboarding.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/onboarding.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/onboarding.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/onboarding.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/onboarding.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/onboarding.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/onboarding.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/onboarding_.$provider.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/onboarding_.$provider.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/onboarding_.$provider.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/onboarding_.$provider.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/onboarding_.$provider.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/onboarding_.$provider.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/onboarding_.$provider.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/onboarding_.$provider.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/reset-password.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/reset-password.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/reset-password.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/reset-password.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/reset-password.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/reset-password.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/reset-password.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/reset-password.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/signup.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/signup.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/signup.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/signup.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/verify.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/verify.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/verify.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/verify.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/verify.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/verify.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/verify.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/verify.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/webauthn+/authentication.ts b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/webauthn+/authentication.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/webauthn+/authentication.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/webauthn+/authentication.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/webauthn+/registration.ts b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/webauthn+/registration.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/webauthn+/registration.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/webauthn+/registration.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/webauthn+/utils.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/webauthn+/utils.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_auth+/webauthn+/utils.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_auth+/webauthn+/utils.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/about.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/about.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/about.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/about.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/index.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/index.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/index.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/index.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/docker.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/docker.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/docker.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/docker.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/eslint.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/eslint.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/eslint.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/eslint.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/faker.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/faker.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/faker.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/faker.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/fly.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/fly.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/fly.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/fly.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/github.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/github.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/github.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/github.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/logos.ts b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/logos.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/logos.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/logos.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/msw.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/msw.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/msw.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/msw.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/playwright.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/playwright.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/playwright.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/playwright.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/prettier.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/prettier.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/prettier.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/prettier.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/prisma.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/prisma.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/prisma.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/prisma.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/radix.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/radix.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/radix.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/radix.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/react-email.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/react-email.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/react-email.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/react-email.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/remix.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/remix.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/remix.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/remix.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/resend.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/resend.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/resend.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/resend.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/sentry.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/sentry.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/sentry.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/sentry.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/shadcn-ui.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/shadcn-ui.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/shadcn-ui.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/shadcn-ui.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/sqlite.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/sqlite.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/sqlite.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/sqlite.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/stars.jpg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/stars.jpg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/stars.jpg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/stars.jpg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/tailwind.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/tailwind.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/tailwind.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/tailwind.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/testing-library.png b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/testing-library.png
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/testing-library.png
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/testing-library.png
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/typescript.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/typescript.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/typescript.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/typescript.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/vitest.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/vitest.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/vitest.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/vitest.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/zod.svg b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/zod.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/logos/zod.svg
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/logos/zod.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/privacy.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/privacy.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/privacy.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/privacy.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/support.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/support.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/support.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/support.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/tos.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/tos.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_marketing+/tos.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_marketing+/tos.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_seo+/robots[.]txt.ts b/exercises/02.authentication/04.problem.protected-logic/app/routes/_seo+/robots[.]txt.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_seo+/robots[.]txt.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_seo+/robots[.]txt.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/_seo+/sitemap[.]xml.ts b/exercises/02.authentication/04.problem.protected-logic/app/routes/_seo+/sitemap[.]xml.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/_seo+/sitemap[.]xml.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/_seo+/sitemap[.]xml.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/admin+/cache.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/admin+/cache.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/admin+/cache.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/admin+/cache.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/admin+/cache_.lru.$cacheKey.ts b/exercises/02.authentication/04.problem.protected-logic/app/routes/admin+/cache_.lru.$cacheKey.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/admin+/cache_.lru.$cacheKey.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/admin+/cache_.lru.$cacheKey.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/admin+/cache_.sqlite.$cacheKey.ts b/exercises/02.authentication/04.problem.protected-logic/app/routes/admin+/cache_.sqlite.$cacheKey.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/admin+/cache_.sqlite.$cacheKey.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/admin+/cache_.sqlite.$cacheKey.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/admin+/cache_.sqlite.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/routes/admin+/cache_.sqlite.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/admin+/cache_.sqlite.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/admin+/cache_.sqlite.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/admin+/cache_.sqlite.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/admin+/cache_.sqlite.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/admin+/cache_.sqlite.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/admin+/cache_.sqlite.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/me.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/me.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/me.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/me.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/resources+/download-user-data.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/resources+/download-user-data.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/resources+/download-user-data.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/resources+/download-user-data.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/resources+/healthcheck.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/resources+/healthcheck.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/resources+/healthcheck.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/resources+/healthcheck.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/resources+/images.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/resources+/images.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/resources+/images.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/resources+/images.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/resources+/theme-switch.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/resources+/theme-switch.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/resources+/theme-switch.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/resources+/theme-switch.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.change-email.server.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.change-email.server.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.change-email.server.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.change-email.server.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.change-email.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.change-email.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.change-email.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.change-email.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.connections.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.connections.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.connections.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.connections.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/routes/settings+/profile.index.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.index.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/routes/settings+/profile.index.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.index.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.passkeys.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.passkeys.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.passkeys.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.passkeys.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.password.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.password.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.password.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.password.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.password_.create.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.password_.create.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.password_.create.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.password_.create.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.photo.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.photo.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.photo.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.photo.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.two-factor.disable.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.two-factor.disable.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.two-factor.disable.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.two-factor.disable.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.two-factor.index.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.two-factor.index.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.two-factor.index.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.two-factor.index.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.two-factor.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.two-factor.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.two-factor.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.two-factor.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.two-factor.verify.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.two-factor.verify.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/settings+/profile.two-factor.verify.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/settings+/profile.two-factor.verify.tsx
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username.test.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username.test.tsx
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/app/routes/users+/$username.test.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username.test.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/users+/$username.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/users+/$username.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/users+/$username_+/__note-editor.server.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username_+/__note-editor.server.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/users+/$username_+/__note-editor.server.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username_+/__note-editor.server.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/routes/users+/$username_+/__note-editor.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username_+/__note-editor.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/routes/users+/$username_+/__note-editor.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username_+/__note-editor.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/users+/$username_+/notes.$noteId.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username_+/notes.$noteId.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/users+/$username_+/notes.$noteId.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username_+/notes.$noteId.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/users+/$username_+/notes.$noteId_.edit.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/users+/$username_+/notes.index.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username_+/notes.index.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/users+/$username_+/notes.index.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username_+/notes.index.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/users+/$username_+/notes.new.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username_+/notes.new.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/users+/$username_+/notes.new.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username_+/notes.new.tsx
diff --git a/exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username_+/notes.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username_+/notes.tsx
new file mode 100644
index 0000000..ded41ca
--- /dev/null
+++ b/exercises/02.authentication/04.problem.protected-logic/app/routes/users+/$username_+/notes.tsx
@@ -0,0 +1,105 @@
+import { invariantResponse } from '@epic-web/invariant'
+import { Img } from 'openimg/react'
+import { Link, NavLink, Outlet } from 'react-router'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { cn, getUserImgSrc } from '#app/utils/misc.tsx'
+import { useOptionalUser } from '#app/utils/user.ts'
+import { type Route } from './+types/notes.ts'
+
+export async function loader({ params }: Route.LoaderArgs) {
+ const owner = await prisma.user.findFirst({
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ image: { select: { objectKey: true } },
+ notes: { select: { id: true, title: true } },
+ },
+ where: { username: params.username },
+ })
+
+ invariantResponse(owner, 'Owner not found', { status: 404 })
+
+ return { owner }
+}
+
+export default function NotesRoute({ loaderData }: Route.ComponentProps) {
+ const user = useOptionalUser()
+ const isOwner = user?.id === loaderData.owner.id
+ const ownerDisplayName = loaderData.owner.name ?? loaderData.owner.username
+ const navLinkDefaultClassName =
+ 'line-clamp-2 block rounded-l-full py-2 pl-8 pr-6 text-base lg:text-xl'
+ return (
+
+
+
+
+
+
+
+ {ownerDisplayName}'s Notes
+
+
+
+ {isOwner ? (
+
+
+ cn(navLinkDefaultClassName, isActive && 'bg-accent')
+ }
+ >
+ New Note
+
+
+ ) : null}
+ {loaderData.owner.notes.map((note) => (
+
+
+ cn(navLinkDefaultClassName, isActive && 'bg-accent')
+ }
+ >
+ {note.title}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return (
+ (
+ No user with the username "{params.username}" exists
+ ),
+ }}
+ />
+ )
+}
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/routes/users+/index.tsx b/exercises/02.authentication/04.problem.protected-logic/app/routes/users+/index.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/routes/users+/index.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/routes/users+/index.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/styles/tailwind.css b/exercises/02.authentication/04.problem.protected-logic/app/styles/tailwind.css
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/styles/tailwind.css
rename to exercises/02.authentication/04.problem.protected-logic/app/styles/tailwind.css
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/auth.server.test.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/auth.server.test.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/auth.server.test.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/auth.server.test.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/auth.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/auth.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/auth.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/auth.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/cache.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/cache.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/cache.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/cache.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/client-hints.tsx b/exercises/02.authentication/04.problem.protected-logic/app/utils/client-hints.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/client-hints.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/client-hints.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/connections.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/connections.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/connections.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/connections.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/connections.tsx b/exercises/02.authentication/04.problem.protected-logic/app/utils/connections.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/connections.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/connections.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/db.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/db.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/db.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/db.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/email.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/email.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/email.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/email.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/env.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/env.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/env.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/env.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/headers.server.test.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/headers.server.test.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/headers.server.test.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/headers.server.test.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/headers.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/headers.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/headers.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/headers.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/honeypot.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/honeypot.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/honeypot.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/honeypot.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/litefs.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/litefs.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/litefs.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/litefs.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/misc.error-message.test.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/misc.error-message.test.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/misc.error-message.test.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/misc.error-message.test.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/misc.tsx b/exercises/02.authentication/04.problem.protected-logic/app/utils/misc.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/misc.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/misc.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/misc.use-double-check.test.tsx b/exercises/02.authentication/04.problem.protected-logic/app/utils/misc.use-double-check.test.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/misc.use-double-check.test.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/misc.use-double-check.test.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/monitoring.client.tsx b/exercises/02.authentication/04.problem.protected-logic/app/utils/monitoring.client.tsx
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/monitoring.client.tsx
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/monitoring.client.tsx
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/nonce-provider.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/nonce-provider.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/nonce-provider.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/nonce-provider.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/permissions.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/permissions.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/permissions.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/permissions.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/providers/constants.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/providers/constants.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/providers/constants.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/providers/constants.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/providers/github.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/providers/github.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/providers/github.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/providers/github.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/providers/provider.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/providers/provider.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/providers/provider.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/providers/provider.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/redirect-cookie.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/redirect-cookie.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/redirect-cookie.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/redirect-cookie.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/request-info.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/request-info.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/request-info.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/request-info.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/session.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/session.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/session.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/session.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/storage.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/storage.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/storage.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/storage.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/theme.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/theme.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/theme.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/theme.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/timing.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/timing.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/timing.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/timing.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/toast.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/toast.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/toast.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/toast.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/totp.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/totp.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/totp.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/totp.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/user-validation.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/user-validation.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/user-validation.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/user-validation.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/user.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/user.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/user.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/user.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/app/utils/verification.server.ts b/exercises/02.authentication/04.problem.protected-logic/app/utils/verification.server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/app/utils/verification.server.ts
rename to exercises/02.authentication/04.problem.protected-logic/app/utils/verification.server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/components.json b/exercises/02.authentication/04.problem.protected-logic/components.json
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/components.json
rename to exercises/02.authentication/04.problem.protected-logic/components.json
diff --git a/exercises/03.guides/01.problem.recording-interactions/eslint.config.js b/exercises/02.authentication/04.problem.protected-logic/eslint.config.js
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/eslint.config.js
rename to exercises/02.authentication/04.problem.protected-logic/eslint.config.js
diff --git a/exercises/03.guides/01.problem.recording-interactions/fly.toml b/exercises/02.authentication/04.problem.protected-logic/fly.toml
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/fly.toml
rename to exercises/02.authentication/04.problem.protected-logic/fly.toml
diff --git a/exercises/03.guides/01.problem.recording-interactions/index.js b/exercises/02.authentication/04.problem.protected-logic/index.js
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/index.js
rename to exercises/02.authentication/04.problem.protected-logic/index.js
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/Dockerfile b/exercises/02.authentication/04.problem.protected-logic/other/Dockerfile
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/Dockerfile
rename to exercises/02.authentication/04.problem.protected-logic/other/Dockerfile
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/Dockerfile.dockerignore b/exercises/02.authentication/04.problem.protected-logic/other/Dockerfile.dockerignore
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/Dockerfile.dockerignore
rename to exercises/02.authentication/04.problem.protected-logic/other/Dockerfile.dockerignore
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/README.md b/exercises/02.authentication/04.problem.protected-logic/other/README.md
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/README.md
rename to exercises/02.authentication/04.problem.protected-logic/other/README.md
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/build-server.ts b/exercises/02.authentication/04.problem.protected-logic/other/build-server.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/build-server.ts
rename to exercises/02.authentication/04.problem.protected-logic/other/build-server.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/litefs.yml b/exercises/02.authentication/04.problem.protected-logic/other/litefs.yml
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/litefs.yml
rename to exercises/02.authentication/04.problem.protected-logic/other/litefs.yml
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/sly/sly.json b/exercises/02.authentication/04.problem.protected-logic/other/sly/sly.json
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/sly/sly.json
rename to exercises/02.authentication/04.problem.protected-logic/other/sly/sly.json
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/sly/transform-icon.ts b/exercises/02.authentication/04.problem.protected-logic/other/sly/transform-icon.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/sly/transform-icon.ts
rename to exercises/02.authentication/04.problem.protected-logic/other/sly/transform-icon.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/README.md b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/README.md
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/README.md
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/README.md
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/arrow-left.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/arrow-left.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/arrow-left.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/arrow-left.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/arrow-right.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/arrow-right.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/arrow-right.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/arrow-right.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/avatar.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/avatar.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/avatar.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/avatar.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/camera.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/camera.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/camera.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/camera.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/check.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/check.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/check.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/check.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/clock.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/clock.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/clock.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/clock.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/cross-1.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/cross-1.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/cross-1.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/cross-1.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/dots-horizontal.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/dots-horizontal.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/dots-horizontal.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/dots-horizontal.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/download.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/download.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/download.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/download.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/envelope-closed.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/envelope-closed.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/envelope-closed.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/envelope-closed.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/exit.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/exit.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/exit.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/exit.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/file-text.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/file-text.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/file-text.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/file-text.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/github-logo.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/github-logo.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/github-logo.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/github-logo.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/laptop.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/laptop.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/laptop.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/laptop.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/link-2.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/link-2.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/link-2.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/link-2.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/lock-closed.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/lock-closed.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/lock-closed.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/lock-closed.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/lock-open-1.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/lock-open-1.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/lock-open-1.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/lock-open-1.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/magnifying-glass.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/magnifying-glass.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/magnifying-glass.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/magnifying-glass.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/moon.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/moon.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/moon.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/moon.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/passkey.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/passkey.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/passkey.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/passkey.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/pencil-1.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/pencil-1.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/pencil-1.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/pencil-1.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/pencil-2.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/pencil-2.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/pencil-2.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/pencil-2.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/plus.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/plus.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/plus.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/plus.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/question-mark-circled.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/question-mark-circled.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/question-mark-circled.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/question-mark-circled.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/reset.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/reset.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/reset.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/reset.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/sun.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/sun.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/sun.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/sun.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/trash.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/trash.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/trash.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/trash.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/other/svg-icons/update.svg b/exercises/02.authentication/04.problem.protected-logic/other/svg-icons/update.svg
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/other/svg-icons/update.svg
rename to exercises/02.authentication/04.problem.protected-logic/other/svg-icons/update.svg
diff --git a/exercises/03.guides/01.problem.recording-interactions/package-lock.json b/exercises/02.authentication/04.problem.protected-logic/package-lock.json
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/package-lock.json
rename to exercises/02.authentication/04.problem.protected-logic/package-lock.json
diff --git a/exercises/02.test-setup/03.solution.authentication/package.json b/exercises/02.authentication/04.problem.protected-logic/package.json
similarity index 98%
rename from exercises/02.test-setup/03.solution.authentication/package.json
rename to exercises/02.authentication/04.problem.protected-logic/package.json
index a9eed9a..3f68d89 100644
--- a/exercises/02.test-setup/03.solution.authentication/package.json
+++ b/exercises/02.authentication/04.problem.protected-logic/package.json
@@ -1,5 +1,5 @@
{
- "name": "exercises_02.test-setup_03.solution.authentication",
+ "name": "exercises_02.authentication_04.problem.protected-logic",
"private": true,
"sideEffects": false,
"type": "module",
@@ -115,7 +115,7 @@
"devDependencies": {
"@epic-web/config": "^1.20.1",
"@faker-js/faker": "^9.7.0",
- "@playwright/test": "^1.52.0",
+ "@playwright/test": "^1.57.0",
"@react-router/dev": "^7.5.3",
"@sly-cli/sly": "^2.1.1",
"@testing-library/dom": "^10.4.0",
diff --git a/exercises/03.guides/01.problem.recording-interactions/playwright.config.ts b/exercises/02.authentication/04.problem.protected-logic/playwright.config.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/playwright.config.ts
rename to exercises/02.authentication/04.problem.protected-logic/playwright.config.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/prisma/migrations/20250221233640_init/migration.sql b/exercises/02.authentication/04.problem.protected-logic/prisma/migrations/20250221233640_init/migration.sql
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/prisma/migrations/20250221233640_init/migration.sql
rename to exercises/02.authentication/04.problem.protected-logic/prisma/migrations/20250221233640_init/migration.sql
diff --git a/exercises/03.guides/01.problem.recording-interactions/prisma/migrations/migration_lock.toml b/exercises/02.authentication/04.problem.protected-logic/prisma/migrations/migration_lock.toml
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/prisma/migrations/migration_lock.toml
rename to exercises/02.authentication/04.problem.protected-logic/prisma/migrations/migration_lock.toml
diff --git a/exercises/03.guides/01.problem.recording-interactions/prisma/schema.prisma b/exercises/02.authentication/04.problem.protected-logic/prisma/schema.prisma
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/prisma/schema.prisma
rename to exercises/02.authentication/04.problem.protected-logic/prisma/schema.prisma
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/prisma/seed.ts b/exercises/02.authentication/04.problem.protected-logic/prisma/seed.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/prisma/seed.ts
rename to exercises/02.authentication/04.problem.protected-logic/prisma/seed.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/prisma/sql/searchUsers.sql b/exercises/02.authentication/04.problem.protected-logic/prisma/sql/searchUsers.sql
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/prisma/sql/searchUsers.sql
rename to exercises/02.authentication/04.problem.protected-logic/prisma/sql/searchUsers.sql
diff --git a/exercises/03.guides/01.problem.recording-interactions/public/favicon.ico b/exercises/02.authentication/04.problem.protected-logic/public/favicon.ico
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/public/favicon.ico
rename to exercises/02.authentication/04.problem.protected-logic/public/favicon.ico
diff --git a/exercises/03.guides/01.problem.recording-interactions/public/favicons/README.md b/exercises/02.authentication/04.problem.protected-logic/public/favicons/README.md
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/public/favicons/README.md
rename to exercises/02.authentication/04.problem.protected-logic/public/favicons/README.md
diff --git a/exercises/03.guides/01.problem.recording-interactions/public/favicons/android-chrome-192x192.png b/exercises/02.authentication/04.problem.protected-logic/public/favicons/android-chrome-192x192.png
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/public/favicons/android-chrome-192x192.png
rename to exercises/02.authentication/04.problem.protected-logic/public/favicons/android-chrome-192x192.png
diff --git a/exercises/03.guides/01.problem.recording-interactions/public/favicons/android-chrome-512x512.png b/exercises/02.authentication/04.problem.protected-logic/public/favicons/android-chrome-512x512.png
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/public/favicons/android-chrome-512x512.png
rename to exercises/02.authentication/04.problem.protected-logic/public/favicons/android-chrome-512x512.png
diff --git a/exercises/03.guides/01.problem.recording-interactions/public/img/user.png b/exercises/02.authentication/04.problem.protected-logic/public/img/user.png
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/public/img/user.png
rename to exercises/02.authentication/04.problem.protected-logic/public/img/user.png
diff --git a/exercises/03.guides/01.problem.recording-interactions/public/site.webmanifest b/exercises/02.authentication/04.problem.protected-logic/public/site.webmanifest
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/public/site.webmanifest
rename to exercises/02.authentication/04.problem.protected-logic/public/site.webmanifest
diff --git a/exercises/03.guides/01.problem.recording-interactions/react-router.config.ts b/exercises/02.authentication/04.problem.protected-logic/react-router.config.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/react-router.config.ts
rename to exercises/02.authentication/04.problem.protected-logic/react-router.config.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/server/dev-server.js b/exercises/02.authentication/04.problem.protected-logic/server/dev-server.js
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/server/dev-server.js
rename to exercises/02.authentication/04.problem.protected-logic/server/dev-server.js
diff --git a/exercises/03.guides/01.problem.recording-interactions/server/index.ts b/exercises/02.authentication/04.problem.protected-logic/server/index.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/server/index.ts
rename to exercises/02.authentication/04.problem.protected-logic/server/index.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/server/utils/monitoring.ts b/exercises/02.authentication/04.problem.protected-logic/server/utils/monitoring.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/server/utils/monitoring.ts
rename to exercises/02.authentication/04.problem.protected-logic/server/utils/monitoring.ts
diff --git a/exercises/02.test-setup/01.solution.custom-fixtures/tests/db-utils.ts b/exercises/02.authentication/04.problem.protected-logic/tests/db-utils.ts
similarity index 100%
rename from exercises/02.test-setup/01.solution.custom-fixtures/tests/db-utils.ts
rename to exercises/02.authentication/04.problem.protected-logic/tests/db-utils.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/tests/e2e/notes-create.test.ts b/exercises/02.authentication/04.problem.protected-logic/tests/e2e/notes-create.test.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/tests/e2e/notes-create.test.ts
rename to exercises/02.authentication/04.problem.protected-logic/tests/e2e/notes-create.test.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/tests/setup/custom-matchers.ts b/exercises/02.authentication/04.problem.protected-logic/tests/setup/custom-matchers.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/tests/setup/custom-matchers.ts
rename to exercises/02.authentication/04.problem.protected-logic/tests/setup/custom-matchers.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/tests/setup/db-setup.ts b/exercises/02.authentication/04.problem.protected-logic/tests/setup/db-setup.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/tests/setup/db-setup.ts
rename to exercises/02.authentication/04.problem.protected-logic/tests/setup/db-setup.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/tests/setup/global-setup.ts b/exercises/02.authentication/04.problem.protected-logic/tests/setup/global-setup.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/tests/setup/global-setup.ts
rename to exercises/02.authentication/04.problem.protected-logic/tests/setup/global-setup.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/tests/setup/setup-test-env.ts b/exercises/02.authentication/04.problem.protected-logic/tests/setup/setup-test-env.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/tests/setup/setup-test-env.ts
rename to exercises/02.authentication/04.problem.protected-logic/tests/setup/setup-test-env.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/tests/test-extend.ts b/exercises/02.authentication/04.problem.protected-logic/tests/test-extend.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/tests/test-extend.ts
rename to exercises/02.authentication/04.problem.protected-logic/tests/test-extend.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/tests/utils.ts b/exercises/02.authentication/04.problem.protected-logic/tests/utils.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/tests/utils.ts
rename to exercises/02.authentication/04.problem.protected-logic/tests/utils.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/tsconfig.json b/exercises/02.authentication/04.problem.protected-logic/tsconfig.json
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/tsconfig.json
rename to exercises/02.authentication/04.problem.protected-logic/tsconfig.json
diff --git a/exercises/03.guides/01.problem.recording-interactions/types/deps.d.ts b/exercises/02.authentication/04.problem.protected-logic/types/deps.d.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/types/deps.d.ts
rename to exercises/02.authentication/04.problem.protected-logic/types/deps.d.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/types/env.env.d.ts b/exercises/02.authentication/04.problem.protected-logic/types/env.env.d.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/types/env.env.d.ts
rename to exercises/02.authentication/04.problem.protected-logic/types/env.env.d.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/types/icon-name.d.ts b/exercises/02.authentication/04.problem.protected-logic/types/icon-name.d.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/types/icon-name.d.ts
rename to exercises/02.authentication/04.problem.protected-logic/types/icon-name.d.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/types/reset.d.ts b/exercises/02.authentication/04.problem.protected-logic/types/reset.d.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/types/reset.d.ts
rename to exercises/02.authentication/04.problem.protected-logic/types/reset.d.ts
diff --git a/exercises/03.guides/01.problem.recording-interactions/vite.config.ts b/exercises/02.authentication/04.problem.protected-logic/vite.config.ts
similarity index 100%
rename from exercises/03.guides/01.problem.recording-interactions/vite.config.ts
rename to exercises/02.authentication/04.problem.protected-logic/vite.config.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/.env b/exercises/02.authentication/04.solution.protected-logic/.env
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/.env
rename to exercises/02.authentication/04.solution.protected-logic/.env
diff --git a/exercises/03.guides/01.solution.recording-interactions/.env.example b/exercises/02.authentication/04.solution.protected-logic/.env.example
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/.env.example
rename to exercises/02.authentication/04.solution.protected-logic/.env.example
diff --git a/exercises/03.guides/01.solution.recording-interactions/.gitignore b/exercises/02.authentication/04.solution.protected-logic/.gitignore
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/.gitignore
rename to exercises/02.authentication/04.solution.protected-logic/.gitignore
diff --git a/exercises/03.guides/01.solution.recording-interactions/.npmrc b/exercises/02.authentication/04.solution.protected-logic/.npmrc
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/.npmrc
rename to exercises/02.authentication/04.solution.protected-logic/.npmrc
diff --git a/exercises/03.guides/01.solution.recording-interactions/.prettierignore b/exercises/02.authentication/04.solution.protected-logic/.prettierignore
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/.prettierignore
rename to exercises/02.authentication/04.solution.protected-logic/.prettierignore
diff --git a/exercises/03.guides/02.solution.test-annotations/.vscode/extensions.json b/exercises/02.authentication/04.solution.protected-logic/.vscode/extensions.json
similarity index 75%
rename from exercises/03.guides/02.solution.test-annotations/.vscode/extensions.json
rename to exercises/02.authentication/04.solution.protected-logic/.vscode/extensions.json
index f724eea..3c0a690 100644
--- a/exercises/03.guides/02.solution.test-annotations/.vscode/extensions.json
+++ b/exercises/02.authentication/04.solution.protected-logic/.vscode/extensions.json
@@ -6,7 +6,6 @@
"prisma.prisma",
"qwtel.sqlite-viewer",
"yoavbls.pretty-ts-errors",
- "github.vscode-github-actions",
- "ms-playwright.playwright"
+ "github.vscode-github-actions"
]
}
diff --git a/exercises/03.guides/01.solution.recording-interactions/.vscode/remix.code-snippets b/exercises/02.authentication/04.solution.protected-logic/.vscode/remix.code-snippets
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/.vscode/remix.code-snippets
rename to exercises/02.authentication/04.solution.protected-logic/.vscode/remix.code-snippets
diff --git a/exercises/03.guides/01.solution.recording-interactions/.vscode/settings.json b/exercises/02.authentication/04.solution.protected-logic/.vscode/settings.json
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/.vscode/settings.json
rename to exercises/02.authentication/04.solution.protected-logic/.vscode/settings.json
diff --git a/exercises/02.test-setup/03.solution.authentication/README.mdx b/exercises/02.authentication/04.solution.protected-logic/README.mdx
similarity index 86%
rename from exercises/02.test-setup/03.solution.authentication/README.mdx
rename to exercises/02.authentication/04.solution.protected-logic/README.mdx
index a03787c..f2db9c6 100644
--- a/exercises/02.test-setup/03.solution.authentication/README.mdx
+++ b/exercises/02.authentication/04.solution.protected-logic/README.mdx
@@ -1,4 +1,4 @@
-# Authentication
+# Protected logic
Good job! 👏
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/assets/favicons/apple-touch-icon.png b/exercises/02.authentication/04.solution.protected-logic/app/assets/favicons/apple-touch-icon.png
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/assets/favicons/apple-touch-icon.png
rename to exercises/02.authentication/04.solution.protected-logic/app/assets/favicons/apple-touch-icon.png
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/assets/favicons/favicon.svg b/exercises/02.authentication/04.solution.protected-logic/app/assets/favicons/favicon.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/assets/favicons/favicon.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/assets/favicons/favicon.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/error-boundary.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/error-boundary.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/error-boundary.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/error-boundary.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/floating-toolbar.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/floating-toolbar.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/floating-toolbar.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/floating-toolbar.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/forms.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/forms.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/forms.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/forms.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/progress-bar.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/progress-bar.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/progress-bar.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/progress-bar.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/search-bar.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/search-bar.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/search-bar.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/search-bar.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/spacer.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/spacer.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/spacer.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/spacer.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/toaster.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/toaster.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/toaster.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/toaster.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/ui/README.md b/exercises/02.authentication/04.solution.protected-logic/app/components/ui/README.md
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/ui/README.md
rename to exercises/02.authentication/04.solution.protected-logic/app/components/ui/README.md
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/ui/button.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/ui/button.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/ui/button.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/ui/button.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/ui/checkbox.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/ui/checkbox.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/ui/checkbox.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/ui/checkbox.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/ui/dropdown-menu.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/ui/dropdown-menu.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/ui/dropdown-menu.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/ui/dropdown-menu.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/ui/icon.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/ui/icon.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/ui/icon.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/ui/icon.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/ui/input-otp.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/ui/input-otp.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/ui/input-otp.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/ui/input-otp.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/ui/input.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/ui/input.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/ui/input.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/ui/input.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/ui/label.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/ui/label.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/ui/label.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/ui/label.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/ui/sonner.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/ui/sonner.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/ui/sonner.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/ui/sonner.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/ui/status-button.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/ui/status-button.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/ui/status-button.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/ui/status-button.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/ui/textarea.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/ui/textarea.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/ui/textarea.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/ui/textarea.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/ui/tooltip.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/ui/tooltip.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/ui/tooltip.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/ui/tooltip.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/components/user-dropdown.tsx b/exercises/02.authentication/04.solution.protected-logic/app/components/user-dropdown.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/components/user-dropdown.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/components/user-dropdown.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/entry.client.tsx b/exercises/02.authentication/04.solution.protected-logic/app/entry.client.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/entry.client.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/entry.client.tsx
diff --git a/exercises/02.authentication/04.solution.protected-logic/app/entry.server.tsx b/exercises/02.authentication/04.solution.protected-logic/app/entry.server.tsx
new file mode 100644
index 0000000..8d8b1de
--- /dev/null
+++ b/exercises/02.authentication/04.solution.protected-logic/app/entry.server.tsx
@@ -0,0 +1,143 @@
+import crypto from 'node:crypto'
+import { PassThrough } from 'node:stream'
+import { styleText } from 'node:util'
+import { contentSecurity } from '@nichtsam/helmet/content'
+import { createReadableStreamFromReadable } from '@react-router/node'
+import * as Sentry from '@sentry/react-router'
+import { isbot } from 'isbot'
+import { renderToPipeableStream } from 'react-dom/server'
+import {
+ ServerRouter,
+ type LoaderFunctionArgs,
+ type ActionFunctionArgs,
+ type HandleDocumentRequestFunction,
+} from 'react-router'
+import { getEnv, init } from './utils/env.server.ts'
+import { getInstanceInfo } from './utils/litefs.server.ts'
+import { NonceProvider } from './utils/nonce-provider.ts'
+import { makeTimings } from './utils/timing.server.ts'
+
+export const streamTimeout = 5000
+
+init()
+global.ENV = getEnv()
+
+const MODE = process.env.NODE_ENV ?? 'development'
+
+type DocRequestArgs = Parameters
+
+export default async function handleRequest(...args: DocRequestArgs) {
+ const [request, responseStatusCode, responseHeaders, reactRouterContext] =
+ args
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ responseHeaders.set('fly-primary-instance', primaryInstance)
+ responseHeaders.set('fly-instance', currentInstance)
+
+ if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
+ responseHeaders.append('Document-Policy', 'js-profiling')
+ }
+
+ const callbackName = isbot(request.headers.get('user-agent'))
+ ? 'onAllReady'
+ : 'onShellReady'
+
+ const nonce = crypto.randomBytes(16).toString('hex')
+ return new Promise(async (resolve, reject) => {
+ let didError = false
+ // NOTE: this timing will only include things that are rendered in the shell
+ // and will not include suspended components and deferred loaders
+ const timings = makeTimings('render', 'renderToPipeableStream')
+
+ const { pipe, abort } = renderToPipeableStream(
+
+
+ ,
+ {
+ [callbackName]: () => {
+ const body = new PassThrough()
+ responseHeaders.set('Content-Type', 'text/html')
+ responseHeaders.append('Server-Timing', timings.toString())
+
+ contentSecurity(responseHeaders, {
+ crossOriginEmbedderPolicy: false,
+ contentSecurityPolicy: {
+ // NOTE: Remove reportOnly when you're ready to enforce this CSP
+ reportOnly: true,
+ directives: {
+ fetch: {
+ 'connect-src': [
+ MODE === 'development' ? 'ws:' : undefined,
+ process.env.SENTRY_DSN ? '*.sentry.io' : undefined,
+ "'self'",
+ ],
+ 'font-src': ["'self'"],
+ 'frame-src': ["'self'"],
+ 'img-src': ["'self'", 'data:'],
+ 'script-src': [
+ "'strict-dynamic'",
+ "'self'",
+ `'nonce-${nonce}'`,
+ ],
+ 'script-src-attr': [`'nonce-${nonce}'`],
+ },
+ },
+ },
+ xFrameOptions: false,
+ })
+
+ resolve(
+ new Response(createReadableStreamFromReadable(body), {
+ headers: responseHeaders,
+ status: didError ? 500 : responseStatusCode,
+ }),
+ )
+ pipe(body)
+ },
+ onShellError: (err: unknown) => {
+ reject(err)
+ },
+ onError: () => {
+ didError = true
+ },
+ nonce,
+ },
+ )
+
+ setTimeout(abort, streamTimeout + 5000)
+ })
+}
+
+export async function handleDataRequest(response: Response) {
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ response.headers.set('fly-primary-instance', primaryInstance)
+ response.headers.set('fly-instance', currentInstance)
+
+ return response
+}
+
+export function handleError(
+ error: unknown,
+ { request }: LoaderFunctionArgs | ActionFunctionArgs,
+): void {
+ // Skip capturing if the request is aborted as Remix docs suggest
+ // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
+ if (request.signal.aborted) {
+ return
+ }
+
+ if (error instanceof Error) {
+ console.error(styleText('red', String(error.stack)))
+ } else {
+ console.error(error)
+ }
+
+ Sentry.captureException(error)
+}
diff --git a/exercises/03.guides/02.solution.test-annotations/app/root.tsx b/exercises/02.authentication/04.solution.protected-logic/app/root.tsx
similarity index 100%
rename from exercises/03.guides/02.solution.test-annotations/app/root.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/root.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes.ts b/exercises/02.authentication/04.solution.protected-logic/app/routes.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/routes.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/$.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/$.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/$.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/$.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/auth.$provider.callback.test.ts b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/auth.$provider.callback.test.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/_auth+/auth.$provider.callback.test.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/auth.$provider.callback.test.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/auth.$provider.callback.ts b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/auth.$provider.callback.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/auth.$provider.callback.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/auth.$provider.callback.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/auth_.$provider.ts b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/auth_.$provider.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/auth_.$provider.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/auth_.$provider.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/forgot-password.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/forgot-password.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/forgot-password.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/forgot-password.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/login.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/login.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/login.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/login.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/login.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/login.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/login.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/login.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/logout.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/logout.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/logout.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/logout.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/onboarding.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/onboarding.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/onboarding.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/onboarding.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/onboarding.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/onboarding.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/onboarding.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/onboarding.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/onboarding_.$provider.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/onboarding_.$provider.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/onboarding_.$provider.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/onboarding_.$provider.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/onboarding_.$provider.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/onboarding_.$provider.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/onboarding_.$provider.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/onboarding_.$provider.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/reset-password.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/reset-password.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/reset-password.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/reset-password.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/reset-password.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/reset-password.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/reset-password.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/reset-password.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/signup.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/signup.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/signup.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/signup.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/verify.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/verify.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/verify.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/verify.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/verify.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/verify.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/verify.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/verify.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/webauthn+/authentication.ts b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/webauthn+/authentication.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/webauthn+/authentication.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/webauthn+/authentication.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/webauthn+/registration.ts b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/webauthn+/registration.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/webauthn+/registration.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/webauthn+/registration.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/webauthn+/utils.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/webauthn+/utils.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_auth+/webauthn+/utils.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_auth+/webauthn+/utils.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/about.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/about.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/about.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/about.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/index.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/index.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/index.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/index.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/docker.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/docker.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/docker.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/docker.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/eslint.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/eslint.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/eslint.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/eslint.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/faker.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/faker.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/faker.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/faker.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/fly.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/fly.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/fly.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/fly.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/github.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/github.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/github.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/github.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/logos.ts b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/logos.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/logos.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/logos.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/msw.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/msw.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/msw.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/msw.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/playwright.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/playwright.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/playwright.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/playwright.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/prettier.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/prettier.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/prettier.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/prettier.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/prisma.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/prisma.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/prisma.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/prisma.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/radix.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/radix.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/radix.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/radix.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/react-email.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/react-email.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/react-email.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/react-email.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/remix.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/remix.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/remix.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/remix.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/resend.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/resend.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/resend.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/resend.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/sentry.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/sentry.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/sentry.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/sentry.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/shadcn-ui.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/shadcn-ui.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/shadcn-ui.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/shadcn-ui.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/sqlite.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/sqlite.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/sqlite.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/sqlite.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/stars.jpg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/stars.jpg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/stars.jpg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/stars.jpg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/tailwind.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/tailwind.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/tailwind.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/tailwind.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/testing-library.png b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/testing-library.png
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/testing-library.png
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/testing-library.png
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/typescript.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/typescript.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/typescript.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/typescript.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/vitest.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/vitest.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/vitest.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/vitest.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/zod.svg b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/zod.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/logos/zod.svg
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/logos/zod.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/privacy.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/privacy.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/privacy.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/privacy.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/support.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/support.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/support.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/support.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/tos.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/tos.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_marketing+/tos.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_marketing+/tos.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_seo+/robots[.]txt.ts b/exercises/02.authentication/04.solution.protected-logic/app/routes/_seo+/robots[.]txt.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_seo+/robots[.]txt.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_seo+/robots[.]txt.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/_seo+/sitemap[.]xml.ts b/exercises/02.authentication/04.solution.protected-logic/app/routes/_seo+/sitemap[.]xml.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/_seo+/sitemap[.]xml.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/_seo+/sitemap[.]xml.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/admin+/cache.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/admin+/cache.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/admin+/cache.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/admin+/cache.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/admin+/cache_.lru.$cacheKey.ts b/exercises/02.authentication/04.solution.protected-logic/app/routes/admin+/cache_.lru.$cacheKey.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/admin+/cache_.lru.$cacheKey.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/admin+/cache_.lru.$cacheKey.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/admin+/cache_.sqlite.$cacheKey.ts b/exercises/02.authentication/04.solution.protected-logic/app/routes/admin+/cache_.sqlite.$cacheKey.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/admin+/cache_.sqlite.$cacheKey.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/admin+/cache_.sqlite.$cacheKey.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/admin+/cache_.sqlite.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/routes/admin+/cache_.sqlite.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/admin+/cache_.sqlite.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/admin+/cache_.sqlite.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/admin+/cache_.sqlite.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/admin+/cache_.sqlite.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/admin+/cache_.sqlite.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/admin+/cache_.sqlite.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/me.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/me.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/me.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/me.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/resources+/download-user-data.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/resources+/download-user-data.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/resources+/download-user-data.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/resources+/download-user-data.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/resources+/healthcheck.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/resources+/healthcheck.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/resources+/healthcheck.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/resources+/healthcheck.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/resources+/images.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/resources+/images.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/resources+/images.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/resources+/images.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/resources+/theme-switch.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/resources+/theme-switch.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/resources+/theme-switch.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/resources+/theme-switch.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.change-email.server.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.change-email.server.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.change-email.server.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.change-email.server.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.change-email.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.change-email.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.change-email.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.change-email.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.connections.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.connections.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.connections.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.connections.tsx
diff --git a/exercises/03.guides/02.solution.test-annotations/app/routes/settings+/profile.index.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.index.tsx
similarity index 100%
rename from exercises/03.guides/02.solution.test-annotations/app/routes/settings+/profile.index.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.index.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.passkeys.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.passkeys.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.passkeys.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.passkeys.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.password.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.password.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.password.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.password.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.password_.create.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.password_.create.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.password_.create.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.password_.create.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.photo.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.photo.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.photo.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.photo.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.two-factor.disable.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.two-factor.disable.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.two-factor.disable.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.two-factor.disable.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.two-factor.index.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.two-factor.index.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.two-factor.index.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.two-factor.index.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.two-factor.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.two-factor.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.two-factor.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.two-factor.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.two-factor.verify.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.two-factor.verify.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/settings+/profile.two-factor.verify.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/settings+/profile.two-factor.verify.tsx
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username.test.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username.test.tsx
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username.test.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username.test.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/users+/$username.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/users+/$username.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/users+/$username_+/__note-editor.server.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username_+/__note-editor.server.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/users+/$username_+/__note-editor.server.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username_+/__note-editor.server.tsx
diff --git a/exercises/03.guides/02.solution.test-annotations/app/routes/users+/$username_+/__note-editor.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username_+/__note-editor.tsx
similarity index 100%
rename from exercises/03.guides/02.solution.test-annotations/app/routes/users+/$username_+/__note-editor.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username_+/__note-editor.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/users+/$username_+/notes.$noteId.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username_+/notes.$noteId.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/users+/$username_+/notes.$noteId.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username_+/notes.$noteId.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/users+/$username_+/notes.$noteId_.edit.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username_+/notes.$noteId_.edit.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/users+/$username_+/notes.index.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username_+/notes.index.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/users+/$username_+/notes.index.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username_+/notes.index.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/users+/$username_+/notes.new.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username_+/notes.new.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/users+/$username_+/notes.new.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username_+/notes.new.tsx
diff --git a/exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username_+/notes.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username_+/notes.tsx
new file mode 100644
index 0000000..ded41ca
--- /dev/null
+++ b/exercises/02.authentication/04.solution.protected-logic/app/routes/users+/$username_+/notes.tsx
@@ -0,0 +1,105 @@
+import { invariantResponse } from '@epic-web/invariant'
+import { Img } from 'openimg/react'
+import { Link, NavLink, Outlet } from 'react-router'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+import { prisma } from '#app/utils/db.server.ts'
+import { cn, getUserImgSrc } from '#app/utils/misc.tsx'
+import { useOptionalUser } from '#app/utils/user.ts'
+import { type Route } from './+types/notes.ts'
+
+export async function loader({ params }: Route.LoaderArgs) {
+ const owner = await prisma.user.findFirst({
+ select: {
+ id: true,
+ name: true,
+ username: true,
+ image: { select: { objectKey: true } },
+ notes: { select: { id: true, title: true } },
+ },
+ where: { username: params.username },
+ })
+
+ invariantResponse(owner, 'Owner not found', { status: 404 })
+
+ return { owner }
+}
+
+export default function NotesRoute({ loaderData }: Route.ComponentProps) {
+ const user = useOptionalUser()
+ const isOwner = user?.id === loaderData.owner.id
+ const ownerDisplayName = loaderData.owner.name ?? loaderData.owner.username
+ const navLinkDefaultClassName =
+ 'line-clamp-2 block rounded-l-full py-2 pl-8 pr-6 text-base lg:text-xl'
+ return (
+
+
+
+
+
+
+
+ {ownerDisplayName}'s Notes
+
+
+
+ {isOwner ? (
+
+
+ cn(navLinkDefaultClassName, isActive && 'bg-accent')
+ }
+ >
+ New Note
+
+
+ ) : null}
+ {loaderData.owner.notes.map((note) => (
+
+
+ cn(navLinkDefaultClassName, isActive && 'bg-accent')
+ }
+ >
+ {note.title}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+}
+
+export function ErrorBoundary() {
+ return (
+ (
+ No user with the username "{params.username}" exists
+ ),
+ }}
+ />
+ )
+}
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/routes/users+/index.tsx b/exercises/02.authentication/04.solution.protected-logic/app/routes/users+/index.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/routes/users+/index.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/routes/users+/index.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/styles/tailwind.css b/exercises/02.authentication/04.solution.protected-logic/app/styles/tailwind.css
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/styles/tailwind.css
rename to exercises/02.authentication/04.solution.protected-logic/app/styles/tailwind.css
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/auth.server.test.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/auth.server.test.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/auth.server.test.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/auth.server.test.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/auth.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/auth.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/auth.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/auth.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/cache.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/cache.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/cache.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/cache.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/client-hints.tsx b/exercises/02.authentication/04.solution.protected-logic/app/utils/client-hints.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/client-hints.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/client-hints.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/connections.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/connections.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/connections.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/connections.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/connections.tsx b/exercises/02.authentication/04.solution.protected-logic/app/utils/connections.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/connections.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/connections.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/db.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/db.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/db.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/db.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/email.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/email.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/email.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/email.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/env.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/env.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/env.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/env.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/headers.server.test.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/headers.server.test.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/headers.server.test.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/headers.server.test.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/headers.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/headers.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/headers.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/headers.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/honeypot.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/honeypot.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/honeypot.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/honeypot.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/litefs.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/litefs.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/litefs.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/litefs.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/misc.error-message.test.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/misc.error-message.test.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/misc.error-message.test.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/misc.error-message.test.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/misc.tsx b/exercises/02.authentication/04.solution.protected-logic/app/utils/misc.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/misc.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/misc.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/misc.use-double-check.test.tsx b/exercises/02.authentication/04.solution.protected-logic/app/utils/misc.use-double-check.test.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/misc.use-double-check.test.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/misc.use-double-check.test.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/monitoring.client.tsx b/exercises/02.authentication/04.solution.protected-logic/app/utils/monitoring.client.tsx
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/monitoring.client.tsx
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/monitoring.client.tsx
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/nonce-provider.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/nonce-provider.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/nonce-provider.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/nonce-provider.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/permissions.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/permissions.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/permissions.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/permissions.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/providers/constants.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/providers/constants.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/providers/constants.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/providers/constants.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/providers/github.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/providers/github.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/providers/github.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/providers/github.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/providers/provider.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/providers/provider.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/providers/provider.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/providers/provider.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/redirect-cookie.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/redirect-cookie.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/redirect-cookie.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/redirect-cookie.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/request-info.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/request-info.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/request-info.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/request-info.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/session.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/session.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/session.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/session.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/storage.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/storage.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/storage.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/storage.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/theme.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/theme.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/theme.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/theme.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/timing.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/timing.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/timing.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/timing.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/toast.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/toast.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/toast.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/toast.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/totp.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/totp.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/totp.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/totp.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/user-validation.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/user-validation.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/user-validation.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/user-validation.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/user.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/user.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/user.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/user.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/app/utils/verification.server.ts b/exercises/02.authentication/04.solution.protected-logic/app/utils/verification.server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/app/utils/verification.server.ts
rename to exercises/02.authentication/04.solution.protected-logic/app/utils/verification.server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/components.json b/exercises/02.authentication/04.solution.protected-logic/components.json
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/components.json
rename to exercises/02.authentication/04.solution.protected-logic/components.json
diff --git a/exercises/03.guides/01.solution.recording-interactions/eslint.config.js b/exercises/02.authentication/04.solution.protected-logic/eslint.config.js
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/eslint.config.js
rename to exercises/02.authentication/04.solution.protected-logic/eslint.config.js
diff --git a/exercises/03.guides/01.solution.recording-interactions/fly.toml b/exercises/02.authentication/04.solution.protected-logic/fly.toml
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/fly.toml
rename to exercises/02.authentication/04.solution.protected-logic/fly.toml
diff --git a/exercises/03.guides/01.solution.recording-interactions/index.js b/exercises/02.authentication/04.solution.protected-logic/index.js
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/index.js
rename to exercises/02.authentication/04.solution.protected-logic/index.js
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/Dockerfile b/exercises/02.authentication/04.solution.protected-logic/other/Dockerfile
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/Dockerfile
rename to exercises/02.authentication/04.solution.protected-logic/other/Dockerfile
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/Dockerfile.dockerignore b/exercises/02.authentication/04.solution.protected-logic/other/Dockerfile.dockerignore
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/Dockerfile.dockerignore
rename to exercises/02.authentication/04.solution.protected-logic/other/Dockerfile.dockerignore
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/README.md b/exercises/02.authentication/04.solution.protected-logic/other/README.md
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/README.md
rename to exercises/02.authentication/04.solution.protected-logic/other/README.md
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/build-server.ts b/exercises/02.authentication/04.solution.protected-logic/other/build-server.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/build-server.ts
rename to exercises/02.authentication/04.solution.protected-logic/other/build-server.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/litefs.yml b/exercises/02.authentication/04.solution.protected-logic/other/litefs.yml
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/litefs.yml
rename to exercises/02.authentication/04.solution.protected-logic/other/litefs.yml
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/sly/sly.json b/exercises/02.authentication/04.solution.protected-logic/other/sly/sly.json
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/sly/sly.json
rename to exercises/02.authentication/04.solution.protected-logic/other/sly/sly.json
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/sly/transform-icon.ts b/exercises/02.authentication/04.solution.protected-logic/other/sly/transform-icon.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/sly/transform-icon.ts
rename to exercises/02.authentication/04.solution.protected-logic/other/sly/transform-icon.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/README.md b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/README.md
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/README.md
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/README.md
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/arrow-left.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/arrow-left.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/arrow-left.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/arrow-left.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/arrow-right.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/arrow-right.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/arrow-right.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/arrow-right.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/avatar.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/avatar.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/avatar.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/avatar.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/camera.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/camera.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/camera.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/camera.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/check.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/check.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/check.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/check.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/clock.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/clock.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/clock.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/clock.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/cross-1.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/cross-1.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/cross-1.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/cross-1.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/dots-horizontal.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/dots-horizontal.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/dots-horizontal.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/dots-horizontal.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/download.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/download.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/download.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/download.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/envelope-closed.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/envelope-closed.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/envelope-closed.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/envelope-closed.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/exit.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/exit.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/exit.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/exit.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/file-text.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/file-text.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/file-text.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/file-text.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/github-logo.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/github-logo.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/github-logo.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/github-logo.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/laptop.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/laptop.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/laptop.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/laptop.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/link-2.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/link-2.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/link-2.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/link-2.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/lock-closed.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/lock-closed.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/lock-closed.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/lock-closed.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/lock-open-1.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/lock-open-1.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/lock-open-1.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/lock-open-1.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/magnifying-glass.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/magnifying-glass.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/magnifying-glass.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/magnifying-glass.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/moon.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/moon.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/moon.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/moon.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/passkey.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/passkey.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/passkey.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/passkey.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/pencil-1.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/pencil-1.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/pencil-1.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/pencil-1.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/pencil-2.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/pencil-2.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/pencil-2.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/pencil-2.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/plus.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/plus.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/plus.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/plus.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/question-mark-circled.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/question-mark-circled.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/question-mark-circled.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/question-mark-circled.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/reset.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/reset.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/reset.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/reset.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/sun.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/sun.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/sun.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/sun.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/trash.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/trash.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/trash.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/trash.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/other/svg-icons/update.svg b/exercises/02.authentication/04.solution.protected-logic/other/svg-icons/update.svg
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/other/svg-icons/update.svg
rename to exercises/02.authentication/04.solution.protected-logic/other/svg-icons/update.svg
diff --git a/exercises/03.guides/01.solution.recording-interactions/package-lock.json b/exercises/02.authentication/04.solution.protected-logic/package-lock.json
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/package-lock.json
rename to exercises/02.authentication/04.solution.protected-logic/package-lock.json
diff --git a/exercises/02.test-setup/01.problem.custom-fixtures/package.json b/exercises/02.authentication/04.solution.protected-logic/package.json
similarity index 97%
rename from exercises/02.test-setup/01.problem.custom-fixtures/package.json
rename to exercises/02.authentication/04.solution.protected-logic/package.json
index a150022..ec69d42 100644
--- a/exercises/02.test-setup/01.problem.custom-fixtures/package.json
+++ b/exercises/02.authentication/04.solution.protected-logic/package.json
@@ -1,5 +1,5 @@
{
- "name": "exercises_02.test-setup_01.problem.custom-fixtures",
+ "name": "exercises_02.authentication_04.solution.protected-logic",
"private": true,
"sideEffects": false,
"type": "module",
@@ -115,7 +115,7 @@
"devDependencies": {
"@epic-web/config": "^1.20.1",
"@faker-js/faker": "^9.7.0",
- "@playwright/test": "^1.52.0",
+ "@playwright/test": "^1.57.0",
"@react-router/dev": "^7.5.3",
"@sly-cli/sly": "^2.1.1",
"@testing-library/dom": "^10.4.0",
@@ -145,7 +145,7 @@
"jsdom": "^25.0.1",
"msw": "^2.7.6",
"npm-run-all": "^4.1.5",
- "playwright-persona": "^0.2.5",
+ "playwright-persona": "^0.2.6",
"prettier": "^3.5.3",
"prettier-plugin-sql": "^0.19.0",
"prettier-plugin-tailwindcss": "^0.6.11",
diff --git a/exercises/03.guides/01.solution.recording-interactions/playwright.config.ts b/exercises/02.authentication/04.solution.protected-logic/playwright.config.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/playwright.config.ts
rename to exercises/02.authentication/04.solution.protected-logic/playwright.config.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/prisma/migrations/20250221233640_init/migration.sql b/exercises/02.authentication/04.solution.protected-logic/prisma/migrations/20250221233640_init/migration.sql
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/prisma/migrations/20250221233640_init/migration.sql
rename to exercises/02.authentication/04.solution.protected-logic/prisma/migrations/20250221233640_init/migration.sql
diff --git a/exercises/03.guides/01.solution.recording-interactions/prisma/migrations/migration_lock.toml b/exercises/02.authentication/04.solution.protected-logic/prisma/migrations/migration_lock.toml
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/prisma/migrations/migration_lock.toml
rename to exercises/02.authentication/04.solution.protected-logic/prisma/migrations/migration_lock.toml
diff --git a/exercises/03.guides/01.solution.recording-interactions/prisma/schema.prisma b/exercises/02.authentication/04.solution.protected-logic/prisma/schema.prisma
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/prisma/schema.prisma
rename to exercises/02.authentication/04.solution.protected-logic/prisma/schema.prisma
diff --git a/exercises/02.test-setup/03.problem.authentication/prisma/seed.ts b/exercises/02.authentication/04.solution.protected-logic/prisma/seed.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/prisma/seed.ts
rename to exercises/02.authentication/04.solution.protected-logic/prisma/seed.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/prisma/sql/searchUsers.sql b/exercises/02.authentication/04.solution.protected-logic/prisma/sql/searchUsers.sql
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/prisma/sql/searchUsers.sql
rename to exercises/02.authentication/04.solution.protected-logic/prisma/sql/searchUsers.sql
diff --git a/exercises/03.guides/01.solution.recording-interactions/public/favicon.ico b/exercises/02.authentication/04.solution.protected-logic/public/favicon.ico
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/public/favicon.ico
rename to exercises/02.authentication/04.solution.protected-logic/public/favicon.ico
diff --git a/exercises/03.guides/01.solution.recording-interactions/public/favicons/README.md b/exercises/02.authentication/04.solution.protected-logic/public/favicons/README.md
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/public/favicons/README.md
rename to exercises/02.authentication/04.solution.protected-logic/public/favicons/README.md
diff --git a/exercises/03.guides/01.solution.recording-interactions/public/favicons/android-chrome-192x192.png b/exercises/02.authentication/04.solution.protected-logic/public/favicons/android-chrome-192x192.png
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/public/favicons/android-chrome-192x192.png
rename to exercises/02.authentication/04.solution.protected-logic/public/favicons/android-chrome-192x192.png
diff --git a/exercises/03.guides/01.solution.recording-interactions/public/favicons/android-chrome-512x512.png b/exercises/02.authentication/04.solution.protected-logic/public/favicons/android-chrome-512x512.png
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/public/favicons/android-chrome-512x512.png
rename to exercises/02.authentication/04.solution.protected-logic/public/favicons/android-chrome-512x512.png
diff --git a/exercises/03.guides/01.solution.recording-interactions/public/img/user.png b/exercises/02.authentication/04.solution.protected-logic/public/img/user.png
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/public/img/user.png
rename to exercises/02.authentication/04.solution.protected-logic/public/img/user.png
diff --git a/exercises/03.guides/01.solution.recording-interactions/public/site.webmanifest b/exercises/02.authentication/04.solution.protected-logic/public/site.webmanifest
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/public/site.webmanifest
rename to exercises/02.authentication/04.solution.protected-logic/public/site.webmanifest
diff --git a/exercises/03.guides/01.solution.recording-interactions/react-router.config.ts b/exercises/02.authentication/04.solution.protected-logic/react-router.config.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/react-router.config.ts
rename to exercises/02.authentication/04.solution.protected-logic/react-router.config.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/server/dev-server.js b/exercises/02.authentication/04.solution.protected-logic/server/dev-server.js
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/server/dev-server.js
rename to exercises/02.authentication/04.solution.protected-logic/server/dev-server.js
diff --git a/exercises/03.guides/01.solution.recording-interactions/server/index.ts b/exercises/02.authentication/04.solution.protected-logic/server/index.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/server/index.ts
rename to exercises/02.authentication/04.solution.protected-logic/server/index.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/server/utils/monitoring.ts b/exercises/02.authentication/04.solution.protected-logic/server/utils/monitoring.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/server/utils/monitoring.ts
rename to exercises/02.authentication/04.solution.protected-logic/server/utils/monitoring.ts
diff --git a/exercises/02.test-setup/03.problem.authentication/tests/db-utils.ts b/exercises/02.authentication/04.solution.protected-logic/tests/db-utils.ts
similarity index 100%
rename from exercises/02.test-setup/03.problem.authentication/tests/db-utils.ts
rename to exercises/02.authentication/04.solution.protected-logic/tests/db-utils.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/tests/e2e/notes-create.test.ts b/exercises/02.authentication/04.solution.protected-logic/tests/e2e/notes-create.test.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/tests/e2e/notes-create.test.ts
rename to exercises/02.authentication/04.solution.protected-logic/tests/e2e/notes-create.test.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/tests/setup/custom-matchers.ts b/exercises/02.authentication/04.solution.protected-logic/tests/setup/custom-matchers.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/tests/setup/custom-matchers.ts
rename to exercises/02.authentication/04.solution.protected-logic/tests/setup/custom-matchers.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/tests/setup/db-setup.ts b/exercises/02.authentication/04.solution.protected-logic/tests/setup/db-setup.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/tests/setup/db-setup.ts
rename to exercises/02.authentication/04.solution.protected-logic/tests/setup/db-setup.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/tests/setup/global-setup.ts b/exercises/02.authentication/04.solution.protected-logic/tests/setup/global-setup.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/tests/setup/global-setup.ts
rename to exercises/02.authentication/04.solution.protected-logic/tests/setup/global-setup.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/tests/setup/setup-test-env.ts b/exercises/02.authentication/04.solution.protected-logic/tests/setup/setup-test-env.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/tests/setup/setup-test-env.ts
rename to exercises/02.authentication/04.solution.protected-logic/tests/setup/setup-test-env.ts
diff --git a/exercises/02.test-setup/03.solution.authentication/tests/test-extend.ts b/exercises/02.authentication/04.solution.protected-logic/tests/test-extend.ts
similarity index 100%
rename from exercises/02.test-setup/03.solution.authentication/tests/test-extend.ts
rename to exercises/02.authentication/04.solution.protected-logic/tests/test-extend.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/tests/utils.ts b/exercises/02.authentication/04.solution.protected-logic/tests/utils.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/tests/utils.ts
rename to exercises/02.authentication/04.solution.protected-logic/tests/utils.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/tsconfig.json b/exercises/02.authentication/04.solution.protected-logic/tsconfig.json
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/tsconfig.json
rename to exercises/02.authentication/04.solution.protected-logic/tsconfig.json
diff --git a/exercises/03.guides/01.solution.recording-interactions/types/deps.d.ts b/exercises/02.authentication/04.solution.protected-logic/types/deps.d.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/types/deps.d.ts
rename to exercises/02.authentication/04.solution.protected-logic/types/deps.d.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/types/env.env.d.ts b/exercises/02.authentication/04.solution.protected-logic/types/env.env.d.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/types/env.env.d.ts
rename to exercises/02.authentication/04.solution.protected-logic/types/env.env.d.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/types/icon-name.d.ts b/exercises/02.authentication/04.solution.protected-logic/types/icon-name.d.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/types/icon-name.d.ts
rename to exercises/02.authentication/04.solution.protected-logic/types/icon-name.d.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/types/reset.d.ts b/exercises/02.authentication/04.solution.protected-logic/types/reset.d.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/types/reset.d.ts
rename to exercises/02.authentication/04.solution.protected-logic/types/reset.d.ts
diff --git a/exercises/03.guides/01.solution.recording-interactions/vite.config.ts b/exercises/02.authentication/04.solution.protected-logic/vite.config.ts
similarity index 100%
rename from exercises/03.guides/01.solution.recording-interactions/vite.config.ts
rename to exercises/02.authentication/04.solution.protected-logic/vite.config.ts
diff --git a/exercises/02.authentication/FINISHED.mdx b/exercises/02.authentication/FINISHED.mdx
new file mode 100644
index 0000000..66a783b
--- /dev/null
+++ b/exercises/02.authentication/FINISHED.mdx
@@ -0,0 +1 @@
+# Authentication
\ No newline at end of file
diff --git a/exercises/02.authentication/README.mdx b/exercises/02.authentication/README.mdx
new file mode 100644
index 0000000..f46cdf9
--- /dev/null
+++ b/exercises/02.authentication/README.mdx
@@ -0,0 +1,4 @@
+# Authentication
+
+- Show how to test different authentication options your application might have.
+- Show how to test logic that is _behind_ authentication (i.e. requires an authenticated user).
diff --git a/exercises/02.test-setup/02.problem.mock-databases/README.mdx b/exercises/02.test-setup/02.problem.mock-databases/README.mdx
deleted file mode 100644
index 33411d5..0000000
--- a/exercises/02.test-setup/02.problem.mock-databases/README.mdx
+++ /dev/null
@@ -1,11 +0,0 @@
-# Mock databases
-
-Problem: Your app under test **must not** communicate with the actual database.
-
-Mocking database connections is database-specific. There are serveral kinds of databases you might be using:
-
-- **File-based** (SQLite). Just point it to a local file;
-- **Server-based** (Postgres). You can run that DB instance locally in a Docker image and point your DB client to that locally running DB instance.
-- **In-memory databases** (Redis). Run database locally (that's usually easier and doesn't require Docker), connect to the locally running instance.
-
-> Some database clients, like Supabase, might also expose an additional layer between your application and the database. You might decide to tap into that, too.
diff --git a/exercises/02.test-setup/02.problem.mock-databases/package.json b/exercises/02.test-setup/02.problem.mock-databases/package.json
deleted file mode 100644
index bf797b9..0000000
--- a/exercises/02.test-setup/02.problem.mock-databases/package.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "name": "exercises_02.test-setup_02.problem.mock-databases"
-}
diff --git a/exercises/02.test-setup/02.solution.mock-databases/README.mdx b/exercises/02.test-setup/02.solution.mock-databases/README.mdx
deleted file mode 100644
index e3f649a..0000000
--- a/exercises/02.test-setup/02.solution.mock-databases/README.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
-# Mock databases
-
-Good job! 👏
-
-Take a quick rest and let's continue.
-
-## Related materials
-
-- [PostgreSQL test setup with Prisma](https://www.prisma.io/docs/orm/prisma-client/testing/integration-testing)
-- [Redus test setup (Node.js)](https://redis.io/docs/latest/develop/clients/nodejs/)
diff --git a/exercises/02.test-setup/02.solution.mock-databases/package.json b/exercises/02.test-setup/02.solution.mock-databases/package.json
deleted file mode 100644
index 334bcf0..0000000
--- a/exercises/02.test-setup/02.solution.mock-databases/package.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "name": "exercises_02.test-setup_02.solution.mock-databases"
-}
diff --git a/exercises/02.test-setup/03.problem.authentication/app/entry.server.tsx b/exercises/02.test-setup/03.problem.authentication/app/entry.server.tsx
deleted file mode 100644
index 99fdd4b..0000000
--- a/exercises/02.test-setup/03.problem.authentication/app/entry.server.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import crypto from 'node:crypto'
-import { PassThrough } from 'node:stream'
-import { styleText } from 'node:util'
-import { contentSecurity } from '@nichtsam/helmet/content'
-import { createReadableStreamFromReadable } from '@react-router/node'
-import * as Sentry from '@sentry/react-router'
-import { isbot } from 'isbot'
-import { renderToPipeableStream } from 'react-dom/server'
-import {
- ServerRouter,
- type LoaderFunctionArgs,
- type ActionFunctionArgs,
- type HandleDocumentRequestFunction,
-} from 'react-router'
-import { getEnv, init } from './utils/env.server.ts'
-import { getInstanceInfo } from './utils/litefs.server.ts'
-import { NonceProvider } from './utils/nonce-provider.ts'
-import { makeTimings } from './utils/timing.server.ts'
-
-export const streamTimeout = 5000
-
-init()
-global.ENV = getEnv()
-
-const MODE = process.env.NODE_ENV ?? 'development'
-
-type DocRequestArgs = Parameters
-
-export default async function handleRequest(...args: DocRequestArgs) {
- const [request, responseStatusCode, responseHeaders, reactRouterContext] =
- args
- const { currentInstance, primaryInstance } = await getInstanceInfo()
- responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
- responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
- responseHeaders.set('fly-primary-instance', primaryInstance)
- responseHeaders.set('fly-instance', currentInstance)
-
- if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
- responseHeaders.append('Document-Policy', 'js-profiling')
- }
-
- const callbackName = isbot(request.headers.get('user-agent'))
- ? 'onAllReady'
- : 'onShellReady'
-
- const nonce = crypto.randomBytes(16).toString('hex')
- return new Promise(async (resolve, reject) => {
- let didError = false
- // NOTE: this timing will only include things that are rendered in the shell
- // and will not include suspended components and deferred loaders
- const timings = makeTimings('render', 'renderToPipeableStream')
-
- const { pipe, abort } = renderToPipeableStream(
-
-
- ,
- {
- [callbackName]: () => {
- const body = new PassThrough()
- responseHeaders.set('Content-Type', 'text/html')
- responseHeaders.append('Server-Timing', timings.toString())
-
- contentSecurity(responseHeaders, {
- crossOriginEmbedderPolicy: false,
- contentSecurityPolicy: {
- // NOTE: Remove reportOnly when you're ready to enforce this CSP
- reportOnly: true,
- directives: {
- fetch: {
- 'connect-src': [
- MODE === 'development' ? 'ws:' : undefined,
- process.env.SENTRY_DSN ? '*.sentry.io' : undefined,
- "'self'",
- ],
- 'font-src': ["'self'"],
- 'frame-src': ["'self'"],
- 'img-src': ["'self'", 'data:'],
- 'script-src': [
- "'strict-dynamic'",
- "'self'",
- `'nonce-${nonce}'`,
- ],
- 'script-src-attr': [`'nonce-${nonce}'`],
- },
- },
- },
- })
-
- resolve(
- new Response(createReadableStreamFromReadable(body), {
- headers: responseHeaders,
- status: didError ? 500 : responseStatusCode,
- }),
- )
- pipe(body)
- },
- onShellError: (err: unknown) => {
- reject(err)
- },
- onError: () => {
- didError = true
- },
- nonce,
- },
- )
-
- setTimeout(abort, streamTimeout + 5000)
- })
-}
-
-export async function handleDataRequest(response: Response) {
- const { currentInstance, primaryInstance } = await getInstanceInfo()
- response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown')
- response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
- response.headers.set('fly-primary-instance', primaryInstance)
- response.headers.set('fly-instance', currentInstance)
-
- return response
-}
-
-export function handleError(
- error: unknown,
- { request }: LoaderFunctionArgs | ActionFunctionArgs,
-): void {
- // Skip capturing if the request is aborted as Remix docs suggest
- // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
- if (request.signal.aborted) {
- return
- }
-
- if (error instanceof Error) {
- console.error(styleText('red', String(error.stack)))
- } else {
- console.error(error)
- }
-
- Sentry.captureException(error)
-}
diff --git a/exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username_+/notes.tsx b/exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username_+/notes.tsx
deleted file mode 100644
index 90e7b3c..0000000
--- a/exercises/02.test-setup/03.problem.authentication/app/routes/users+/$username_+/notes.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import { invariantResponse } from '@epic-web/invariant'
-import { Img } from 'openimg/react'
-import { Link, NavLink, Outlet } from 'react-router'
-import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
-import { Icon } from '#app/components/ui/icon.tsx'
-import { prisma } from '#app/utils/db.server.ts'
-import { cn, getUserImgSrc } from '#app/utils/misc.tsx'
-import { useOptionalUser } from '#app/utils/user.ts'
-import { type Route } from './+types/notes.ts'
-
-export async function loader({ params }: Route.LoaderArgs) {
- const owner = await prisma.user.findFirst({
- select: {
- id: true,
- name: true,
- username: true,
- image: { select: { objectKey: true } },
- notes: { select: { id: true, title: true } },
- },
- where: { username: params.username },
- })
-
- invariantResponse(owner, 'Owner not found', { status: 404 })
-
- return { owner }
-}
-
-export default function NotesRoute({ loaderData }: Route.ComponentProps) {
- const user = useOptionalUser()
- const isOwner = user?.id === loaderData.owner.id
- const ownerDisplayName = loaderData.owner.name ?? loaderData.owner.username
- const navLinkDefaultClassName =
- 'line-clamp-2 block rounded-l-full py-2 pl-8 pr-6 text-base lg:text-xl'
- return (
-
-
-
-
-
-
-
- {ownerDisplayName}'s Notes
-
-
-
- {isOwner ? (
-
-
- cn(navLinkDefaultClassName, isActive && 'bg-accent')
- }
- >
- New Note
-
-
- ) : null}
- {loaderData.owner.notes.map((note) => (
-
-
- cn(navLinkDefaultClassName, isActive && 'bg-accent')
- }
- >
- {note.title}
-
-
- ))}
-
-
-
-
-
-
-
-
- )
-}
-
-export function ErrorBoundary() {
- return (
- (
- No user with the username "{params.username}" exists
- ),
- }}
- />
- )
-}
diff --git a/exercises/02.test-setup/03.solution.authentication/app/entry.server.tsx b/exercises/02.test-setup/03.solution.authentication/app/entry.server.tsx
deleted file mode 100644
index 99fdd4b..0000000
--- a/exercises/02.test-setup/03.solution.authentication/app/entry.server.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import crypto from 'node:crypto'
-import { PassThrough } from 'node:stream'
-import { styleText } from 'node:util'
-import { contentSecurity } from '@nichtsam/helmet/content'
-import { createReadableStreamFromReadable } from '@react-router/node'
-import * as Sentry from '@sentry/react-router'
-import { isbot } from 'isbot'
-import { renderToPipeableStream } from 'react-dom/server'
-import {
- ServerRouter,
- type LoaderFunctionArgs,
- type ActionFunctionArgs,
- type HandleDocumentRequestFunction,
-} from 'react-router'
-import { getEnv, init } from './utils/env.server.ts'
-import { getInstanceInfo } from './utils/litefs.server.ts'
-import { NonceProvider } from './utils/nonce-provider.ts'
-import { makeTimings } from './utils/timing.server.ts'
-
-export const streamTimeout = 5000
-
-init()
-global.ENV = getEnv()
-
-const MODE = process.env.NODE_ENV ?? 'development'
-
-type DocRequestArgs = Parameters
-
-export default async function handleRequest(...args: DocRequestArgs) {
- const [request, responseStatusCode, responseHeaders, reactRouterContext] =
- args
- const { currentInstance, primaryInstance } = await getInstanceInfo()
- responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
- responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
- responseHeaders.set('fly-primary-instance', primaryInstance)
- responseHeaders.set('fly-instance', currentInstance)
-
- if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
- responseHeaders.append('Document-Policy', 'js-profiling')
- }
-
- const callbackName = isbot(request.headers.get('user-agent'))
- ? 'onAllReady'
- : 'onShellReady'
-
- const nonce = crypto.randomBytes(16).toString('hex')
- return new Promise(async (resolve, reject) => {
- let didError = false
- // NOTE: this timing will only include things that are rendered in the shell
- // and will not include suspended components and deferred loaders
- const timings = makeTimings('render', 'renderToPipeableStream')
-
- const { pipe, abort } = renderToPipeableStream(
-
-
- ,
- {
- [callbackName]: () => {
- const body = new PassThrough()
- responseHeaders.set('Content-Type', 'text/html')
- responseHeaders.append('Server-Timing', timings.toString())
-
- contentSecurity(responseHeaders, {
- crossOriginEmbedderPolicy: false,
- contentSecurityPolicy: {
- // NOTE: Remove reportOnly when you're ready to enforce this CSP
- reportOnly: true,
- directives: {
- fetch: {
- 'connect-src': [
- MODE === 'development' ? 'ws:' : undefined,
- process.env.SENTRY_DSN ? '*.sentry.io' : undefined,
- "'self'",
- ],
- 'font-src': ["'self'"],
- 'frame-src': ["'self'"],
- 'img-src': ["'self'", 'data:'],
- 'script-src': [
- "'strict-dynamic'",
- "'self'",
- `'nonce-${nonce}'`,
- ],
- 'script-src-attr': [`'nonce-${nonce}'`],
- },
- },
- },
- })
-
- resolve(
- new Response(createReadableStreamFromReadable(body), {
- headers: responseHeaders,
- status: didError ? 500 : responseStatusCode,
- }),
- )
- pipe(body)
- },
- onShellError: (err: unknown) => {
- reject(err)
- },
- onError: () => {
- didError = true
- },
- nonce,
- },
- )
-
- setTimeout(abort, streamTimeout + 5000)
- })
-}
-
-export async function handleDataRequest(response: Response) {
- const { currentInstance, primaryInstance } = await getInstanceInfo()
- response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown')
- response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
- response.headers.set('fly-primary-instance', primaryInstance)
- response.headers.set('fly-instance', currentInstance)
-
- return response
-}
-
-export function handleError(
- error: unknown,
- { request }: LoaderFunctionArgs | ActionFunctionArgs,
-): void {
- // Skip capturing if the request is aborted as Remix docs suggest
- // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
- if (request.signal.aborted) {
- return
- }
-
- if (error instanceof Error) {
- console.error(styleText('red', String(error.stack)))
- } else {
- console.error(error)
- }
-
- Sentry.captureException(error)
-}
diff --git a/exercises/02.test-setup/03.solution.authentication/app/routes/users+/$username_+/notes.tsx b/exercises/02.test-setup/03.solution.authentication/app/routes/users+/$username_+/notes.tsx
deleted file mode 100644
index 90e7b3c..0000000
--- a/exercises/02.test-setup/03.solution.authentication/app/routes/users+/$username_+/notes.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import { invariantResponse } from '@epic-web/invariant'
-import { Img } from 'openimg/react'
-import { Link, NavLink, Outlet } from 'react-router'
-import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
-import { Icon } from '#app/components/ui/icon.tsx'
-import { prisma } from '#app/utils/db.server.ts'
-import { cn, getUserImgSrc } from '#app/utils/misc.tsx'
-import { useOptionalUser } from '#app/utils/user.ts'
-import { type Route } from './+types/notes.ts'
-
-export async function loader({ params }: Route.LoaderArgs) {
- const owner = await prisma.user.findFirst({
- select: {
- id: true,
- name: true,
- username: true,
- image: { select: { objectKey: true } },
- notes: { select: { id: true, title: true } },
- },
- where: { username: params.username },
- })
-
- invariantResponse(owner, 'Owner not found', { status: 404 })
-
- return { owner }
-}
-
-export default function NotesRoute({ loaderData }: Route.ComponentProps) {
- const user = useOptionalUser()
- const isOwner = user?.id === loaderData.owner.id
- const ownerDisplayName = loaderData.owner.name ?? loaderData.owner.username
- const navLinkDefaultClassName =
- 'line-clamp-2 block rounded-l-full py-2 pl-8 pr-6 text-base lg:text-xl'
- return (
-
-
-
-
-
-
-
- {ownerDisplayName}'s Notes
-
-
-
- {isOwner ? (
-
-
- cn(navLinkDefaultClassName, isActive && 'bg-accent')
- }
- >
- New Note
-
-
- ) : null}
- {loaderData.owner.notes.map((note) => (
-
-
- cn(navLinkDefaultClassName, isActive && 'bg-accent')
- }
- >
- {note.title}
-
-
- ))}
-
-
-
-
-
-
-
-
- )
-}
-
-export function ErrorBoundary() {
- return (
- (
- No user with the username "{params.username}" exists
- ),
- }}
- />
- )
-}
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/entry.server.tsx b/exercises/02.test-setup/04.problem.api-mocking/app/entry.server.tsx
deleted file mode 100644
index 391b16a..0000000
--- a/exercises/02.test-setup/04.problem.api-mocking/app/entry.server.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import crypto from 'node:crypto'
-import { PassThrough } from 'node:stream'
-import { styleText } from 'node:util'
-import { contentSecurity } from '@nichtsam/helmet/content'
-import { createReadableStreamFromReadable } from '@react-router/node'
-import * as Sentry from '@sentry/react-router'
-import { isbot } from 'isbot'
-import { renderToPipeableStream } from 'react-dom/server'
-import {
- ServerRouter,
- type LoaderFunctionArgs,
- type ActionFunctionArgs,
- type HandleDocumentRequestFunction,
-} from 'react-router'
-import { getEnv, init } from './utils/env.server.ts'
-import { getInstanceInfo } from './utils/litefs.server.ts'
-import { NonceProvider } from './utils/nonce-provider.ts'
-import { makeTimings } from './utils/timing.server.ts'
-
-export const streamTimeout = 5000
-
-init()
-global.ENV = getEnv()
-
-const MODE = process.env.NODE_ENV ?? 'development'
-
-type DocRequestArgs = Parameters
-
-export default async function handleRequest(...args: DocRequestArgs) {
- const [request, responseStatusCode, responseHeaders, reactRouterContext] =
- args
- const { currentInstance, primaryInstance } = await getInstanceInfo()
- responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
- responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
- responseHeaders.set('fly-primary-instance', primaryInstance)
- responseHeaders.set('fly-instance', currentInstance)
-
- if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
- responseHeaders.append('Document-Policy', 'js-profiling')
- }
-
- const callbackName = isbot(request.headers.get('user-agent'))
- ? 'onAllReady'
- : 'onShellReady'
-
- const nonce = crypto.randomBytes(16).toString('hex')
- return new Promise(async (resolve, reject) => {
- let didError = false
- // NOTE: this timing will only include things that are rendered in the shell
- // and will not include suspended components and deferred loaders
- const timings = makeTimings('render', 'renderToPipeableStream')
-
- const { pipe, abort } = renderToPipeableStream(
-
-
- ,
- {
- [callbackName]: () => {
- const body = new PassThrough()
- responseHeaders.set('Content-Type', 'text/html')
- responseHeaders.append('Server-Timing', timings.toString())
-
- contentSecurity(responseHeaders, {
- crossOriginEmbedderPolicy: false,
- contentSecurityPolicy: {
- // NOTE: Remove reportOnly when you're ready to enforce this CSP
- reportOnly: true,
- directives: {
- fetch: {
- 'connect-src': [
- MODE === 'development' ? 'ws:' : undefined,
- process.env.SENTRY_DSN ? '*.sentry.io' : undefined,
- 'maps.googleapis.com',
- "'self'",
- ],
- 'font-src': ["'self'"],
- 'frame-src': ["'self'"],
- 'img-src': ["'self'", 'data:'],
- 'script-src': [
- "'strict-dynamic'",
- "'self'",
- `'nonce-${nonce}'`,
- ],
- 'script-src-attr': [`'nonce-${nonce}'`],
- },
- },
- },
- })
-
- resolve(
- new Response(createReadableStreamFromReadable(body), {
- headers: responseHeaders,
- status: didError ? 500 : responseStatusCode,
- }),
- )
- pipe(body)
- },
- onShellError: (err: unknown) => {
- reject(err)
- },
- onError: () => {
- didError = true
- },
- nonce,
- },
- )
-
- setTimeout(abort, streamTimeout + 5000)
- })
-}
-
-export async function handleDataRequest(response: Response) {
- const { currentInstance, primaryInstance } = await getInstanceInfo()
- response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown')
- response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
- response.headers.set('fly-primary-instance', primaryInstance)
- response.headers.set('fly-instance', currentInstance)
-
- return response
-}
-
-export function handleError(
- error: unknown,
- { request }: LoaderFunctionArgs | ActionFunctionArgs,
-): void {
- // Skip capturing if the request is aborted as Remix docs suggest
- // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
- if (request.signal.aborted) {
- return
- }
-
- if (error instanceof Error) {
- console.error(styleText('red', String(error.stack)))
- } else {
- console.error(error)
- }
-
- Sentry.captureException(error)
-}
diff --git a/exercises/02.test-setup/04.problem.api-mocking/app/routes/users+/$username_+/notes.tsx b/exercises/02.test-setup/04.problem.api-mocking/app/routes/users+/$username_+/notes.tsx
deleted file mode 100644
index 90e7b3c..0000000
--- a/exercises/02.test-setup/04.problem.api-mocking/app/routes/users+/$username_+/notes.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import { invariantResponse } from '@epic-web/invariant'
-import { Img } from 'openimg/react'
-import { Link, NavLink, Outlet } from 'react-router'
-import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
-import { Icon } from '#app/components/ui/icon.tsx'
-import { prisma } from '#app/utils/db.server.ts'
-import { cn, getUserImgSrc } from '#app/utils/misc.tsx'
-import { useOptionalUser } from '#app/utils/user.ts'
-import { type Route } from './+types/notes.ts'
-
-export async function loader({ params }: Route.LoaderArgs) {
- const owner = await prisma.user.findFirst({
- select: {
- id: true,
- name: true,
- username: true,
- image: { select: { objectKey: true } },
- notes: { select: { id: true, title: true } },
- },
- where: { username: params.username },
- })
-
- invariantResponse(owner, 'Owner not found', { status: 404 })
-
- return { owner }
-}
-
-export default function NotesRoute({ loaderData }: Route.ComponentProps) {
- const user = useOptionalUser()
- const isOwner = user?.id === loaderData.owner.id
- const ownerDisplayName = loaderData.owner.name ?? loaderData.owner.username
- const navLinkDefaultClassName =
- 'line-clamp-2 block rounded-l-full py-2 pl-8 pr-6 text-base lg:text-xl'
- return (
-
-
-
-
-
-
-
- {ownerDisplayName}'s Notes
-
-
-
- {isOwner ? (
-
-
- cn(navLinkDefaultClassName, isActive && 'bg-accent')
- }
- >
- New Note
-
-
- ) : null}
- {loaderData.owner.notes.map((note) => (
-
-
- cn(navLinkDefaultClassName, isActive && 'bg-accent')
- }
- >
- {note.title}
-
-
- ))}
-
-
-
-
-
-
-
-
- )
-}
-
-export function ErrorBoundary() {
- return (
- (
- No user with the username "{params.username}" exists
- ),
- }}
- />
- )
-}
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/entry.server.tsx b/exercises/02.test-setup/04.solution.api-mocking/app/entry.server.tsx
deleted file mode 100644
index 391b16a..0000000
--- a/exercises/02.test-setup/04.solution.api-mocking/app/entry.server.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import crypto from 'node:crypto'
-import { PassThrough } from 'node:stream'
-import { styleText } from 'node:util'
-import { contentSecurity } from '@nichtsam/helmet/content'
-import { createReadableStreamFromReadable } from '@react-router/node'
-import * as Sentry from '@sentry/react-router'
-import { isbot } from 'isbot'
-import { renderToPipeableStream } from 'react-dom/server'
-import {
- ServerRouter,
- type LoaderFunctionArgs,
- type ActionFunctionArgs,
- type HandleDocumentRequestFunction,
-} from 'react-router'
-import { getEnv, init } from './utils/env.server.ts'
-import { getInstanceInfo } from './utils/litefs.server.ts'
-import { NonceProvider } from './utils/nonce-provider.ts'
-import { makeTimings } from './utils/timing.server.ts'
-
-export const streamTimeout = 5000
-
-init()
-global.ENV = getEnv()
-
-const MODE = process.env.NODE_ENV ?? 'development'
-
-type DocRequestArgs = Parameters
-
-export default async function handleRequest(...args: DocRequestArgs) {
- const [request, responseStatusCode, responseHeaders, reactRouterContext] =
- args
- const { currentInstance, primaryInstance } = await getInstanceInfo()
- responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
- responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
- responseHeaders.set('fly-primary-instance', primaryInstance)
- responseHeaders.set('fly-instance', currentInstance)
-
- if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
- responseHeaders.append('Document-Policy', 'js-profiling')
- }
-
- const callbackName = isbot(request.headers.get('user-agent'))
- ? 'onAllReady'
- : 'onShellReady'
-
- const nonce = crypto.randomBytes(16).toString('hex')
- return new Promise(async (resolve, reject) => {
- let didError = false
- // NOTE: this timing will only include things that are rendered in the shell
- // and will not include suspended components and deferred loaders
- const timings = makeTimings('render', 'renderToPipeableStream')
-
- const { pipe, abort } = renderToPipeableStream(
-
-
- ,
- {
- [callbackName]: () => {
- const body = new PassThrough()
- responseHeaders.set('Content-Type', 'text/html')
- responseHeaders.append('Server-Timing', timings.toString())
-
- contentSecurity(responseHeaders, {
- crossOriginEmbedderPolicy: false,
- contentSecurityPolicy: {
- // NOTE: Remove reportOnly when you're ready to enforce this CSP
- reportOnly: true,
- directives: {
- fetch: {
- 'connect-src': [
- MODE === 'development' ? 'ws:' : undefined,
- process.env.SENTRY_DSN ? '*.sentry.io' : undefined,
- 'maps.googleapis.com',
- "'self'",
- ],
- 'font-src': ["'self'"],
- 'frame-src': ["'self'"],
- 'img-src': ["'self'", 'data:'],
- 'script-src': [
- "'strict-dynamic'",
- "'self'",
- `'nonce-${nonce}'`,
- ],
- 'script-src-attr': [`'nonce-${nonce}'`],
- },
- },
- },
- })
-
- resolve(
- new Response(createReadableStreamFromReadable(body), {
- headers: responseHeaders,
- status: didError ? 500 : responseStatusCode,
- }),
- )
- pipe(body)
- },
- onShellError: (err: unknown) => {
- reject(err)
- },
- onError: () => {
- didError = true
- },
- nonce,
- },
- )
-
- setTimeout(abort, streamTimeout + 5000)
- })
-}
-
-export async function handleDataRequest(response: Response) {
- const { currentInstance, primaryInstance } = await getInstanceInfo()
- response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown')
- response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
- response.headers.set('fly-primary-instance', primaryInstance)
- response.headers.set('fly-instance', currentInstance)
-
- return response
-}
-
-export function handleError(
- error: unknown,
- { request }: LoaderFunctionArgs | ActionFunctionArgs,
-): void {
- // Skip capturing if the request is aborted as Remix docs suggest
- // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
- if (request.signal.aborted) {
- return
- }
-
- if (error instanceof Error) {
- console.error(styleText('red', String(error.stack)))
- } else {
- console.error(error)
- }
-
- Sentry.captureException(error)
-}
diff --git a/exercises/02.test-setup/04.solution.api-mocking/app/routes/users+/$username_+/notes.tsx b/exercises/02.test-setup/04.solution.api-mocking/app/routes/users+/$username_+/notes.tsx
deleted file mode 100644
index 90e7b3c..0000000
--- a/exercises/02.test-setup/04.solution.api-mocking/app/routes/users+/$username_+/notes.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import { invariantResponse } from '@epic-web/invariant'
-import { Img } from 'openimg/react'
-import { Link, NavLink, Outlet } from 'react-router'
-import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
-import { Icon } from '#app/components/ui/icon.tsx'
-import { prisma } from '#app/utils/db.server.ts'
-import { cn, getUserImgSrc } from '#app/utils/misc.tsx'
-import { useOptionalUser } from '#app/utils/user.ts'
-import { type Route } from './+types/notes.ts'
-
-export async function loader({ params }: Route.LoaderArgs) {
- const owner = await prisma.user.findFirst({
- select: {
- id: true,
- name: true,
- username: true,
- image: { select: { objectKey: true } },
- notes: { select: { id: true, title: true } },
- },
- where: { username: params.username },
- })
-
- invariantResponse(owner, 'Owner not found', { status: 404 })
-
- return { owner }
-}
-
-export default function NotesRoute({ loaderData }: Route.ComponentProps) {
- const user = useOptionalUser()
- const isOwner = user?.id === loaderData.owner.id
- const ownerDisplayName = loaderData.owner.name ?? loaderData.owner.username
- const navLinkDefaultClassName =
- 'line-clamp-2 block rounded-l-full py-2 pl-8 pr-6 text-base lg:text-xl'
- return (
-
-
-
-
-
-
-
- {ownerDisplayName}'s Notes
-
-
-
- {isOwner ? (
-
-
- cn(navLinkDefaultClassName, isActive && 'bg-accent')
- }
- >
- New Note
-
-
- ) : null}
- {loaderData.owner.notes.map((note) => (
-
-
- cn(navLinkDefaultClassName, isActive && 'bg-accent')
- }
- >
- {note.title}
-
-
- ))}
-
-
-
-
-
-
-
-
- )
-}
-
-export function ErrorBoundary() {
- return (
- (
- No user with the username "{params.username}" exists
- ),
- }}
- />
- )
-}
diff --git a/exercises/02.test-setup/05.problem.test-data/app/entry.server.tsx b/exercises/02.test-setup/05.problem.test-data/app/entry.server.tsx
deleted file mode 100644
index 99fdd4b..0000000
--- a/exercises/02.test-setup/05.problem.test-data/app/entry.server.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import crypto from 'node:crypto'
-import { PassThrough } from 'node:stream'
-import { styleText } from 'node:util'
-import { contentSecurity } from '@nichtsam/helmet/content'
-import { createReadableStreamFromReadable } from '@react-router/node'
-import * as Sentry from '@sentry/react-router'
-import { isbot } from 'isbot'
-import { renderToPipeableStream } from 'react-dom/server'
-import {
- ServerRouter,
- type LoaderFunctionArgs,
- type ActionFunctionArgs,
- type HandleDocumentRequestFunction,
-} from 'react-router'
-import { getEnv, init } from './utils/env.server.ts'
-import { getInstanceInfo } from './utils/litefs.server.ts'
-import { NonceProvider } from './utils/nonce-provider.ts'
-import { makeTimings } from './utils/timing.server.ts'
-
-export const streamTimeout = 5000
-
-init()
-global.ENV = getEnv()
-
-const MODE = process.env.NODE_ENV ?? 'development'
-
-type DocRequestArgs = Parameters
-
-export default async function handleRequest(...args: DocRequestArgs) {
- const [request, responseStatusCode, responseHeaders, reactRouterContext] =
- args
- const { currentInstance, primaryInstance } = await getInstanceInfo()
- responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
- responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
- responseHeaders.set('fly-primary-instance', primaryInstance)
- responseHeaders.set('fly-instance', currentInstance)
-
- if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
- responseHeaders.append('Document-Policy', 'js-profiling')
- }
-
- const callbackName = isbot(request.headers.get('user-agent'))
- ? 'onAllReady'
- : 'onShellReady'
-
- const nonce = crypto.randomBytes(16).toString('hex')
- return new Promise(async (resolve, reject) => {
- let didError = false
- // NOTE: this timing will only include things that are rendered in the shell
- // and will not include suspended components and deferred loaders
- const timings = makeTimings('render', 'renderToPipeableStream')
-
- const { pipe, abort } = renderToPipeableStream(
-
-
- ,
- {
- [callbackName]: () => {
- const body = new PassThrough()
- responseHeaders.set('Content-Type', 'text/html')
- responseHeaders.append('Server-Timing', timings.toString())
-
- contentSecurity(responseHeaders, {
- crossOriginEmbedderPolicy: false,
- contentSecurityPolicy: {
- // NOTE: Remove reportOnly when you're ready to enforce this CSP
- reportOnly: true,
- directives: {
- fetch: {
- 'connect-src': [
- MODE === 'development' ? 'ws:' : undefined,
- process.env.SENTRY_DSN ? '*.sentry.io' : undefined,
- "'self'",
- ],
- 'font-src': ["'self'"],
- 'frame-src': ["'self'"],
- 'img-src': ["'self'", 'data:'],
- 'script-src': [
- "'strict-dynamic'",
- "'self'",
- `'nonce-${nonce}'`,
- ],
- 'script-src-attr': [`'nonce-${nonce}'`],
- },
- },
- },
- })
-
- resolve(
- new Response(createReadableStreamFromReadable(body), {
- headers: responseHeaders,
- status: didError ? 500 : responseStatusCode,
- }),
- )
- pipe(body)
- },
- onShellError: (err: unknown) => {
- reject(err)
- },
- onError: () => {
- didError = true
- },
- nonce,
- },
- )
-
- setTimeout(abort, streamTimeout + 5000)
- })
-}
-
-export async function handleDataRequest(response: Response) {
- const { currentInstance, primaryInstance } = await getInstanceInfo()
- response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown')
- response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
- response.headers.set('fly-primary-instance', primaryInstance)
- response.headers.set('fly-instance', currentInstance)
-
- return response
-}
-
-export function handleError(
- error: unknown,
- { request }: LoaderFunctionArgs | ActionFunctionArgs,
-): void {
- // Skip capturing if the request is aborted as Remix docs suggest
- // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
- if (request.signal.aborted) {
- return
- }
-
- if (error instanceof Error) {
- console.error(styleText('red', String(error.stack)))
- } else {
- console.error(error)
- }
-
- Sentry.captureException(error)
-}
diff --git a/exercises/02.test-setup/05.solution.test-data/app/entry.server.tsx b/exercises/02.test-setup/05.solution.test-data/app/entry.server.tsx
deleted file mode 100644
index 99fdd4b..0000000
--- a/exercises/02.test-setup/05.solution.test-data/app/entry.server.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import crypto from 'node:crypto'
-import { PassThrough } from 'node:stream'
-import { styleText } from 'node:util'
-import { contentSecurity } from '@nichtsam/helmet/content'
-import { createReadableStreamFromReadable } from '@react-router/node'
-import * as Sentry from '@sentry/react-router'
-import { isbot } from 'isbot'
-import { renderToPipeableStream } from 'react-dom/server'
-import {
- ServerRouter,
- type LoaderFunctionArgs,
- type ActionFunctionArgs,
- type HandleDocumentRequestFunction,
-} from 'react-router'
-import { getEnv, init } from './utils/env.server.ts'
-import { getInstanceInfo } from './utils/litefs.server.ts'
-import { NonceProvider } from './utils/nonce-provider.ts'
-import { makeTimings } from './utils/timing.server.ts'
-
-export const streamTimeout = 5000
-
-init()
-global.ENV = getEnv()
-
-const MODE = process.env.NODE_ENV ?? 'development'
-
-type DocRequestArgs = Parameters
-
-export default async function handleRequest(...args: DocRequestArgs) {
- const [request, responseStatusCode, responseHeaders, reactRouterContext] =
- args
- const { currentInstance, primaryInstance } = await getInstanceInfo()
- responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
- responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
- responseHeaders.set('fly-primary-instance', primaryInstance)
- responseHeaders.set('fly-instance', currentInstance)
-
- if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
- responseHeaders.append('Document-Policy', 'js-profiling')
- }
-
- const callbackName = isbot(request.headers.get('user-agent'))
- ? 'onAllReady'
- : 'onShellReady'
-
- const nonce = crypto.randomBytes(16).toString('hex')
- return new Promise(async (resolve, reject) => {
- let didError = false
- // NOTE: this timing will only include things that are rendered in the shell
- // and will not include suspended components and deferred loaders
- const timings = makeTimings('render', 'renderToPipeableStream')
-
- const { pipe, abort } = renderToPipeableStream(
-
-
- ,
- {
- [callbackName]: () => {
- const body = new PassThrough()
- responseHeaders.set('Content-Type', 'text/html')
- responseHeaders.append('Server-Timing', timings.toString())
-
- contentSecurity(responseHeaders, {
- crossOriginEmbedderPolicy: false,
- contentSecurityPolicy: {
- // NOTE: Remove reportOnly when you're ready to enforce this CSP
- reportOnly: true,
- directives: {
- fetch: {
- 'connect-src': [
- MODE === 'development' ? 'ws:' : undefined,
- process.env.SENTRY_DSN ? '*.sentry.io' : undefined,
- "'self'",
- ],
- 'font-src': ["'self'"],
- 'frame-src': ["'self'"],
- 'img-src': ["'self'", 'data:'],
- 'script-src': [
- "'strict-dynamic'",
- "'self'",
- `'nonce-${nonce}'`,
- ],
- 'script-src-attr': [`'nonce-${nonce}'`],
- },
- },
- },
- })
-
- resolve(
- new Response(createReadableStreamFromReadable(body), {
- headers: responseHeaders,
- status: didError ? 500 : responseStatusCode,
- }),
- )
- pipe(body)
- },
- onShellError: (err: unknown) => {
- reject(err)
- },
- onError: () => {
- didError = true
- },
- nonce,
- },
- )
-
- setTimeout(abort, streamTimeout + 5000)
- })
-}
-
-export async function handleDataRequest(response: Response) {
- const { currentInstance, primaryInstance } = await getInstanceInfo()
- response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown')
- response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
- response.headers.set('fly-primary-instance', primaryInstance)
- response.headers.set('fly-instance', currentInstance)
-
- return response
-}
-
-export function handleError(
- error: unknown,
- { request }: LoaderFunctionArgs | ActionFunctionArgs,
-): void {
- // Skip capturing if the request is aborted as Remix docs suggest
- // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
- if (request.signal.aborted) {
- return
- }
-
- if (error instanceof Error) {
- console.error(styleText('red', String(error.stack)))
- } else {
- console.error(error)
- }
-
- Sentry.captureException(error)
-}
diff --git a/exercises/02.test-setup/FINISHED.mdx b/exercises/02.test-setup/FINISHED.mdx
deleted file mode 100644
index f593c92..0000000
--- a/exercises/02.test-setup/FINISHED.mdx
+++ /dev/null
@@ -1,5 +0,0 @@
-# Test setup
-
-Congratulations, you've completed this exercise block! 🥳 Let's have a short rest and continue.
-
-Meanwhile, I would be grateful if you _filled out the feedback form_ on your right. Your feedback helps me polish the exercises and improve the workshop before recording it. Thank you!
diff --git a/exercises/02.test-setup/README.mdx b/exercises/02.test-setup/README.mdx
deleted file mode 100644
index 5263799..0000000
--- a/exercises/02.test-setup/README.mdx
+++ /dev/null
@@ -1 +0,0 @@
-# Test setup
diff --git a/exercises/03.guides/02.problem.test-annotations/.env b/exercises/03.guides/01.problem.mock-database/.env
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/.env
rename to exercises/03.guides/01.problem.mock-database/.env
diff --git a/exercises/03.guides/02.problem.test-annotations/.env.example b/exercises/03.guides/01.problem.mock-database/.env.example
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/.env.example
rename to exercises/03.guides/01.problem.mock-database/.env.example
diff --git a/exercises/03.guides/02.problem.test-annotations/.gitignore b/exercises/03.guides/01.problem.mock-database/.gitignore
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/.gitignore
rename to exercises/03.guides/01.problem.mock-database/.gitignore
diff --git a/exercises/03.guides/02.problem.test-annotations/.npmrc b/exercises/03.guides/01.problem.mock-database/.npmrc
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/.npmrc
rename to exercises/03.guides/01.problem.mock-database/.npmrc
diff --git a/exercises/03.guides/02.problem.test-annotations/.prettierignore b/exercises/03.guides/01.problem.mock-database/.prettierignore
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/.prettierignore
rename to exercises/03.guides/01.problem.mock-database/.prettierignore
diff --git a/exercises/03.guides/03.solution.blocking-unneeded-requests/.vscode/extensions.json b/exercises/03.guides/01.problem.mock-database/.vscode/extensions.json
similarity index 75%
rename from exercises/03.guides/03.solution.blocking-unneeded-requests/.vscode/extensions.json
rename to exercises/03.guides/01.problem.mock-database/.vscode/extensions.json
index f724eea..3c0a690 100644
--- a/exercises/03.guides/03.solution.blocking-unneeded-requests/.vscode/extensions.json
+++ b/exercises/03.guides/01.problem.mock-database/.vscode/extensions.json
@@ -6,7 +6,6 @@
"prisma.prisma",
"qwtel.sqlite-viewer",
"yoavbls.pretty-ts-errors",
- "github.vscode-github-actions",
- "ms-playwright.playwright"
+ "github.vscode-github-actions"
]
}
diff --git a/exercises/03.guides/02.problem.test-annotations/.vscode/remix.code-snippets b/exercises/03.guides/01.problem.mock-database/.vscode/remix.code-snippets
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/.vscode/remix.code-snippets
rename to exercises/03.guides/01.problem.mock-database/.vscode/remix.code-snippets
diff --git a/exercises/03.guides/02.problem.test-annotations/.vscode/settings.json b/exercises/03.guides/01.problem.mock-database/.vscode/settings.json
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/.vscode/settings.json
rename to exercises/03.guides/01.problem.mock-database/.vscode/settings.json
diff --git a/exercises/03.guides/01.problem.mock-database/README.mdx b/exercises/03.guides/01.problem.mock-database/README.mdx
new file mode 100644
index 0000000..4bbc33a
--- /dev/null
+++ b/exercises/03.guides/01.problem.mock-database/README.mdx
@@ -0,0 +1,10 @@
+# Mock database
+
+- File-based mock database in SQLite.
+- Configuring environment to use a different `DATABASE_URL`.
+
+## Changes
+
+- Created `.env.test` and put a different `DATABASE_URL` there, pointing at `./prisma/test.db`.
+- Created `playwright.setup.ts` to (1) reset the test DB before the test run; (2) prepare the test database using the same `prisma:setup` script.
+- Configured `globalSetup` in `playwright.config.ts`.
diff --git a/exercises/03.guides/02.problem.test-annotations/app/assets/favicons/apple-touch-icon.png b/exercises/03.guides/01.problem.mock-database/app/assets/favicons/apple-touch-icon.png
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/assets/favicons/apple-touch-icon.png
rename to exercises/03.guides/01.problem.mock-database/app/assets/favicons/apple-touch-icon.png
diff --git a/exercises/03.guides/02.problem.test-annotations/app/assets/favicons/favicon.svg b/exercises/03.guides/01.problem.mock-database/app/assets/favicons/favicon.svg
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/assets/favicons/favicon.svg
rename to exercises/03.guides/01.problem.mock-database/app/assets/favicons/favicon.svg
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/error-boundary.tsx b/exercises/03.guides/01.problem.mock-database/app/components/error-boundary.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/error-boundary.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/error-boundary.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/floating-toolbar.tsx b/exercises/03.guides/01.problem.mock-database/app/components/floating-toolbar.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/floating-toolbar.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/floating-toolbar.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/forms.tsx b/exercises/03.guides/01.problem.mock-database/app/components/forms.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/forms.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/forms.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/progress-bar.tsx b/exercises/03.guides/01.problem.mock-database/app/components/progress-bar.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/progress-bar.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/progress-bar.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/search-bar.tsx b/exercises/03.guides/01.problem.mock-database/app/components/search-bar.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/search-bar.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/search-bar.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/spacer.tsx b/exercises/03.guides/01.problem.mock-database/app/components/spacer.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/spacer.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/spacer.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/toaster.tsx b/exercises/03.guides/01.problem.mock-database/app/components/toaster.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/toaster.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/toaster.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/ui/README.md b/exercises/03.guides/01.problem.mock-database/app/components/ui/README.md
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/ui/README.md
rename to exercises/03.guides/01.problem.mock-database/app/components/ui/README.md
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/ui/button.tsx b/exercises/03.guides/01.problem.mock-database/app/components/ui/button.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/ui/button.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/ui/button.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/ui/checkbox.tsx b/exercises/03.guides/01.problem.mock-database/app/components/ui/checkbox.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/ui/checkbox.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/ui/checkbox.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/ui/dropdown-menu.tsx b/exercises/03.guides/01.problem.mock-database/app/components/ui/dropdown-menu.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/ui/dropdown-menu.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/ui/dropdown-menu.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/ui/icon.tsx b/exercises/03.guides/01.problem.mock-database/app/components/ui/icon.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/ui/icon.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/ui/icon.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/ui/input-otp.tsx b/exercises/03.guides/01.problem.mock-database/app/components/ui/input-otp.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/ui/input-otp.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/ui/input-otp.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/ui/input.tsx b/exercises/03.guides/01.problem.mock-database/app/components/ui/input.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/ui/input.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/ui/input.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/ui/label.tsx b/exercises/03.guides/01.problem.mock-database/app/components/ui/label.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/ui/label.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/ui/label.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/ui/sonner.tsx b/exercises/03.guides/01.problem.mock-database/app/components/ui/sonner.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/ui/sonner.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/ui/sonner.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/ui/status-button.tsx b/exercises/03.guides/01.problem.mock-database/app/components/ui/status-button.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/ui/status-button.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/ui/status-button.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/ui/textarea.tsx b/exercises/03.guides/01.problem.mock-database/app/components/ui/textarea.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/ui/textarea.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/ui/textarea.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/ui/tooltip.tsx b/exercises/03.guides/01.problem.mock-database/app/components/ui/tooltip.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/ui/tooltip.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/ui/tooltip.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/components/user-dropdown.tsx b/exercises/03.guides/01.problem.mock-database/app/components/user-dropdown.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/components/user-dropdown.tsx
rename to exercises/03.guides/01.problem.mock-database/app/components/user-dropdown.tsx
diff --git a/exercises/03.guides/02.problem.test-annotations/app/entry.client.tsx b/exercises/03.guides/01.problem.mock-database/app/entry.client.tsx
similarity index 100%
rename from exercises/03.guides/02.problem.test-annotations/app/entry.client.tsx
rename to exercises/03.guides/01.problem.mock-database/app/entry.client.tsx
diff --git a/exercises/03.guides/01.problem.mock-database/app/entry.server.tsx b/exercises/03.guides/01.problem.mock-database/app/entry.server.tsx
new file mode 100644
index 0000000..8d8b1de
--- /dev/null
+++ b/exercises/03.guides/01.problem.mock-database/app/entry.server.tsx
@@ -0,0 +1,143 @@
+import crypto from 'node:crypto'
+import { PassThrough } from 'node:stream'
+import { styleText } from 'node:util'
+import { contentSecurity } from '@nichtsam/helmet/content'
+import { createReadableStreamFromReadable } from '@react-router/node'
+import * as Sentry from '@sentry/react-router'
+import { isbot } from 'isbot'
+import { renderToPipeableStream } from 'react-dom/server'
+import {
+ ServerRouter,
+ type LoaderFunctionArgs,
+ type ActionFunctionArgs,
+ type HandleDocumentRequestFunction,
+} from 'react-router'
+import { getEnv, init } from './utils/env.server.ts'
+import { getInstanceInfo } from './utils/litefs.server.ts'
+import { NonceProvider } from './utils/nonce-provider.ts'
+import { makeTimings } from './utils/timing.server.ts'
+
+export const streamTimeout = 5000
+
+init()
+global.ENV = getEnv()
+
+const MODE = process.env.NODE_ENV ?? 'development'
+
+type DocRequestArgs = Parameters
+
+export default async function handleRequest(...args: DocRequestArgs) {
+ const [request, responseStatusCode, responseHeaders, reactRouterContext] =
+ args
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ responseHeaders.set('fly-primary-instance', primaryInstance)
+ responseHeaders.set('fly-instance', currentInstance)
+
+ if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
+ responseHeaders.append('Document-Policy', 'js-profiling')
+ }
+
+ const callbackName = isbot(request.headers.get('user-agent'))
+ ? 'onAllReady'
+ : 'onShellReady'
+
+ const nonce = crypto.randomBytes(16).toString('hex')
+ return new Promise(async (resolve, reject) => {
+ let didError = false
+ // NOTE: this timing will only include things that are rendered in the shell
+ // and will not include suspended components and deferred loaders
+ const timings = makeTimings('render', 'renderToPipeableStream')
+
+ const { pipe, abort } = renderToPipeableStream(
+
+
+ ,
+ {
+ [callbackName]: () => {
+ const body = new PassThrough()
+ responseHeaders.set('Content-Type', 'text/html')
+ responseHeaders.append('Server-Timing', timings.toString())
+
+ contentSecurity(responseHeaders, {
+ crossOriginEmbedderPolicy: false,
+ contentSecurityPolicy: {
+ // NOTE: Remove reportOnly when you're ready to enforce this CSP
+ reportOnly: true,
+ directives: {
+ fetch: {
+ 'connect-src': [
+ MODE === 'development' ? 'ws:' : undefined,
+ process.env.SENTRY_DSN ? '*.sentry.io' : undefined,
+ "'self'",
+ ],
+ 'font-src': ["'self'"],
+ 'frame-src': ["'self'"],
+ 'img-src': ["'self'", 'data:'],
+ 'script-src': [
+ "'strict-dynamic'",
+ "'self'",
+ `'nonce-${nonce}'`,
+ ],
+ 'script-src-attr': [`'nonce-${nonce}'`],
+ },
+ },
+ },
+ xFrameOptions: false,
+ })
+
+ resolve(
+ new Response(createReadableStreamFromReadable(body), {
+ headers: responseHeaders,
+ status: didError ? 500 : responseStatusCode,
+ }),
+ )
+ pipe(body)
+ },
+ onShellError: (err: unknown) => {
+ reject(err)
+ },
+ onError: () => {
+ didError = true
+ },
+ nonce,
+ },
+ )
+
+ setTimeout(abort, streamTimeout + 5000)
+ })
+}
+
+export async function handleDataRequest(response: Response) {
+ const { currentInstance, primaryInstance } = await getInstanceInfo()
+ response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown')
+ response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
+ response.headers.set('fly-primary-instance', primaryInstance)
+ response.headers.set('fly-instance', currentInstance)
+
+ return response
+}
+
+export function handleError(
+ error: unknown,
+ { request }: LoaderFunctionArgs | ActionFunctionArgs,
+): void {
+ // Skip capturing if the request is aborted as Remix docs suggest
+ // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
+ if (request.signal.aborted) {
+ return
+ }
+
+ if (error instanceof Error) {
+ console.error(styleText('red', String(error.stack)))
+ } else {
+ console.error(error)
+ }
+
+ Sentry.captureException(error)
+}
diff --git a/exercises/03.guides/03.problem.blocking-unneeded-requests/app/root.tsx b/exercises/03.guides/01.problem.mock-database/app/root.tsx
similarity index 98%
rename from exercises/03.guides/03.problem.blocking-unneeded-requests/app/root.tsx
rename to exercises/03.guides/01.problem.mock-database/app/root.tsx
index 5bbdc2f..0f0d1cb 100644
--- a/exercises/03.guides/03.problem.blocking-unneeded-requests/app/root.tsx
+++ b/exercises/03.guides/01.problem.mock-database/app/root.tsx
@@ -166,11 +166,6 @@ function Document({
__html: `window.ENV = ${JSON.stringify(env)}`,
}}
/>
-