first
This commit is contained in:
commit
fa9d154db9
260 files changed
+17643
No files matched your search
@@ -0,0 +1,142 @@
|
||||
# استقرار Production روی Coolify
|
||||
|
||||
این پروژه با سه سرویس اجرا میشود:
|
||||
|
||||
- `nginx`: فقط سرویسدهنده فایلهای `/static/` روی پورت 80
|
||||
- `web`: اجرای Django با Gunicorn روی پورت داخلی 8000
|
||||
- `notification-worker`: اجرای دائمی `python manage.py process_notifications` برای SMS و Bale
|
||||
|
||||
PostgreSQL جزو این Compose نیست و از دیتابیس مدیریتشده موجود استفاده میشود. Supervisor لازم نیست؛ هر process یک container مستقل دارد و `restart: unless-stopped` بازیابی آن را انجام میدهد.
|
||||
|
||||
## 1. Volumeهای Coolify
|
||||
|
||||
پیش از اولین deploy دو Docker volume با همین نامها ایجاد کنید:
|
||||
|
||||
```text
|
||||
customer_ticket_static
|
||||
customer_ticket_media
|
||||
```
|
||||
|
||||
در صورت انتخاب نام دیگر، متغیرهای `STATIC_VOLUME_NAME` و `MEDIA_VOLUME_NAME` را مطابق آن تنظیم کنید. media حاوی آپلودهای ماندگار است. static در هر startup دوباره با `collectstatic` تکمیل میشود اما برای اشتراک بین web و Nginx volume لازم دارد.
|
||||
|
||||
## 2. دامنهها و Traefik خود Coolify
|
||||
|
||||
دامنه اصلی HTTPS را در Coolify مستقیماً به سرویس `web` و پورت `8000` متصل کنید. Traefik خود Coolify TLS، reverse proxy و هدر `X-Forwarded-Proto` را مدیریت میکند؛ Nginx جلوی Django قرار نمیگیرد.
|
||||
|
||||
برای همان دامنه، routeهای `/static/`، `/media/branding/` و `/protected-media/` را به سرویس `nginx` و پورت `80` بدهید و مطمئن شوید routeهای path-based نسبت به route عمومی web اولویت بالاتری دارند. برای `notification-worker` هیچ domain یا host port عمومی نسازید. اگر نسخه Coolify شما تعریف چند route روی یک دامنه را از UI پشتیبانی نمیکند، routing rule متناظر Traefik را برای این سه path به nginx اضافه کنید؛ معماری containerها همچنان بدون proxy_pass بین Nginx و Django است.
|
||||
|
||||
فایلهای آپلودی تیکت عمومی نیستند: Django ابتدا مجوز را بررسی میکند و سپس کاربر را با یک URL امضاشده پنجدقیقهای به `/protected-media/` روی Nginx میفرستد. branding عمومی زیر `/media/branding/` مستقیماً از Nginx سرو میشود.
|
||||
|
||||
## 3. متغیرهای ضروری
|
||||
|
||||
مقادیر زیر را در Environment Variables برنامه Coolify بسازید:
|
||||
|
||||
```dotenv
|
||||
DJANGO_SECRET_KEY=<random-secret-at-least-50-characters>
|
||||
DJANGO_ALLOWED_HOSTS=support.example.com
|
||||
CSRF_TRUSTED_ORIGINS=https://support.example.com
|
||||
PUBLIC_BASE_URL=https://support.example.com
|
||||
WEBOTP_HOST=support.example.com
|
||||
TIME_ZONE=Asia/Tehran
|
||||
|
||||
DATABASE_URL=postgresql://postgres:<URL_ENCODED_PASSWORD>@<INTERNAL_HOST>:5432/customerTicket
|
||||
DB_CONN_MAX_AGE=60
|
||||
DB_CONNECT_TIMEOUT=10
|
||||
DB_SSLMODE=prefer
|
||||
DATABASE_WAIT_TIMEOUT=60
|
||||
|
||||
STATIC_VOLUME_NAME=customer_ticket_static
|
||||
MEDIA_VOLUME_NAME=customer_ticket_media
|
||||
MEDIA_SIGNING_SECRET=<separate-random-secret-at-least-32-characters>
|
||||
```
|
||||
|
||||
نام دیتابیس انتهای URL باید نام واقعی دیتابیس باشد. اگر دیتابیس ساختهشده `customerTicket` است، URL باید به `/customerTicket` ختم شود؛ URLی که به `/postgres` ختم میشود به دیتابیس پیشفرض `postgres` متصل خواهد شد. اگر password شامل `@`، `:`، `/`، `#` یا `%` است آن را URL-encode کنید.
|
||||
|
||||
میتوانید بهجای `DATABASE_URL` آن را خالی بگذارید و `POSTGRES_DB`، `POSTGRES_USER`، `POSTGRES_PASSWORD`، `POSTGRES_HOST` و `POSTGRES_PORT` را جداگانه تعریف کنید. یکی از این دو روش را انتخاب کنید.
|
||||
|
||||
## 4. تنظیم SMS
|
||||
|
||||
فقط provider مورد استفاده را انتخاب و secretهای همان provider را پر کنید.
|
||||
|
||||
### 0098SMS
|
||||
|
||||
```dotenv
|
||||
SMS_PROVIDER_CLASS=apps.integrations.sms.sms0098.ZeroZeroNinetyEightSMSProvider
|
||||
SMS0098_USERNAME=<username>
|
||||
SMS0098_PASSWORD=<password>
|
||||
SMS0098_FROM=<sender-number>
|
||||
SMS0098_API_URL=http://www.0098sms.com/sendsmslink.aspx
|
||||
```
|
||||
|
||||
### Behin SMS
|
||||
|
||||
```dotenv
|
||||
SMS_PROVIDER_CLASS=apps.integrations.sms.behin.BehinSMSProvider
|
||||
BEHIN_SMS_USERNAME=<username>
|
||||
BEHIN_SMS_PASSWORD=<password>
|
||||
BEHIN_SMS_SPECIAL_NUMBER=<sender-number>
|
||||
BEHIN_SMS_WSDL_URL=http://panel.behinsms.com/SMSWS/SOAP.asmx?wsdl
|
||||
```
|
||||
|
||||
### HTTP JSON provider
|
||||
|
||||
```dotenv
|
||||
SMS_PROVIDER_CLASS=apps.integrations.sms.http.HTTPSMSProvider
|
||||
SMS_API_URL=https://provider.example/api/messages
|
||||
SMS_API_TOKEN=<token>
|
||||
SMS_SENDER=<sender>
|
||||
```
|
||||
|
||||
## 5. تنظیم Bale و worker
|
||||
|
||||
```dotenv
|
||||
BALE_API_BASE=https://tapi.bale.ai
|
||||
BALE_BOT_TOKEN=<bot-token>
|
||||
BALE_GROUP_CHAT_ID=<group-chat-id>
|
||||
INTEGRATION_HTTP_TIMEOUT_SECONDS=10
|
||||
MAX_NOTIFICATION_ATTEMPTS=5
|
||||
```
|
||||
|
||||
در production نبودن `BALE_BOT_TOKEN` خطای قابلمشاهده ایجاد میکند و job را بیصدا SENT علامت نمیزند. worker هر پنج ثانیه outbox دیتابیس را بررسی میکند، با قفل `skip_locked` از پردازش همزمان جلوگیری میکند و خطاها را با backoff تا سقف `MAX_NOTIFICATION_ATTEMPTS` دوباره امتحان میکند.
|
||||
|
||||
سیاست تأخیر پیامک پاسخ کارشناس و اعلان پاسخ مشتری در Bale از مدل `NotificationPolicy` در پنل مدیریت دیتابیس کنترل میشود؛ متغیر قدیمی `STAFF_REPLY_SMS_THROTTLE_SECONDS` مرجع اصلی این رفتار نیست.
|
||||
|
||||
## 6. تنظیمات امنیتی
|
||||
|
||||
```dotenv
|
||||
SECURE_SSL_REDIRECT=true
|
||||
SECURE_HSTS_SECONDS=31536000
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS=true
|
||||
SECURE_HSTS_PRELOAD=false
|
||||
```
|
||||
|
||||
`SECURE_HSTS_PRELOAD` را فقط وقتی true کنید که تمام subdomainها همیشه HTTPS هستند. `DJANGO_ALLOWED_HOSTS` فقط hostname و `CSRF_TRUSTED_ORIGINS` origin کامل همراه `https://` است. برای چند مقدار از کاما استفاده کنید.
|
||||
|
||||
## 7. ترتیب اولین deploy
|
||||
|
||||
1. volumeها و database خالی را ایجاد کنید.
|
||||
2. Environment Variables را در Coolify وارد کنید.
|
||||
3. Compose را deploy، دامنه اصلی را به `web:8000` و pathهای static/media گفتهشده را به `nginx:80` متصل کنید.
|
||||
4. log سرویس `web` را برای موفقیت migrate، bootstrap، collectstatic و Gunicorn بررسی کنید.
|
||||
5. log سرویس `notification-worker` را بررسی کنید؛ container باید Running بماند.
|
||||
6. یکبار superuser بسازید:
|
||||
|
||||
```bash
|
||||
docker compose exec web python manage.py createsuperuser
|
||||
```
|
||||
|
||||
7. در پنل مدیریت، SMS templateها و `NotificationPolicy`ها را بررسی کنید و با گیرنده آزمایشی مجاز یک SMS و یک پیام Bale واقعی بفرستید.
|
||||
|
||||
## 8. کنترل سلامت و عیبیابی
|
||||
|
||||
- `GET /healthz/` هم web server و هم اتصال دیتابیس را بررسی میکند.
|
||||
- `docker compose ps` باید `web` و `nginx` را healthy و worker را Running نشان دهد.
|
||||
- `docker compose logs --tail=200 web notification-worker nginx` خطاهای startup و provider را نمایش میدهد.
|
||||
- وضعیت outbox را میتوان در Django admin یا shell از مدل `NotificationJob` بررسی کرد؛ FAILED و `last_error` دلیل خطا را نگه میدارند.
|
||||
- برای پردازش کنترلشده یک job مشخص: `python manage.py process_notifications --once --job-id <ID>`.
|
||||
|
||||
ارسال اعلان ماهیت at-least-once دارد: اگر provider پیام را بپذیرد ولی process پیش از ثبت نتیجه در PostgreSQL قطع شود، retry ممکن است پیام تکراری بسازد. `dedupe_key` جلوی ساخت job تکراری در برنامه را میگیرد، اما تضمین exactly-once به پشتیبانی idempotency از طرف provider نیاز دارد.
|
||||
|
||||
## 9. Mirrorهای ایرانی
|
||||
|
||||
Dockerfile بهطور پیشفرض PyPI لیارا را از آدرس `https://package-mirror.liara.ir/repository/pypi/simple` و mirror Debian لیارا را استفاده میکند. اگر mirror Debian در محیط شما متفاوت است، build argument با نام `DEBIAN_MIRROR_HOST` را تغییر دهید. برای image مربوط به Nginx نیز میتوانید `NGINX_IMAGE` را به مسیر registry mirror مورد اعتماد خود تغییر دهید.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Organizational ticketing rollout
|
||||
|
||||
The organizational layer is additive: legacy tickets with no unit keep their existing
|
||||
visibility rules, while every ticket assigned to a unit is isolated by unit membership
|
||||
and database-managed Django permissions.
|
||||
|
||||
## Admin setup order
|
||||
|
||||
1. Create organizational units under **Staff → Organizational units**.
|
||||
2. Select the Django permissions inherited by every member of each unit.
|
||||
3. Assign staff through unit memberships and choose one primary unit.
|
||||
4. Classify external contacts as customer, supplier, carrier, or other.
|
||||
5. Create unit-owned contact groups and assign each contact to any number of groups.
|
||||
6. Create optional unit-specific SMS templates. A template with no unit is the global
|
||||
fallback. Supported variables include `recipient_name`, `unit_name`, `company_name`,
|
||||
`magic_link`, `ticket_number`, and `subject`.
|
||||
7. Grant `tickets.dispatch_group_tickets` to units allowed to create bulk dispatches.
|
||||
Grant `accounts.view_all_magic_links` only to senior auditors who may inspect every
|
||||
unit; other Admin users see only links from their active unit memberships.
|
||||
|
||||
Useful unit permissions include `tickets.view_ticket`, `tickets.claim_ticket`,
|
||||
`tickets.transfer_ticket`, `tickets.share_ticket`, `tickets.change_ticket`,
|
||||
`tickets.change_ticket_priority`, `tickets.delete_ticket`, and
|
||||
`messaging.add_message`.
|
||||
|
||||
## Security invariants
|
||||
|
||||
- Each group recipient gets a separate `Ticket`, notification job, and magic link.
|
||||
- External users can access only tickets whose `customer_id` is their own user ID.
|
||||
- Unit filtering happens in both list querysets and object-level policy checks.
|
||||
- Every valid magic-link opening appends an immutable access-log row and atomically
|
||||
increments its view counter.
|
||||
- Raw magic-link tokens are never stored; only SHA-256 digests are persisted.
|
||||
- Group dispatch runs inside a database transaction, preventing partially-created
|
||||
batches when validation or ticket creation fails.
|
||||
|
||||
Apply schema changes with:
|
||||
|
||||
```powershell
|
||||
.\.venv\Scripts\python.exe manage.py migrate
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
# ممیزی حلقه عملیاتی سامانه تیکتینگ سازمانی
|
||||
|
||||
این سند وضعیت پیادهسازی را در برابر نیازمندی شرکت لوله گستر گلپایگان ثبت میکند. معیار «تکمیل» فقط وجود مدل یا صفحه نیست؛ مسیر باید از ایجاد داده تا مجوز، ارسال، مشاهده، پاسخ، ممیزی و گزارشگیری قابل آزمون باشد.
|
||||
|
||||
| حوزه | مسیر عملیاتی | وضعیت فعلی | شاهد اجرایی |
|
||||
|---|---|---|---|
|
||||
| RBAC واحدی | عضویت کاربر → permission واحد/عضو → queryset و object policy | تکمیل | `OrganizationalUnit`, `UnitMembership`, `TicketPolicy` و تست ایزولهسازی واحد |
|
||||
| ایجاد تیکت سازمانی | انتخاب واحد مجاز → مخاطب/نوع/گروه → تیکت واحدی → SMS | تکمیل | فرم staff اکنون permission پویا و مالکیت واحد را اعمال میکند |
|
||||
| مخاطبان چندگروهی | مخاطب → چند `ContactGroup` متعلق به واحدهای مجاز | تکمیل | رابطه Many-to-Many و فرم محدودشده به واحدهای قابل مدیریت |
|
||||
| ارسال گروهی ایزوله | گروه → یک تیکت خصوصی برای هر مخاطب → یک job و Magic Link مستقل | تکمیل | `dispatch_group_tickets` و تست جداسازی گیرندگان |
|
||||
| اسناد گروهی | یک یا چند سند → ذخیره مستقل داخل پیام اولیه هر تیکت | تکمیل | `files` در فرم dispatch و تست مسیر ذخیره مجزا |
|
||||
| پاسخ خارجی | Magic Link بدون حساب → فقط همان تیکت → متن/فایل → اعلان Bale | تکمیل | session grant محدود به یک ticket و تست پاسخ ناشناس |
|
||||
| ممیزی Magic Link | سازنده/واحد/گیرنده → هر بازدید با زمان/IP/UA → شمارنده | تکمیل | `TicketMagicLink`, `MagicLinkAccessLog` و تست بازدید |
|
||||
| عملیات امنیتی لینک | فیلتر وضعیت/واحد/بازه → ابطال فوری با permission واحدی | تکمیل | staff audit، pagination و endpoint ابطال POST |
|
||||
| گزارش ارسال گروهی | وضعیت gateway → بازشدن لینک → پاسخ مخاطب → مقایسه | تکمیل در سطح قابل اثبات | وضعیت پایدار dispatch، پاسخ آخر و شمارنده بازدید |
|
||||
| بازیابی ارسال خطادار | FAILED → اقدام مجاز واحد → PENDING → worker retry | تکمیل | endpoint تلاش مجدد و تست عدم دسترسی واحد دیگر |
|
||||
| الگوهای SMS | الگوی واحدی + fallback عمومی + متغیرهای کنترلشده | تکمیل | `SMSTemplate`, validation و فرم واحدی |
|
||||
| فیلتر پیشرفته | واحدها، نوع ارسال، وضعیت، مخاطب، گروهها، تاریخ و tag | تکمیل | `filter_staff_tickets` و UI فیلتر ترکیبی |
|
||||
| پردازش غیرهمزمان | outbox قفلشونده → retry/backoff → SMS/Bale | تکمیل | `process_notifications`, `skip_locked` و سرویس worker در Compose |
|
||||
| رهگیری provider | نتیجه gateway → شناسه مرجع ذخیرهشده | تکمیل | `provider_message_id` در `NotificationJob` |
|
||||
| Delivery Receipt مخابراتی | callback اپراتور → وضعیت تحویل گوشی | نیازمند قرارداد اپراتور | API فعلی 0098SMS فقط پذیرش درخواست را برمیگرداند؛ `SENT` به معنی پذیرش gateway است، نه تحویل قطعی |
|
||||
|
||||
## مرزهای امنیتی
|
||||
|
||||
- مخاطب بیرونی با Magic Link فقط به یک تیکت دسترسی میگیرد و به فهرست تیکتها یا تیکت دیگر دسترسی ندارد.
|
||||
- یادداشت داخلی و پیوست آن برای مشتری و session جادویی پنهان است.
|
||||
- queryهای staff ابتدا با واحدهای قابل مشاهده محدود میشوند و سپس فیلترهای کاربر اعمال میشوند.
|
||||
- ابطال لینک و retry ارسال فقط با permission پویا در همان واحد انجام میشود.
|
||||
- شناسه provider ذخیره میشود، اما متن پیام، credential درگاه و token خام Magic Link در گزارشها ذخیره یا نمایش داده نمیشود.
|
||||
|
||||
## الزام استقرار
|
||||
|
||||
- `web` و `notification-worker` باید همزمان و تحت supervisor یا Docker Compose با restart policy اجرا شوند.
|
||||
- production باید از `config.settings.prod`، HTTPS، secret قوی و `PUBLIC_BASE_URL` عمومی HTTPS استفاده کند.
|
||||
- برای ادعای «تحویل به گوشی»، endpoint و امضای webhook اپراتور منتخب باید مشخص شود؛ تا آن زمان UI باید بین پذیرش gateway و بازشدن لینک تفاوت بگذارد.
|
||||
@@ -0,0 +1,41 @@
|
||||
# QA permission and representative matrix
|
||||
|
||||
This matrix is the maintained regression contract for role, unit, permission, scope,
|
||||
and primary workflows. Representatives are deterministic records created by
|
||||
`python manage.py bootstrap`; tests do not depend on mutable production data.
|
||||
|
||||
| Unit | Role | Representative | Scope | Expected access |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| فروش | کارشناس فروش | کیمیا غفاری (`kimiya_ghaffari`) | `SELF` | Public unassigned sales tickets, tickets created by or assigned to self, reply in that scope |
|
||||
| فروش | سرپرست فروش | مریم فتوت (`maryam_fotovat`) | `UNIT` | All sales-unit tickets and normal sales workflows |
|
||||
| فروش | مدیر فروش | مرتضی سخائی فرد (`morteza_sokhaeifard`) | `UNIT` + manager role permissions | All sales-unit tickets and manager actions |
|
||||
| بارگیری انبار | کارشناس بارگیری | خانم سبزعلی (`ms_sabzali`) | `UNIT` | Warehouse-loading-loading tickets only; create bulk/inquiry and view own operations |
|
||||
| مالی / حسابداری | کارشناس مالی | خانم فرشته یاوری (`fereshteh_yavari`) | `UNIT` | Finance tickets, categories, and contact groups only |
|
||||
| مدیریت ارشد | مدیرعامل | مصطفی سخائی فرد (`mostafa_sokhaeifard`) | Broad/global explicit permissions | All tickets, all bulk dispatches, all inquiries |
|
||||
|
||||
## Permission coverage contract
|
||||
|
||||
| Capability | Limited scope | Unit scope | Broad scope |
|
||||
| --- | --- | --- | --- |
|
||||
| Ticket view/reply | Public unassigned + created/assigned self | All permitted tickets in own unit | `view_all_tickets` |
|
||||
| Staff ticket creation | Primary unit only | Primary unit only | Explicit `add_ticket`; unit/category validation still applies |
|
||||
| Category / expert / queue | Selected/primary unit only | Selected/primary unit only | Still validated against selected unit |
|
||||
| Contact group | Primary unit only | Primary unit only | Explicit permission plus unit validation |
|
||||
| Bulk ticket | `create_bulk_dispatch`, own records | Same within permitted unit | `view_all_bulk_dispatches` |
|
||||
| Inquiry | `create_inquiry`, own records | Same within permitted unit | `view_all_inquiries` |
|
||||
|
||||
The public “general queue” is a routing state, not a separate database model: it is
|
||||
stored as the selected unit with `selected_expert = NULL`.
|
||||
|
||||
## Permanent commands
|
||||
|
||||
```powershell
|
||||
# Backend/API/integration regression
|
||||
.\.venv\Scripts\python.exe -m pytest tests -m "not e2e" -q
|
||||
|
||||
# Local real-browser regression (managed Chromium + isolated live_server)
|
||||
.\.venv\Scripts\python.exe -m pytest tests\e2e -m "e2e and not external_e2e" -q
|
||||
|
||||
# Read-only deployed-environment smoke (requires environment variables in README)
|
||||
.\.venv\Scripts\python.exe -m pytest tests\e2e\test_external_environment_smoke.py -m external_e2e -q
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
# Central color theme
|
||||
|
||||
The application uses one cohesive blue/navy theme. All colors are centralized in `static/src/tailwind.css`; templates do not define brand hex colors.
|
||||
|
||||
## Palette and roles
|
||||
|
||||
- `--color-ink: #08376b` — navigation, headings and structural contrast
|
||||
- `--color-sea: #0693e3` — primary actions, links and active states
|
||||
- `--color-sea-dark: #1977ce` — depth, secondary blue and hover states
|
||||
- `--color-coral: #fa9f00` — selective attention, highlights and warnings
|
||||
- `--color-cream: #eeeeee` — page background
|
||||
- `--color-soft: #eaeaea` — secondary surfaces and conversation canvas
|
||||
- `--color-paper: #ffffff` — elevated cards
|
||||
- `--color-mint`, `--color-sun` — supporting light-blue and warm tints
|
||||
|
||||
Conversation surfaces and shadows are also variables in the same `:root` block.
|
||||
|
||||
## Changing the theme later
|
||||
|
||||
Edit only the `@theme` and `:root` token values near the top of `static/src/tailwind.css`, then run:
|
||||
|
||||
```powershell
|
||||
npm run css:build
|
||||
```
|
||||
|
||||
Component rules consume semantic names such as `ink`, `sea`, `coral`, and `paper`, so no template rewrite is required.
|
||||
Reference in new issue
Block a user