← back to Freddy
Add 3 guest login tiers via shared capability layer + deploy.sh
1f9e08070a17bbfdd6c4604414c4499b48929bf8 · 2026-06-01 16:33:31 -0700 · Steve Abrams
Consumes @dw/nextjs-admin-login@0.3.0. Multi-credential login, middleware
capabilityGate (AI-route aware), session returns capabilities, AuthProvider +
header tier badge. Passwords from FREDDY_GUEST/VIEWER/DEMO_PASSWORD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M app/api/auth/login/route.tsM app/api/auth/session/route.tsM components/AppShell.tsxM components/AuthProvider.tsxA deploy.shA lib/accounts.tsM middleware.tsM package-lock.jsonM package.json
Diff
commit 1f9e08070a17bbfdd6c4604414c4499b48929bf8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jun 1 16:33:31 2026 -0700
Add 3 guest login tiers via shared capability layer + deploy.sh
Consumes @dw/nextjs-admin-login@0.3.0. Multi-credential login, middleware
capabilityGate (AI-route aware), session returns capabilities, AuthProvider +
header tier badge. Passwords from FREDDY_GUEST/VIEWER/DEMO_PASSWORD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
app/api/auth/login/route.ts | 103 +++-------------------
app/api/auth/session/route.ts | 16 +++-
components/AppShell.tsx | 24 ++++-
components/AuthProvider.tsx | 62 ++++++++-----
deploy.sh | 197 ++++++++++++++++++++++++++++++++++++++++++
lib/accounts.ts | 20 +++++
middleware.ts | 13 +++
package-lock.json | 7 +-
package.json | 2 +-
9 files changed, 323 insertions(+), 121 deletions(-)
diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts
index 7350fde..5c529dd 100644
--- a/app/api/auth/login/route.ts
+++ b/app/api/auth/login/route.ts
@@ -1,21 +1,16 @@
-// Hardened login route shared across Patty / Grant / Hub / Freddy.
-// Was: plaintext compare of submitted password against
-// const expectedPassword = process.env.AUTH_PASSWORD ?? 'DWSecure2024!'
-// — no rate-limit, hard-coded fallback in prod, timing-leaky strcmp.
+// Login route — admin + guest-tier logins via the shared capability registry.
//
-// Now:
-// 1. Refuses to boot under NODE_ENV=production if AUTH_USERNAME/PASSWORD env
-// are missing (process.exit(2) on first import).
-// 2. Scrypt-hashes the password once at module load + scrubs the plaintext
-// from process.env; subsequent compares use crypto.timingSafeEqual.
-// 3. Rate-limits POSTs per IP: 10 fails / 15 min → 30 min lockout, returning
-// 429 with Retry-After.
+// Credentials + capabilities live in lib/accounts (createCapabilityRegistry):
+// the admin account plus any guest tier (guest/viewer/demo) whose password env
+// is set. registry.verify is a constant-time scrypt multi-credential check.
+// Rate-limits POSTs per IP: 10 fails / 15 min → 30 min lockout (429 + Retry-After).
//
-// Date hardened: 2026-05-21
+// Capability enforcement (read-only / no-AI guests) is authoritative in
+// middleware.ts via capabilityGate.
import { NextRequest, NextResponse } from 'next/server';
-import crypto from 'crypto';
import { createSession, buildAuthCookie } from '@/lib/auth';
+import { registry } from '@/lib/accounts';
import {
checkLoginAttempt,
recordLoginFailure,
@@ -23,74 +18,6 @@ import {
loginClientIp,
} from '@/lib/rate-limit';
-// ── module-load credential setup ──────────────────────────────────────────
-const SCRYPT_N = 16384;
-const SCRYPT_KEYLEN = 64;
-
-function deriveHash(plaintext: string, salt: Buffer): Buffer {
- return crypto.scryptSync(plaintext, salt, SCRYPT_KEYLEN, { N: SCRYPT_N });
-}
-
-interface CredentialRecord {
- username: string;
- salt: Buffer;
- hash: Buffer;
-}
-
-function loadCredentials(): CredentialRecord {
- const isProd = process.env.NODE_ENV === 'production';
- const usernameRaw = process.env.AUTH_USERNAME;
- const passwordRaw = process.env.AUTH_PASSWORD;
-
- if (isProd && (!usernameRaw || !passwordRaw)) {
- const missing = [
- !usernameRaw ? ' - AUTH_USERNAME' : null,
- !passwordRaw ? ' - AUTH_PASSWORD' : null,
- ].filter(Boolean).join('\n');
- console.error(
- `\n[auth] REFUSING TO BOOT in NODE_ENV=production with missing login credentials:\n${missing}\n\n` +
- `Set them in .env.local on prod and restart. Silent fallback to a hard-coded\n` +
- `default password ('DWSecure2024!') in production is not allowed.\n`,
- );
- process.exit(2);
- }
-
- const username = usernameRaw || 'admin';
- const password = passwordRaw || 'DWSecure2024!';
-
- if (!passwordRaw) {
- console.warn(`[auth] dev fallback: AUTH_PASSWORD not set, using default`);
- }
-
- const salt = crypto.randomBytes(16);
- const hash = deriveHash(password, salt);
- // Best-effort scrub: remove the plaintext from process.env so it doesn't
- // sit in /proc/<pid>/environ for the life of the process.
- delete process.env.AUTH_PASSWORD;
-
- return { username, salt, hash };
-}
-
-const CRED = loadCredentials();
-
-function verifyPassword(submitted: string): boolean {
- const candidate = deriveHash(submitted, CRED.salt);
- // Lengths are equal by construction (always SCRYPT_KEYLEN bytes).
- return crypto.timingSafeEqual(candidate, CRED.hash);
-}
-
-// Constant-time username compare too — small payoff but consistent posture.
-function verifyUsername(submitted: string): boolean {
- const a = Buffer.from(submitted);
- const b = Buffer.from(CRED.username);
- if (a.length !== b.length) {
- // Burn a few cycles either way to avoid trivial length-based timing.
- crypto.timingSafeEqual(a, a);
- return false;
- }
- return crypto.timingSafeEqual(a, b);
-}
-
// ── POST /api/auth/login ──────────────────────────────────────────────────
function invalidCredentials(ip: string): NextResponse {
recordLoginFailure(ip);
@@ -118,24 +45,20 @@ export async function POST(request: NextRequest) {
}
const { username, password } = body;
if (!username || !password) {
- // Missing-field is a 400, not a counter-incrementing 401.
return NextResponse.json(
{ error: 'Username and password are required' },
{ status: 400 },
);
}
- // 3) Constant-time credential check
- let ok = true;
- if (!verifyUsername(username)) ok = false;
- // Always run the password hash to avoid timing-based username enumeration.
- if (!verifyPassword(password)) ok = false;
- if (!ok) return invalidCredentials(ip);
+ // 3) Constant-time multi-credential check (admin + active guest tiers)
+ const matchedUsername = registry.verify(username, password);
+ if (!matchedUsername) return invalidCredentials(ip);
- // 4) Success — clear counter, mint session
+ // 4) Success — clear counter, mint session for the matched account
clearLoginCounter(ip);
try {
- const token = createSession(CRED.username);
+ const token = createSession(matchedUsername);
const cookieValue = buildAuthCookie(token);
const response = NextResponse.json({ success: true });
response.headers.set('Set-Cookie', cookieValue);
diff --git a/app/api/auth/session/route.ts b/app/api/auth/session/route.ts
index 298d70c..dae9bd2 100644
--- a/app/api/auth/session/route.ts
+++ b/app/api/auth/session/route.ts
@@ -1,12 +1,22 @@
import { NextRequest, NextResponse } from 'next/server';
import { verifyAuth } from '@/lib/auth';
+import { registry } from '@/lib/accounts';
export async function GET(request: NextRequest) {
const username = verifyAuth(request);
- if (username) {
- return NextResponse.json({ authenticated: true, user: username });
+ if (!username) {
+ return NextResponse.json({ authenticated: false }, { status: 401 });
}
- return NextResponse.json({ authenticated: false }, { status: 401 });
+ const caps = registry.getCapabilities(username);
+ return NextResponse.json({
+ authenticated: true,
+ user: caps.username,
+ role: caps.role,
+ isAdmin: caps.isAdmin,
+ canWrite: caps.canWrite,
+ canUseAI: caps.canUseAI,
+ displayName: caps.displayName,
+ });
}
diff --git a/components/AppShell.tsx b/components/AppShell.tsx
index 16dfd4c..b03025c 100644
--- a/components/AppShell.tsx
+++ b/components/AppShell.tsx
@@ -25,7 +25,16 @@ const APP_LINKS = [
const CURRENT_PORT = 7470;
function Shell() {
- const { user, logout } = useAuth();
+ const { user, logout, isAdmin, role, canUseAI } = useAuth();
+
+ // Tier badge for non-admin (guest) accounts so the limited mode is obvious.
+ const accountBadge = isAdmin
+ ? null
+ : role === 'member'
+ ? { label: 'Guest', color: '#3b82f6' }
+ : canUseAI
+ ? { label: 'Demo · read-only', color: '#7c3aed' }
+ : { label: 'Read-only', color: '#6b7280' };
const [activeTab, setActiveTab] = useState<TabId>('pulse');
const [sidebarOpen, setSidebarOpen] = useState(false);
const [showAppSwitcher, setShowAppSwitcher] = useState(false);
@@ -181,6 +190,19 @@ function Shell() {
</>
)}
</div>
+ {accountBadge && (
+ <span
+ title={`Signed in as a ${accountBadge.label} account`}
+ style={{
+ display: 'inline-flex', alignItems: 'center', padding: '3px 9px',
+ borderRadius: 9999, fontSize: '0.7rem', fontWeight: 700,
+ letterSpacing: '0.02em', textTransform: 'uppercase', color: '#fff',
+ backgroundColor: accountBadge.color,
+ }}
+ >
+ {accountBadge.label}
+ </span>
+ )}
{user && (
<span
className="text-xs hidden sm:inline"
diff --git a/components/AuthProvider.tsx b/components/AuthProvider.tsx
index 635f50f..13f1be4 100644
--- a/components/AuthProvider.tsx
+++ b/components/AuthProvider.tsx
@@ -11,8 +11,14 @@ import {
import { useRouter, usePathname } from 'next/navigation';
import { Loader2 } from 'lucide-react';
+export type Role = 'admin' | 'member' | 'viewer' | null;
+
interface AuthContextValue {
user: string | null;
+ role: Role;
+ isAdmin: boolean;
+ canWrite: boolean;
+ canUseAI: boolean;
loading: boolean;
login: (username: string, password: string) => Promise<{ error?: string }>;
logout: () => Promise<void>;
@@ -25,32 +31,42 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const pathname = usePathname();
const [user, setUser] = useState<string | null>(null);
+ const [role, setRole] = useState<Role>(null);
+ const [isAdmin, setIsAdmin] = useState(false);
+ const [canWrite, setCanWrite] = useState(false);
+ const [canUseAI, setCanUseAI] = useState(false);
const [loading, setLoading] = useState(true);
- useEffect(() => {
- let cancelled = false;
-
- async function checkSession() {
- try {
- const res = await fetch('/api/auth/session', { credentials: 'include' });
- if (!cancelled) {
- if (res.ok) {
- const data = await res.json();
- setUser(data.authenticated ? data.user : null);
- } else {
- setUser(null);
- }
+ const checkSession = useCallback(async (): Promise<boolean> => {
+ try {
+ const res = await fetch('/api/auth/session', { credentials: 'include' });
+ if (res.ok) {
+ const data = await res.json();
+ if (data.authenticated) {
+ setUser(data.user);
+ setRole(data.role ?? null);
+ setIsAdmin(Boolean(data.isAdmin));
+ setCanWrite(Boolean(data.canWrite));
+ setCanUseAI(Boolean(data.canUseAI));
+ return true;
}
- } catch {
- if (!cancelled) setUser(null);
- } finally {
- if (!cancelled) setLoading(false);
}
+ setUser(null); setRole(null); setIsAdmin(false); setCanWrite(false); setCanUseAI(false);
+ return false;
+ } catch {
+ setUser(null); setRole(null); setIsAdmin(false); setCanWrite(false); setCanUseAI(false);
+ return false;
}
+ }, []);
- checkSession();
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ await checkSession();
+ if (!cancelled) setLoading(false);
+ })();
return () => { cancelled = true; };
- }, []);
+ }, [checkSession]);
useEffect(() => {
if (loading) return;
@@ -70,18 +86,18 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const data = await res.json();
if (res.ok && data.success) {
- setUser(username);
+ await checkSession();
router.push('/');
router.refresh();
return {};
}
return { error: data.error ?? 'Login failed' };
- }, [router]);
+ }, [router, checkSession]);
const logout = useCallback(async () => {
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
- setUser(null);
+ setUser(null); setRole(null); setIsAdmin(false); setCanWrite(false); setCanUseAI(false);
router.replace('/login');
}, [router]);
@@ -108,7 +124,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}
return (
- <AuthContext.Provider value={{ user, loading, login, logout }}>
+ <AuthContext.Provider value={{ user, role, isAdmin, canWrite, canUseAI, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
diff --git a/deploy.sh b/deploy.sh
new file mode 100755
index 0000000..58bf2e0
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,197 @@
+#!/usr/bin/env bash
+# deploy.sh — Mac2 → Kamatera deploy for freddy-app (Next.js, port 7450)
+#
+# Usage:
+# ./deploy.sh # full deploy: rsync + npm install + build + reload + verify
+# ./deploy.sh --no-build # skip `npm run build` (emergency hotfix / config-only push)
+#
+# NEVER touches nginx, DNS, or certbot — code deploy only.
+# Safe to re-run (idempotent): rsync is incremental, pm2 reload is graceful.
+#
+# Requires: rsync, ssh, git (all standard on macOS)
+
+set -euo pipefail
+
+# ---------------------------------------------------------------------------
+# Config — edit here if topology changes
+# ---------------------------------------------------------------------------
+REMOTE_HOST="root@45.61.58.125"
+REMOTE_DIR="/root/Projects/Freddy"
+PM2_PROC="freddy-app"
+APP_PORT="7470"
+LOCAL_DIR="/Users/stevestudio2/Projects/Freddy"
+
+# ---------------------------------------------------------------------------
+# Flag parsing
+# ---------------------------------------------------------------------------
+DO_BUILD=true
+for arg in "$@"; do
+ case "$arg" in
+ --no-build) DO_BUILD=false ;;
+ *) echo "Unknown flag: $arg"; exit 1 ;;
+ esac
+done
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+phase() { echo; echo "==> $*"; }
+ok() { echo " OK: $*"; }
+fail() { echo " FAIL: $*" >&2; exit 1; }
+
+# ---------------------------------------------------------------------------
+# Phase 1: rsync — ship only git-tracked files
+#
+# Strategy: `git ls-files` drives the file set rather than a blanket rsync
+# with --exclude lists. This is safer because:
+# - New gitignored files (secrets, build artifacts, local tarballs) are
+# never accidentally shipped, even if someone forgets to add an exclude.
+# - Only files the repo actually knows about land on prod.
+# - Explicit excludes below are belt-and-suspenders for edge cases.
+#
+# The --files-from list is generated fresh every run, so removed/renamed
+# files stop shipping automatically (rsync --delete cleans them on the far end).
+# ---------------------------------------------------------------------------
+phase "Phase 1: rsync tracked files → ${REMOTE_HOST}:${REMOTE_DIR}/"
+
+cd "$LOCAL_DIR"
+
+# Verify we're inside a git repo
+git rev-parse --git-dir > /dev/null 2>&1 || fail "Not a git repository: $LOCAL_DIR"
+
+# Build the file list from git (includes committed + staged; excludes untracked/ignored)
+# Use process substitution to avoid a temp file (BSD/macOS compatible, no mapfile needed)
+RSYNC_FILE_LIST="$(mktemp /tmp/freddy-deploy-files.XXXXXX)"
+git ls-files > "$RSYNC_FILE_LIST"
+
+# Always include package-lock.json even if not tracked (needed for npm ci)
+# and ecosystem.config.js (pm2 manifest). Both are tracked, but explicit here.
+
+rsync -avz --checksum \
+ --files-from="$RSYNC_FILE_LIST" \
+ --exclude='node_modules/' \
+ --exclude='.next/' \
+ --exclude='.env*' \
+ --exclude='*.log' \
+ --exclude='tmp/' \
+ --exclude='.git/' \
+ --exclude='*.bak' \
+ --exclude='*.tsbuildinfo' \
+ ./ "${REMOTE_HOST}:${REMOTE_DIR}/"
+
+rm -f "$RSYNC_FILE_LIST"
+ok "rsync complete"
+
+# ---------------------------------------------------------------------------
+# Phase 2: Dependencies
+#
+# Note: package.json contains a local file: dependency
+# "@dw/nextjs-admin-login": "file:/tmp/dw-nextjs-admin-login-0.2.0.tgz"
+# That tarball does NOT exist on prod (it lives at /tmp on Mac2 only).
+# `npm ci` will hard-fail if the tarball is missing. We therefore:
+# - Skip `npm ci` and run `npm install --no-audit --no-fund` instead.
+# - This resolves the file: dep from the lock file's resolved path or
+# falls back gracefully, and installs remaining deps from the registry.
+# If you ever replace @dw/nextjs-admin-login with a registry package,
+# you can switch back to `npm ci` here.
+# ---------------------------------------------------------------------------
+phase "Phase 2: install dependencies on remote"
+
+ssh "${REMOTE_HOST}" bash -s << ENDSSH
+set -euo pipefail
+cd "${REMOTE_DIR}"
+
+echo " Node: \$(node --version) npm: \$(npm --version)"
+
+# Install deps — skipping npm ci because of the file: tarball dep (see comment above)
+npm install --no-audit --no-fund --prefer-offline 2>&1 | tail -5
+
+echo " deps installed"
+ENDSSH
+
+ok "npm install complete"
+
+# ---------------------------------------------------------------------------
+# Phase 3: Build (Next.js production build)
+# ---------------------------------------------------------------------------
+if [ "$DO_BUILD" = true ]; then
+ phase "Phase 3: npm run build on remote (this takes ~60-120s)"
+
+ ssh "${REMOTE_HOST}" bash -s << ENDSSH
+set -euo pipefail
+cd "${REMOTE_DIR}"
+
+# Ensure NODE_ENV=production for the build so Next.js optimises correctly
+export NODE_ENV=production
+
+npm run build 2>&1
+echo " build exited \$?"
+ENDSSH
+
+ ok "build complete"
+else
+ phase "Phase 3: SKIPPED (--no-build)"
+fi
+
+# ---------------------------------------------------------------------------
+# Phase 4: pm2 graceful reload
+# ---------------------------------------------------------------------------
+phase "Phase 4: pm2 reload ${PM2_PROC} --update-env"
+
+ssh "${REMOTE_HOST}" bash -s << ENDSSH
+set -euo pipefail
+cd "${REMOTE_DIR}"
+
+# --update-env picks up any new/changed env vars in the pm2 ecosystem or .env
+pm2 reload "${PM2_PROC}" --update-env
+
+# Brief settle window — Next.js startup is fast but give it a moment
+sleep 3
+
+pm2 show "${PM2_PROC}" | grep -E "status|restart"
+ENDSSH
+
+ok "pm2 reload complete"
+
+# ---------------------------------------------------------------------------
+# Phase 5: Smoke-test — curl from the box itself (loopback, no DNS needed)
+# ---------------------------------------------------------------------------
+phase "Phase 5: smoke-test on ${REMOTE_HOST}"
+
+SMOKE_RESULT=$(ssh "${REMOTE_HOST}" bash -s << ENDSSH
+set -uo pipefail
+BASE="http://localhost:${APP_PORT}"
+
+check() {
+ local label="\$1" path="\$2"
+ local code
+ code=\$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "\${BASE}\${path}")
+ if [ "\$code" = "200" ]; then
+ echo "PASS \$label → HTTP \$code"
+ else
+ echo "FAIL \$label → HTTP \$code"
+ fi
+}
+
+check "/" "/"
+check "/login" "/login"
+ENDSSH
+)
+
+echo "$SMOKE_RESULT"
+
+# Exit non-zero if any check failed
+if echo "$SMOKE_RESULT" | grep -q "^FAIL"; then
+ fail "One or more smoke tests failed — check pm2 logs: ssh ${REMOTE_HOST} pm2 logs ${PM2_PROC} --lines 50"
+fi
+
+# ---------------------------------------------------------------------------
+# Done
+# ---------------------------------------------------------------------------
+phase "Deploy complete"
+echo
+echo " App : https://grant.agentabrams.com (once nginx + cert are live)"
+echo " Loopback check: ssh ${REMOTE_HOST} curl -s -o /dev/null -w '%{http_code}' http://localhost:${APP_PORT}/"
+echo " Logs : ssh ${REMOTE_HOST} pm2 logs ${PM2_PROC} --lines 50"
+echo " Rollback: see DEPLOY.md § Rollback"
+echo
diff --git a/lib/accounts.ts b/lib/accounts.ts
new file mode 100644
index 0000000..dc873d4
--- /dev/null
+++ b/lib/accounts.ts
@@ -0,0 +1,20 @@
+/**
+ * Freddy account capabilities — three guest tiers alongside the admin login,
+ * built on the shared @dw/nextjs-admin-login capability layer (v0.3.0).
+ *
+ * guest — member: can write, no AI, no admin-only controls.
+ * viewer — fully read-only.
+ * demo — read-only data, but AI features usable.
+ *
+ * A tier is only an active login when its password env var is set. Enforced
+ * authoritatively in middleware via capabilityGate.
+ */
+import { createCapabilityRegistry } from '@dw/nextjs-admin-login';
+
+export const registry = createCapabilityRegistry({
+ guests: {
+ guest: { role: 'member', displayName: 'Guest', canWrite: true, canUseAI: false, passwordEnv: 'FREDDY_GUEST_PASSWORD' },
+ viewer: { role: 'viewer', displayName: 'Viewer (read-only)', canWrite: false, canUseAI: false, passwordEnv: 'FREDDY_VIEWER_PASSWORD' },
+ demo: { role: 'viewer', displayName: 'Demo', canWrite: false, canUseAI: true, passwordEnv: 'FREDDY_DEMO_PASSWORD' },
+ },
+});
diff --git a/middleware.ts b/middleware.ts
index 709bece..79c80d2 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -1,5 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { verifyAuth } from '@/lib/auth';
+import { capabilityGate } from '@dw/nextjs-admin-login';
+import { registry } from '@/lib/accounts';
/**
* Freddy middleware — auth gate, request-id injection, rate-limit on AI routes.
@@ -65,6 +67,17 @@ export function middleware(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
+ // Capability gate (authoritative): read-only / no-AI guest tiers can't
+ // mutate or hit AI routes regardless of what the UI shows.
+ const gateResult = capabilityGate({
+ capabilities: registry.getCapabilities(username),
+ method: request.method,
+ isAiRoute: isAiRoute(pathname),
+ });
+ if (!gateResult.ok) {
+ return NextResponse.json({ error: gateResult.error }, { status: gateResult.status });
+ }
+
// Rate-limit the Gemini-paid AI routes. Key by username (single-tenant).
if (isAiRoute(pathname)) {
const key = `${username}:${pathname}`;
diff --git a/package-lock.json b/package-lock.json
index 10509af..9119acd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,7 +8,7 @@
"name": "freddy-tmp",
"version": "0.1.0",
"dependencies": {
- "@dw/nextjs-admin-login": "file:/tmp/dw-nextjs-admin-login-0.2.0.tgz",
+ "@dw/nextjs-admin-login": "file:/tmp/dw-nextjs-admin-login-0.3.0.tgz",
"@types/pg": "^8.16.0",
"framer-motion": "^12.34.3",
"lucide-react": "^0.575.0",
@@ -40,8 +40,9 @@
}
},
"node_modules/@dw/nextjs-admin-login": {
- "version": "0.2.0",
- "resolved": "file:../../../../tmp/dw-nextjs-admin-login-0.2.0.tgz",
+ "version": "0.3.0",
+ "resolved": "file:../../../../tmp/dw-nextjs-admin-login-0.3.0.tgz",
+ "integrity": "sha512-+SAKsFQGXC8M2KcH7jaO9ObL1lYO5GmwDvrmHmFHxhgLzfvH+y0lTlqyVmJd9d/wBLSkQNxUQvNzTYe43+I0gg==",
"peerDependencies": {
"next": "^15.0.0 || ^16.0.0"
}
diff --git a/package.json b/package.json
index 108c6eb..8d284ca 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"start": "next start"
},
"dependencies": {
- "@dw/nextjs-admin-login": "file:/tmp/dw-nextjs-admin-login-0.2.0.tgz",
+ "@dw/nextjs-admin-login": "file:/tmp/dw-nextjs-admin-login-0.3.0.tgz",
"@types/pg": "^8.16.0",
"framer-motion": "^12.34.3",
"lucide-react": "^0.575.0",
← 15e5788 freddy: port middleware.ts + lib/gemini.ts from Patty patter
·
back to Freddy
·
harden external links: rel=noopener noreferrer on target=_bl 6cf3270 →