Compare commits

..

3 Commits

Author SHA1 Message Date
3a4e393e7a fix(admin-dashboard): redirect to login on session fetch failure
Wrap the session check in beforeLoad with a try/catch so
network errors, 404s, and other failures redirect to the
login page instead of showing an error boundary.

Co-authored-by: Ona <no-reply@ona.com>
2026-03-24 21:10:31 +00:00
9b0ac1cd4e feat: add admin dashboard app (#91)
* feat: add admin dashboard app

- React + Vite + TanStack Router + TanStack Query
- Auth with better-auth (login, session, admin guard)
- Source config management (WeatherKit credentials, user config)
- Feed query panel
- Location push card
- General settings with health check
- CORS middleware for cross-origin auth
- Disable CSRF check in dev mode
- Sonner toasts for mutation feedback

Co-authored-by: Ona <no-reply@ona.com>

* fix: use useQuery instead of getQueryData

Co-authored-by: Ona <no-reply@ona.com>

* refactor: remove backend changes from dashboard PR

Backend CORS/CSRF changes moved to #92.
Source registry removed (sources hardcoded in frontend).

Co-authored-by: Ona <no-reply@ona.com>

---------

Co-authored-by: Ona <no-reply@ona.com>
2026-03-23 00:31:34 +00:00
35c6371d48 fix(backend): add CORS and disable CSRF in dev (#92)
* fix(backend): add CORS middleware and disable CSRF in dev

- Add CORS middleware for /api/auth/* and global routes
- Disable better-auth CSRF origin check when NODE_ENV != production

Co-authored-by: Ona <no-reply@ona.com>

* fix: gate permissive CORS to dev only

In production, only origins listed in CORS_ORIGINS env
var are allowed. In dev, any origin is reflected back.

Co-authored-by: Ona <no-reply@ona.com>

---------

Co-authored-by: Ona <no-reply@ona.com>
2026-03-23 00:31:23 +00:00
3 changed files with 41 additions and 4 deletions

View File

@@ -47,10 +47,15 @@ export const Route = createRoute({
getParentRoute: () => rootRoute,
id: "dashboard",
beforeLoad: async ({ context }) => {
const session = await context.queryClient.ensureQueryData({
queryKey: ["session"],
queryFn: getSession,
})
let session: Awaited<ReturnType<typeof getSession>> | null = null
try {
session = await context.queryClient.ensureQueryData({
queryKey: ["session"],
queryFn: getSession,
})
} catch {
throw redirect({ to: "/login" })
}
if (!session?.user) {
throw redirect({ to: "/login" })
}

View File

@@ -16,6 +16,9 @@ export function createAuth(db: Database) {
provider: "pg",
schema,
}),
advanced: {
disableCSRFCheck: process.env.NODE_ENV !== "production",
},
emailAndPassword: {
enabled: true,
},

View File

@@ -1,4 +1,5 @@
import { Hono } from "hono"
import { cors } from "hono/cors"
import { registerAdminHttpHandlers } from "./admin/http.ts"
import { createRequireAdmin } from "./auth/admin-middleware.ts"
@@ -50,6 +51,34 @@ function main() {
const app = new Hono()
const isDev = process.env.NODE_ENV !== "production"
const allowedOrigins = process.env.CORS_ORIGINS?.split(",").map((o) => o.trim()) ?? []
function resolveOrigin(origin: string): string | undefined {
if (isDev) return origin
return allowedOrigins.includes(origin) ? origin : undefined
}
app.use(
"/api/auth/*",
cors({
origin: resolveOrigin,
allowHeaders: ["Content-Type", "Authorization"],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length"],
maxAge: 600,
credentials: true,
}),
)
app.use(
"*",
cors({
origin: resolveOrigin,
credentials: true,
}),
)
app.get("/health", (c) => c.json({ status: "ok" }))
const authSessionMiddleware = createRequireSession(auth)