İçeriğe geç

Customer Portal CRM — Implementation Plan

Derin

For 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


# Auth & Session
lib/customer-session.ts — iron-session config + helpers
contexts/CustomerSessionContext.tsx — React Context for client components
lib/file-storage.ts — Secure file upload/download utilities
# i18n
messages/tr.json — MODIFY: add CustomerPortal + AdminCustomer namespaces
messages/en.json — MODIFY: add CustomerPortal + AdminCustomer namespaces
# Customer Portal Pages
app/[locale]/customer/login/page.tsx
app/[locale]/customer/forgot-password/page.tsx
app/[locale]/customer/(portal)/layout.tsx
app/[locale]/customer/(portal)/dashboard/page.tsx
app/[locale]/customer/(portal)/services/page.tsx
app/[locale]/customer/(portal)/services/[id]/page.tsx
app/[locale]/customer/(portal)/files/page.tsx
app/[locale]/customer/(portal)/files/[id]/page.tsx
app/[locale]/customer/(portal)/development-requests/page.tsx
app/[locale]/customer/(portal)/development-requests/[id]/page.tsx
app/[locale]/customer/(portal)/invoices/page.tsx
app/[locale]/customer/(portal)/invoices/[id]/page.tsx
app/[locale]/customer/(portal)/api-docs/page.tsx
app/[locale]/customer/(portal)/profile/page.tsx
# Customer Portal Components
components/customer/CustomerSidebar.tsx
components/customer/CustomerHeader.tsx
components/customer/CustomerLayoutWrapper.tsx
components/customer/DashboardStats.tsx
components/customer/ServiceCard.tsx
components/customer/ServiceDetail.tsx
components/customer/FileList.tsx
components/customer/FileDownloadButton.tsx
components/customer/InvoiceTable.tsx
components/customer/DevRequestCard.tsx
components/customer/ApiKeyDisplay.tsx
components/customer/ChangelogTimeline.tsx
components/customer/ProfileForm.tsx
# Admin New Pages
app/[locale]/admin/(panel)/customers/page.tsx
app/[locale]/admin/(panel)/customers/create/page.tsx
app/[locale]/admin/(panel)/customers/[id]/edit/page.tsx
app/[locale]/admin/(panel)/customers/[id]/services/page.tsx
app/[locale]/admin/(panel)/services/page.tsx
app/[locale]/admin/(panel)/services/[id]/edit/page.tsx
app/[locale]/admin/(panel)/services/types/page.tsx
app/[locale]/admin/(panel)/files/page.tsx
app/[locale]/admin/(panel)/files/upload/page.tsx
app/[locale]/admin/(panel)/invoices/page.tsx
app/[locale]/admin/(panel)/invoices/create/page.tsx
app/[locale]/admin/(panel)/invoices/[id]/edit/page.tsx
app/[locale]/admin/(panel)/development-requests/page.tsx
app/[locale]/admin/(panel)/development-requests/[id]/edit/page.tsx
app/[locale]/admin/(panel)/api-changelog/page.tsx
app/[locale]/admin/(panel)/api-changelog/create/page.tsx
app/[locale]/admin/(panel)/api-changelog/[id]/edit/page.tsx
# Admin New Components
components/admin/CustomerForm.tsx
components/admin/ServiceForm.tsx
components/admin/FileUploadForm.tsx
components/admin/InvoiceForm.tsx
components/admin/ApiChangelogForm.tsx
# Server Actions
app/lib/customer-actions.ts — Customer portal server actions
app/lib/admin-customer-actions.ts — Admin CRUD server actions for customer management
# API Routes
app/api/customer/auth/login/route.ts
app/api/customer/auth/logout/route.ts
app/api/customer/files/[id]/download/route.ts
app/api/customer/session/route.ts
prisma/schema.prisma — Add 7 new models + AdminUser relation
prisma/seed.ts — Add ServiceType seed data
proxy.ts — Add customer route handling
.gitignore — Add .superpowers/ and /uploads/
components/admin/AdminSidebar.tsx — Add customer management nav items
messages/tr.json — Add new translation namespaces
messages/en.json — Add new translation namespaces

Files:

  • Modify: prisma/schema.prisma

  • Modify: prisma/seed.ts

  • Modify: .gitignore

  • Modify: package.json (install iron-session)

  • Step 1: Install iron-session

Terminal window
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

Terminal window
npx prisma migrate dev --name add_customer_portal_models

Expected: 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 ServiceTypes
const 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
Terminal window
npx prisma db seed

Expected: “ServiceTypes seeded” in output.

  • Step 6: Generate Prisma client
Terminal window
npx prisma generate
  • Step 7: Commit
Terminal window
git add -A && git commit -m "feat: Customer Portal veritabanı şeması — 7 yeni tablo + iron-session bağımlılığı"

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
Terminal window
# Generate a 32+ char secret
echo "CUSTOMER_SESSION_SECRET=$(openssl rand -hex 32)" >> .env
  • Step 4: Commit
Terminal window
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 + redirect
if (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
Terminal window
npm run build

Expected: Build succeeds without errors.

  • Step 3: Commit
Terminal window
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 customerLogin action instead of authenticate

  • Brand it as “Customer Portal” with glassmorphism styling

  • Use font-tech for headings, brand-red accent

  • Background: #030304 with 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
Terminal window
git add -A && git commit -m "feat: müşteri auth sistemi — login, logout, session API, rate limiting"

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-tech uppercase, 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 customerLogout server action

  • Step 3: Create CustomerHeader component

Create components/customer/CustomerHeader.tsx — client component:

  • font-tech uppercase 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
Terminal window
npm run build
  • Step 6: Commit
Terminal window
git add -A && git commit -m "feat: müşteri portalı layout — glassmorphism sidebar, header, auth guard"

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

Terminal window
git add messages/ && git commit -m "feat: i18n — CustomerPortal + AdminCustomer çeviri namespace'leri (TR + EN)"

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 record
  • updateCustomer(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 customer
  • updateService(prevState, formData) — update service fields
  • deleteService(id) — delete service
  • createServiceType(prevState, formData) — create new service type
  • updateServiceType(prevState, formData) — update service type
  • deleteServiceType(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 useActionState hook with server action

  • Follow existing admin form patterns (see components/admin/BlogPostForm.tsx for 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
Terminal window
git add -A && git commit -m "feat: admin müşteri CRUD — liste, oluştur, düzenle, servisler + sidebar güncelleme"

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 useActionState with 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

Terminal window
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 accessible
const 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 via saveFile(), 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 useActionState with 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

Terminal window
mkdir -p uploads/customer-files
echo '*' > uploads/customer-files/.gitignore
echo '!.gitignore' >> uploads/customer-files/.gitignore
  • Step 7: Commit
Terminal window
git add -A && git commit -m "feat: admin dosya yönetimi — güvenli yükleme, listeleme, file-storage utility"

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
Terminal window
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
Terminal window
git add -A && git commit -m "feat: admin dev istekleri + API changelog CRUD"

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-tech for 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 validation
const session = await validateCustomerSession();
if (!session) redirect('/customer/login');
// Parallel data fetching
const [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 stats
const 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
Terminal window
git add -A && git commit -m "feat: müşteri dashboard — stat kartları, servis kartları, aktivite akışı"

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
Terminal window
git add -A && git commit -m "feat: müşteri servislerim — liste + detay sayfası"

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
Terminal window
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
Terminal window
git add -A && git commit -m "feat: müşteri faturalar + geliştirme istekleri sayfaları"

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:

  1. Profile info: name, company, phone, locale dropdown — uses updateCustomerProfile action
  2. Change password: current password, new password — uses changeCustomerPassword action Both use useActionState.
  • Step 5: Create profile page

Server component: auth guard + fetch customer data. Render <ProfileForm customer={customer} />.

  • Step 6: Commit
Terminal window
git add -A && git commit -m "feat: müşteri API docs + profil sayfası — API key, changelog, şifre değişikliği"

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
Terminal window
npm run build

Expected: Build succeeds. All pages compile.

  • Step 3: Test auth isolation manually
  1. Open /customer/login — should show customer login page
  2. Try accessing /customer/dashboard without login — should redirect to /customer/login
  3. Login as admin at /admin/login — verify admin session works
  4. In same browser, open /customer/login — should show login (separate cookie)
  5. Verify admin cannot access /customer/dashboard content (middleware + server component guard)
  • Step 4: Final commit
Terminal window
git add -A && git commit -m "feat: Customer Portal CRM — Faz 1 tamamlandı"

TaskDescriptionEstimated Steps
1Database schema & dependencies7
2iron-session auth infrastructure4
3Middleware update3
4Customer auth — login/logout5
5Customer portal layout & nav6
6i18n translations3
7Admin — customer CRUD8
8Admin — service types & services5
9Admin — file management7
10Admin — invoices4
11Admin — dev requests & changelog5
12Customer — dashboard4
13Customer — services4
14Customer — secure file download4
15Customer — invoices & dev requests5
16Customer — API docs & profile6
17Forgot password & final integration4
Total84 steps