Customer Portal CRM — Implementation Plan
DerinFor agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build a customer-facing CRM portal with admin CRUD management, iron-session auth, and complete security isolation from the existing admin panel.
Architecture: Monolithic extension of the existing Next.js 16 app. Customer auth via iron-session (separate from admin NextAuth). All data managed by admin via CRUD. Customer portal is read-only (except profile). Glassmorphism UI matching landing page design system.
Tech Stack: Next.js 16 (App Router), Prisma 7 + PostgreSQL, iron-session v8, bcryptjs, next-intl, Tailwind CSS, Lucide React, React Context API
Spec: docs/superpowers/specs/2026-03-27-customer-portal-crm-design.md
File Structure Overview
Bölüm başlığı “File Structure Overview”New Files to Create
Bölüm başlığı “New Files to Create”# Auth & Sessionlib/customer-session.ts — iron-session config + helperscontexts/CustomerSessionContext.tsx — React Context for client componentslib/file-storage.ts — Secure file upload/download utilities
# i18nmessages/tr.json — MODIFY: add CustomerPortal + AdminCustomer namespacesmessages/en.json — MODIFY: add CustomerPortal + AdminCustomer namespaces
# Customer Portal Pagesapp/[locale]/customer/login/page.tsxapp/[locale]/customer/forgot-password/page.tsxapp/[locale]/customer/(portal)/layout.tsxapp/[locale]/customer/(portal)/dashboard/page.tsxapp/[locale]/customer/(portal)/services/page.tsxapp/[locale]/customer/(portal)/services/[id]/page.tsxapp/[locale]/customer/(portal)/files/page.tsxapp/[locale]/customer/(portal)/files/[id]/page.tsxapp/[locale]/customer/(portal)/development-requests/page.tsxapp/[locale]/customer/(portal)/development-requests/[id]/page.tsxapp/[locale]/customer/(portal)/invoices/page.tsxapp/[locale]/customer/(portal)/invoices/[id]/page.tsxapp/[locale]/customer/(portal)/api-docs/page.tsxapp/[locale]/customer/(portal)/profile/page.tsx
# Customer Portal Componentscomponents/customer/CustomerSidebar.tsxcomponents/customer/CustomerHeader.tsxcomponents/customer/CustomerLayoutWrapper.tsxcomponents/customer/DashboardStats.tsxcomponents/customer/ServiceCard.tsxcomponents/customer/ServiceDetail.tsxcomponents/customer/FileList.tsxcomponents/customer/FileDownloadButton.tsxcomponents/customer/InvoiceTable.tsxcomponents/customer/DevRequestCard.tsxcomponents/customer/ApiKeyDisplay.tsxcomponents/customer/ChangelogTimeline.tsxcomponents/customer/ProfileForm.tsx
# Admin New Pagesapp/[locale]/admin/(panel)/customers/page.tsxapp/[locale]/admin/(panel)/customers/create/page.tsxapp/[locale]/admin/(panel)/customers/[id]/edit/page.tsxapp/[locale]/admin/(panel)/customers/[id]/services/page.tsxapp/[locale]/admin/(panel)/services/page.tsxapp/[locale]/admin/(panel)/services/[id]/edit/page.tsxapp/[locale]/admin/(panel)/services/types/page.tsxapp/[locale]/admin/(panel)/files/page.tsxapp/[locale]/admin/(panel)/files/upload/page.tsxapp/[locale]/admin/(panel)/invoices/page.tsxapp/[locale]/admin/(panel)/invoices/create/page.tsxapp/[locale]/admin/(panel)/invoices/[id]/edit/page.tsxapp/[locale]/admin/(panel)/development-requests/page.tsxapp/[locale]/admin/(panel)/development-requests/[id]/edit/page.tsxapp/[locale]/admin/(panel)/api-changelog/page.tsxapp/[locale]/admin/(panel)/api-changelog/create/page.tsxapp/[locale]/admin/(panel)/api-changelog/[id]/edit/page.tsx
# Admin New Componentscomponents/admin/CustomerForm.tsxcomponents/admin/ServiceForm.tsxcomponents/admin/FileUploadForm.tsxcomponents/admin/InvoiceForm.tsxcomponents/admin/ApiChangelogForm.tsx
# Server Actionsapp/lib/customer-actions.ts — Customer portal server actionsapp/lib/admin-customer-actions.ts — Admin CRUD server actions for customer management
# API Routesapp/api/customer/auth/login/route.tsapp/api/customer/auth/logout/route.tsapp/api/customer/files/[id]/download/route.tsapp/api/customer/session/route.tsFiles to Modify
Bölüm başlığı “Files to Modify”prisma/schema.prisma — Add 7 new models + AdminUser relationprisma/seed.ts — Add ServiceType seed dataproxy.ts — Add customer route handling.gitignore — Add .superpowers/ and /uploads/components/admin/AdminSidebar.tsx — Add customer management nav itemsmessages/tr.json — Add new translation namespacesmessages/en.json — Add new translation namespacesTask 1: Database Schema & Dependencies
Bölüm başlığı “Task 1: Database Schema & Dependencies”Files:
-
Modify:
prisma/schema.prisma -
Modify:
prisma/seed.ts -
Modify:
.gitignore -
Modify:
package.json(install iron-session) -
Step 1: Install iron-session
npm install iron-session- Step 2: Update Prisma schema — add all 7 new models
Open prisma/schema.prisma and append the following models after the existing Faq model. Also add a files relation to the existing AdminUser model.
Add to AdminUser:
model AdminUser { // ... existing fields ... files File[] // NEW: track who uploaded files}Add new models (exact code in spec Section 3):
-
Customer -
CustomerSession -
ServiceType -
Service -
File -
Invoice -
DevelopmentRequest -
ApiChangelog -
Step 3: Run Prisma migration
npx prisma migrate dev --name add_customer_portal_modelsExpected: Migration creates 7 new tables + updates AdminUser.
- Step 4: Update seed file — add ServiceType seed data
In prisma/seed.ts, after the AdminUser seed block, add:
// Seed ServiceTypesconst serviceTypes = [ { slug: 'license', name: 'License' }, { slug: 'hosting', name: 'Hosting' }, { slug: 'domain', name: 'Domain' }, { slug: 'api', name: 'API Access' }, { slug: 'development', name: 'Development' },];
for (const st of serviceTypes) { await prisma.serviceType.upsert({ where: { slug: st.slug }, update: {}, create: st, });}console.log('ServiceTypes seeded');- Step 5: Run seed
npx prisma db seedExpected: “ServiceTypes seeded” in output.
- Step 6: Generate Prisma client
npx prisma generate- Step 7: Commit
git add -A && git commit -m "feat: Customer Portal veritabanı şeması — 7 yeni tablo + iron-session bağımlılığı"Task 2: iron-session Auth Infrastructure
Bölüm başlığı “Task 2: iron-session Auth Infrastructure”Files:
-
Create:
lib/customer-session.ts -
Create:
contexts/CustomerSessionContext.tsx -
Step 1: Create iron-session configuration and helpers
Create lib/customer-session.ts:
import { getIronSession, IronSession } from 'iron-session';import { cookies } from 'next/headers';import prisma from '@/lib/prisma';
export interface CustomerSessionData { sessionId: number; customerId: number; role: 'customer'; email: string; name: string; company: string | null; locale: string;}
const sessionOptions = { password: process.env.CUSTOMER_SESSION_SECRET!, cookieName: 'customer-session', cookieOptions: { secure: process.env.NODE_ENV === 'production', httpOnly: true, sameSite: 'lax' as const, maxAge: 60 * 60 * 24 * 7, // 7 days },};
export async function getCustomerSession(): Promise<IronSession<CustomerSessionData>> { const cookieStore = await cookies(); return getIronSession<CustomerSessionData>(cookieStore, sessionOptions);}
/** * Validate customer session against DB. * Returns customer data if valid, null if invalid. */export async function validateCustomerSession(): Promise<CustomerSessionData | null> { const session = await getCustomerSession();
if (!session.sessionId || !session.customerId) { return null; }
// Verify session exists in DB and customer is active const dbSession = await prisma.customerSession.findUnique({ where: { id: session.sessionId }, include: { customer: true }, });
if (!dbSession || dbSession.expiresAt < new Date() || !dbSession.customer.isActive) { session.destroy(); return null; }
return { sessionId: session.sessionId, customerId: session.customerId, role: 'customer', email: dbSession.customer.email, name: dbSession.customer.name, company: dbSession.customer.company, locale: dbSession.customer.locale, };}- Step 2: Create CustomerSessionContext for client components
Create contexts/CustomerSessionContext.tsx:
'use client';
import { createContext, useContext, ReactNode } from 'react';
interface CustomerSession { customerId: number; email: string; name: string; company: string | null; locale: string;}
const CustomerSessionContext = createContext<CustomerSession | null>(null);
export function CustomerSessionProvider({ session, children,}: { session: CustomerSession; children: ReactNode;}) { return ( <CustomerSessionContext.Provider value={session}> {children} </CustomerSessionContext.Provider> );}
export function useCustomerSession(): CustomerSession { const session = useContext(CustomerSessionContext); if (!session) { throw new Error('useCustomerSession must be used within CustomerSessionProvider'); } return session;}- Step 3: Add CUSTOMER_SESSION_SECRET to .env
# Generate a 32+ char secretecho "CUSTOMER_SESSION_SECRET=$(openssl rand -hex 32)" >> .env- Step 4: Commit
git add -A && git commit -m "feat: iron-session auth altyapısı — session config, validation, React Context"Task 3: Middleware Update — Customer Route Protection
Bölüm başlığı “Task 3: Middleware Update — Customer Route Protection”Files:
-
Modify:
proxy.ts -
Step 1: Update proxy.ts to handle customer routes
Add customer route patterns and cookie check alongside existing admin logic. The key additions:
// After existing ADMIN_PATH and AUTH_PATH constants, add:const CUSTOMER_PATH = /^\/(?:([a-z]{2})\/)?customer(?:\/.*)?$/;const CUSTOMER_AUTH_PATH = /^\/(?:([a-z]{2})\/)?customer\/(login|forgot-password)$/;
// Add customer session cookie check function:function hasCustomerSessionCookie(req: NextRequest): boolean { return req.cookies.has('customer-session');}In the proxy() function, add a new block BEFORE the intl middleware fallthrough:
// Customer routes — lightweight cookie check + redirectif (CUSTOMER_PATH.test(pathname)) { const isAuthPath = CUSTOMER_AUTH_PATH.test(pathname); const hasSession = hasCustomerSessionCookie(req); const localeMatch = pathname.match(/^\/([a-z]{2})\//); const locale = localeMatch ? localeMatch[1] : 'en'; const prefix = locale === 'en' ? '' : `${locale}/`;
// /customer or /{locale}/customer → redirect to dashboard or login if (/^\/(?:([a-z]{2})\/)?customer\/?$/.test(pathname)) { const target = hasSession ? 'customer/dashboard' : 'customer/login'; return NextResponse.redirect(new URL(`/${prefix}${target}`, req.url)); }
// Protected page without session → login if (!isAuthPath && !hasSession) { return NextResponse.redirect(new URL(`/${prefix}customer/login`, req.url)); }
// Login page with session → dashboard if (isAuthPath && hasSession) { return NextResponse.redirect(new URL(`/${prefix}customer/dashboard`, req.url)); }
// Customer without locale prefix → rewrite to /en/customer/... if (!localeMatch) { const url = req.nextUrl.clone(); url.pathname = `/en${pathname}`; return NextResponse.rewrite(url); }
return NextResponse.next();}Also add cross-access prevention: if an admin session cookie is present on /customer/* routes, or a customer session cookie on /admin/* routes, allow it — they are separate cookies and don’t conflict. The real role check happens in Server Components.
- Step 2: Verify existing admin auth still works
npm run buildExpected: Build succeeds without errors.
- Step 3: Commit
git add proxy.ts && git commit -m "feat: middleware — customer route koruması eklendi, admin ile izole"Task 4: Customer Auth — Login, Logout, Session API
Bölüm başlığı “Task 4: Customer Auth — Login, Logout, Session API”Files:
-
Create:
app/lib/customer-actions.ts -
Create:
app/[locale]/customer/login/page.tsx -
Create:
app/api/customer/auth/login/route.ts -
Create:
app/api/customer/auth/logout/route.ts -
Create:
app/api/customer/session/route.ts -
Step 1: Create customer server actions
Create app/lib/customer-actions.ts with customerLogin and customerLogout server actions:
'use server';
import prisma from '@/lib/prisma';import bcrypt from 'bcryptjs';import { getCustomerSession, validateCustomerSession } from '@/lib/customer-session';import { redirect } from 'next/navigation';import { headers } from 'next/headers';import { rateLimit } from '@/lib/rate-limit';import crypto from 'crypto';
export async function customerLogin( prevState: string | undefined, formData: FormData,): Promise<string | undefined> { const email = formData.get('email') as string; const password = formData.get('password') as string;
if (!email || !password) { return 'Email ve şifre gereklidir.'; }
// Rate limiting const headersList = await headers(); const ip = headersList.get('x-forwarded-for') || 'unknown'; const { allowed } = rateLimit(ip, { maxRequests: 5, windowMs: 15 * 60 * 1000 }); if (!allowed) { return 'Çok fazla giriş denemesi. 15 dakika sonra tekrar deneyin.'; }
const customer = await prisma.customer.findUnique({ where: { email } }); if (!customer || !customer.isActive) { return 'Giriş bilgileri hatalı.'; }
const valid = await bcrypt.compare(password, customer.password); if (!valid) { return 'Giriş bilgileri hatalı.'; }
// Create DB session const dbSession = await prisma.customerSession.create({ data: { customerId: customer.id, sessionToken: crypto.randomUUID(), ipAddress: ip, userAgent: headersList.get('user-agent') || null, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days }, });
// Set iron-session cookie const session = await getCustomerSession(); session.sessionId = dbSession.id; session.customerId = customer.id; session.role = 'customer'; session.email = customer.email; session.name = customer.name; session.company = customer.company; session.locale = customer.locale; await session.save();
redirect(`/${customer.locale}/customer/dashboard`);}
export async function customerLogout(): Promise<void> { const session = await getCustomerSession();
if (session.sessionId) { // Delete DB session await prisma.customerSession.delete({ where: { id: session.sessionId }, }).catch(() => {}); // Ignore if already deleted }
session.destroy(); redirect('/customer/login');}
export async function updateCustomerProfile( prevState: string | undefined, formData: FormData,): Promise<string | undefined> { const sessionData = await validateCustomerSession(); if (!sessionData) { redirect('/customer/login'); }
const name = formData.get('name') as string; const company = formData.get('company') as string; const phone = formData.get('phone') as string; const locale = formData.get('locale') as string;
await prisma.customer.update({ where: { id: sessionData.customerId }, data: { name, company, phone, locale }, });
return 'Profil güncellendi.';}
export async function changeCustomerPassword( prevState: string | undefined, formData: FormData,): Promise<string | undefined> { const sessionData = await validateCustomerSession(); if (!sessionData) { redirect('/customer/login'); }
const currentPassword = formData.get('currentPassword') as string; const newPassword = formData.get('newPassword') as string;
if (!currentPassword || !newPassword || newPassword.length < 8) { return 'Şifre en az 8 karakter olmalıdır.'; }
const customer = await prisma.customer.findUnique({ where: { id: sessionData.customerId }, });
if (!customer) return 'Müşteri bulunamadı.';
const valid = await bcrypt.compare(currentPassword, customer.password); if (!valid) return 'Mevcut şifre hatalı.';
const hash = await bcrypt.hash(newPassword, 12); await prisma.customer.update({ where: { id: sessionData.customerId }, data: { password: hash }, });
return 'Şifre değiştirildi.';}- Step 2: Create customer login page
Create app/[locale]/customer/login/page.tsx — server component with a client form. Follow the existing admin login pattern at app/[locale]/admin/login/page.tsx but:
-
Use
customerLoginaction instead ofauthenticate -
Brand it as “Customer Portal” with glassmorphism styling
-
Use
font-techfor headings, brand-red accent -
Background:
#030304with grid overlay and red glow orb -
Step 3: Create session API route for client components
Create app/api/customer/session/route.ts:
import { validateCustomerSession } from '@/lib/customer-session';import { NextResponse } from 'next/server';
export async function GET() { const session = await validateCustomerSession(); if (!session) { return NextResponse.json({ authenticated: false }, { status: 401 }); } return NextResponse.json({ authenticated: true, ...session });}- Step 4: Create logout API route
Create app/api/customer/auth/logout/route.ts:
import { getCustomerSession } from '@/lib/customer-session';import prisma from '@/lib/prisma';import { NextResponse } from 'next/server';
export async function POST() { const session = await getCustomerSession(); if (session.sessionId) { await prisma.customerSession.delete({ where: { id: session.sessionId }, }).catch(() => {}); } session.destroy(); return NextResponse.json({ success: true });}- Step 5: Commit
git add -A && git commit -m "feat: müşteri auth sistemi — login, logout, session API, rate limiting"Task 5: Customer Portal Layout & Navigation
Bölüm başlığı “Task 5: Customer Portal Layout & Navigation”Files:
-
Create:
components/customer/CustomerSidebar.tsx -
Create:
components/customer/CustomerHeader.tsx -
Create:
components/customer/CustomerLayoutWrapper.tsx -
Create:
app/[locale]/customer/(portal)/layout.tsx -
Create:
app/[locale]/customer/layout.tsx -
Step 1: Create customer layout (locale level)
Create app/[locale]/customer/layout.tsx:
import { ReactNode } from 'react';
export const metadata = { robots: { index: false, follow: false },};
export default function CustomerLayout({ children }: { children: ReactNode }) { return children;}- Step 2: Create CustomerSidebar component
Create components/customer/CustomerSidebar.tsx — client component using Lucide icons and @/i18n/navigation Link. Follow the approved glassmorphism mockup design:
-
Background:
bg-white/2 backdrop-blur-[20px] -
Border:
border-r border-white/6 -
Red glow orb: absolute positioned
bg-red-500/6 blur-[80px] -
Logo:
font-techuppercase, brand-red span -
User card: red gradient avatar, name + plan
-
Nav items: grouped with uppercase labels, active state
bg-red-500/8 border border-red-500/15 -
Badges: count (red bg), new (green bg), warn (amber bg)
-
Lucide icons:
LayoutDashboard,Wrench,FolderOpen,Code,CreditCard,Zap,User,LogOut -
Mobile responsive: hamburger toggle (same pattern as AdminSidebar)
-
Logout: calls
customerLogoutserver action -
Step 3: Create CustomerHeader component
Create components/customer/CustomerHeader.tsx — client component:
-
font-techuppercase title -
Breadcrumb text
-
Language selector showing customer’s locale
-
Background:
bg-white/1 backdrop-blur-[10px] border-b border-white/6 -
Step 4: Create portal layout with auth guard
Create app/[locale]/customer/(portal)/layout.tsx:
import { ReactNode } from 'react';import { redirect } from 'next/navigation';import { validateCustomerSession } from '@/lib/customer-session';import { CustomerSessionProvider } from '@/contexts/CustomerSessionContext';import CustomerSidebar from '@/components/customer/CustomerSidebar';import CustomerHeader from '@/components/customer/CustomerHeader';
export const dynamic = 'force-dynamic';
export default async function PortalLayout({ children }: { children: ReactNode }) { const session = await validateCustomerSession(); if (!session) { redirect('/customer/login'); }
return ( <CustomerSessionProvider session={{ customerId: session.customerId, email: session.email, name: session.name, company: session.company, locale: session.locale, }}> <div className="min-h-screen bg-[#030304]"> {/* Grid overlay */} <div className="fixed inset-0 bg-[linear-gradient(to_right,#80808008_1px,transparent_1px),linear-gradient(to_bottom,#80808008_1px,transparent_1px)] bg-size-[60px_60px] pointer-events-none" /> {/* Red glow orb */} <div className="fixed -top-20 right-48 w-72 h-72 bg-red-500/4 rounded-full blur-[120px] pointer-events-none" /> <div className="fixed bottom-24 left-96 w-48 h-48 bg-blue-500/3 rounded-full blur-[120px] pointer-events-none" />
<CustomerSidebar /> <div className="lg:ml-[250px] min-h-screen flex flex-col relative z-10"> <CustomerHeader /> <main className="flex-1 p-4 lg:p-7 overflow-x-hidden"> {children} </main> </div> </div> </CustomerSessionProvider> );}- Step 5: Verify build
npm run build- Step 6: Commit
git add -A && git commit -m "feat: müşteri portalı layout — glassmorphism sidebar, header, auth guard"Task 6: i18n — Translation Namespaces
Bölüm başlığı “Task 6: i18n — Translation Namespaces”Files:
-
Modify:
messages/tr.json -
Modify:
messages/en.json -
Step 1: Add CustomerPortal namespace to tr.json
Add to messages/tr.json:
"CustomerPortal": { "dashboard": "Dashboard", "services": "Servislerim", "files": "Dosyalar", "devRequests": "Geliştirme İstekleri", "invoices": "Faturalar", "api": "API", "profile": "Profil", "logout": "Çıkış Yap", "mainMenu": "Ana Menü", "financeTech": "Finans & Teknik", "activeServices": "Aktif Servisler", "upcomingRenewals": "Yaklaşan Yenileme", "openRequests": "Açık İstekler", "lastInvoice": "Son Fatura", "allActive": "Tümü aktif", "daysLeft": "{days} gün kaldı", "withinDays": "{days} gün içinde", "inProgress": "Devam ediyor", "paid": "Ödendi", "pending": "Beklemede", "overdue": "Gecikmiş", "cancelled": "İptal", "active": "Aktif", "expired": "Süresi doldu", "suspended": "Askıda", "recentActivities": "Son Aktiviteler", "upcomingDates": "Yaklaşan Tarihler", "noUpcoming": "Yaklaşan başka tarih yok", "version": "Versiyon", "download": "İndir", "fileSize": "Boyut", "uploadDate": "Yükleme Tarihi", "changelog": "Değişiklik Notları", "invoiceNumber": "Fatura No", "amount": "Tutar", "dueDate": "Son Ödeme", "status": "Durum", "priority": "Öncelik", "low": "Düşük", "medium": "Orta", "high": "Yüksek", "estimatedPrice": "Tahmini Fiyat", "finalPrice": "Son Fiyat", "requestDate": "Talep Tarihi", "completedDate": "Tamamlanma Tarihi", "apiKeys": "API Anahtarları", "apiChangelog": "API Güncelleme Notları", "maskedKey": "Gizli Anahtar", "profileInfo": "Profil Bilgileri", "changePassword": "Şifre Değiştir", "currentPassword": "Mevcut Şifre", "newPassword": "Yeni Şifre", "save": "Kaydet", "login": "Giriş Yap", "email": "E-posta", "password": "Şifre", "loginTitle": "Müşteri Girişi", "loginSubtitle": "Hesabınıza giriş yapın", "invalidCredentials": "Giriş bilgileri hatalı.", "tooManyAttempts": "Çok fazla giriş denemesi. 15 dakika sonra tekrar deneyin.", "unlimited": "Sınırsız", "monthly": "Aylık", "yearly": "Yıllık", "oneTime": "Tek Seferlik", "until": "{date} tarihine kadar", "customerPanel": "Müşteri Paneli", "noData": "Veri bulunamadı"},"AdminCustomer": { "customers": "Müşteriler", "createCustomer": "Yeni Müşteri", "editCustomer": "Müşteri Düzenle", "customerServices": "Müşteri Servisleri", "serviceTypes": "Servis Tipleri", "allServices": "Tüm Servisler", "editService": "Servis Düzenle", "fileManagement": "Dosya Yönetimi", "uploadFile": "Dosya Yükle", "allInvoices": "Tüm Faturalar", "createInvoice": "Yeni Fatura", "editInvoice": "Fatura Düzenle", "devRequests": "Geliştirme İstekleri", "editDevRequest": "İstek Düzenle", "apiChangelog": "API Changelog", "createChangelog": "Yeni Changelog", "editChangelog": "Changelog Düzenle", "name": "Ad Soyad", "email": "E-posta", "company": "Firma", "phone": "Telefon", "locale": "Dil", "isActive": "Aktif", "password": "Şifre", "passwordHelp": "En az 8 karakter", "save": "Kaydet", "cancel": "İptal", "delete": "Sil", "confirmDelete": "Silmek istediğinize emin misiniz?", "created": "Oluşturuldu", "updated": "Güncellendi", "actions": "İşlemler", "selectCustomer": "Müşteri Seç", "selectService": "Servis Seç", "selectServiceType": "Servis Tipi Seç", "assignToCustomer": "Müşteriye Ata", "file": "Dosya", "version": "Versiyon", "changelogNotes": "Değişiklik Notları", "invoiceItems": "Fatura Kalemleri", "addItem": "Kalem Ekle", "description": "Açıklama", "quantity": "Adet", "unitPrice": "Birim Fiyat", "total": "Toplam"}- Step 2: Add CustomerPortal namespace to en.json
Same structure as TR, all keys must have exact EN counterparts or next-intl will throw runtime errors on missing keys. Write out the full EN block — do not skip any key. Key examples:
-
"dashboard": "Dashboard","services": "My Services","files": "Files" -
"loginTitle": "Customer Login","loginSubtitle": "Sign in to your account" -
"activeServices": "Active Services","upcomingRenewals": "Upcoming Renewals" -
"daysLeft": "{days} days left","until": "Until {date}" -
All status/priority strings, all form labels, all page titles — every single TR key needs an EN equivalent.
-
Step 3: Commit
git add messages/ && git commit -m "feat: i18n — CustomerPortal + AdminCustomer çeviri namespace'leri (TR + EN)"Task 7: Admin CRUD — Customer Management
Bölüm başlığı “Task 7: Admin CRUD — Customer Management”Files:
-
Create:
app/lib/admin-customer-actions.ts -
Create:
app/[locale]/admin/(panel)/customers/page.tsx -
Create:
app/[locale]/admin/(panel)/customers/create/page.tsx -
Create:
app/[locale]/admin/(panel)/customers/[id]/edit/page.tsx -
Create:
app/[locale]/admin/(panel)/customers/[id]/services/page.tsx -
Create:
components/admin/CustomerForm.tsx -
Modify:
components/admin/AdminSidebar.tsx -
Step 1: Create admin customer server actions
Create app/lib/admin-customer-actions.ts with:
createCustomer(prevState, formData)— hash password with bcrypt, create Customer recordupdateCustomer(prevState, formData)— update name, email, company, phone, locale, isActive. If password provided, hash and update.deleteCustomer(id)— delete customer (cascades to sessions, services, etc.)createService(prevState, formData)— create Service linked to customerupdateService(prevState, formData)— update service fieldsdeleteService(id)— delete servicecreateServiceType(prevState, formData)— create new service typeupdateServiceType(prevState, formData)— update service typedeleteServiceType(id)— delete service type (only if no services linked)
All actions must check const session = await auth(); if (!session?.user) return { error: 'Yetkisiz işlem.' }; at the top (same pattern as existing admin actions in app/lib/actions.ts).
All mutations call revalidatePath for relevant paths.
- Step 2: Create CustomerForm component
Create components/admin/CustomerForm.tsx — client component with fields:
-
name (required), email (required), password (required for create, optional for edit)
-
company, phone, locale (dropdown with 24 locales), isActive (checkbox)
-
Uses
useActionStatehook with server action -
Follow existing admin form patterns (see
components/admin/BlogPostForm.tsxfor reference) -
Step 3: Create customers list page
Create app/[locale]/admin/(panel)/customers/page.tsx — server component:
-
Auth guard:
const session = await auth(); if (!session?.user) redirect('/admin/login'); -
Fetch all customers with
prisma.customer.findMany({ include: { _count: { select: { services: true } } }, orderBy: { createdAt: 'desc' } }) -
Display table: name, email, company, services count, isActive badge, createdAt, actions (edit/view services)
-
Link to create page
-
Step 4: Create customer create page
Create app/[locale]/admin/(panel)/customers/create/page.tsx — server component with auth guard, renders <CustomerForm /> in create mode.
- Step 5: Create customer edit page
Create app/[locale]/admin/(panel)/customers/[id]/edit/page.tsx — server component:
-
Auth guard + fetch customer by id
-
Render
<CustomerForm customer={customer} />in edit mode -
Step 6: Create customer’s services page
Create app/[locale]/admin/(panel)/customers/[id]/services/page.tsx — server component:
-
Shows all services for this customer
-
Quick-add service form
-
Links to service edit pages
-
Step 7: Update AdminSidebar with new navigation items
Modify components/admin/AdminSidebar.tsx — add new menu items after existing ones:
// Add to menuItems array:{ name: 'Müşteriler', href: '/admin/customers', icon: Users },{ name: 'Servisler', href: '/admin/services', icon: Wrench },{ name: 'Dosyalar', href: '/admin/files', icon: FolderOpen },{ name: 'Faturalar', href: '/admin/invoices', icon: CreditCard },{ name: 'Dev İstekleri', href: '/admin/development-requests', icon: Code },{ name: 'API Changelog', href: '/admin/api-changelog', icon: Zap },Import new Lucide icons: Users, Wrench, FolderOpen, CreditCard, Code, Zap.
- Step 8: Commit
git add -A && git commit -m "feat: admin müşteri CRUD — liste, oluştur, düzenle, servisler + sidebar güncelleme"Task 8: Admin CRUD — Service Types & Services
Bölüm başlığı “Task 8: Admin CRUD — Service Types & Services”Files:
-
Create:
app/[locale]/admin/(panel)/services/page.tsx -
Create:
app/[locale]/admin/(panel)/services/[id]/edit/page.tsx -
Create:
app/[locale]/admin/(panel)/services/types/page.tsx -
Create:
components/admin/ServiceForm.tsx -
Step 1: Create ServiceForm component
Create components/admin/ServiceForm.tsx — client component:
-
Customer selector (dropdown), service type selector (dropdown)
-
name, description, status (dropdown: active/expired/suspended/cancelled)
-
startDate, endDate (date inputs), autoRenew (checkbox)
-
price, currency (dropdown: EUR/USD/TRY/GBP), billingCycle (dropdown: monthly/yearly/one-time)
-
metadata (JSON editor — a textarea for now, JSON.parse validation)
-
Uses
useActionStatewith server action -
Step 2: Create services list page
Create app/[locale]/admin/(panel)/services/page.tsx:
-
Fetch all services with customer name and service type
-
Table: customer, name, type, status, dates, price, actions
-
Filter by status, service type
-
Step 3: Create service edit page
Create app/[locale]/admin/(panel)/services/[id]/edit/page.tsx:
-
Fetch service with relations
-
Render
<ServiceForm service={service} /> -
Step 4: Create service types CRUD page
Create app/[locale]/admin/(panel)/services/types/page.tsx:
-
Inline CRUD: list types + inline form to add/edit
-
Delete only if no linked services (show count)
-
Step 5: Commit
git add -A && git commit -m "feat: admin servis CRUD — servis listesi, düzenleme, servis tipleri yönetimi"Task 9: Admin CRUD — File Management (Secure Upload)
Bölüm başlığı “Task 9: Admin CRUD — File Management (Secure Upload)”Files:
-
Create:
lib/file-storage.ts -
Create:
app/[locale]/admin/(panel)/files/page.tsx -
Create:
app/[locale]/admin/(panel)/files/upload/page.tsx -
Create:
components/admin/FileUploadForm.tsx -
Step 1: Create file storage utility
Create lib/file-storage.ts:
import { randomUUID } from 'crypto';import path from 'path';import { mkdir, writeFile, stat } from 'fs/promises';import { createReadStream } from 'fs';
// Store files outside public/ — NEVER directly accessibleconst UPLOAD_DIR = path.join(process.cwd(), 'uploads', 'customer-files');
export async function saveFile(file: File): Promise<{ fileName: string; originalName: string; filePath: string; fileSize: number; mimeType: string;}> { await mkdir(UPLOAD_DIR, { recursive: true });
const ext = path.extname(file.name); const fileName = `${randomUUID()}${ext}`; const filePath = path.join(UPLOAD_DIR, fileName); const buffer = Buffer.from(await file.arrayBuffer());
await writeFile(filePath, buffer);
return { fileName, originalName: file.name, filePath, fileSize: buffer.length, mimeType: file.type || 'application/octet-stream', };}
export function getFileStream(filePath: string) { return createReadStream(filePath);}
export async function getFileStats(filePath: string) { return stat(filePath);}- Step 2: Add file upload/delete actions to admin-customer-actions.ts
Add to app/lib/admin-customer-actions.ts:
-
uploadFile(prevState, formData)— save file viasaveFile(), create File record with customerId, serviceId, version, changelog, uploadedBy -
deleteFile(id)— delete File record + unlink physical file -
Step 3: Create FileUploadForm component
Create components/admin/FileUploadForm.tsx:
-
Customer selector, optional service selector
-
File input (accept
.zip,.sql,.tar.gz,.rar) -
Version string, changelog textarea
-
Uses
useActionStatewith server action -
Shows upload progress indication
-
Step 4: Create files list page
Create app/[locale]/admin/(panel)/files/page.tsx:
-
Fetch all files with customer name and admin uploader name
-
Table: customer, originalName, version, fileSize (formatted), uploadedBy, createdAt, actions (delete)
-
Filter by customer
-
Step 5: Create file upload page
Create app/[locale]/admin/(panel)/files/upload/page.tsx:
-
Auth guard + render
<FileUploadForm /> -
Pass customers list and services list for dropdowns
-
Step 6: Create uploads directory
mkdir -p uploads/customer-filesecho '*' > uploads/customer-files/.gitignoreecho '!.gitignore' >> uploads/customer-files/.gitignore- Step 7: Commit
git add -A && git commit -m "feat: admin dosya yönetimi — güvenli yükleme, listeleme, file-storage utility"Task 10: Admin CRUD — Invoices
Bölüm başlığı “Task 10: Admin CRUD — Invoices”Files:
-
Create:
app/[locale]/admin/(panel)/invoices/page.tsx -
Create:
app/[locale]/admin/(panel)/invoices/create/page.tsx -
Create:
app/[locale]/admin/(panel)/invoices/[id]/edit/page.tsx -
Create:
components/admin/InvoiceForm.tsx -
Step 1: Add invoice actions to admin-customer-actions.ts
Add:
-
createInvoice(prevState, formData)— create Invoice with JSON items -
updateInvoice(prevState, formData)— update invoice fields + items -
deleteInvoice(id)— delete invoice -
Step 2: Create InvoiceForm component
Create components/admin/InvoiceForm.tsx:
-
Customer selector, invoiceNumber (auto-generate suggestion:
INV-YYYY-NNN) -
title, description
-
Dynamic line items: add/remove rows with description, quantity, unitPrice, total (auto-calculated)
-
amount (auto-sum from items), currency, status dropdown
-
dueDate, paidAt
-
Step 3: Create invoices list, create, edit pages
Follow same pattern as customers: list with table, create page with form, edit page with pre-filled form. Auth guard on all.
- Step 4: Commit
git add -A && git commit -m "feat: admin fatura CRUD — oluştur, düzenle, dinamik kalem sistemi"Task 11: Admin CRUD — Dev Requests & API Changelog
Bölüm başlığı “Task 11: Admin CRUD — Dev Requests & API Changelog”Files:
-
Create:
app/[locale]/admin/(panel)/development-requests/page.tsx -
Create:
app/[locale]/admin/(panel)/development-requests/[id]/edit/page.tsx -
Create:
app/[locale]/admin/(panel)/api-changelog/page.tsx -
Create:
app/[locale]/admin/(panel)/api-changelog/create/page.tsx -
Create:
app/[locale]/admin/(panel)/api-changelog/[id]/edit/page.tsx -
Create:
components/admin/ApiChangelogForm.tsx -
Step 1: Add dev request and changelog actions
Add to app/lib/admin-customer-actions.ts:
-
createDevRequest(prevState, formData)— create DevelopmentRequest -
updateDevRequest(prevState, formData)— update status, priority, prices, completedAt -
deleteDevRequest(id) -
createApiChangelog(prevState, formData)— create ApiChangelog entry -
updateApiChangelog(prevState, formData) -
deleteApiChangelog(id) -
Step 2: Create dev requests list and edit pages
List page: table with customer, title, status badge, priority badge, dates, prices, actions. Edit page: form with status dropdown, priority, estimatedPrice, finalPrice, completedAt.
Note: Dev requests are created by admin on behalf of customer (not by customer themselves). Customer sees them read-only.
- Step 3: Create ApiChangelogForm component
Create components/admin/ApiChangelogForm.tsx:
-
version, title, content (markdown textarea), publishedAt (date)
-
Step 4: Create changelog list, create, edit pages
Standard CRUD pages. List shows version, title, publishedAt.
- Step 5: Commit
git add -A && git commit -m "feat: admin dev istekleri + API changelog CRUD"Task 12: Customer Portal — Dashboard
Bölüm başlığı “Task 12: Customer Portal — Dashboard”Files:
-
Create:
app/[locale]/customer/(portal)/dashboard/page.tsx -
Create:
components/customer/DashboardStats.tsx -
Create:
components/customer/ServiceCard.tsx -
Step 1: Create DashboardStats component
Create components/customer/DashboardStats.tsx — server component:
-
Receives stats data as props
-
Renders 4-column grid of stat cards
-
Each card: glass style
bg-white/2 border border-white/6 rounded-[10px] -
Bottom accent line with color-specific gradient
-
font-techfor labels and values -
Status colors: red (services), amber (renewals), blue (requests), green (invoice)
-
Step 2: Create ServiceCard component
Create components/customer/ServiceCard.tsx:
-
Glass card with service name, status badge, detail text, progress bar
-
Progress bar: calculated from startDate/endDate (percentage of time elapsed)
-
Warning border if expiring within 30 days
-
Clickable: links to service detail page
-
Step 3: Create dashboard page
Create app/[locale]/customer/(portal)/dashboard/page.tsx — server component:
// Auth validationconst session = await validateCustomerSession();if (!session) redirect('/customer/login');
// Parallel data fetchingconst [services, invoices, devRequests, files, changelog] = await Promise.all([ prisma.service.findMany({ where: { customerId: session.customerId }, include: { serviceType: true }, orderBy: { endDate: 'asc' }, }), prisma.invoice.findMany({ where: { customerId: session.customerId }, orderBy: { createdAt: 'desc' }, take: 1, }), prisma.developmentRequest.findMany({ where: { customerId: session.customerId, status: { in: ['pending', 'in_progress'] } }, }), prisma.file.findMany({ where: { customerId: session.customerId }, orderBy: { createdAt: 'desc' }, take: 3, }), prisma.apiChangelog.findMany({ orderBy: { publishedAt: 'desc' }, take: 3, }),]);
// Compute statsconst activeServices = services.filter(s => s.status === 'active').length;const upcomingRenewals = services.filter(s => { if (!s.endDate) return false; const daysLeft = Math.ceil((s.endDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)); return daysLeft > 0 && daysLeft <= 30;}).length;Render: stats grid → service cards (2x2) → bottom panels (activity + upcoming dates).
- Step 4: Commit
git add -A && git commit -m "feat: müşteri dashboard — stat kartları, servis kartları, aktivite akışı"Task 13: Customer Portal — Services
Bölüm başlığı “Task 13: Customer Portal — Services”Files:
-
Create:
app/[locale]/customer/(portal)/services/page.tsx -
Create:
app/[locale]/customer/(portal)/services/[id]/page.tsx -
Create:
components/customer/ServiceDetail.tsx -
Step 1: Create services list page
Server component with auth guard. Fetch all services for customer with serviceType included. Display as filterable card grid (filter by status, type). Each card links to detail page.
- Step 2: Create ServiceDetail component
Create components/customer/ServiceDetail.tsx:
-
Full service info: name, type, status, dates, price, billing cycle
-
Metadata display: render key-value pairs from JSON metadata (serverIp, ram, domain, etc.)
-
Associated files list (if any)
-
Glass card styling
-
Step 3: Create service detail page
Server component: auth guard + ownership check (service.customerId === session.customerId). Fetch service with serviceType, files. Render <ServiceDetail />.
- Step 4: Commit
git add -A && git commit -m "feat: müşteri servislerim — liste + detay sayfası"Task 14: Customer Portal — Secure File Download
Bölüm başlığı “Task 14: Customer Portal — Secure File Download”Files:
-
Create:
app/[locale]/customer/(portal)/files/page.tsx -
Create:
app/[locale]/customer/(portal)/files/[id]/page.tsx -
Create:
app/api/customer/files/[id]/download/route.ts -
Create:
components/customer/FileList.tsx -
Create:
components/customer/FileDownloadButton.tsx -
Step 1: Create secure download API route
Create app/api/customer/files/[id]/download/route.ts:
import { validateCustomerSession } from '@/lib/customer-session';import prisma from '@/lib/prisma';import { getFileStream, getFileStats } from '@/lib/file-storage';import { NextRequest, NextResponse } from 'next/server';import { Readable } from 'stream';
export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> }) { const { id } = await params; const fileId = parseInt(id, 10); if (isNaN(fileId)) { return NextResponse.json({ error: 'Invalid file ID' }, { status: 400 }); }
// Step 1: Session validation const session = await validateCustomerSession(); if (!session) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); }
// Step 2: Role check (already guaranteed by validateCustomerSession)
// Step 3: Ownership check const file = await prisma.file.findUnique({ where: { id: fileId }, include: { customer: true }, });
if (!file || file.customerId !== session.customerId) { return NextResponse.json({ error: 'Not found' }, { status: 404 }); }
// Step 4: Account active check if (!file.customer.isActive) { return NextResponse.json({ error: 'Account inactive' }, { status: 403 }); }
// Log download console.log(`[FILE_DOWNLOAD] customer=${session.customerId} file=${file.id} name=${file.originalName} at=${new Date().toISOString()}`);
// Stream file try { const stats = await getFileStats(file.filePath); const stream = getFileStream(file.filePath); const webStream = Readable.toWeb(stream) as ReadableStream;
return new NextResponse(webStream, { headers: { 'Content-Type': file.mimeType, 'Content-Disposition': `attachment; filename="${file.originalName}"`, 'Content-Length': stats.size.toString(), 'Cache-Control': 'no-store', }, }); } catch { return NextResponse.json({ error: 'File not found on disk' }, { status: 404 }); }}- Step 2: Create FileList and FileDownloadButton components
FileList.tsx — displays files grouped by service or ungrouped, with version badge, size, date.
FileDownloadButton.tsx — client component, triggers download via /api/customer/files/[id]/download.
- Step 3: Create files list and detail pages
List: all files for customer, sorted by createdAt desc. Group by version or service. Detail: file info + changelog + download button.
- Step 4: Commit
git add -A && git commit -m "feat: müşteri dosyalar — güvenli indirme API, 4 katmanlı doğrulama, stream"Task 15: Customer Portal — Invoices & Dev Requests
Bölüm başlığı “Task 15: Customer Portal — Invoices & Dev Requests”Files:
-
Create:
app/[locale]/customer/(portal)/invoices/page.tsx -
Create:
app/[locale]/customer/(portal)/invoices/[id]/page.tsx -
Create:
app/[locale]/customer/(portal)/development-requests/page.tsx -
Create:
app/[locale]/customer/(portal)/development-requests/[id]/page.tsx -
Create:
components/customer/InvoiceTable.tsx -
Create:
components/customer/DevRequestCard.tsx -
Step 1: Create InvoiceTable component
Glass-styled table: invoice number, title, amount, currency, status badge (color-coded), dueDate, paidAt. Clickable rows link to detail.
- Step 2: Create invoices list and detail pages
List: auth guard + fetch invoices for customer, render <InvoiceTable />.
Detail: full invoice info + line items breakdown (from JSON items field), status badge, dates.
- Step 3: Create DevRequestCard component
Glass card: title, status badge, priority badge, dates, estimated/final prices. Clickable to detail.
- Step 4: Create dev requests list and detail pages
List: auth guard + fetch dev requests for customer. Detail: full request info, status timeline, price info.
- Step 5: Commit
git add -A && git commit -m "feat: müşteri faturalar + geliştirme istekleri sayfaları"Task 16: Customer Portal — API Docs & Profile
Bölüm başlığı “Task 16: Customer Portal — API Docs & Profile”Files:
-
Create:
app/[locale]/customer/(portal)/api-docs/page.tsx -
Create:
app/[locale]/customer/(portal)/profile/page.tsx -
Create:
components/customer/ApiKeyDisplay.tsx -
Create:
components/customer/ChangelogTimeline.tsx -
Create:
components/customer/ProfileForm.tsx -
Step 1: Create ApiKeyDisplay component
Displays masked API key from service metadata (e.g., sk-****-****-abcd). Click to reveal (client component with state toggle). Copy to clipboard button.
- Step 2: Create ChangelogTimeline component
Vertical timeline of API changelog entries. Each entry: version badge, title, content (markdown rendered), publishedAt date.
- Step 3: Create API docs page
Server component: auth guard. Fetch customer’s API-type services (for keys) + global ApiChangelog entries. Render ApiKeyDisplay for each API service + ChangelogTimeline.
- Step 4: Create ProfileForm component
Client component with two sections:
- Profile info: name, company, phone, locale dropdown — uses
updateCustomerProfileaction - Change password: current password, new password — uses
changeCustomerPasswordaction Both useuseActionState.
- Step 5: Create profile page
Server component: auth guard + fetch customer data. Render <ProfileForm customer={customer} />.
- Step 6: Commit
git add -A && git commit -m "feat: müşteri API docs + profil sayfası — API key, changelog, şifre değişikliği"Task 17: Forgot Password Page & Final Integration
Bölüm başlığı “Task 17: Forgot Password Page & Final Integration”Files:
-
Create:
app/[locale]/customer/forgot-password/page.tsx -
Step 1: Create forgot password placeholder page
Since email system is Phase 3, create a simple page that says “Şifre sıfırlama için lütfen yöneticiniz ile iletişime geçin” (Contact your administrator for password reset). This is intentional — admin-driven model, no self-service yet.
- Step 2: Verify full build
npm run buildExpected: Build succeeds. All pages compile.
- Step 3: Test auth isolation manually
- Open
/customer/login— should show customer login page - Try accessing
/customer/dashboardwithout login — should redirect to/customer/login - Login as admin at
/admin/login— verify admin session works - In same browser, open
/customer/login— should show login (separate cookie) - Verify admin cannot access
/customer/dashboardcontent (middleware + server component guard)
- Step 4: Final commit
git add -A && git commit -m "feat: Customer Portal CRM — Faz 1 tamamlandı"Summary
Bölüm başlığı “Summary”| Task | Description | Estimated Steps |
|---|---|---|
| 1 | Database schema & dependencies | 7 |
| 2 | iron-session auth infrastructure | 4 |
| 3 | Middleware update | 3 |
| 4 | Customer auth — login/logout | 5 |
| 5 | Customer portal layout & nav | 6 |
| 6 | i18n translations | 3 |
| 7 | Admin — customer CRUD | 8 |
| 8 | Admin — service types & services | 5 |
| 9 | Admin — file management | 7 |
| 10 | Admin — invoices | 4 |
| 11 | Admin — dev requests & changelog | 5 |
| 12 | Customer — dashboard | 4 |
| 13 | Customer — services | 4 |
| 14 | Customer — secure file download | 4 |
| 15 | Customer — invoices & dev requests | 5 |
| 16 | Customer — API docs & profile | 6 |
| 17 | Forgot password & final integration | 4 |
| Total | 84 steps |