From e163d0281b18bea38f68502246d07053bb1d5b35 Mon Sep 17 00:00:00 2001
From: Andres J
Date: Fri, 6 Feb 2026 21:33:59 -0600
Subject: [PATCH 1/3] =?UTF-8?q?ui(router-fm):=20integrar=20Pico=20CSS=20po?=
=?UTF-8?q?r=20CDN;=20simplificar=20login/signup=20con=20formularios=20sem?=
=?UTF-8?q?=C3=A1nticos?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
apps/react-app-router-fm/app/root.tsx | 11 +-
apps/react-app-router-fm/app/routes/login.tsx | 97 +++++---------
.../react-app-router-fm/app/routes/signup.tsx | 124 +++++++-----------
3 files changed, 92 insertions(+), 140 deletions(-)
diff --git a/apps/react-app-router-fm/app/root.tsx b/apps/react-app-router-fm/app/root.tsx
index d29b412f..8ef1d70c 100644
--- a/apps/react-app-router-fm/app/root.tsx
+++ b/apps/react-app-router-fm/app/root.tsx
@@ -20,6 +20,10 @@ export const links: LinksFunction = () => [
href:
"https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
},
+ {
+ rel: "stylesheet",
+ href: "https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css",
+ },
]
export function Layout({ children }: { children: React.ReactNode }) {
@@ -28,12 +32,15 @@ export function Layout({ children }: { children: React.ReactNode }) {
+
-
- {children}
+
+ {children}
diff --git a/apps/react-app-router-fm/app/routes/login.tsx b/apps/react-app-router-fm/app/routes/login.tsx
index e0b22373..74cbab52 100644
--- a/apps/react-app-router-fm/app/routes/login.tsx
+++ b/apps/react-app-router-fm/app/routes/login.tsx
@@ -30,66 +30,41 @@ export default function Login() {
}
return (
-
-
-
-
- Sign in to your account
-
-
-
-
-
-
+
+ Sign in to your account
+
+
+
)
}
diff --git a/apps/react-app-router-fm/app/routes/signup.tsx b/apps/react-app-router-fm/app/routes/signup.tsx
index 859e48bb..a418aaa0 100644
--- a/apps/react-app-router-fm/app/routes/signup.tsx
+++ b/apps/react-app-router-fm/app/routes/signup.tsx
@@ -32,82 +32,52 @@ export default function SignUp() {
}
return (
-
-
-
-
- Create an account
-
-
-
-
-
-
+
+ Create an account
+
+
+
)
}
From 1094dd4a6dc62461cc8e795a49a0fea30a2439d3 Mon Sep 17 00:00:00 2001
From: Andres J
Date: Fri, 6 Feb 2026 23:02:35 -0600
Subject: [PATCH 2/3] feat(router-fm): responsive nav UX, responsive todo
container, protect todo route with auth guard; better-auth pg pool for
migrate CLI
---
apps/react-app-router-fm/app/app-nav.tsx | 49 ++++-
.../app/lib/better-auth-options.server.ts | 6 +-
apps/react-app-router-fm/app/root.tsx | 13 +-
apps/react-app-router-fm/app/routes.tsx | 2 +-
apps/react-app-router-fm/app/routes/about.tsx | 98 ---------
apps/react-app-router-fm/app/routes/login.tsx | 72 +++----
.../react-app-router-fm/app/routes/signup.tsx | 96 ++++-----
.../app/routes/todo-app.tsx | 193 ++++++++++++++++++
8 files changed, 329 insertions(+), 200 deletions(-)
delete mode 100644 apps/react-app-router-fm/app/routes/about.tsx
create mode 100644 apps/react-app-router-fm/app/routes/todo-app.tsx
diff --git a/apps/react-app-router-fm/app/app-nav.tsx b/apps/react-app-router-fm/app/app-nav.tsx
index 8b23a3eb..3c5f6ad6 100644
--- a/apps/react-app-router-fm/app/app-nav.tsx
+++ b/apps/react-app-router-fm/app/app-nav.tsx
@@ -1,16 +1,47 @@
-import { NavLink } from "react-router"
+import { NavLink, useNavigate } from "react-router"
import { authClient } from "./lib/auth-client.js"
export function AppNav() {
+ const navigate = useNavigate()
return (
-
- )
-}
diff --git a/apps/react-app-router-fm/app/routes/login.tsx b/apps/react-app-router-fm/app/routes/login.tsx
index 74cbab52..d77b256c 100644
--- a/apps/react-app-router-fm/app/routes/login.tsx
+++ b/apps/react-app-router-fm/app/routes/login.tsx
@@ -30,41 +30,43 @@ export default function Login() {
}
return (
-
- Sign in to your account
-
-
-
+
+
+
)
}
diff --git a/apps/react-app-router-fm/app/routes/signup.tsx b/apps/react-app-router-fm/app/routes/signup.tsx
index a418aaa0..f2be9eb7 100644
--- a/apps/react-app-router-fm/app/routes/signup.tsx
+++ b/apps/react-app-router-fm/app/routes/signup.tsx
@@ -32,52 +32,56 @@ export default function SignUp() {
}
return (
-
- Create an account
-
-
-
+
+
+
)
}
diff --git a/apps/react-app-router-fm/app/routes/todo-app.tsx b/apps/react-app-router-fm/app/routes/todo-app.tsx
new file mode 100644
index 00000000..f71763eb
--- /dev/null
+++ b/apps/react-app-router-fm/app/routes/todo-app.tsx
@@ -0,0 +1,193 @@
+import type { Route } from "./+types/todo-app.js"
+import * as Effect from "effect/Effect"
+import { ActionArgsContext, httpFailure, httpRedirect, httpSuccess } from "@effectify/react-router"
+import { withLoaderEffect } from "../lib/runtime.server.js"
+import { withActionEffect } from "../lib/runtime.server.js"
+import { randomUUID } from "node:crypto"
+import * as PrismaRepository from "@prisma/effect/prisma-repository.js"
+import { TodoId, TodoModel } from "@prisma/effect/models/Todo.js"
+import { TodoStatus } from "@prisma/enums.js"
+import { Form, useActionData, useSubmit } from "react-router"
+import { useState } from "react"
+import { withBetterAuthGuard, withBetterAuthGuardAction } from "@effectify/react-router-better-auth"
+
+export const loader = Effect.gen(function*() {
+ const todoRepo = yield* PrismaRepository.make(TodoModel, {
+ modelName: "todo",
+ spanPrefix: "todo",
+ })
+
+ const todos = yield* todoRepo.findMany({})
+ return yield* httpSuccess({
+ todos,
+ })
+})
+ .pipe(withBetterAuthGuard.with({ redirectOnFail: "/login" }))
+ .pipe(withLoaderEffect)
+
+export const action = Effect.gen(function*() {
+ const { request } = yield* ActionArgsContext
+ const formData = yield* Effect.tryPromise(() => request.formData())
+ const intent = String(formData.get("intent") ?? "create")
+ const id = String(formData.get("id") ?? "")
+ const title = String(formData.get("title") ?? "")
+ const content = String(formData.get("content") ?? "")
+
+ const todoRepo = yield* PrismaRepository.make(TodoModel, {
+ modelName: "todo",
+ spanPrefix: "todo",
+ })
+
+ if (intent === "delete") {
+ if (!id) {
+ return yield* httpFailure("Missing id")
+ }
+ yield* todoRepo.delete({
+ where: { id: TodoId.make(id) },
+ })
+ return yield* httpRedirect("/todo-app")
+ }
+
+ if (intent === "update") {
+ if (!id) {
+ return yield* httpFailure("Missing id")
+ }
+ if (!title.trim()) {
+ return yield* httpFailure("Title is required")
+ }
+ yield* todoRepo.update({
+ where: { id: TodoId.make(id) },
+ data: { title, content },
+ })
+ return yield* httpRedirect("/todo-app")
+ }
+
+ if (intent === "toggle-status") {
+ if (!id) {
+ return yield* httpFailure("Missing id")
+ }
+ const statusStr = String(formData.get("status") ?? "")
+ const status = statusStr === "COMPLETED" ? TodoStatus.COMPLETED : TodoStatus.PENDING
+ yield* todoRepo.update({
+ where: { id: TodoId.make(id) },
+ data: { status },
+ })
+ return yield* httpRedirect("/todo-app")
+ }
+
+ if (!title.trim()) {
+ return yield* httpFailure("Title is required")
+ }
+
+ yield* todoRepo.create({
+ data: {
+ id: TodoId.make(randomUUID()),
+ title,
+ content,
+ published: false,
+ authorId: 1,
+ status: TodoStatus.PENDING,
+ },
+ })
+
+ return yield* httpRedirect("/todo-app")
+})
+ .pipe(withBetterAuthGuardAction.with({ redirectOnFail: "/login" }))
+ .pipe(withActionEffect)
+
+export default function TodoApp({
+ loaderData,
+}: Route.ComponentProps) {
+ const actionData = useActionData()
+ const submit = useSubmit()
+ const [editingId, setEditingId] = useState(null)
+ if (loaderData.ok) {
+ return (
+
+
+ Todo App
+ {loaderData.data?.todos?.length} items
+
+
+
+
+ )
+ }
+ return (
+
+
+ Todo App
+
+
+ )
+}
From 6f2ba957cdcc016633e6459814bedd1e72437a1f Mon Sep 17 00:00:00 2001
From: Andres J
Date: Fri, 6 Feb 2026 23:21:56 -0600
Subject: [PATCH 3/3] refactor(auth): improve security and user data isolation
- Replace Pico CSS with classless version and add integrity/crossorigin for security
- Centralize database pool usage and add validation for DATABASE_URL
- Use React Router Link for internal navigation instead of anchor tags
- Add ARIA attributes to error messages for accessibility
- Remove console.error in signup to avoid exposing error details
- Implement user-specific todo operations by filtering by authorId
- Use updateMany/deleteMany with user ID constraints to prevent unauthorized access
---
.../app/lib/better-auth-options.server.ts | 4 +--
apps/react-app-router-fm/app/lib/prisma.ts | 5 ++-
apps/react-app-router-fm/app/root.tsx | 4 ++-
apps/react-app-router-fm/app/routes/login.tsx | 8 +++--
.../react-app-router-fm/app/routes/signup.tsx | 9 ++---
.../app/routes/todo-app.tsx | 34 ++++++++++++++-----
6 files changed, 43 insertions(+), 21 deletions(-)
diff --git a/apps/react-app-router-fm/app/lib/better-auth-options.server.ts b/apps/react-app-router-fm/app/lib/better-auth-options.server.ts
index 94de0e93..79ca89ea 100644
--- a/apps/react-app-router-fm/app/lib/better-auth-options.server.ts
+++ b/apps/react-app-router-fm/app/lib/better-auth-options.server.ts
@@ -4,9 +4,7 @@
import { betterAuth } from "better-auth"
import { openAPI } from "better-auth/plugins"
import type { BetterAuthOptions } from "better-auth/types"
-import { Pool } from "pg"
-
-const pool = new Pool({ connectionString: `${process.env.DATABASE_URL}` })
+import { pool } from "./prisma.js"
// const __filename = fileURLToPath(import.meta.url)
// const __dirname = dirname(__filename)
diff --git a/apps/react-app-router-fm/app/lib/prisma.ts b/apps/react-app-router-fm/app/lib/prisma.ts
index a15d9789..5e5505a5 100644
--- a/apps/react-app-router-fm/app/lib/prisma.ts
+++ b/apps/react-app-router-fm/app/lib/prisma.ts
@@ -3,7 +3,10 @@ import { PrismaPg } from "@prisma/adapter-pg"
import { Pool } from "pg"
import { PrismaClient } from "../../prisma/generated/client.js"
-const connectionString = `${process.env.DATABASE_URL}`
+const connectionString = process.env.DATABASE_URL
+if (!connectionString || connectionString.trim().length === 0) {
+ throw new Error("Missing DATABASE_URL environment variable")
+}
export const pool = new Pool({ connectionString })
const adapter = new PrismaPg({ connectionString })
diff --git a/apps/react-app-router-fm/app/root.tsx b/apps/react-app-router-fm/app/root.tsx
index e0ed024d..c5c79099 100644
--- a/apps/react-app-router-fm/app/root.tsx
+++ b/apps/react-app-router-fm/app/root.tsx
@@ -17,7 +17,9 @@ export const links: LinksFunction = () => [
},
{
rel: "stylesheet",
- href: "https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css",
+ href: "https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css",
+ integrity: "sha384-NZhm4G1I7BpEGdjDKnzEfy3d78xvy7ECKUwwnKTYi036z42IyF056PbHfpQLIYgL",
+ crossOrigin: "anonymous",
},
{
rel: "stylesheet",
diff --git a/apps/react-app-router-fm/app/routes/login.tsx b/apps/react-app-router-fm/app/routes/login.tsx
index d77b256c..5eac3f9d 100644
--- a/apps/react-app-router-fm/app/routes/login.tsx
+++ b/apps/react-app-router-fm/app/routes/login.tsx
@@ -1,6 +1,6 @@
import { useState } from "react"
import { authClient } from "./../lib/auth-client.js"
-import { useNavigate } from "react-router"
+import { Link, useNavigate } from "react-router"
export default function Login() {
const [email, setEmail] = useState("")
@@ -60,11 +60,13 @@ export default function Login() {
onChange={(e) => setPassword(e.target.value)}
/>
- {error && {error}}
+ {error && (
+ {error}
+ )}
- Don't have an account? Sign up
+ Don't have an account? Sign up
diff --git a/apps/react-app-router-fm/app/routes/signup.tsx b/apps/react-app-router-fm/app/routes/signup.tsx
index f2be9eb7..4a67cc4b 100644
--- a/apps/react-app-router-fm/app/routes/signup.tsx
+++ b/apps/react-app-router-fm/app/routes/signup.tsx
@@ -1,6 +1,6 @@
import { useState } from "react"
import { authClient } from "./../lib/auth-client.js"
-import { useNavigate } from "react-router"
+import { Link, useNavigate } from "react-router"
export default function SignUp() {
const [name, setName] = useState("")
@@ -24,7 +24,6 @@ export default function SignUp() {
navigate("/")
},
onError: (ctx) => {
- console.error("[SignUp] Sign up error:", ctx.error)
setError(ctx.error.message)
},
},
@@ -75,11 +74,13 @@ export default function SignUp() {
onChange={(e) => setPassword(e.target.value)}
/>
- {error && {error}}
+ {error && (
+ {error}
+ )}
- Already have an account? Sign in
+ Already have an account? Sign in
diff --git a/apps/react-app-router-fm/app/routes/todo-app.tsx b/apps/react-app-router-fm/app/routes/todo-app.tsx
index f71763eb..2404d6ee 100644
--- a/apps/react-app-router-fm/app/routes/todo-app.tsx
+++ b/apps/react-app-router-fm/app/routes/todo-app.tsx
@@ -10,6 +10,7 @@ import { TodoStatus } from "@prisma/enums.js"
import { Form, useActionData, useSubmit } from "react-router"
import { useState } from "react"
import { withBetterAuthGuard, withBetterAuthGuardAction } from "@effectify/react-router-better-auth"
+import { AuthService } from "@effectify/node-better-auth"
export const loader = Effect.gen(function*() {
const todoRepo = yield* PrismaRepository.make(TodoModel, {
@@ -17,7 +18,12 @@ export const loader = Effect.gen(function*() {
spanPrefix: "todo",
})
- const todos = yield* todoRepo.findMany({})
+ const { user } = yield* AuthService.AuthContext
+ const currentUserId = Number(user.id)
+ if (!Number.isInteger(currentUserId)) {
+ return yield* httpSuccess({ todos: [] })
+ }
+ const todos = yield* todoRepo.findMany({ where: { authorId: currentUserId } })
return yield* httpSuccess({
todos,
})
@@ -38,12 +44,18 @@ export const action = Effect.gen(function*() {
spanPrefix: "todo",
})
+ const { user } = yield* AuthService.AuthContext
+ const currentUserId = Number(user.id)
+ if (!Number.isInteger(currentUserId)) {
+ return yield* httpFailure("Invalid user id")
+ }
+
if (intent === "delete") {
if (!id) {
return yield* httpFailure("Missing id")
}
- yield* todoRepo.delete({
- where: { id: TodoId.make(id) },
+ yield* todoRepo.deleteMany({
+ where: { id: TodoId.make(id), authorId: currentUserId },
})
return yield* httpRedirect("/todo-app")
}
@@ -55,8 +67,8 @@ export const action = Effect.gen(function*() {
if (!title.trim()) {
return yield* httpFailure("Title is required")
}
- yield* todoRepo.update({
- where: { id: TodoId.make(id) },
+ yield* todoRepo.updateMany({
+ where: { id: TodoId.make(id), authorId: currentUserId },
data: { title, content },
})
return yield* httpRedirect("/todo-app")
@@ -68,8 +80,8 @@ export const action = Effect.gen(function*() {
}
const statusStr = String(formData.get("status") ?? "")
const status = statusStr === "COMPLETED" ? TodoStatus.COMPLETED : TodoStatus.PENDING
- yield* todoRepo.update({
- where: { id: TodoId.make(id) },
+ yield* todoRepo.updateMany({
+ where: { id: TodoId.make(id), authorId: currentUserId },
data: { status },
})
return yield* httpRedirect("/todo-app")
@@ -85,7 +97,7 @@ export const action = Effect.gen(function*() {
title,
content,
published: false,
- authorId: 1,
+ authorId: currentUserId,
status: TodoStatus.PENDING,
},
})
@@ -116,7 +128,11 @@ export default function TodoApp({
{actionData && actionData.ok === false && actionData.errors?.length ?
- {String(actionData.errors[0])} :
+ (
+
+ {String(actionData.errors[0])}
+
+ ) :
null}