# Slice 1: Multi-Tenant Foundation ## Admin Creates Vendor → Vendor Owner Logs In **Status**: 🔄 IN PROGRESS **Timeline**: Week 1 (5 days) **Current Progress**: Backend ~90%, Frontend ~60% ## 🎯 Slice Objectives Establish the multi-tenant foundation with complete vendor isolation and admin capabilities. ### User Stories - ✅ As a Super Admin, I can create vendors through the admin interface - [ ] As a Super Admin, I can manage vendor accounts (verify, activate, deactivate) - ✅ As a Vendor Owner, I can log into my vendor-specific admin interface - ✅ The system correctly isolates vendor contexts (subdomain + path-based) ### Success Criteria - ✅ Admin can log into admin interface - ✅ Admin can create new vendors with auto-generated owner accounts - ✅ System generates secure temporary passwords - ✅ Vendor owner can log into vendor-specific interface - [ ] Vendor context detection works in dev (path) and prod (subdomain) modes - [ ] Database properly isolates vendor data - [ ] All API endpoints protected with JWT authentication - ✅ Frontend integrates seamlessly with backend ## 📋 Backend Implementation ### Database Models (✅ Complete) #### User Model (`models/database/user.py`) ```python class User(Base, TimestampMixin): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) email = Column(String, unique=True, nullable=False, index=True) username = Column(String, unique=True, nullable=False, index=True) hashed_password = Column(String, nullable=False) role = Column(String, nullable=False) # 'admin' or 'user' is_active = Column(Boolean, default=True) # Relationships owned_vendors = relationship("Vendor", back_populates="owner") vendor_memberships = relationship("VendorUser", back_populates="user") ``` #### Vendor Model (`models/database/vendor.py`) ```python class Vendor(Base, TimestampMixin): __tablename__ = "vendors" id = Column(Integer, primary_key=True, index=True) vendor_code = Column(String, unique=True, nullable=False, index=True) subdomain = Column(String(100), unique=True, nullable=False, index=True) name = Column(String, nullable=False) description = Column(Text) owner_user_id = Column(Integer, ForeignKey("users.id"), nullable=False) # Business info business_email = Column(String) business_phone = Column(String) website = Column(String) business_address = Column(Text) tax_number = Column(String) # Status is_active = Column(Boolean, default=True) is_verified = Column(Boolean, default=False) verified_at = Column(DateTime, nullable=True) # Configuration theme_config = Column(JSON, default=dict) letzshop_csv_url_fr = Column(String) letzshop_csv_url_en = Column(String) letzshop_csv_url_de = Column(String) # Relationships owner = relationship("User", back_populates="owned_vendors") roles = relationship("Role", back_populates="vendor", cascade="all, delete-orphan") team_members = relationship("VendorUser", back_populates="vendor") ``` #### Role Model (`models/database/vendor.py`) ```python class Role(Base, TimestampMixin): __tablename__ = "roles" id = Column(Integer, primary_key=True, index=True) vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False) name = Column(String, nullable=False) # Owner, Manager, Editor, Viewer permissions = Column(JSON, default=list) vendor = relationship("Vendor", back_populates="roles") vendor_users = relationship("VendorUser", back_populates="role") ``` ### Pydantic Schemas (✅ Complete) #### Vendor Schemas (`models/schema/vendor.py`) ```python class VendorCreate(BaseModel): vendor_code: str = Field(..., min_length=2, max_length=50) name: str = Field(..., min_length=2, max_length=200) subdomain: str = Field(..., min_length=2, max_length=100) owner_email: EmailStr # NEW for Slice 1 description: Optional[str] = None business_email: Optional[EmailStr] = None business_phone: Optional[str] = None website: Optional[str] = None @validator('vendor_code') def vendor_code_uppercase(cls, v): return v.upper() @validator('subdomain') def subdomain_lowercase(cls, v): return v.lower().strip() class VendorCreateResponse(BaseModel): """Response after creating vendor - includes generated credentials""" id: int vendor_code: str subdomain: str name: str owner_user_id: int owner_email: str owner_username: str temporary_password: str # Shown only once! is_active: bool is_verified: bool created_at: datetime ``` ### Service Layer (✅ Complete) #### Admin Service (`app/services/admin_service.py`) **Key Method**: `create_vendor_with_owner()` ```python async def create_vendor_with_owner( self, vendor_data: VendorCreate, db: Session ) -> Dict[str, Any]: """ Creates vendor + owner user + default roles Returns vendor details with temporary password """ # 1. Generate owner username owner_username = f"{vendor_data.subdomain}_owner" # 2. Generate secure temporary password temp_password = self._generate_temp_password() # 3. Create owner user owner_user = User( email=vendor_data.owner_email, username=owner_username, hashed_password=self.auth_manager.hash_password(temp_password), role="user", is_active=True ) db.add(owner_user) db.flush() # Get owner_user.id # 4. Create vendor vendor = Vendor( vendor_code=vendor_data.vendor_code, name=vendor_data.name, subdomain=vendor_data.subdomain, owner_user_id=owner_user.id, is_verified=True, # Auto-verify admin-created vendors verified_at=datetime.utcnow(), # ... other fields ) db.add(vendor) db.flush() # Get vendor.id # 5. Create default roles default_roles = ["Owner", "Manager", "Editor", "Viewer"] for role_name in default_roles: role = Role( vendor_id=vendor.id, name=role_name, permissions=self._get_default_permissions(role_name) ) db.add(role) # 6. Link owner to Owner role owner_role = db.query(Role).filter( Role.vendor_id == vendor.id, Role.name == "Owner" ).first() vendor_user = VendorUser( vendor_id=vendor.id, user_id=owner_user.id, role_id=owner_role.id, is_active=True ) db.add(vendor_user) db.commit() return { "vendor": vendor, "owner_username": owner_username, "temporary_password": temp_password } ``` ### API Endpoints (✅ Complete) #### Admin Endpoints (`app/api/v1/admin.py`) ```python @router.post("/vendors", response_model=VendorCreateResponse) async def create_vendor( vendor_data: VendorCreate, current_user: User = Depends(get_current_admin_user), db: Session = Depends(get_db) ): """Create new vendor with owner account""" result = await admin_service.create_vendor_with_owner(vendor_data, db) return VendorCreateResponse( id=result["vendor"].id, vendor_code=result["vendor"].vendor_code, subdomain=result["vendor"].subdomain, name=result["vendor"].name, owner_user_id=result["vendor"].owner_user_id, owner_email=vendor_data.owner_email, owner_username=result["owner_username"], temporary_password=result["temporary_password"], is_active=result["vendor"].is_active, is_verified=result["vendor"].is_verified, created_at=result["vendor"].created_at ) @router.get("/vendors", response_model=VendorListResponse) async def list_vendors( skip: int = 0, limit: int = 100, is_active: Optional[bool] = None, is_verified: Optional[bool] = None, current_user: User = Depends(get_current_admin_user), db: Session = Depends(get_db) ): """List all vendors with filtering""" return await admin_service.get_vendors(db, skip, limit, is_active, is_verified) @router.get("/dashboard", response_model=AdminDashboardResponse) async def get_admin_dashboard( current_user: User = Depends(get_current_admin_user), db: Session = Depends(get_db) ): """Get admin dashboard statistics""" return await admin_service.get_dashboard_stats(db) @router.put("/vendors/{vendor_id}/verify") async def verify_vendor( vendor_id: int, current_user: User = Depends(get_current_admin_user), db: Session = Depends(get_db) ): """Verify/unverify vendor""" return await admin_service.toggle_vendor_verification(vendor_id, db) @router.put("/vendors/{vendor_id}/status") async def toggle_vendor_status( vendor_id: int, current_user: User = Depends(get_current_admin_user), db: Session = Depends(get_db) ): """Activate/deactivate vendor""" return await admin_service.toggle_vendor_status(vendor_id, db) ``` ### Middleware (✅ Complete) #### Vendor Context Detection (`middleware/vendor_context.py`) ```python async def vendor_context_middleware(request: Request, call_next): """ Detects vendor context from: 1. Subdomain: vendor.platform.com (production) 2. Path: /vendor/VENDOR_CODE/ (development) """ vendor_context = None # Skip for admin/API routes if request.url.path.startswith(("/api/", "/admin/", "/static/")): response = await call_next(request) return response # 1. Try subdomain detection (production) host = request.headers.get("host", "").split(":")[0] parts = host.split(".") if len(parts) > 2: subdomain = parts[0] vendor = get_vendor_by_subdomain(subdomain) if vendor: vendor_context = vendor # 2. Try path detection (development) if not vendor_context: path_parts = request.url.path.split("/") if len(path_parts) > 2 and path_parts[1] == "vendor": vendor_code = path_parts[2].upper() vendor = get_vendor_by_code(vendor_code) if vendor: vendor_context = vendor request.state.vendor = vendor_context response = await call_next(request) return response ``` ## 🎨 Frontend Implementation ### Template Structure (Jinja2) #### Base Template (`templates/base.html`) ```html
Sign in to continue
| Vendor Code | Name | Subdomain | Status | Created |
|---|---|---|---|---|
No vendors yet. Create your first vendor!
The vendor you're trying to access doesn't exist.
Vendor Code: {{ vendor.vendor_code }}
Product import from Letzshop marketplace will be available soon!