İçeriğe geç

Customer Portal CRM — Design Specification

Derin

Date: 2026-03-27 Status: Approved Scope: Phase 1 — Customer-facing portal + admin management


A customer-facing CRM portal for ECU Tuning Portal’s B2B SaaS white-label software business. Customers log in to view their products, services, files, invoices, development requests, and API information. Admin manages everything via CRUD operations.

  • Monolithic — built into the existing Next.js 16 app, extending current Prisma/i18n/Tailwind stack
  • Admin-driven — no automation, no self-registration, admin controls everything via CRUD
  • Zero cross-access — admin and customer auth systems are physically isolated
  • No over-engineering — simplest working solution, CRUD-first, automate later

  • Library: NextAuth v5 (Auth.js) — Credentials provider
  • Cookie: authjs.session-token (encrypted JWT)
  • Table: AdminUser
  • Login: /admin/login
  • Library: iron-session v8
  • Cookie: customer-session (encrypted via @hapi/iron — signed + encrypted, not JWT)
  • Tables: Customer + CustomerSession
  • Login: /customer/login
  • Session flow:
    1. Customer submits email + password to /customer/login
    2. Server Action: lookup Customer table, bcrypt password verification
    3. Create CustomerSession record in DB
    4. iron-session encrypts {sessionId, customerId, role: "customer"} into cookie
    5. On each request: decrypt cookie → verify CustomerSession exists in DB → check isActive
    6. Logout: delete CustomerSession from DB + destroy iron-session cookie
  • Server Components: Direct iron-session access via getIronSession(await cookies(), config)
  • Server Actions: Same — direct access
  • Client Components: React Context — <CustomerSessionProvider> wraps portal layout, useCustomerSession() hook
  • No external state library needed — session data is read-mostly, React Context is sufficient
LayerScopeMechanism
Proxy (middleware)Optimistic redirectCookie presence + role check → redirect wrong role to correct login
Server ComponentPage-level authgetIronSession() + role verification → redirect if unauthorized
Server ActionPer-mutation guardSession + role + customerId ownership check → reject unauthorized data access
  • Files stored outside public/ — no direct URL access
  • Download only via /api/customer/files/[id]/download
  • 4-step verification: session exists → role is customer → file belongs to customer → account is active
  • File streamed (not loaded into memory)
  • File path never exposed in URLs
  • All downloads logged (who, when, which file)
  • Two different cookies (authjs.session-token vs customer-session)
  • Admin and customer can be logged in simultaneously in the same browser
  • No shared session state, no shared auth logic
  • Zero changes to existing NextAuth admin configuration

model Customer {
id Int @id @default(autoincrement())
email String @unique
password String // bcrypt hash
name String
company String?
phone String?
locale String @default("tr")
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions CustomerSession[]
services Service[]
files File[]
invoices Invoice[]
developmentRequests DevelopmentRequest[]
}
model CustomerSession {
id Int @id @default(autoincrement())
customerId Int
sessionToken String @unique
ipAddress String?
userAgent String?
expiresAt DateTime
createdAt DateTime @default(now())
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
}
model ServiceType {
id Int @id @default(autoincrement())
slug String @unique
name String
description String?
createdAt DateTime @default(now())
services Service[]
}

Seed data: license, hosting, domain, api, development

model Service {
id Int @id @default(autoincrement())
customerId Int
serviceTypeId Int
name String // e.g., "Enterprise License — tuningshop.de"
description String?
status String @default("active") // active | expired | suspended | cancelled
startDate DateTime?
endDate DateTime?
autoRenew Boolean @default(false)
price Decimal? @db.Decimal(10, 2)
currency String @default("EUR")
billingCycle String? // monthly | yearly | one-time
metadata Json? // flexible type-specific data (serverIp, ram, disk, registrar, apiKey, rateLimit, etc.)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
serviceType ServiceType @relation(fields: [serviceTypeId], references: [id])
files File[]
developmentRequests DevelopmentRequest[]
}
model File {
id Int @id @default(autoincrement())
customerId Int
serviceId Int? // optional — can be general or service-specific
fileName String // stored filename (UUID-based)
originalName String // original upload filename
filePath String // server path (outside public/)
fileSize BigInt
mimeType String
version String // e.g., "2.4.1"
changelog String? // version release notes
uploadedBy Int // FK → AdminUser
createdAt DateTime @default(now())
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
service Service? @relation(fields: [serviceId], references: [id])
admin AdminUser @relation(fields: [uploadedBy], references: [id])
}
model Invoice {
id Int @id @default(autoincrement())
customerId Int
invoiceNumber String @unique // e.g., "INV-2026-001"
title String
description String?
amount Decimal @db.Decimal(10, 2)
currency String @default("EUR")
status String @default("pending") // paid | pending | overdue | cancelled
items Json // array of line items: [{description, quantity, unitPrice, total}]
paidAt DateTime?
dueDate DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
}
model DevelopmentRequest {
id Int @id @default(autoincrement())
customerId Int
serviceId Int? // optional link to a service
title String
description String
status String @default("pending") // pending | in_progress | completed | cancelled
priority String @default("medium") // low | medium | high
estimatedPrice Decimal? @db.Decimal(10, 2)
finalPrice Decimal? @db.Decimal(10, 2)
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
service Service? @relation(fields: [serviceId], references: [id])
}
model ApiChangelog {
id Int @id @default(autoincrement())
version String
title String
content String // markdown
publishedAt DateTime
createdAt DateTime @default(now())
}
  • metadata Json field on Service — stores type-specific data (serverIp, ram, disk for hosting; registrar, nameservers for domain; apiKey, rateLimit for API). No schema migration needed when adding new service types.
  • items Json field on Invoice — line items array. Supports mixed invoices (multiple services + extras in one invoice).
  • File.serviceId optional — file can be general (e.g., source code delivery) or service-specific.
  • ServiceType as separate table — admin can CRUD new types without code changes.
  • AdminUser relation on File — tracks who uploaded each file.

app/[locale]/customer/
├── login/page.tsx ← Public
├── forgot-password/page.tsx ← Public
├── (portal)/ ← Auth-protected (iron-session)
│ ├── layout.tsx ← Session check + Sidebar + Header + CustomerSessionProvider
│ ├── dashboard/page.tsx
│ ├── services/
│ │ ├── page.tsx ← All services (filterable)
│ │ └── [id]/page.tsx ← Service detail
│ ├── files/
│ │ ├── page.tsx ← Files list (versioned)
│ │ └── [id]/page.tsx ← File detail + download
│ ├── development-requests/
│ │ ├── page.tsx ← Request list
│ │ └── [id]/page.tsx ← Request detail
│ ├── invoices/
│ │ ├── page.tsx ← Invoice list
│ │ └── [id]/page.tsx ← Invoice detail
│ ├── api-docs/
│ │ └── page.tsx ← API Keys + Changelog
│ └── profile/
│ └── page.tsx ← Profile + password change
app/[locale]/admin/(panel)/
├── customers/
│ ├── page.tsx ← Customer list
│ ├── create/page.tsx ← Create customer
│ └── [id]/
│ ├── edit/page.tsx ← Edit customer
│ └── services/page.tsx ← Customer's services
├── services/
│ ├── page.tsx ← All services (admin view)
│ ├── [id]/edit/page.tsx ← Edit service
│ └── types/page.tsx ← Service types CRUD
├── files/
│ ├── page.tsx ← File management
│ └── upload/page.tsx ← Upload file (assign to customer)
├── invoices/
│ ├── page.tsx ← All invoices
│ ├── create/page.tsx ← Create invoice
│ └── [id]/edit/page.tsx ← Edit invoice
├── development-requests/
│ ├── page.tsx ← Manage dev requests
│ └── [id]/edit/page.tsx ← Edit dev request
└── api-changelog/
├── page.tsx ← Changelog list
├── create/page.tsx ← New changelog entry
└── [id]/edit/page.tsx ← Edit changelog entry
app/api/customer/
├── auth/
│ ├── login/route.ts ← Customer login
│ └── logout/route.ts ← Customer logout
├── files/
│ └── [id]/download/route.ts ← Secure file download (stream)
└── session/route.ts ← Session endpoint for client components

components/
├── customer/ ← Customer portal components
│ ├── CustomerSidebar.tsx ← Glassmorphism sidebar (brand-red accent)
│ ├── CustomerHeader.tsx ← Top bar (title, breadcrumb, lang)
│ ├── CustomerLayoutWrapper.tsx ← Layout wrapper
│ ├── DashboardStats.tsx ← Stat cards (4-column grid)
│ ├── ServiceCard.tsx ← Service summary card (progress bar)
│ ├── ServiceDetail.tsx ← Service detail view
│ ├── FileList.tsx ← File list with versions
│ ├── FileDownloadButton.tsx ← Secure download button
│ ├── InvoiceTable.tsx ← Invoice table
│ ├── DevRequestCard.tsx ← Dev request card
│ ├── ApiKeyDisplay.tsx ← Masked API key display
│ ├── ChangelogTimeline.tsx ← Changelog timeline
│ └── ProfileForm.tsx ← Profile edit form
├── admin/ ← New admin components
│ ├── CustomerForm.tsx ← Create/edit customer
│ ├── ServiceForm.tsx ← Create/edit service
│ ├── FileUploadForm.tsx ← Upload file
│ ├── InvoiceForm.tsx ← Create invoice
│ └── ApiChangelogForm.tsx ← Changelog entry form
lib/
├── customer-session.ts ← iron-session config + getCustomerSession() helper
└── file-storage.ts ← Secure file upload/download utilities
contexts/
└── CustomerSessionContext.tsx ← React Context + Provider + useCustomerSession() hook

The customer portal follows the existing landing page design system exactly:

  • Background: #030304 (near-black, same as Hero/Features sections)
  • Panel surfaces: rgba(255,255,255,0.02) with backdrop-blur-xl (glass effect)
  • Borders: rgba(255,255,255,0.06) (matches border-white/5 ~ border-white/10)
  • Primary accent: #ef4444 (brand-red) — active states, glows, stat highlights
  • Status colors: Green #22c55e (active), Amber #f59e0b (warning), Blue #3b82f6 (info)
  • Red glow orbs: rgba(239,68,68,0.04–0.06) with blur(120px) — ambient depth
  • Headings/Labels/Badges: Chakra Petch (font-tech) — uppercase, tracking-wide
  • Body/Description: Inter (font-sans)
  • Badge pattern: font-tech uppercase tracking-[0.2em] text-xs
  • Glass cards: bg-white/2 backdrop-blur-xl border border-white/6 rounded-[10px]
  • Stat cards: Glass card + bottom accent gradient line (2px)
  • Service cards: Glass card + progress bar + hover border-red animation
  • Warning cards: border-amber-500/25
  • Background: rgba(255,255,255,0.02) + backdrop-blur(20px)
  • Red glow orb in top-left corner
  • Active nav item: rgba(239,68,68,0.08) bg + rgba(239,68,68,0.15) border
  • User avatar: Red gradient (#ef4444#b91c1c) with glow shadow
  • Grouped navigation with uppercase labels
  • 60px grid overlay (matching landing page pattern)
  • Red + blue glow orbs for ambient depth
  1. Top: 4-column stat cards (active services count, upcoming renewals, open requests, last invoice)
  2. Middle: 2x2 service card grid with progress bars
  3. Bottom: 2-column — recent activity feed + upcoming dates

Note: All UI labels shown in the mockups are design-time placeholders. Implementation must use next-intl translation keys from the CustomerPortal namespace — never hardcode strings.


  • Uses existing next-intl infrastructure (24 locales configured)
  • Customer portal translation namespace: CustomerPortal (separate from public site translations)
  • Phase 1 delivers: Turkish (tr) + English (en) translations only
  • Customer’s preferred locale stored in Customer.locale field
  • Portal respects customer’s locale preference (set by admin at creation, changeable in profile)
  • All 24 locale translations can be added later without code changes

  • Customer auth (iron-session) + database schema
  • Admin CRUD: customers, services, service types, files, invoices, dev requests, API changelog
  • Customer portal: dashboard, services, files (versioned + secure download), invoices, dev requests, API (keys + changelog), profile
  • i18n infrastructure (24-locale ready, TR + EN delivered)
  • Three-layer security isolation
  • Landing page pricing page fully managed from admin panel
  • Prices, features, descriptions, all copy — admin-controlled
  • Multi-language content management
  • Infobip + Brevo integration
  • Email + SMS notifications
  • Service expiry reminders, new file notifications
  • Automated alerts
  • Update package sales
  • Stripe integration
  • Automatic invoice generation
  • Payment-gated file access control

ConcernMitigation
Cross-access (admin ↔ customer)Physically separate auth libraries, separate cookies, 3-layer enforcement
File theftFiles outside public/, 4-step auth check on download, stream-based delivery, download logging
Session hijackingiron-session encryption (@hapi/iron), DB-backed session validation, IP/UA tracking
Session revocationDelete CustomerSession from DB → immediate invalidation
Brute forceRate limiting on login endpoint (existing lib/rate-limit.ts in-memory limiter)
Data leakageEvery query filtered by customerId — customer can only see own data
Account deactivationisActive check on every request — instant lockout