Compare commits
2
Commits
v1.0.0
..
82414e8a93
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82414e8a93 | ||
|
|
165efc3269 |
@@ -0,0 +1,160 @@
|
|||||||
|
# AGENT.md — AnwarPi for AI agents
|
||||||
|
|
||||||
|
Orientation doc for AI agents (coding assistants, autonomous maintainers, content
|
||||||
|
pipelines) working on this repository. Read this before changing anything.
|
||||||
|
|
||||||
|
## What this is
|
||||||
|
|
||||||
|
**AnwarPi (أنور بي آي)** — an Arabic-first, RTL platform for learning practical
|
||||||
|
AI at **https://anwarpy.ai**. Free guides for everyone, VIP-gated deep-dives, a
|
||||||
|
searchable library/catalog, moderated comments, member accounts, and a Stripe
|
||||||
|
subscription that is fully wired but public-facing "coming soon" until keys are
|
||||||
|
set. Content is also published programmatically by an external agent pipeline
|
||||||
|
through a key-authenticated API.
|
||||||
|
|
||||||
|
Canonical domain: `anwarpy.ai` (`anwarpi.ai` is legacy). Owner: Ahmed
|
||||||
|
(ahmedalkhair.sa@gmail.com). Owner constraint: **never change site flow/UX
|
||||||
|
without explicit approval** (e.g. no CAPTCHA, no aggressive limits).
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
| Component | Choice |
|
||||||
|
|---|---|
|
||||||
|
| Framework | Next.js 15 (App Router, `output: 'standalone'`), React 19 |
|
||||||
|
| CMS/Auth | Payload 3.90 (`@payloadcms/next`), Lexical editor |
|
||||||
|
| Database | PostgreSQL 16 via `@payloadcms/db-postgres` |
|
||||||
|
| Styling | Plain CSS files per page (see `web/src/app/(frontend)/styles/`), dark-first brand (see `brand-spec.md`) |
|
||||||
|
| Email | Cloudflare Email Routing (inbound) + Cloudflare Email Sending REST (outbound) — **not SMTP** (docs/adr/0003) |
|
||||||
|
| Payments | Stripe SDK, webhook at `/api/stripe/webhook` |
|
||||||
|
| Edge | Traefik 2/3 with Docker provider (labels in `web/docker-compose.vps.yml`) |
|
||||||
|
|
||||||
|
## Repo layout
|
||||||
|
|
||||||
|
```
|
||||||
|
web/ ← the entire application (deploy unit)
|
||||||
|
src/
|
||||||
|
payload.config.ts ← Payload config: collections, postgres adapter
|
||||||
|
seed.ts ← `npm run seed`
|
||||||
|
middleware.ts ← Next middleware: auth rate limits (F2)
|
||||||
|
collections/ ← Users, Posts, Media, Files, PostLikes, Comments
|
||||||
|
migrations/ ← Payload migrations (`payload migrate` runs on boot)
|
||||||
|
lib/ ← viewer.ts (session helper), mail.ts, format.ts, topics.ts, agent-*
|
||||||
|
app/
|
||||||
|
(frontend)/ ← public Arabic site (page.tsx per route, styles/ css)
|
||||||
|
(payload)/ ← Payload admin + API catch-all
|
||||||
|
api/ ← custom REST routes (posts, auth, subscribe, stripe, agent, files)
|
||||||
|
public/ ← brand/, covers/, og-default.png
|
||||||
|
Dockerfile ← standalone build; CMD runs `npx payload migrate` first
|
||||||
|
docker-compose.vps.yml ← prod: web + postgres, Traefik labels (rate limits)
|
||||||
|
GLOSSARY.md ← canonical domain vocabulary — USE THESE TERMS
|
||||||
|
docs/adr/ ← architecture decision records — do not contradict
|
||||||
|
brand-spec.md ← visual identity: dark blue-black, silver beams, cyan-teal accent
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data model (Payload collections)
|
||||||
|
|
||||||
|
- **Users** — auth collection. `role: 'free' | 'vip' | 'admin'` (server-forced),
|
||||||
|
`emailVerified`, `onboarded`, `termsVersion`. Login lockout: 5 attempts /
|
||||||
|
15 min; admin accounts never lock (hook clears lock before login).
|
||||||
|
- **Posts** — guides. `access: 'free' | 'vip'`, `slug`, category (وكلاء، نماذج،
|
||||||
|
استرجاع، سلامة، أدوات، أعمال), cover image (1200×630), Lexical body, likes/views.
|
||||||
|
- **Comments** — moderated: `pending → approved | rejected`. Only `approved` is
|
||||||
|
public. Authors cannot edit/delete their own comments (moderated model,
|
||||||
|
docs/adr/0002). Display name is a snapshot of `fullName` at submit time.
|
||||||
|
- **Media / Files** — uploads (files served via `/api/files/[id]`).
|
||||||
|
- **PostLikes** — one like per member per post.
|
||||||
|
|
||||||
|
## Trust model (critical — do not break)
|
||||||
|
|
||||||
|
1. **Roles are server-forced.** `POST /api/users` and any client write cannot
|
||||||
|
set `role`, `emailVerified`, `stripeCustomerId`, or `verificationToken` —
|
||||||
|
the `beforeChange` hook in `Users.ts` deletes them for untrusted operations.
|
||||||
|
Only `req.user.role === 'admin'` or `context.systemActivation === true`
|
||||||
|
(payment activation from server code) counts as trusted.
|
||||||
|
2. **Entitlement is derived, never stored on the user**: viewer may read a
|
||||||
|
post's body iff `post.access === 'free' || role in ('vip','admin')`. Gating
|
||||||
|
happens server-side (locked body is stripped from API responses).
|
||||||
|
3. **Comments require verification** (`emailVerified`), admins exempt.
|
||||||
|
4. Client requests can never mint VIP (free `/payment-success` visits redirect
|
||||||
|
to `/subscription`; docs/adr/0004, 0005).
|
||||||
|
|
||||||
|
## Auth & rate limiting
|
||||||
|
|
||||||
|
- Passwords: Payload local auth → **PBKDF2-SHA256, 600 000 iterations, 32
|
||||||
|
bytes** (NOT bcrypt — verified in payload's `generatePasswordSaltHash`).
|
||||||
|
- `web/src/middleware.ts` (F2): per-IP in-memory buckets —
|
||||||
|
`POST /api/users` 5/h, `POST /api/users/login` 10/15min,
|
||||||
|
`POST /api/users/forgot-password` 5/h. Over limit → 429 JSON
|
||||||
|
`{"error":"محاولات كثيرة جدًا. حاول بعد قليل."}` + `Retry-After`.
|
||||||
|
**In-memory is correct for the single-container deployment**; moving to
|
||||||
|
horizontal scaling requires a shared store (Redis) — do not silently assume
|
||||||
|
one replica.
|
||||||
|
- Timing equalization (F7): `Users.ts` `beforeOperation` login hook runs a
|
||||||
|
dummy PBKDF2 (same params, salt `anwarpy-dummy-salt-v1`) when no user
|
||||||
|
matches, so ghost emails are not enumerable by response time. Keep this when
|
||||||
|
touching the hook.
|
||||||
|
- Traefik edge (F1, VPS only): pages `pagelimit` 60/min burst 120 +
|
||||||
|
`connlimit` 50 in-flight per IP; API `apilimit` 90/min burst 180.
|
||||||
|
- `Strict-Transport-Security: max-age=63072000; includeSubDomains` on all
|
||||||
|
routes (`web/next.config.mjs`). Do not add `preload`.
|
||||||
|
|
||||||
|
## Key routes
|
||||||
|
|
||||||
|
Frontend: `/` (home), `/articles`, `/catalog`, `/posts/[slug]` (gated body,
|
||||||
|
engagement, comments), `/signup`, `/login`, `/forgot-password`,
|
||||||
|
`/verify-email`, `/subscription`, `/payment-success`, `/payment-cancelled`,
|
||||||
|
`/vip-onboarding`, `/onboarding`, `/account-settings`, `/about`, `/privacy`,
|
||||||
|
`/terms`. Admin UI: `/admin`.
|
||||||
|
|
||||||
|
Custom API (see `web/src/app/api/`): `posts` (list) + `posts/[slug]` (detail —
|
||||||
|
strips locked bodies), `posts/[slug]/{like,view,comments}`,
|
||||||
|
`auth/{verify-email,resend-verification}`, `users` REST is Payload's own,
|
||||||
|
`subscribe`, `stripe/webhook` (returns 503 until `STRIPE_SECRET_KEY` +
|
||||||
|
`STRIPE_WEBHOOK_SECRET` exist; verifies `stripe-signature` once configured),
|
||||||
|
`agent/posts` (POST/PUT with `x-agent-key` header — external publishing), and
|
||||||
|
`files/[id]`.
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
`DATABASE_URI`, `PAYLOAD_SECRET`, `NEXT_PUBLIC_SITE_URL`, `AGENT_API_KEY`,
|
||||||
|
`MAIL_FROM`, `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`, plus optional
|
||||||
|
`STRIPE_SECRET_KEY` / `STRIPE_WEBHOOK_SECRET`. Prod values live in
|
||||||
|
`/root/anwarpi/.env.production` on the VPS — never commit `.env*`.
|
||||||
|
|
||||||
|
## Development & verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web && npm install
|
||||||
|
npm run dev # localhost:3000, /admin for CMS
|
||||||
|
npm run build # MUST exit 0 — typescript.ignoreBuildErrors is false
|
||||||
|
npm run seed
|
||||||
|
```
|
||||||
|
|
||||||
|
- Regenerates `payload-types.ts` automatically on build
|
||||||
|
(`payload generate:types`).
|
||||||
|
- Schema changes require a migration (`payload migrate:create`); the container
|
||||||
|
CMD runs `payload migrate` on every deploy — no manual DB steps.
|
||||||
|
- Auth endpoints are rate-limited in dev too; restart the dev server to reset
|
||||||
|
buckets when testing.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Single VPS `147.79.102.201`, app at `/root/anwarpi`, managed by
|
||||||
|
`docker compose -f docker-compose.vps.yml` (web + postgres + Traefik proxy
|
||||||
|
network). Update flow: rsync `web/` → `/root/anwarpi/` excluding
|
||||||
|
`node_modules .next .git media files-private '.env*' '*.tsbuildinfo'`, then
|
||||||
|
`docker compose -f docker-compose.vps.yml up -d --build` (label changes force
|
||||||
|
recreate; migration runs on boot). Traefik labels only affect the VPS.
|
||||||
|
|
||||||
|
## House rules
|
||||||
|
|
||||||
|
- Follow the **reuse ladder**: existing helper → stdlib → platform feature →
|
||||||
|
installed dependency → then write the complete version.
|
||||||
|
- **Do not add UX changes** (new limits, captchas, gates) without owner
|
||||||
|
approval; thresholds are deliberately generous for humans.
|
||||||
|
- New decisions → add an ADR in `docs/adr/` (next number, `NNNN-slug.md`).
|
||||||
|
- Use GLOSSARY terms exactly (عضوية/entitlement, مفتوح/unlocked, موثّق/
|
||||||
|
verified, كتابة موثوقة/trusted write).
|
||||||
|
- **Pentest context**: the 2026-10-09 report items F1–F9 are closed; see PR #1
|
||||||
|
and release v1.0.0 on Gitea for what was done and verified. Do not
|
||||||
|
re-implement.
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
# GLOSSARY — أنور بي آي
|
||||||
|
|
||||||
|
Implementation-free vocabulary shared across code, QA, and product talk.
|
||||||
|
|
||||||
|
## العضوية / Entitlement
|
||||||
|
|
||||||
|
A user's paid access level. Server-derived from `users.role`: `vip` or `admin` counts as entitled; `free` does not. Never cached client-side as a boolean.
|
||||||
|
|
||||||
|
## مفتوح / Unlocked
|
||||||
|
|
||||||
|
Whether the current viewer may read a given post's full body: `post.access === 'free' || entitled`. Derived per request; never stored on the user.
|
||||||
|
|
||||||
|
## تعليق / Comment
|
||||||
|
|
||||||
|
A moderated note by an authenticated member on a published post. Lifecycle `pending → approved | rejected`; only `approved` is publicly visible; only admins move status; authors cannot edit or delete their own comments (moderated model). Authors see their own `pending` comments on the post page tagged «قيد المراجعة».
|
||||||
|
|
||||||
|
## اسم العرض / Display name
|
||||||
|
|
||||||
|
The public author label of a comment: a snapshot of `users.fullName` taken at submit time, or the fixed word «عضو» when absent. Never the email.
|
||||||
|
|
||||||
|
## الهوية / Identity
|
||||||
|
|
||||||
|
- **Canonical domain: `anwarpy.ai`** — the Cloudflare zone (`anwarpi.ai` references are legacy being replaced; see repo history).
|
||||||
|
- **Contact inbox: `hello@anwarpy.ai`** — receives via Cloudflare Email Routing, forwards to ahmedalkhair.sa@gmail.com.
|
||||||
|
- **Sender: `noreply@anwarpy.ai`** — sends via Cloudflare Email Sending (REST).
|
||||||
|
|
||||||
|
## الاشتراك قريبًا / Coming soon
|
||||||
|
|
||||||
|
The public state of the subscribe flow until Stripe keys exist: `POST /api/subscribe` returns `{ comingSoon: true }` and no `url`; the UI renders a disabled «قريباً» button. No VIP is granted without a Stripe-verified session — neither via the subscribe API nor via `/payment-success` (free users are redirected to `/subscription`).
|
||||||
|
|
||||||
|
## كتابة موثوقة / Trusted write
|
||||||
|
|
||||||
|
An operation on a member record performed by server code (email verification, payment activation, billing webhook, seed), as opposed to a client request. Client requests can never set a member's privilege, verification, or billing fields.
|
||||||
|
|
||||||
|
## موثّق / Verified
|
||||||
|
|
||||||
|
Whether the member confirmed their email address. Required to comment; admins are exempt; set only by server code, never by client requests.
|
||||||
@@ -1,37 +1,180 @@
|
|||||||
# أنور بي آي
|
<div align="center">
|
||||||
|
|
||||||
منصة عربية ثابتة لتعلّم الذكاء الاصطناعي، تتضمن محتوى مجانيًا ومقالات VIP وتجربة اشتراك وتهيئة للمشترك الجديد. الواجهة متجاوبة مع الهاتف واللوحي وسطح المكتب.
|
<img src="web/public/brand/icon-256.png" alt="شعار أنور بي آي" width="120"/>
|
||||||
|
|
||||||
## الصفحات الرئيسية
|
# أنور بي آي — AnwarPi
|
||||||
|
|
||||||
- الصفحة الرئيسية والكتالوج التعليمي
|
**محتوى عربي عملي لتعلّم الذكاء الاصطناعي، بلا ضجيج.**
|
||||||
- مقال مجاني ومعاينة مقال VIP ومقال VIP كامل
|
|
||||||
- الاشتراك وتهيئة مشترك VIP جديد
|
|
||||||
- تسجيل الدخول واستعادة كلمة المرور وإعدادات الحساب
|
|
||||||
- سياسة الخصوصية
|
|
||||||
|
|
||||||
## التشغيل محليًا
|
Arabic-first platform for learning AI: free guides, VIP deep-dives, a searchable library, and moderated community — built with **Next.js 15 + Payload 3 + PostgreSQL** behind Traefik.
|
||||||
|
|
||||||
لا يحتاج المشروع إلى تثبيت حزم. شغّل خادمًا محليًا من مجلد المشروع:
|
**[anwarpy.ai](https://anwarpy.ai)** · [دليل الوكيل الآلي (AGENT.md)](AGENT.md) · [مسرد المصطلحات](GLOSSARY.md) · [قرارات التصميم](docs/adr)
|
||||||
|
|
||||||
```bash
|
</div>
|
||||||
python3 -m http.server 8000
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖼️ لقطات من الموقع
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><strong>الصفحة الرئيسية</strong></td>
|
||||||
|
<td align="center"><strong>المكتبة</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="docs/screenshots/home.png" alt="الصفحة الرئيسية — أنور بي آي" width="470"/></td>
|
||||||
|
<td><img src="docs/screenshots/articles.png" alt="المكتبة — أنور بي آي" width="470"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><strong>صفحة دليل</strong></td>
|
||||||
|
<td align="center"><strong>العضوية</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="docs/screenshots/post.png" alt="صفحة دليل — أنور بي آي" width="470"/></td>
|
||||||
|
<td><img src="docs/screenshots/subscription.png" alt="العضوية — أنور بي آي" width="470"/></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## ما هو أنور بي آي؟
|
||||||
|
|
||||||
|
منصة عربية لتعلّم الذكاء الاصطناعي بشكل عملي: أدلة مجانية للجميع، وأدلة متعمقة لأعضاء **VIP**، ومكتبة قابلة للبحث والتصفية، وتعليقات مُراقَّبة، ومسارات تعلّم (كتالوج). الواجهة عربية بالكامل (RTL) وتعمل على الهاتف واللوحي وسطح المكتب.
|
||||||
|
|
||||||
|
## ✨ المميزات
|
||||||
|
|
||||||
|
- 📚 **المكتبة** — كل الأدلة مع بحث وتصفية حسب التصنيف (وكلاء، نماذج، استرجاع، سلامة، أدوات، أعمال).
|
||||||
|
- 🔓 **بوابة محتوى** — الأدلة المجانية كاملة؛ أدلة VIP تظهر بمعاينة مقفلة حتى يصبح القارئ مشتركًا.
|
||||||
|
- 👤 **حسابات** — تسجيل، دخول، استعادة كلمة المرور، تحقق البريد، وإعدادات حساب كاملة.
|
||||||
|
- 💬 **تعليقات مُراقَّبة** — دورة حياة `pending → approved`؛ يظهر العلني المعتمَد فقط.
|
||||||
|
- 🗺️ **الكتالوج والمسارات** — رحلة تعلّم مرتّبة حسب المستوى (مبتدئ/متوسط/متقدم).
|
||||||
|
- 🤖 **نشر آلي** — API للوكلاء الخارجيين (`/api/agent/posts` بمفتاح `x-agent-key`) لنشر الأدلة من خط إدارة المحتوى.
|
||||||
|
- 💳 **عضوية VIP** — Stripe جاهز؛ حتى تفعيل المفاتيح يعمل زر الاشتراك بوضع «قريبًا».
|
||||||
|
- 🌓 **هوية بصرية داكنة** — أشعة فضية ولمسة سماوية؛ انظر `brand-spec.md`.
|
||||||
|
|
||||||
|
## 🔄 كيف يعمل الموقع؟ (شرح التدفق)
|
||||||
|
|
||||||
|
### تدفق الزائر
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart RL
|
||||||
|
Z[الزائر] --> A[الصفحة الرئيسية]
|
||||||
|
A --> B[المكتبة / الكتالوج]
|
||||||
|
B --> C{نوع الدليل؟}
|
||||||
|
C -->|مجاني| D[قراءة كاملة + إعجاب وتعليق]
|
||||||
|
C -->|VIP| E[معاينة مقفلة]
|
||||||
|
E --> F[إنشاء حساب / دخول]
|
||||||
|
F --> G{مشترك VIP؟}
|
||||||
|
G -->|نعم| D
|
||||||
|
G -->|لا| H[صفحة العضوية]
|
||||||
|
H --> I[تهيئة مشترك VIP جديد]
|
||||||
|
I --> D
|
||||||
```
|
```
|
||||||
|
|
||||||
ثم افتح `http://localhost:8000`.
|
1. **تصفّح** — الزائر يفتح الرئيسية، يتصفح المكتبة أو الكتالوج، ثم يفتح دليلًا.
|
||||||
|
2. **قراءة** — الدليل المجاني يُقرأ كاملًا. دليل VIP يظهر جزءًا منه فقط مع طبقة قفل.
|
||||||
|
3. **حساب** — التسجيل يمنح دور `free` فقط (الخادم يجبر ذلك؛ طلب العميل لا يستطيع ترقية نفسه). بعد التحقق من البريد يمكن التعليق.
|
||||||
|
4. **ترقية** — صفحة العضوية تقود لـ Stripe؛ بعد الدفع الناجح يُفعَّل دور `vip` عبر مسار موثوق من الخادم، ثم معالجة تهيئة VIP الجديد.
|
||||||
|
|
||||||
## التحقق قبل النشر
|
### تدفق المحتوى (نشر الأدلة)
|
||||||
|
|
||||||
```bash
|
```mermaid
|
||||||
python3 scripts/check_site.py
|
flowchart LR
|
||||||
|
T[مواضيع المالك في AppFlowy] --> P[خط النشر الآلي]
|
||||||
|
P -->|POST/PUT مع x-agent-key| A[API الوكلاء]
|
||||||
|
A --> V[التحقق والتغطية الافتراضية للغلاف]
|
||||||
|
V --> D[(PostgreSQL)]
|
||||||
|
D --> S[anwarpy.ai]
|
||||||
```
|
```
|
||||||
|
|
||||||
يتحقق الفحص من صفحات HTML والروابط والأصول المحلية وبيانات العرض وسياسة أمان المحتوى.
|
- يملك المالك قائمة مواضيع؛ يعمل خط النشر FIFO (دليل واحد لكل تشغيل، الاثنين/الأربعاء/الجمعة).
|
||||||
|
- كل دليل يُوسم `free` أو `vip`؛ غير الموسوم = مجاني.
|
||||||
|
- الأغلفة 1200×630، ولكل تصنيف غلاف افتراضي احتياطي.
|
||||||
|
|
||||||
## النشر
|
### تدفق الطلب عبر الخادم
|
||||||
|
|
||||||
المستودع الرئيسي مستضاف على Gitea، والفرع الجاهز للنشر هو `main`. يحتوي المشروع أيضًا على مسار عمل GitHub Pages عند نسخ المستودع إلى GitHub وتفعيل Pages من GitHub Actions.
|
```
|
||||||
|
المتصفح → Traefik (حدود المعدل: pagelimit / apilimit / connlimit)
|
||||||
|
→ Next.js 15 (الصفحات + /api)
|
||||||
|
→ Payload 3 (التحكم بالوصول + الخطافات)
|
||||||
|
→ PostgreSQL 16
|
||||||
|
```
|
||||||
|
|
||||||
## حدود النسخة الحالية
|
## 🏗️ البنية التقنية
|
||||||
|
|
||||||
الموقع نموذج واجهة ثابتة. تسجيل الدخول واستعادة كلمة المرور والدفع والعضوية وحماية محتوى VIP محاكاة داخل المتصفح وليست حماية إنتاجية. لا تستخدمه لجمع بيانات أو مدفوعات حقيقية قبل إضافة مصادقة وتفويض ودفع من جهة الخادم.
|
| الطبقة | التقنية |
|
||||||
|
|---|---|
|
||||||
|
| الواجهة | Next.js 15 (App Router) + React 19، عربية RTL |
|
||||||
|
| لوحة التحكم | Payload 3 (`/admin`) مع محرر Lexical |
|
||||||
|
| قاعدة البيانات | PostgreSQL 16 عبر `@payloadcms/db-postgres` |
|
||||||
|
| المصادقة | جلسات Payload، تجزئة PBKDF2-SHA256 600k تكرار |
|
||||||
|
| الوسيط | `web/src/middleware.ts` — حدود معدل لمسارات المصادقة |
|
||||||
|
| الحاوية | Docker Compose + Traefik (TLS + حدود معدل الحافة) |
|
||||||
|
| البريد | Cloudflare (توجيه + إرسال REST) |
|
||||||
|
| المدفوعات | Stripe (جاهز، بوضع «قريبًا» حتى المفاتيح) |
|
||||||
|
|
||||||
|
## 🚀 التشغيل محليًا
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
أنشئ `web/.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DATABASE_URI=postgres://anwarpi:anwarpi@127.0.0.1:5432/anwarpi
|
||||||
|
PAYLOAD_SECRET=<سري عشوائي طويل>
|
||||||
|
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
||||||
|
AGENT_API_KEY=<مفتاح نشر الوكلاء>
|
||||||
|
MAIL_FROM=noreply@anwarpy.ai
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev # http://localhost:3000 — لوحة التحكم على /admin
|
||||||
|
npm run build # تحقق قبل النشر
|
||||||
|
npm run seed # تعبئة بيانات تجريبية
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📡 أهم مسارات الـ API
|
||||||
|
|
||||||
|
| المسار | الوصف |
|
||||||
|
|---|---|
|
||||||
|
| `POST /api/users` | تسجيل (حد 5/ساعة لكل IP) |
|
||||||
|
| `POST /api/users/login` | دخول (حد 10/15دقيقة) |
|
||||||
|
| `POST /api/users/forgot-password` | استعادة (حد 5/ساعة) |
|
||||||
|
| `GET/PATCH /api/users/me` | حسابي |
|
||||||
|
| `GET /api/posts` و `GET /api/posts/[slug]` | الأدلة (المحتوى المقفل يُقتطع حسب الدور) |
|
||||||
|
| `POST /api/posts/[slug]/like` / `view` / `comments` | تفاعلات |
|
||||||
|
| `POST/PUT /api/agent/posts` | نشر آلي — يتطلب ترويسة `x-agent-key` |
|
||||||
|
| `POST /api/subscribe` | اشتراك Stripe (يعيد `comingSoon` حتى المفاتيح) |
|
||||||
|
| `POST /api/stripe/webhook` | ويبهوك Stripe — يتحقق من التوقيع |
|
||||||
|
|
||||||
|
## 🔐 الأمان
|
||||||
|
|
||||||
|
- **حدود المعدل** (وسيط Next): تسجيل 5/ساعة، دخول 10/15دقيقة، استعادة 5/ساعة — لكل IP، استجابة 429 مع `Retry-After` ورسالة عامة.
|
||||||
|
- **حدود الحافة** (Traefik): 60 طلب/دقيقة للصفحات (انفجار 120) و50 طلبًا جاريًا لكل IP، و`apilimit` للـ API.
|
||||||
|
- **قاعدة الكتابة الموثوقة**: طلبات العميل لا تستطيع تعيين الدور أو التحقق أو حقول الفواتير — تُحذف في خطاف `beforeChange` (`verificationToken`, `stripeCustomerId`, `role`, `emailVerified`).
|
||||||
|
- **HSTS** `max-age=63072000; includeSubDomains` على كل المسارات.
|
||||||
|
- **تكافؤ زمن الدخول**: البريد غير الموجود يستهلك نفس كلفة PBKDF2 (منع تعداد المستخدمين بالزمن).
|
||||||
|
- **التعليقات مُراقَّبة** ولا تُنشر قبل اعتماد المشرف.
|
||||||
|
|
||||||
|
## 📚 الوثائق
|
||||||
|
|
||||||
|
| الملف | المحتوى |
|
||||||
|
|---|---|
|
||||||
|
| [AGENT.md](AGENT.md) | شرح التطبيق والبنية للوكلاء الآليين (AI agents) |
|
||||||
|
| [GLOSSARY.md](GLOSSARY.md) | مسرد المصطلحات الموحّد (عضوية، مفتوح، موثّق…) |
|
||||||
|
| [docs/adr](docs/adr) | قرارات التصميم (ADRs) |
|
||||||
|
| [brand-spec.md](brand-spec.md) | الهوية البصرية والألوان |
|
||||||
|
| [web/docker-compose.vps.yml](web/docker-compose.vps.yml) | تكوين النشر على الـ VPS |
|
||||||
|
|
||||||
|
## 🚢 النشر
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rsync -az --exclude node_modules --exclude .next --exclude .git \
|
||||||
|
--exclude media --exclude files-private --exclude '.env*' --exclude '*.tsbuildinfo' \
|
||||||
|
web/ root@<VPS>:/root/anwarpi/
|
||||||
|
|
||||||
|
ssh root@<VPS> "cd /root/anwarpi && docker compose -f docker-compose.vps.yml up -d --build"
|
||||||
|
```
|
||||||
|
|
||||||
|
أمر التشغيل داخل الحاوية ينفّذ `payload migrate` تلقائيًا قبل الإقلاع.
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# ADR 0001 — Comments endpoint lives at /api/posts/[slug]/comments, not /api/comments
|
||||||
|
|
||||||
|
Date: 2026-10-03
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The comments API needs a route. The conventional-looking path `/api/comments` collides with Payload: it auto-mounts REST CRUD for every collection at `/api/<slug>`. A `comments` collection therefore already serves `/api/comments` for the admin panel, and moderation (approve/reject) runs through that panel. An app-router route at the same path would shadow and break the panel's CRUD.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Comments are created at `POST /api/posts/[slug]/comments` (`src/app/api/posts/[slug]/comments/route.ts`), matching the existing `/api/posts/[slug]/like` pattern. Admin moderation keeps using Payload's own REST/admin panel.
|
||||||
|
|
||||||
|
## Tests for a real ADR
|
||||||
|
|
||||||
|
- **Hard to reverse**: the endpoint is a public contract consumed by the client form; moving it breaks every deployed client.
|
||||||
|
- **Surprising**: the collision with Payload's auto-mounted REST is non-obvious — a developer would naturally reach for `/api/comments`.
|
||||||
|
- **Real trade-off**: a shorter conventional path vs. a working admin moderation panel.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# ADR 0002 — Moderated comments with server-forced author
|
||||||
|
|
||||||
|
Date: 2026-10-03
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Comments are user-generated content on a public site. Risks: identity spoofing (a request forging `author`), spam (no gate), and orphaned identity data (nested user lookups through restrictive read access). QA-D04 required a real backend for comments.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
- Comments default to `status: 'pending'`; only `approved` are publicly visible. The read access rule returns `{ status: { equals: 'approved' } }` for non-admins.
|
||||||
|
- The author is forced server-side on create, twice: a `beforeChange` hook on the collection and the custom route — REST-created comments can never spoof identity.
|
||||||
|
- `authorName` is denormalized at submit time (snapshot of `fullName`, else «عضو») to avoid nested user-read access issues when rendering; the email is never exposed.
|
||||||
|
- Approval/rejection is admin-only, done in the Payload admin panel. Authors cannot edit or delete their comments (moderated model).
|
||||||
|
- Authors see their own pending comments rendered on the post page tagged «قيد المراجعة» (via `overrideAccess` query filtered to their own author id), so a submitted comment does not "vanish".
|
||||||
|
|
||||||
|
## Tests for a real ADR
|
||||||
|
|
||||||
|
- **Hard to reverse**: the display-name snapshot and status lifecycle are visible in every rendered comment and in the DB.
|
||||||
|
- **Surprising**: forcing author in two places (hook + route) and `overrideAccess` for the pending query are non-obvious.
|
||||||
|
- **Real trade-off**: moderation latency for users vs. spam safety; denormalization vs. live join through restricted user reads.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# ADR 0003 — Cloudflare Email Sending REST, not SMTP
|
||||||
|
|
||||||
|
Date: 2026-10-03
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The site needs exactly one transactional email: the verification mail sent by `POST /api/auth/resend-verification` via `sendVerificationEmail(to, token)` in `src/lib/mail.ts`. The first implementation used nodemailer over generic SMTP. Cloudflare Email Service (the mail provider chosen for anwarpy.ai) has **no SMTP interface** — it exposes a REST endpoint only — so the nodemailer path could never be activated on the target host.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
- `src/lib/mail.ts` sends via `POST https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/email/sending/send` using `fetch`, authenticated with `Authorization: Bearer ${CLOUDFLARE_API_TOKEN}`.
|
||||||
|
- The `from` field uses the REST shape `{ address, name }` (key is `address`, **not** `email`); default address `noreply@anwarpy.ai` (override: `MAIL_FROM`).
|
||||||
|
- Active only when `CLOUDFLARE_API_TOKEN` **and** `CLOUDFLARE_ACCOUNT_ID` are set; otherwise `{ sent: false }` (existing degrade-gracefully contract — the resend route answers 503 with a clear message).
|
||||||
|
- Non-OK response or fetch throw: `console.error` the error body, return `{ sent: false }`. No retry logic (single-shot; the resend route re-invokes).
|
||||||
|
- nodemailer is uninstalled; the exported signature `sendVerificationEmail(to: string, token: string): Promise<{ sent: boolean }>` is unchanged.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The API token needs the Email Sending permission; the sender domain must be onboarded in Cloudflare (SPF/DKIM records created via `wrangler email sending dns get`).
|
||||||
|
- `smtpConfigured()` and all `SMTP_*` env vars are gone; anything expecting them must be updated when mail features grow.
|
||||||
|
- Adding more email types later means extending this one module — it is the single mail boundary.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# ADR 0004 — Coming-soon subscribe on public launch (no simulated VIP)
|
||||||
|
|
||||||
|
Date: 2026-10-03
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Until Stripe keys exist, the subscribe flow had two self-grant holes that are fatal on a public site:
|
||||||
|
|
||||||
|
1. `POST /api/subscribe` (no keys) returned `{ url: '/payment-success?simulated=1' }`.
|
||||||
|
2. `/payment-success` without a `session_id` set `activate = true` — i.e. any logged-in user visiting the URL became `role: 'vip'` for free.
|
||||||
|
|
||||||
|
On the public internet this lets anyone self-grant VIP.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
- With no Stripe keys, `POST /api/subscribe` returns `{ comingSoon: true }` and no `url`; `SubscribeButton` already renders a disabled «قريباً» button whenever the response has no `url`.
|
||||||
|
- `/payment-success` no longer activates anything without a session: the `else if (!sessionId) { activate = true }` branch is deleted and `simulated` is removed from the page props. After the activation block, free users (`role` not `vip`/`admin`) are redirected to `/subscription`. VIP/admin users pass through to the success copy.
|
||||||
|
- The real Stripe path is unchanged: `session_id` present + `STRIPE_SECRET_KEY` set → session verified with Stripe → VIP activated.
|
||||||
|
- When Stripe keys land, the same pages switch to real Checkout without further flow changes.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- No revenue until Stripe is configured; VIP can still be granted manually by an admin in `/admin`.
|
||||||
|
- Anyone with a saved `?simulated=1` link gets redirected — the query param is simply ignored now.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# ADR 0005 — Member trust model (server-only privilege/verification/billing fields)
|
||||||
|
|
||||||
|
Date: 2026-10-09
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The 2026-10-08 pentest (findings #1, #3, #4, #6) confirmed that anonymous or self-authenticated
|
||||||
|
client requests could write privileged fields on `users`:
|
||||||
|
|
||||||
|
1. `POST /api/users` accepted client-supplied `emailVerified`, `role`, `onboarded`,
|
||||||
|
`stripeCustomerId`, `verificationToken` — e.g. instant self-verified admin.
|
||||||
|
2. Self-`PATCH /api/users/{id}` accepted the same fields on update.
|
||||||
|
3. Per-account login lockout (no time-box) let an attacker lock the admin out of the site
|
||||||
|
indefinitely with wrong-password floods.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
- `role`, `emailVerified`, `verificationToken`, `stripeCustomerId`, `onboarded` are stripped from
|
||||||
|
every **untrusted write** in `Users.beforeChange` (`web/src/collections/Users.ts`). Untrusted =
|
||||||
|
no authenticated admin **and** no `context.systemActivation === true`. On create, the hook forces
|
||||||
|
`role: 'free'`, `emailVerified: false`, `onboarded: false`.
|
||||||
|
- Server-side writers opt back in with `context: { systemActivation: true }` on their local-API
|
||||||
|
`payload.update`/`create` calls. Enumerated writers: `verify-email`, `resend-verification`,
|
||||||
|
`subscribe`, `stripe/webhook` (×2), `payment-success`, `seed`. **Omitting the context silently
|
||||||
|
no-ops the write (the field is deleted) — that is the intended failure mode**, so a missing
|
||||||
|
context is caught in QA rather than leaking privilege.
|
||||||
|
- Login lockout is per-account, time-boxed: 5 failed attempts → 15-minute lock (`auth:
|
||||||
|
{ maxLoginAttempts: 5, lockTime: 15 * 60 * 1000 }`).
|
||||||
|
- `role === 'admin'` accounts are **exempt from lockout** (owner decision 2026-10-09): a
|
||||||
|
`beforeOperation` hook on `login` pre-clears `lockUntil`/`loginAttempts` via
|
||||||
|
`db.updateOne` (which, like Payload's own `incrementLoginAttempts`, bypasses hooks). Free/VIP
|
||||||
|
accounts keep the normal 5/15-min lockout.
|
||||||
|
- Commenting requires a verified email; admins are exempt (see the 403 gate in
|
||||||
|
`api/posts/[slug]/comments`).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Every future server-side writer of these fields MUST pass `context.systemActivation`; a writer
|
||||||
|
that forgets it silently loses its write.
|
||||||
|
- The admin password has **no brute-force throttle** — compensate with a strong password. If that
|
||||||
|
posture changes, delete the `beforeOperation` hook and the account falls back to 5/15-min lockout.
|
||||||
|
- Client-sent profile fields (`fullName`, `termsVersion`, `interest`, `level`, `goal`, `prefs`) stay
|
||||||
|
writable; only privilege/verification/billing fields are protected.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 175 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
Reference in New Issue
Block a user