diff --git a/CLAUDE.md b/CLAUDE.md index 129a39b..a0ff24a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,8 +7,9 @@ Content language: Russian ## Tech Stack - **Next.js 16** (App Router, TypeScript, Turbopack) -- **Tailwind CSS v4** (light + dark mode, class-based toggle) +- **Tailwind CSS v4** (dark mode only, gold/black theme) - **lucide-react** for icons +- **better-sqlite3** for SQLite database - **Fonts**: Inter (body) + Oswald (headings) via `next/font` - **Hosting**: Vercel (planned) @@ -16,66 +17,120 @@ Content language: Russian - Function declarations for components (not arrow functions) - PascalCase for component files, camelCase for utils - `@/` path alias for imports -- Semantic CSS classes via `@apply`: `surface-base`, `surface-muted`, `heading-text`, `body-text`, `nav-link`, `card`, `contact-item`, `contact-icon`, `theme-border` -- Only Header + ThemeToggle are client components (minimal JS shipped) - `next/image` with `unoptimized` for PNGs that need transparency preserved +- Header nav uses `lg:` breakpoint (1024px) for desktop/mobile switch (9 nav links + CTA need the space) ## Project Structure ``` src/ ├── app/ │ ├── layout.tsx # Root layout, fonts, metadata -│ ├── page.tsx # Landing: Hero → Team → About → Classes → Contact +│ ├── page.tsx # Landing: Hero → About → Team → Classes → MasterClasses → Schedule → Pricing → News → FAQ → Contact │ ├── globals.css # Tailwind imports │ ├── styles/ │ │ ├── theme.css # Theme variables, semantic classes │ │ └── animations.css # Keyframes, scroll reveal, modal animations -│ ├── icon.png # Favicon -│ └── apple-icon.png +│ ├── admin/ +│ │ ├── page.tsx # Dashboard with 11 section cards +│ │ ├── login/ # Password auth +│ │ ├── layout.tsx # Sidebar nav shell +│ │ ├── _components/ # SectionEditor, FormField, ArrayEditor +│ │ ├── meta/ # SEO editor +│ │ ├── hero/ # Hero editor +│ │ ├── about/ # About editor +│ │ ├── team/ # Team list + [id] editor +│ │ ├── classes/ # Classes editor with icon picker +│ │ ├── master-classes/ # MC editor with registrations +│ │ ├── schedule/ # Schedule editor +│ │ ├── pricing/ # Pricing editor +│ │ ├── faq/ # FAQ editor +│ │ ├── news/ # News editor +│ │ └── contact/ # Contact editor +│ └── api/ +│ ├── auth/login/ # POST login +│ ├── logout/ # POST logout +│ ├── admin/ +│ │ ├── sections/[key]/ # GET/PUT section data +│ │ ├── team/ # CRUD team members +│ │ ├── team/[id]/ # GET/PUT/DELETE single member +│ │ ├── team/reorder/ # PUT reorder +│ │ ├── upload/ # POST file upload (whitelisted folders) +│ │ ├── mc-registrations/ # CRUD registrations +│ │ └── validate-instagram/ # GET check username +│ └── master-class-register/ # POST public signup ├── components/ │ ├── layout/ -│ │ ├── Header.tsx # Sticky nav, mobile menu, theme toggle ("use client") +│ │ ├── Header.tsx # Sticky nav, mobile menu, booking modal ("use client") │ │ └── Footer.tsx │ ├── sections/ -│ │ ├── Hero.tsx -│ │ ├── Team.tsx # "use client" — clickable cards + modal -│ │ ├── About.tsx -│ │ ├── Classes.tsx -│ │ └── Contact.tsx +│ │ ├── Hero.tsx # Hero with animated logo, floating hearts +│ │ ├── About.tsx # About with stats (trainers, classes, locations) +│ │ ├── Team.tsx # Carousel + profile view +│ │ ├── Classes.tsx # Showcase layout with icon selector +│ │ ├── MasterClasses.tsx # Cards with signup modal +│ │ ├── Schedule.tsx # Day/group views with filters +│ │ ├── Pricing.tsx # Tabs: prices, rental, rules +│ │ ├── News.tsx # Featured + compact articles +│ │ ├── FAQ.tsx # Accordion with show more +│ │ └── Contact.tsx # Info + Yandex Maps iframe │ └── ui/ │ ├── Button.tsx │ ├── SectionHeading.tsx -│ ├── SocialLinks.tsx -│ ├── ThemeToggle.tsx -│ ├── Reveal.tsx # Intersection Observer scroll reveal -│ └── TeamMemberModal.tsx # "use client" — member popup +│ ├── BookingModal.tsx # Booking form → Instagram DM +│ ├── MasterClassSignupModal.tsx # MC registration form → API +│ ├── NewsModal.tsx # News detail popup +│ ├── Reveal.tsx # Intersection Observer scroll reveal +│ ├── BackToTop.tsx +│ └── ... ├── data/ -│ └── content.ts # ALL Russian text, structured for future CMS +│ └── content.ts # Fallback Russian text (DB takes priority) ├── lib/ -│ └── constants.ts # BRAND constants, NAV_LINKS +│ ├── constants.ts # BRAND constants, NAV_LINKS +│ ├── config.ts # UI_CONFIG (thresholds, counts) +│ ├── db.ts # SQLite DB, migrations, CRUD +│ ├── auth.ts # Token signing (Node.js) +│ ├── auth-edge.ts # Token verification (Edge/Web Crypto) +│ └── content.ts # getContent() — DB with fallback +├── proxy.ts # Middleware: auth guard for /admin/* └── types/ ├── index.ts - ├── content.ts # SiteContent, TeamMember, ClassItem, ContactInfo + ├── content.ts # SiteContent, TeamMember, ClassItem, MasterClassItem, etc. └── navigation.ts ``` ## Brand / Styling -- **Accent**: rose/red (`#e11d48`) -- **Dark mode**: bg `#0a0a0a`, surface `#171717` -- **Light mode**: bg `#fafafa`, surface `#ffffff` -- Logo: transparent PNG, uses `dark:invert` + `unoptimized` +- **Accent**: gold (`#c9a96e` / `hsl(37, 42%, 61%)`) +- **Background**: `#050505` – `#0a0a0a` (dark only) +- **Surface**: `#171717` dark cards +- Logo: transparent PNG heart with gold glow, uses `unoptimized` ## Content Data -- All text lives in `src/data/content.ts` (type-safe, one file to edit) -- 13 team members with photos, Instagram links, and personal descriptions +- Primary source: SQLite database (`db/blackheart.db`) +- Fallback: `src/data/content.ts` (auto-seeds DB on first access) +- Admin panel edits go to DB, site reads from DB via `getContent()` +- 12 team members with photos, Instagram links, bios, victories, education - 6 class types (Exotic Pole Dance, Pole Dance, Body Plastic, etc.) +- Master classes with date/time slots and public registration - 2 addresses in Minsk, Yandex Maps embed with markers -- Contact: phone, Instagram +- Contact: phone, Instagram (no email) + +## Admin Panel +- Password-based auth with HMAC-SHA256 signed JWT (24h TTL) +- Cookie: `bh-admin-token` (httpOnly, secure in prod) +- Auto-save with 800ms debounce on all section editors +- Team members: drag-reorder, photo upload, rich bio (experience, victories, education) +- Master classes: slots, registration viewer, trainer/style autocomplete from existing data +- File upload: whitelisted folders (`team`, `master-classes`, `news`, `classes`), max 5MB, image types only + +## Security Notes +- **CSRF protection**: Double-submit cookie pattern. Login sets `bh-csrf-token` cookie (JS-readable). All admin fetch calls use `adminFetch()` from `src/lib/csrf.ts` which sends the token as `X-CSRF-Token` header. Middleware (`proxy.ts`) validates header matches cookie on POST/PUT/DELETE to `/api/admin/*`. **Always use `adminFetch()` instead of `fetch()` for admin API calls.** +- File upload validates: MIME type, file extension, whitelisted folder (no path traversal) +- API routes validate: input types, string lengths, numeric IDs +- Public MC registration: length-limited but **no rate limiting yet** (add before production) ## AST Index - **Always use the AST index** at `memory/ast-index.md` when searching for components, props, hooks, types, or styles - Contains: component tree, all exports, props, hooks, client/server status, CSS classes, keyframes -- Covers all 31 TS/TSX files + 4 CSS files - Update the index when adding/removing/renaming files or exports ## Database Migrations diff --git a/src/app/admin/_components/FormField.tsx b/src/app/admin/_components/FormField.tsx index 7f54b21..7dfa0a6 100644 --- a/src/app/admin/_components/FormField.tsx +++ b/src/app/admin/_components/FormField.tsx @@ -1,5 +1,6 @@ import { useRef, useEffect, useState } from "react"; import { Plus, X, Upload, Loader2, Link, ImageIcon, Calendar, AlertCircle, MapPin } from "lucide-react"; +import { adminFetch } from "@/lib/csrf"; import type { RichListItem, VictoryItem } from "@/types/content"; interface InputFieldProps { @@ -379,7 +380,7 @@ export function VictoryListField({ label, items, onChange, placeholder, onLinkVa formData.append("file", file); formData.append("folder", "team"); try { - const res = await fetch("/api/admin/upload", { method: "POST", body: formData }); + const res = await adminFetch("/api/admin/upload", { method: "POST", body: formData }); const result = await res.json(); if (result.path) { onChange(items.map((item, i) => (i === index ? { ...item, image: result.path } : item))); diff --git a/src/app/admin/_components/SectionEditor.tsx b/src/app/admin/_components/SectionEditor.tsx index aeb4ca9..4140ade 100644 --- a/src/app/admin/_components/SectionEditor.tsx +++ b/src/app/admin/_components/SectionEditor.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { Loader2, Check, AlertCircle } from "lucide-react"; +import { adminFetch } from "@/lib/csrf"; interface SectionEditorProps { sectionKey: string; @@ -24,7 +25,7 @@ export function SectionEditor({ const initialLoadRef = useRef(true); useEffect(() => { - fetch(`/api/admin/sections/${sectionKey}`) + adminFetch(`/api/admin/sections/${sectionKey}`) .then((r) => { if (!r.ok) throw new Error("Failed to load"); return r.json(); @@ -39,7 +40,7 @@ export function SectionEditor({ setError(""); try { - const res = await fetch(`/api/admin/sections/${sectionKey}`, { + const res = await adminFetch(`/api/admin/sections/${sectionKey}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(dataToSave), diff --git a/src/app/admin/master-classes/page.tsx b/src/app/admin/master-classes/page.tsx index 83bca01..a2cd633 100644 --- a/src/app/admin/master-classes/page.tsx +++ b/src/app/admin/master-classes/page.tsx @@ -5,6 +5,7 @@ import { SectionEditor } from "../_components/SectionEditor"; import { InputField, TextareaField } from "../_components/FormField"; import { ArrayEditor } from "../_components/ArrayEditor"; import { Plus, X, Upload, Loader2, ImageIcon, AlertCircle, Check, ChevronDown, ChevronUp, Instagram, Send, Trash2, Pencil } from "lucide-react"; +import { adminFetch } from "@/lib/csrf"; import type { MasterClassItem, MasterClassSlot } from "@/types/content"; function PriceField({ label, value, onChange, placeholder }: { label: string; value: string; onChange: (v: string) => void; placeholder?: string }) { @@ -335,7 +336,7 @@ function ImageUploadField({ formData.append("file", file); formData.append("folder", "master-classes"); try { - const res = await fetch("/api/admin/upload", { + const res = await adminFetch("/api/admin/upload", { method: "POST", body: formData, }); @@ -506,7 +507,7 @@ function RegistrationRow({ instagram: `@${ig.trim()}`, telegram: tg.trim() ? `@${tg.trim()}` : undefined, }; - const res = await fetch("/api/admin/mc-registrations", { + const res = await adminFetch("/api/admin/mc-registrations", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), @@ -647,7 +648,7 @@ function RegistrationsList({ title }: { title: string }) { useEffect(() => { if (!title) return; - fetch(`/api/admin/mc-registrations?title=${encodeURIComponent(title)}`) + adminFetch(`/api/admin/mc-registrations?title=${encodeURIComponent(title)}`) .then((r) => r.json()) .then((data: McRegistration[]) => { setCount(data.length); @@ -659,7 +660,7 @@ function RegistrationsList({ title }: { title: string }) { function toggle() { if (!open && regs.length === 0 && count !== 0) { setLoading(true); - fetch(`/api/admin/mc-registrations?title=${encodeURIComponent(title)}`) + adminFetch(`/api/admin/mc-registrations?title=${encodeURIComponent(title)}`) .then((r) => r.json()) .then((data: McRegistration[]) => { setRegs(data); @@ -680,7 +681,7 @@ function RegistrationsList({ title }: { title: string }) { instagram: `@${newIg.trim()}`, telegram: newTg.trim() ? `@${newTg.trim()}` : undefined, }; - const res = await fetch("/api/admin/mc-registrations", { + const res = await adminFetch("/api/admin/mc-registrations", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), @@ -705,7 +706,7 @@ function RegistrationsList({ title }: { title: string }) { } async function handleDelete(id: number) { - await fetch(`/api/admin/mc-registrations?id=${id}`, { method: "DELETE" }); + await adminFetch(`/api/admin/mc-registrations?id=${id}`, { method: "DELETE" }); setRegs((prev) => prev.filter((r) => r.id !== id)); setCount((prev) => (prev !== null ? prev - 1 : null)); } @@ -823,7 +824,7 @@ export default function MasterClassesEditorPage() { useEffect(() => { // Fetch trainers from team - fetch("/api/admin/team") + adminFetch("/api/admin/team") .then((r) => r.json()) .then((members: { name: string }[]) => { setTrainers(members.map((m) => m.name)); @@ -831,7 +832,7 @@ export default function MasterClassesEditorPage() { .catch(() => {}); // Fetch styles from classes section - fetch("/api/admin/sections/classes") + adminFetch("/api/admin/sections/classes") .then((r) => r.json()) .then((data: { items: { name: string }[] }) => { setStyles(data.items.map((c) => c.name)); @@ -839,7 +840,7 @@ export default function MasterClassesEditorPage() { .catch(() => {}); // Fetch locations from schedule section - fetch("/api/admin/sections/schedule") + adminFetch("/api/admin/sections/schedule") .then((r) => r.json()) .then((data: { locations: { name: string; address: string }[] }) => { setLocations(data.locations); diff --git a/src/app/admin/news/page.tsx b/src/app/admin/news/page.tsx index 12682a9..1caf6dc 100644 --- a/src/app/admin/news/page.tsx +++ b/src/app/admin/news/page.tsx @@ -5,6 +5,7 @@ import { SectionEditor } from "../_components/SectionEditor"; import { InputField, TextareaField } from "../_components/FormField"; import { ArrayEditor } from "../_components/ArrayEditor"; import { Upload, Loader2, ImageIcon, X } from "lucide-react"; +import { adminFetch } from "@/lib/csrf"; import type { NewsItem } from "@/types/content"; interface NewsData { @@ -30,7 +31,7 @@ function ImageUploadField({ formData.append("file", file); formData.append("folder", "news"); try { - const res = await fetch("/api/admin/upload", { + const res = await adminFetch("/api/admin/upload", { method: "POST", body: formData, }); diff --git a/src/app/admin/schedule/page.tsx b/src/app/admin/schedule/page.tsx index 389e86b..1fc9bbb 100644 --- a/src/app/admin/schedule/page.tsx +++ b/src/app/admin/schedule/page.tsx @@ -4,6 +4,7 @@ import { useState, useEffect, useRef, useCallback, useMemo } from "react"; import { SectionEditor } from "../_components/SectionEditor"; import { InputField, SelectField, TimeRangeField, ToggleField } from "../_components/FormField"; import { Plus, X, Trash2 } from "lucide-react"; +import { adminFetch } from "@/lib/csrf"; import type { ScheduleLocation, ScheduleDay, ScheduleClass } from "@/types/content"; interface ScheduleData { @@ -1113,21 +1114,21 @@ export default function ScheduleEditorPage() { const [classTypes, setClassTypes] = useState([]); useEffect(() => { - fetch("/api/admin/team") + adminFetch("/api/admin/team") .then((r) => r.json()) .then((members: { name: string }[]) => { setTrainers(members.map((m) => m.name)); }) .catch(() => {}); - fetch("/api/admin/sections/contact") + adminFetch("/api/admin/sections/contact") .then((r) => r.json()) .then((contact: { addresses?: string[] }) => { setAddresses(contact.addresses ?? []); }) .catch(() => {}); - fetch("/api/admin/sections/classes") + adminFetch("/api/admin/sections/classes") .then((r) => r.json()) .then((classes: { items?: { name: string }[] }) => { setClassTypes((classes.items ?? []).map((c) => c.name)); diff --git a/src/app/admin/team/[id]/page.tsx b/src/app/admin/team/[id]/page.tsx index 675661e..3d69706 100644 --- a/src/app/admin/team/[id]/page.tsx +++ b/src/app/admin/team/[id]/page.tsx @@ -5,6 +5,7 @@ import { useRouter, useParams } from "next/navigation"; import Image from "next/image"; import { Save, Loader2, Check, ArrowLeft, Upload, AlertCircle } from "lucide-react"; import { InputField, TextareaField, ListField, VictoryListField, VictoryItemListField } from "../../_components/FormField"; +import { adminFetch } from "@/lib/csrf"; import type { RichListItem, VictoryItem } from "@/types/content"; function extractUsername(value: string): string { @@ -55,7 +56,7 @@ export default function TeamMemberEditorPage() { setIgStatus("checking"); igTimerRef.current = setTimeout(async () => { try { - const res = await fetch(`/api/admin/validate-instagram?username=${encodeURIComponent(username)}`); + const res = await adminFetch(`/api/admin/validate-instagram?username=${encodeURIComponent(username)}`); const result = await res.json(); setIgStatus(result.valid ? "valid" : "invalid"); } catch { @@ -106,7 +107,7 @@ export default function TeamMemberEditorPage() { useEffect(() => { if (isNew) return; - fetch(`/api/admin/team/${id}`) + adminFetch(`/api/admin/team/${id}`) .then((r) => r.json()) .then((member) => { const username = extractUsername(member.instagram || ""); @@ -139,7 +140,7 @@ export default function TeamMemberEditorPage() { }; if (isNew) { - const res = await fetch("/api/admin/team", { + const res = await adminFetch("/api/admin/team", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), @@ -148,7 +149,7 @@ export default function TeamMemberEditorPage() { router.push("/admin/team"); } } else { - const res = await fetch(`/api/admin/team/${id}`, { + const res = await adminFetch(`/api/admin/team/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), @@ -171,7 +172,7 @@ export default function TeamMemberEditorPage() { formData.append("folder", "team"); try { - const res = await fetch("/api/admin/upload", { + const res = await adminFetch("/api/admin/upload", { method: "POST", body: formData, }); diff --git a/src/app/admin/team/page.tsx b/src/app/admin/team/page.tsx index 5d8ccc4..05c8c32 100644 --- a/src/app/admin/team/page.tsx +++ b/src/app/admin/team/page.tsx @@ -11,6 +11,7 @@ import { GripVertical, Check, } from "lucide-react"; +import { adminFetch } from "@/lib/csrf"; import type { TeamMember } from "@/types/content"; type Member = TeamMember & { id: number }; @@ -29,7 +30,7 @@ export default function TeamEditorPage() { const itemRefs = useRef<(HTMLDivElement | null)[]>([]); useEffect(() => { - fetch("/api/admin/team") + adminFetch("/api/admin/team") .then((r) => r.json()) .then(setMembers) .finally(() => setLoading(false)); @@ -38,7 +39,7 @@ export default function TeamEditorPage() { const saveOrder = useCallback(async (updated: Member[]) => { setMembers(updated); setSaving(true); - await fetch("/api/admin/team/reorder", { + await adminFetch("/api/admin/team/reorder", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids: updated.map((m) => m.id) }), @@ -159,7 +160,7 @@ export default function TeamEditorPage() { async function deleteMember(id: number) { if (!confirm("Удалить этого участника?")) return; - await fetch(`/api/admin/team/${id}`, { method: "DELETE" }); + await adminFetch(`/api/admin/team/${id}`, { method: "DELETE" }); setMembers((prev) => prev.filter((m) => m.id !== id)); } diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index 6448f5d..e28d19a 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { verifyPassword, signToken, COOKIE_NAME } from "@/lib/auth"; +import { verifyPassword, signToken, generateCsrfToken, COOKIE_NAME, CSRF_COOKIE_NAME } from "@/lib/auth"; export async function POST(request: NextRequest) { const body = await request.json() as { password?: string }; @@ -9,6 +9,7 @@ export async function POST(request: NextRequest) { } const token = signToken(); + const csrfToken = generateCsrfToken(); const response = NextResponse.json({ ok: true }); response.cookies.set(COOKIE_NAME, token, { @@ -16,7 +17,15 @@ export async function POST(request: NextRequest) { secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/", - maxAge: 60 * 60 * 24, // 24 hours + maxAge: 60 * 60 * 24, + }); + + response.cookies.set(CSRF_COOKIE_NAME, csrfToken, { + httpOnly: false, // JS must read this to send as header + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + path: "/", + maxAge: 60 * 60 * 24, }); return response; diff --git a/src/lib/auth.ts b/src/lib/auth.ts index c201125..1d32c61 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -63,4 +63,10 @@ function verifyTokenNode(token: string): boolean { } } +export const CSRF_COOKIE_NAME = "bh-csrf-token"; + +export function generateCsrfToken(): string { + return crypto.randomBytes(32).toString("base64url"); +} + export { COOKIE_NAME }; diff --git a/src/lib/csrf.ts b/src/lib/csrf.ts new file mode 100644 index 0000000..ce7c335 --- /dev/null +++ b/src/lib/csrf.ts @@ -0,0 +1,17 @@ +const CSRF_COOKIE_NAME = "bh-csrf-token"; + +function getCsrfToken(): string { + const match = document.cookie + .split("; ") + .find((c) => c.startsWith(`${CSRF_COOKIE_NAME}=`)); + return match ? match.split("=")[1] : ""; +} + +/** Wrapper around fetch that auto-includes the CSRF token header for admin API calls */ +export function adminFetch(url: string, init?: RequestInit): Promise { + const headers = new Headers(init?.headers); + if (!headers.has("x-csrf-token")) { + headers.set("x-csrf-token", getCsrfToken()); + } + return fetch(url, { ...init, headers }); +} diff --git a/src/proxy.ts b/src/proxy.ts index 75edf39..8e8a71a 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,6 +1,10 @@ import { NextRequest, NextResponse } from "next/server"; import { verifyToken, COOKIE_NAME } from "@/lib/auth-edge"; +const CSRF_COOKIE_NAME = "bh-csrf-token"; +const CSRF_HEADER_NAME = "x-csrf-token"; +const STATE_CHANGING_METHODS = new Set(["POST", "PUT", "DELETE", "PATCH"]); + export async function proxy(request: NextRequest) { const { pathname } = request.nextUrl; @@ -20,6 +24,16 @@ export async function proxy(request: NextRequest) { return NextResponse.redirect(new URL("/admin/login", request.url)); } + // CSRF check on state-changing API requests + if (pathname.startsWith("/api/admin/") && STATE_CHANGING_METHODS.has(request.method)) { + const csrfCookie = request.cookies.get(CSRF_COOKIE_NAME)?.value; + const csrfHeader = request.headers.get(CSRF_HEADER_NAME); + + if (!csrfCookie || !csrfHeader || csrfCookie !== csrfHeader) { + return NextResponse.json({ error: "CSRF token mismatch" }, { status: 403 }); + } + } + return NextResponse.next(); }