docs: rewrite README in English with full architecture reference
Also add itsdangerous to requirements.txt (missing implicit dep of starlette SessionMiddleware) and a create_admin.py script for bootstrapping a system-role user with all permissions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -18,3 +18,5 @@ __pycache__/
|
||||
*.pyc
|
||||
certbot
|
||||
web-resources
|
||||
.coverage
|
||||
password|*
|
||||
|
||||
172
README.md
172
README.md
@@ -1,3 +1,171 @@
|
||||
# ЭВОСИНК (EvoSync)
|
||||
# EvoSync
|
||||
|
||||
Сервис автоматической синхронизации товарного каталога из [Эвотор](https://evotor.ru) в магазин [ВКонтакте](https://vk.com).
|
||||
Web service for syncing a product catalog from Evotor POS → VK Market. Users connect their Evotor account and a VK page; products from the cash register then appear automatically in the VK store.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Docker Compose │
|
||||
│ │
|
||||
Browser / Evotor ─────► web :8000 (FastAPI + Uvicorn) │
|
||||
:8080 │ │ │
|
||||
│ ├── MariaDB :3306 (primary DB) │
|
||||
│ ├── Redis :6379 (Celery broker) │
|
||||
│ │ │
|
||||
│ ├── worker (Celery worker) │
|
||||
│ ├── beat (Celery beat scheduler) │
|
||||
│ └── flower :5555 (queue monitor) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Services
|
||||
|
||||
| Service | Image / Dockerfile | Purpose | External port |
|
||||
|----------|---------------------|----------------------------------------------|---------------|
|
||||
| `web` | `Dockerfile.web` | FastAPI app, runs Alembic migrations on start | 8080 → 8000 |
|
||||
| `worker` | `Dockerfile.web` | Celery worker (sync, health, notifications…) | — |
|
||||
| `beat` | `Dockerfile.web` | Celery Beat — periodic task scheduler | — |
|
||||
| `flower` | `Dockerfile.web` | Celery queue monitoring UI | 5555 |
|
||||
| `db` | `mariadb:11.4` | Primary relational database | — |
|
||||
| `redis` | `redis:7-alpine` | Celery broker and result backend | — |
|
||||
|
||||
### Stack
|
||||
|
||||
- **Python 3.12**, FastAPI 0.115, Uvicorn
|
||||
- **SQLAlchemy 2** + Alembic, MariaDB (PyMySQL)
|
||||
- **Celery 5** + Redis
|
||||
- **Jinja2** — server-side HTML rendering (`web/templates/`)
|
||||
- **Pydantic Settings** — configuration from env vars / `.env`
|
||||
- bcrypt — password hashing
|
||||
- python-json-logger — structured JSON logs to stdout
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
| Table | Purpose |
|
||||
|----------------------|--------------------------------------------------------------------------------|
|
||||
| `users` | User accounts (roles: system / admin / user; statuses: pending / active / suspended) |
|
||||
| `evotor_connections` | User ↔ Evotor link (access_token, api_token returned to Evotor webhooks) |
|
||||
| `vk_connections` | User ↔ VK OAuth link |
|
||||
| `sync_configs` | Per-user sync settings |
|
||||
| `sync_filters` | Product / group inclusion/exclusion filters |
|
||||
| `cached_stores` | Cached list of Evotor stores |
|
||||
| `cached_groups` | Cached Evotor product groups |
|
||||
| `cached_products` | Cached Evotor product catalog |
|
||||
| `roles` | RBAC roles |
|
||||
| `permissions` | RBAC permissions |
|
||||
| `role_permissions` | M2M: role ↔ permission |
|
||||
| `user_roles` | M2M: user ↔ role |
|
||||
|
||||
---
|
||||
|
||||
## Routes
|
||||
|
||||
### Public / Authentication
|
||||
|
||||
| Method | Path | Description |
|
||||
|----------|--------------------|----------------------------------------|
|
||||
| GET | `/` | Redirects to `/profile` or `/login` |
|
||||
| GET | `/health` | Health check (JSON) |
|
||||
| GET/POST | `/register` | User registration |
|
||||
| GET | `/confirm-email` | Email confirmation via token |
|
||||
| GET | `/resend-confirm` | Resend confirmation email |
|
||||
| GET/POST | `/login` | Login |
|
||||
| GET | `/logout` | Logout |
|
||||
| GET/POST | `/forgot-password` | Request password reset |
|
||||
| GET/POST | `/reset-password` | Reset password via token |
|
||||
| GET/POST | `/invite` | Complete registration via invite link |
|
||||
|
||||
### Profile (requires session)
|
||||
|
||||
| Method | Path | Description |
|
||||
|----------|----------------------------|----------------------|
|
||||
| GET | `/profile` | View profile |
|
||||
| GET/POST | `/profile/edit` | Edit profile |
|
||||
| GET/POST | `/profile/change-password` | Change password |
|
||||
| GET/POST | `/profile/delete` | Delete account |
|
||||
|
||||
### Admin panel (`/admin`, roles: admin / system)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------------------------------------|---------------------------|
|
||||
| GET | `/admin/users` | User list |
|
||||
| GET | `/admin/users/{id}` | User detail |
|
||||
| POST | `/admin/users/{id}/activate` | Activate user |
|
||||
| POST | `/admin/users/{id}/suspend` | Suspend user |
|
||||
| POST | `/admin/users/{id}/reset-password` | Reset user password |
|
||||
| POST | `/admin/users/{id}/send-invite` | Send invite email |
|
||||
| POST | `/admin/users/{id}/edit` | Edit user data |
|
||||
| POST | `/admin/users/{id}/delete` | Delete user |
|
||||
| GET | `/admin/roles` | Roles and permissions |
|
||||
| POST | `/admin/roles/{id}/permissions` | Update role permissions |
|
||||
|
||||
### Evotor Webhooks (Bearer `EVOTOR_WEBHOOK_SECRET`)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|----------------|------------------------------------------------------------------|
|
||||
| POST | `/user/create` | Evotor creates/links a user and receives an api_token |
|
||||
| POST | `/user/verify` | Evotor verifies user credentials and receives an api_token |
|
||||
| POST | `/user/token` | Evotor delivers its own access_token for a user |
|
||||
|
||||
### API Docs
|
||||
|
||||
| Path | Description |
|
||||
|----------|-------------|
|
||||
| `/docs` | Swagger UI |
|
||||
| `/redoc` | ReDoc |
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
All settings are read from environment variables or a `.env` file:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|------------------------------------|-------------------------------------|--------------------------------------|
|
||||
| `DATABASE_URL` | `mysql+pymysql://…@db:3306/evosync` | MariaDB connection string |
|
||||
| `REDIS_URL` | `redis://redis:6379/0` | Redis connection string |
|
||||
| `SECRET_KEY` | `change-me-in-production` | Session signing key |
|
||||
| `BASE_URL` | `http://localhost:8000` | Public URL of the service |
|
||||
| `EVOTOR_APP_ID` | — | Evotor application ID |
|
||||
| `EVOTOR_WEBHOOK_SECRET` | — | Bearer secret for webhook endpoints |
|
||||
| `JIVOSITE_WIDGET_ID` | — | JivoSite widget ID |
|
||||
| `VK_DEFAULT_PHOTO_PATH` | `/app/default_product.png` | Fallback image path for VK products |
|
||||
| `VK_API_VERSION` | `5.199` | VK API version |
|
||||
| `CATALOG_REFRESH_INTERVAL_SECONDS` | `3600` | Catalog cache refresh interval |
|
||||
| `INVITE_EXPIRE_HOURS` | `48` | Invite link TTL in hours |
|
||||
| `EMAIL_PROVIDER` | `console` | Email provider (console / smtp / …) |
|
||||
| `SMS_PROVIDER` | `console` | SMS provider |
|
||||
| `FLOWER_USER` / `FLOWER_PASSWORD` | `admin` / `changeme` | Basic Auth credentials for Flower |
|
||||
|
||||
---
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
cp .env.example .env # fill in your values
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
App is available at `http://localhost:8080`.
|
||||
Flower (queue monitor) at `http://localhost:5555`.
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
alembic upgrade head
|
||||
uvicorn web.main:app --reload --port 8000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
pytest --cov=web
|
||||
```
|
||||
|
||||
@@ -5,6 +5,7 @@ jinja2==3.1.4
|
||||
sqlalchemy==2.0.36
|
||||
alembic==1.14.0
|
||||
pymysql==1.1.1
|
||||
itsdangerous>=2.1.0
|
||||
cryptography>=44.0.0
|
||||
bcrypt>=4.2.1
|
||||
pydantic-settings==2.6.1
|
||||
|
||||
78
scripts/create_admin.py
Normal file
78
scripts/create_admin.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
Create (or promote) an admin user with all permissions.
|
||||
|
||||
Usage:
|
||||
python -m scripts.create_admin --email admin@example.com --password secret
|
||||
python -m scripts.create_admin --email admin@example.com --password secret --role system
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from web.auth.password import hash_password
|
||||
from web.database import SessionLocal
|
||||
from web.models.rbac import Permission, Role, UserRole, role_permissions
|
||||
from web.models.user import User, UserRoleEnum, UserStatusEnum
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--email", required=True)
|
||||
parser.add_argument("--password", required=True)
|
||||
parser.add_argument("--first-name", default="Admin")
|
||||
parser.add_argument("--last-name", default="")
|
||||
parser.add_argument("--phone", default="")
|
||||
parser.add_argument("--role", default="system", choices=["admin", "system"])
|
||||
args = parser.parse_args()
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = db.query(User).filter(User.email == args.email).first()
|
||||
if user:
|
||||
user.role = UserRoleEnum(args.role)
|
||||
user.status = UserStatusEnum.active
|
||||
user.is_email_confirmed = True
|
||||
user.password_hash = hash_password(args.password)
|
||||
print(f"Updated existing user: {args.email} → role={args.role}")
|
||||
else:
|
||||
user = User(
|
||||
first_name=args.first_name,
|
||||
last_name=args.last_name,
|
||||
email=args.email,
|
||||
phone=args.phone,
|
||||
password_hash=hash_password(args.password),
|
||||
role=UserRoleEnum(args.role),
|
||||
status=UserStatusEnum.active,
|
||||
is_email_confirmed=True,
|
||||
)
|
||||
db.add(user)
|
||||
db.flush()
|
||||
print(f"Created user: {args.email} role={args.role}")
|
||||
|
||||
# Assign all permissions via the system role
|
||||
system_role = db.query(Role).filter(Role.name == args.role).first()
|
||||
if system_role:
|
||||
existing = db.query(UserRole).filter(
|
||||
UserRole.user_id == user.id,
|
||||
UserRole.role_id == system_role.id,
|
||||
).first()
|
||||
if not existing:
|
||||
db.add(UserRole(user_id=user.id, role_id=system_role.id))
|
||||
|
||||
db.commit()
|
||||
|
||||
# Report permissions this user now has
|
||||
perms = (
|
||||
db.query(Permission.name)
|
||||
.join(role_permissions, Permission.id == role_permissions.c.permission_id)
|
||||
.join(UserRole, UserRole.role_id == role_permissions.c.role_id)
|
||||
.filter(UserRole.user_id == user.id)
|
||||
.all()
|
||||
)
|
||||
print("Permissions:", [p.name for p in perms] or "(inherited via role)")
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user