05/06/2026 Initial commit
This commit is contained in:
@@ -1,3 +1,292 @@
|
||||
# MyPOS
|
||||
# Salon POS — Multi-Tenant SaaS
|
||||
|
||||
POS system for Nails Salon
|
||||
## Overview
|
||||
|
||||
A multi-tenant SaaS POS web application for nail salons, served across two dedicated domains:
|
||||
|
||||
| Domain | Audience | Purpose |
|
||||
|---|---|---|
|
||||
| `admin.mydomain.com` | System Admins | Platform management |
|
||||
| `mydomain.com` | Tenant users | Salon management |
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Backend:** Python 3.11+ / Flask
|
||||
- **Database:** MySQL 8.0+ with SQLAlchemy ORM
|
||||
- **Auth:** Flask-Login (UI) + Flask-JWT-Extended (API)
|
||||
- **Frontend:** Jinja2 + Bootstrap 5 + Vanilla JS
|
||||
- **WSGI:** Gunicorn (2 independent processes)
|
||||
- **Reverse Proxy:** Nginx
|
||||
- **Scheduler:** APScheduler
|
||||
- **Process Manager:** systemd
|
||||
|
||||
---
|
||||
|
||||
## Fresh Install (Production)
|
||||
|
||||
### 1. System dependencies
|
||||
|
||||
```bash
|
||||
sudo apt update && sudo apt install -y python3.11 python3.11-venv python3-pip \
|
||||
mysql-server nginx libmysqlclient-dev build-essential libssl-dev
|
||||
```
|
||||
|
||||
### 2. Application user
|
||||
|
||||
```bash
|
||||
sudo useradd -m -s /bin/bash salonpos
|
||||
sudo mkdir -p /opt/salon_pos
|
||||
sudo chown salonpos:salonpos /opt/salon_pos
|
||||
```
|
||||
|
||||
### 3. Clone and set up virtual environment
|
||||
|
||||
```bash
|
||||
cd /opt/salon_pos
|
||||
python3.11 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 4. Environment configuration
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
chmod 600 .env
|
||||
# Edit .env with your values — SECRET_KEY, ADMIN_SECRET_KEY, DATABASE_URL, MAIL_*, etc.
|
||||
nano .env
|
||||
```
|
||||
|
||||
### 5. MySQL setup
|
||||
|
||||
```sql
|
||||
CREATE DATABASE salon_pos CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER 'salon_pos_app'@'localhost' IDENTIFIED BY 'strong-password-here';
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP
|
||||
ON salon_pos.* TO 'salon_pos_app'@'localhost';
|
||||
|
||||
-- Backup user (read-only)
|
||||
CREATE USER 'salon_pos_backup'@'localhost' IDENTIFIED BY 'backup-password-here';
|
||||
GRANT SELECT, LOCK TABLES ON salon_pos.* TO 'salon_pos_backup'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
```
|
||||
|
||||
Store backup credentials in `/etc/mysql/backup.cnf` (mode 600):
|
||||
```ini
|
||||
[client]
|
||||
user=salon_pos_backup
|
||||
password=backup-password-here
|
||||
host=localhost
|
||||
```
|
||||
|
||||
### 6. Database migrations
|
||||
|
||||
```bash
|
||||
cd /opt/salon_pos
|
||||
source venv/bin/activate
|
||||
flask --app wsgi_tenant:app db init # First time only
|
||||
flask --app wsgi_tenant:app db migrate -m "Phase 1 — initial schema"
|
||||
flask --app wsgi_tenant:app db upgrade
|
||||
```
|
||||
|
||||
### 7. Seed initial data (plans + superadmin)
|
||||
|
||||
```bash
|
||||
flask --app wsgi_admin:app shell
|
||||
```
|
||||
|
||||
```python
|
||||
from app.extensions import db, bcrypt
|
||||
from app.models.platform import Plan, SystemUser
|
||||
|
||||
# Create subscription plans
|
||||
plans = [
|
||||
Plan(name="Starter", price_monthly=29.00, max_staff=3, max_locations=1,
|
||||
features_json={"pos": True, "appointments": True, "customers": True,
|
||||
"services": True, "promotions": True,
|
||||
"appointment_reminders": True, "customer_reviews": True,
|
||||
"reconciliation": True, "basic_reports": True,
|
||||
"inventory": False, "commission": False,
|
||||
"full_reports": False, "multi_location": False,
|
||||
"online_booking": False, "waitlist": False,
|
||||
"marketing": False}),
|
||||
Plan(name="Growth", price_monthly=59.00, max_staff=10, max_locations=3,
|
||||
features_json={"pos": True, "appointments": True, "customers": True,
|
||||
"services": True, "promotions": True,
|
||||
"appointment_reminders": True, "customer_reviews": True,
|
||||
"reconciliation": True, "basic_reports": True,
|
||||
"inventory": True, "commission": True,
|
||||
"full_reports": True, "multi_location": True,
|
||||
"online_booking": True, "waitlist": True,
|
||||
"marketing": False}),
|
||||
Plan(name="Pro", price_monthly=99.00, max_staff=None, max_locations=None,
|
||||
features_json={"pos": True, "appointments": True, "customers": True,
|
||||
"services": True, "promotions": True,
|
||||
"appointment_reminders": True, "customer_reviews": True,
|
||||
"reconciliation": True, "basic_reports": True,
|
||||
"inventory": True, "commission": True,
|
||||
"full_reports": True, "multi_location": True,
|
||||
"online_booking": True, "waitlist": True,
|
||||
"marketing": True}),
|
||||
]
|
||||
db.session.add_all(plans)
|
||||
|
||||
# Create first superadmin
|
||||
admin = SystemUser(
|
||||
email="admin@yourdomain.com",
|
||||
password_hash=bcrypt.generate_password_hash("ChangeMe1!").decode("utf-8"),
|
||||
name="System Admin",
|
||||
role="superadmin",
|
||||
is_active=True,
|
||||
)
|
||||
db.session.add(admin)
|
||||
db.session.commit()
|
||||
print("Seeded successfully.")
|
||||
exit()
|
||||
```
|
||||
|
||||
### 8. Log directories
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/log/salon_pos_admin /var/log/salon_pos_tenant
|
||||
sudo chown salonpos:salonpos /var/log/salon_pos_admin /var/log/salon_pos_tenant
|
||||
```
|
||||
|
||||
### 9. systemd services
|
||||
|
||||
```bash
|
||||
sudo cp deploy/salon_pos_admin.service /etc/systemd/system/
|
||||
sudo cp deploy/salon_pos_tenant.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable salon_pos_admin salon_pos_tenant
|
||||
sudo systemctl start salon_pos_admin salon_pos_tenant
|
||||
sudo systemctl status salon_pos_admin salon_pos_tenant
|
||||
```
|
||||
|
||||
### 10. Nginx
|
||||
|
||||
```bash
|
||||
sudo cp deploy/nginx.conf /etc/nginx/sites-available/salon_pos
|
||||
sudo ln -s /etc/nginx/sites-available/salon_pos /etc/nginx/sites-enabled/
|
||||
# Update IP allowlist in nginx.conf before enabling
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### 11. Backup cron
|
||||
|
||||
```bash
|
||||
sudo cp deploy/backup/db_backup.sh /opt/salon_pos/deploy/backup/
|
||||
sudo chmod +x /opt/salon_pos/deploy/backup/db_backup.sh
|
||||
sudo mkdir -p /var/backups/salon_pos
|
||||
sudo chown salonpos:salonpos /var/backups/salon_pos
|
||||
# Install cron job for salonpos user
|
||||
sudo crontab -u salonpos deploy/backup/backup.cron
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Service Management
|
||||
|
||||
```bash
|
||||
# Status
|
||||
sudo systemctl status salon_pos_admin
|
||||
sudo systemctl status salon_pos_tenant
|
||||
|
||||
# Restart
|
||||
sudo systemctl restart salon_pos_admin
|
||||
sudo systemctl restart salon_pos_tenant
|
||||
|
||||
# View logs
|
||||
sudo journalctl -u salon_pos_admin -f
|
||||
sudo journalctl -u salon_pos_tenant -f
|
||||
tail -f /var/log/salon_pos_admin/error.log
|
||||
tail -f /var/log/salon_pos_tenant/error.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Migrations (Ongoing)
|
||||
|
||||
```bash
|
||||
cd /opt/salon_pos
|
||||
source venv/bin/activate
|
||||
flask --app wsgi_tenant:app db migrate -m "Description of change"
|
||||
flask --app wsgi_tenant:app db upgrade
|
||||
|
||||
# Rollback one step
|
||||
flask --app wsgi_tenant:app db downgrade
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
cd /opt/salon_pos
|
||||
source venv/bin/activate
|
||||
pip install pytest
|
||||
FLASK_ENV=testing pytest tests/ -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monthly Restore Drill (Mandatory)
|
||||
|
||||
```bash
|
||||
# On staging server:
|
||||
LATEST=$(ls -t /var/backups/salon_pos/*.sql.gz | head -1)
|
||||
gunzip -c "$LATEST" | mysql -u root salon_pos_staging
|
||||
echo "Restore drill completed: $LATEST"
|
||||
# Document the result and date in this runbook.
|
||||
```
|
||||
|
||||
**Last restore drill:** _______________
|
||||
**Performed by:** _______________
|
||||
**Result:** _______________
|
||||
|
||||
---
|
||||
|
||||
## Development Setup
|
||||
|
||||
```bash
|
||||
git clone <repo>
|
||||
cd salon_pos
|
||||
python3.11 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
cp .env.example .env
|
||||
# Set FLASK_ENV=development in .env
|
||||
flask --app wsgi_tenant:app db upgrade
|
||||
flask --app wsgi_tenant:app run --port 5001
|
||||
flask --app wsgi_admin:app run --port 5002
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
salon_pos/
|
||||
├── app/
|
||||
│ ├── __init__.py
|
||||
│ ├── extensions.py # Shared Flask extensions
|
||||
│ ├── context.py # load_tenant_context, load_location_context
|
||||
│ ├── decorators.py # @require_role, @tenant_feature_required, @demo_readonly
|
||||
│ ├── security.py # Security headers + input sanitisers
|
||||
│ ├── forms.py # Password validation helper
|
||||
│ ├── models/
|
||||
│ │ ├── platform.py # SystemUser, Tenant, Plan, AuditLog, ...
|
||||
│ │ └── salon.py # All tenant-scoped models
|
||||
│ ├── admin/ # admin.mydomain.com blueprints
|
||||
│ └── tenant/ # mydomain.com blueprints
|
||||
├── config.py # Dev / Prod / Test config classes
|
||||
├── wsgi_admin.py # Gunicorn entrypoint — admin
|
||||
├── wsgi_tenant.py # Gunicorn entrypoint — tenant
|
||||
├── requirements.txt
|
||||
├── .env.example
|
||||
├── deploy/ # systemd units, Nginx config, backup scripts
|
||||
└── tests/ # pytest test suites
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user