feat: add dev startup script, fix push notifications, and scope test sends to device

- pnpm dev:local/dev:reset: orchestrate DB container, migrations, seed, and Next.js dev server in one command; Caddy snippet + docs for HTTPS via dev.ginnoir.com
- Fix dev login on HTTPS: set both authjs.session-token and __Secure-authjs.session-token so Auth.js finds the session regardless of cookie name resolution
- Suppress hydration mismatch on <html> caused by pre-paint script changing data-nav before React hydrates
- VAPID startup warning if keys not configured; remove dead NEXT_PUBLIC_VAPID_PUBLIC_KEY var
- PushOptIn: hydrate subscription state on mount; reuse existing subscription on iOS to avoid redundant prompts
- sendPushToEndpoint: new function to send to a single device endpoint
- sendTestNotification: scoped to the calling device's endpoint (ownership-verified) instead of all user subscriptions
This commit is contained in:
ginnoir
2026-06-01 17:45:20 -05:00
parent 9612a54e52
commit 19308be768
14 changed files with 334 additions and 26 deletions
+1
View File
@@ -149,6 +149,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
data-density={density}
data-nav={initialDataNav}
className={cn(fontVars, isDark ? "dark" : "")}
suppressHydrationWarning
>
<head>
<script dangerouslySetInnerHTML={{ __html: prePaintScript }} />
+10 -6
View File
@@ -2,7 +2,7 @@ import { signIn } from "@/lib/auth";
import { Button } from "@/components/ui/button";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { DEV_LOGIN_COOKIE, isDevLoginEnabled } from "@/lib/dev-login-config";
import { isDevLoginEnabled } from "@/lib/dev-login-config";
import { createDevSession } from "@/lib/dev-login";
import { BrandMark } from "@/components/brand-mark";
@@ -37,13 +37,17 @@ export default function LoginPage() {
<form
action={async () => {
"use server";
// Auth.js may resolve either "authjs.session-token" (HTTP/dev) or
// "__Secure-authjs.session-token" (HTTPS) depending on AUTH_URL,
// trustHost, and proxy headers. Set both so the session is found
// regardless — this is dev-only code, correctness > elegance.
const { sessionToken, expires } = await createDevSession();
const cookieStore = await cookies();
cookieStore.set(DEV_LOGIN_COOKIE, sessionToken, {
httpOnly: true,
sameSite: "lax",
path: "/",
expires,
const base = { httpOnly: true, sameSite: "lax" as const, path: "/", expires };
cookieStore.set("authjs.session-token", sessionToken, base);
cookieStore.set("__Secure-authjs.session-token", sessionToken, {
...base,
secure: true,
});
redirect("/");
}}
+10 -3
View File
@@ -4,7 +4,7 @@ import { and, eq } from "drizzle-orm";
import { db } from "@/lib/db";
import { pushSubscriptions } from "@/modules/_core/schema";
import { getCurrentSession } from "@/lib/session";
import { sendPush } from "@/modules/_core/push";
import { sendPushToEndpoint } from "@/modules/_core/push";
type PushSubscriptionJSON = {
endpoint: string;
@@ -35,9 +35,16 @@ export async function unsubscribeFromPush(endpoint: string): Promise<void> {
.where(and(eq(pushSubscriptions.userId, user.id), eq(pushSubscriptions.endpoint, endpoint)));
}
export async function sendTestNotification(): Promise<void> {
export async function sendTestNotification(endpoint: string): Promise<void> {
const { user } = await getCurrentSession();
await sendPush(user.id, {
// Verify the endpoint belongs to the current user before sending.
const [owned] = await db
.select({ id: pushSubscriptions.id })
.from(pushSubscriptions)
.where(and(eq(pushSubscriptions.userId, user.id), eq(pushSubscriptions.endpoint, endpoint)))
.limit(1);
if (!owned) return;
await sendPushToEndpoint(endpoint, {
title: "famapp test",
body: "Push notifications are working!",
url: "/settings",