- System Context diagram (C4 Level 1) - Container diagram (C4 Level 2) - Flask component diagram (C4 Level 3) - Deployment architecture with NPM proxy - Database schema (PostgreSQL) - External integrations (Gemini AI, Brave Search, PageSpeed) - Network topology (INPI infrastructure) - Security architecture - API endpoints reference - Troubleshooting guide - Data flow diagrams (auth, search, AI chat, SEO audit, news monitoring) All diagrams use Mermaid.js and render automatically on GitHub. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
875 lines
26 KiB
Markdown
875 lines
26 KiB
Markdown
# Authentication Flow
|
|
|
|
**Document Version:** 1.0
|
|
**Last Updated:** 2026-01-10
|
|
**Status:** Production LIVE
|
|
**Flow Type:** User Authentication & Session Management
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
This document describes the **complete authentication flow** for the Norda Biznes Hub application, covering:
|
|
|
|
- **User Registration** with email verification
|
|
- **Login** with session management
|
|
- **Email Verification** process
|
|
- **Password Reset** flow
|
|
- **Session Management** and cookies
|
|
- **Authorization Levels** and access control
|
|
|
|
**Key Technology:**
|
|
- **Authentication Framework:** Flask-Login
|
|
- **Password Hashing:** PBKDF2:SHA256 (werkzeug.security)
|
|
- **Session Storage:** Server-side session cookies
|
|
- **Email Delivery:** Microsoft Graph API (OAuth 2.0)
|
|
- **CSRF Protection:** Flask-WTF
|
|
|
|
**Security Features:**
|
|
- Email verification required before login
|
|
- Secure session cookies (HttpOnly, Secure, SameSite=Lax)
|
|
- CSRF protection on all forms
|
|
- Password strength requirements
|
|
- Input sanitization against XSS
|
|
- Rate limiting on authentication endpoints
|
|
|
|
---
|
|
|
|
## 1. User Registration Flow
|
|
|
|
### 1.1 Registration Sequence Diagram
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
actor User
|
|
participant Browser
|
|
participant Flask as Flask App<br/>(app.py)
|
|
participant DB as PostgreSQL<br/>(users table)
|
|
participant EmailSvc as Email Service<br/>(email_service.py)
|
|
participant MSGraph as Microsoft Graph API
|
|
|
|
User->>Browser: Navigate to /register
|
|
Browser->>Flask: GET /register
|
|
Flask->>Browser: Render register.html
|
|
|
|
User->>Browser: Fill form (email, password, name, company_nip)
|
|
Browser->>Flask: POST /register
|
|
|
|
Note over Flask: CSRF token validated (automatic)
|
|
Note over Flask: Input sanitization
|
|
|
|
Flask->>Flask: validate_email(email)
|
|
Flask->>Flask: validate_password(password)<br/>(8+ chars, uppercase, lowercase, digit)
|
|
Flask->>Flask: validate NIP format (10 digits)
|
|
|
|
Flask->>DB: SELECT * FROM users WHERE email = ?
|
|
DB->>Flask: Check if exists
|
|
|
|
alt Email already exists
|
|
Flask->>Browser: Flash "Email już jest zarejestrowany"
|
|
Browser->>User: Show error message
|
|
else Email available
|
|
Flask->>DB: SELECT * FROM companies WHERE nip = ? AND status = 'active'
|
|
DB->>Flask: Company data (if NORDA member)
|
|
|
|
Flask->>Flask: generate_password_hash(password)<br/>(PBKDF2:SHA256)
|
|
Flask->>Flask: secrets.token_urlsafe(32)<br/>(verification token)
|
|
Flask->>Flask: Calculate token expiry<br/>(now + 24 hours)
|
|
|
|
Flask->>DB: INSERT INTO users<br/>(email, password_hash, name,<br/>verification_token, is_verified=FALSE,<br/>company_id, is_norda_member)
|
|
DB->>Flask: User created (id)
|
|
|
|
Flask->>EmailSvc: send_verification_email(email, token)
|
|
EmailSvc->>MSGraph: POST /users/noreply@nordabiznes.pl/sendMail<br/>(OAuth 2.0 + Bearer token)
|
|
MSGraph->>EmailSvc: 202 Accepted
|
|
|
|
Flask->>Browser: Flash "Sprawdź email"<br/>Redirect to /login
|
|
Browser->>User: Show success message
|
|
end
|
|
```
|
|
|
|
### 1.2 Registration Implementation Details
|
|
|
|
**Route:** `POST /register`
|
|
**File:** `app.py` (lines ~3077-3183)
|
|
**Rate Limit:** 5 requests per hour per IP
|
|
|
|
**Input Fields:**
|
|
- `email` (required, max 255 chars)
|
|
- `password` (required, 8+ chars with complexity requirements)
|
|
- `name` (required, max 255 chars)
|
|
- `company_nip` (required, 10 digits)
|
|
|
|
**Validation Steps:**
|
|
1. **Email validation:** Regex pattern check
|
|
2. **Password validation:**
|
|
- Minimum 8 characters
|
|
- At least 1 uppercase letter
|
|
- At least 1 lowercase letter
|
|
- At least 1 digit
|
|
3. **NIP validation:** Must be exactly 10 digits
|
|
4. **Company membership check:** NIP lookup in `companies` table
|
|
|
|
**Database Operations:**
|
|
```sql
|
|
-- Check email uniqueness
|
|
SELECT * FROM users WHERE email = ?;
|
|
|
|
-- Verify company membership
|
|
SELECT * FROM companies WHERE nip = ? AND status = 'active';
|
|
|
|
-- Create user account
|
|
INSERT INTO users (
|
|
email, password_hash, name, company_nip,
|
|
verification_token, verification_token_expires,
|
|
is_verified, is_norda_member, company_id,
|
|
created_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, FALSE, ?, ?, NOW());
|
|
```
|
|
|
|
**Security Measures:**
|
|
- **CSRF Protection:** Automatic via Flask-WTF
|
|
- **Input Sanitization:** `sanitize_input()` strips HTML/malicious patterns
|
|
- **Password Hashing:** PBKDF2:SHA256 (werkzeug default)
|
|
- **Rate Limiting:** 5 attempts/hour via Flask-Limiter
|
|
- **XSS Prevention:** All user inputs sanitized
|
|
|
|
**Email Verification Token:**
|
|
- Generated via `secrets.token_urlsafe(32)` (256-bit entropy)
|
|
- Stored in `users.verification_token` column
|
|
- Expires after 24 hours
|
|
- Single-use (cleared after verification)
|
|
|
|
---
|
|
|
|
## 2. Email Verification Flow
|
|
|
|
### 2.1 Email Verification Sequence Diagram
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
actor User
|
|
participant Email as Email Client
|
|
participant Browser
|
|
participant Flask as Flask App<br/>(app.py)
|
|
participant DB as PostgreSQL
|
|
|
|
Note over User,Email: User receives verification email
|
|
|
|
User->>Email: Open verification email
|
|
Email->>Browser: Click link:<br/>https://nordabiznes.pl/verify-email/<token>
|
|
|
|
Browser->>Flask: GET /verify-email/<token>
|
|
|
|
Flask->>Flask: Extract token from URL
|
|
Flask->>DB: SELECT * FROM users<br/>WHERE verification_token = ?<br/>AND verification_token_expires > NOW()<br/>AND is_active = TRUE
|
|
|
|
alt Token valid and not expired
|
|
DB->>Flask: User found
|
|
|
|
alt User already verified
|
|
Flask->>Browser: Flash "Email został już zweryfikowany"<br/>Redirect to /login
|
|
else User not verified yet
|
|
Flask->>DB: UPDATE users SET<br/>is_verified = TRUE,<br/>verified_at = NOW(),<br/>verification_token = NULL,<br/>verification_token_expires = NULL<br/>WHERE id = ?
|
|
|
|
DB->>Flask: Update successful
|
|
|
|
Flask->>Browser: Flash "Email zweryfikowany!<br/>Możesz się teraz zalogować"<br/>Redirect to /login
|
|
Browser->>User: Show success message
|
|
end
|
|
|
|
else Token invalid or expired
|
|
Flask->>Browser: Flash "Link weryfikacyjny<br/>jest nieprawidłowy lub wygasł"<br/>Redirect to /login
|
|
Browser->>User: Show error message
|
|
end
|
|
```
|
|
|
|
### 2.2 Email Verification Implementation Details
|
|
|
|
**Route:** `GET /verify-email/<token>`
|
|
**File:** `app.py` (lines ~3369-3405)
|
|
**Rate Limit:** None (public endpoint)
|
|
|
|
**Verification Logic:**
|
|
```python
|
|
# Query user by token
|
|
user = db.query(User).filter(
|
|
User.verification_token == token,
|
|
User.verification_token_expires > datetime.now(),
|
|
User.is_active == True
|
|
).first()
|
|
|
|
if user and not user.is_verified:
|
|
user.is_verified = True
|
|
user.verified_at = datetime.now()
|
|
user.verification_token = None
|
|
user.verification_token_expires = None
|
|
db.commit()
|
|
```
|
|
|
|
**Token Expiry:**
|
|
- Verification tokens expire after **24 hours**
|
|
- Expired tokens cannot be used
|
|
- Users can request new verification email via `/resend-verification`
|
|
|
|
**Database Schema (users table):**
|
|
```sql
|
|
verification_token VARCHAR(255) NULL
|
|
verification_token_expires TIMESTAMP NULL
|
|
is_verified BOOLEAN DEFAULT FALSE
|
|
verified_at TIMESTAMP NULL
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Login Flow
|
|
|
|
### 3.1 Login Sequence Diagram
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
actor User
|
|
participant Browser
|
|
participant Flask as Flask App<br/>(app.py)
|
|
participant FlaskLogin as Flask-Login
|
|
participant DB as PostgreSQL
|
|
participant Session as Session Cookie
|
|
|
|
User->>Browser: Navigate to /login
|
|
Browser->>Flask: GET /login
|
|
Flask->>Browser: Render login.html with CSRF token
|
|
|
|
User->>Browser: Enter email & password<br/>(optional: remember me)
|
|
Browser->>Flask: POST /login<br/>(email, password, remember, csrf_token)
|
|
|
|
Note over Flask: CSRF token validated
|
|
|
|
Flask->>DB: SELECT * FROM users WHERE email = ?
|
|
|
|
alt User not found
|
|
DB->>Flask: No user
|
|
Flask->>Browser: Flash "Nieprawidłowy email lub hasło"
|
|
Browser->>User: Show error
|
|
else User found
|
|
DB->>Flask: User data
|
|
|
|
Flask->>Flask: check_password_hash(<br/>user.password_hash,<br/>password<br/>)
|
|
|
|
alt Password invalid
|
|
Flask->>Browser: Flash "Nieprawidłowy email lub hasło"
|
|
Browser->>User: Show error
|
|
else Password valid
|
|
alt User not active
|
|
Flask->>Browser: Flash "Konto zostało dezaktywowane"
|
|
Browser->>User: Show error
|
|
else User not verified
|
|
Flask->>Browser: Flash "Musisz potwierdzić adres email"
|
|
Browser->>User: Show error with resend link
|
|
else User active and verified
|
|
Flask->>FlaskLogin: login_user(user, remember=remember)
|
|
FlaskLogin->>Session: Set session cookie<br/>(secure, httponly, samesite=Lax)
|
|
Session->>Browser: Store session cookie
|
|
|
|
Flask->>DB: UPDATE users SET last_login = NOW() WHERE id = ?
|
|
DB->>Flask: Update successful
|
|
|
|
Flask->>Browser: Redirect to /dashboard<br/>(or 'next' URL if specified)
|
|
Browser->>User: Show dashboard
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
### 3.2 Login Implementation Details
|
|
|
|
**Route:** `POST /login`
|
|
**File:** `app.py` (lines ~3184-3240)
|
|
**Rate Limit:**
|
|
- **Development:** 1000 requests per hour
|
|
- **Production:** 5 requests per hour per IP
|
|
|
|
**Login Validation Steps:**
|
|
1. Check email exists in database
|
|
2. Verify password hash matches
|
|
3. Check `is_active = TRUE`
|
|
4. Require `is_verified = TRUE`
|
|
5. Create session via Flask-Login
|
|
6. Update `last_login` timestamp
|
|
|
|
**Session Configuration:**
|
|
```python
|
|
# app.py session settings (lines ~161-168)
|
|
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')
|
|
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
|
|
app.config['SESSION_COOKIE_SECURE'] = True # HTTPS only in production
|
|
app.config['SESSION_COOKIE_HTTPONLY'] = True # No JS access
|
|
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # CSRF protection
|
|
```
|
|
|
|
**Flask-Login Configuration:**
|
|
```python
|
|
# app.py Flask-Login setup (lines ~192-196)
|
|
login_manager = LoginManager()
|
|
login_manager.init_app(app)
|
|
login_manager.login_view = 'login'
|
|
login_manager.login_message = 'Zaloguj się, aby uzyskać dostęp do tej strony.'
|
|
```
|
|
|
|
**User Loader Function:**
|
|
```python
|
|
@login_manager.user_loader
|
|
def load_user(user_id):
|
|
"""Load user from database for Flask-Login"""
|
|
db = SessionLocal()
|
|
try:
|
|
return db.query(User).get(int(user_id))
|
|
except:
|
|
return None
|
|
finally:
|
|
db.close()
|
|
```
|
|
|
|
**Remember Me Functionality:**
|
|
- When enabled: Session cookie persists for **7 days**
|
|
- When disabled: Session cookie expires when browser closes
|
|
- Implemented via Flask-Login's `login_user(user, remember=True/False)`
|
|
|
|
**Next URL Redirect:**
|
|
```python
|
|
# Prevent open redirect vulnerability
|
|
next_page = request.args.get('next')
|
|
if next_page and not next_page.startswith('/'):
|
|
next_page = None
|
|
return redirect(next_page or url_for('dashboard'))
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Session Management
|
|
|
|
### 4.1 Session Lifecycle
|
|
|
|
```mermaid
|
|
stateDiagram-v2
|
|
[*] --> Anonymous: User visits site
|
|
Anonymous --> Authenticated: Successful login
|
|
Authenticated --> Authenticated: Normal activity
|
|
Authenticated --> Anonymous: Logout
|
|
Authenticated --> Anonymous: Session expires (7 days)
|
|
Authenticated --> Anonymous: User deactivated
|
|
Anonymous --> [*]
|
|
```
|
|
|
|
### 4.2 Session Cookie Details
|
|
|
|
**Cookie Name:** `session` (Flask default)
|
|
**Storage:** Server-side (encrypted session data)
|
|
**Attributes:**
|
|
- `Secure` = True (HTTPS only in production)
|
|
- `HttpOnly` = True (prevents XSS cookie theft)
|
|
- `SameSite` = Lax (CSRF protection)
|
|
- `Max-Age` = 7 days (with remember me)
|
|
|
|
**Session Data Stored:**
|
|
- User ID (for user loader)
|
|
- CSRF token (automatic via Flask-WTF)
|
|
- Login timestamp
|
|
- Remember me flag
|
|
|
|
**Session Security:**
|
|
- Session cookie is **signed** with `SECRET_KEY`
|
|
- Session data is **encrypted** (Flask built-in)
|
|
- Session is **regenerated** on login (prevents session fixation)
|
|
- Session is **cleared** on logout
|
|
|
|
---
|
|
|
|
## 5. Logout Flow
|
|
|
|
### 5.1 Logout Sequence Diagram
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
actor User
|
|
participant Browser
|
|
participant Flask as Flask App
|
|
participant FlaskLogin as Flask-Login
|
|
participant Session as Session Cookie
|
|
|
|
User->>Browser: Click "Wyloguj" button
|
|
Browser->>Flask: GET /logout
|
|
|
|
Note over Flask: @login_required decorator<br/>verifies user is authenticated
|
|
|
|
Flask->>FlaskLogin: logout_user()
|
|
FlaskLogin->>Session: Clear session data
|
|
Session->>Browser: Delete session cookie
|
|
|
|
Flask->>Browser: Flash "Wylogowano pomyślnie"<br/>Redirect to /
|
|
Browser->>User: Show homepage (logged out)
|
|
```
|
|
|
|
### 5.2 Logout Implementation Details
|
|
|
|
**Route:** `GET /logout`
|
|
**File:** `app.py` (lines ~3242-3248)
|
|
**Authentication:** Required (`@login_required`)
|
|
|
|
**Implementation:**
|
|
```python
|
|
@app.route('/logout')
|
|
@login_required
|
|
def logout():
|
|
"""User logout"""
|
|
logout_user() # Flask-Login clears session
|
|
flash('Wylogowano pomyślnie.', 'success')
|
|
return redirect(url_for('index'))
|
|
```
|
|
|
|
**What Happens on Logout:**
|
|
1. Flask-Login calls `logout_user()`
|
|
2. Session cookie is deleted
|
|
3. User object is removed from `current_user`
|
|
4. Browser redirected to homepage
|
|
5. All subsequent requests are anonymous
|
|
|
|
---
|
|
|
|
## 6. Password Reset Flow
|
|
|
|
### 6.1 Password Reset Sequence Diagram
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
actor User
|
|
participant Browser
|
|
participant Flask as Flask App
|
|
participant DB as PostgreSQL
|
|
participant EmailSvc as Email Service
|
|
participant MSGraph as Microsoft Graph API
|
|
|
|
Note over User,Browser: Phase 1: Request Reset
|
|
|
|
User->>Browser: Navigate to /forgot-password
|
|
Browser->>Flask: GET /forgot-password
|
|
Flask->>Browser: Render forgot_password.html
|
|
|
|
User->>Browser: Enter email address
|
|
Browser->>Flask: POST /forgot-password (email)
|
|
|
|
Flask->>Flask: validate_email(email)
|
|
Flask->>DB: SELECT * FROM users<br/>WHERE email = ? AND is_active = TRUE
|
|
|
|
alt User found
|
|
DB->>Flask: User data
|
|
|
|
Flask->>Flask: secrets.token_urlsafe(32)<br/>(generate reset token)
|
|
Flask->>Flask: Calculate expiry (now + 1 hour)
|
|
|
|
Flask->>DB: UPDATE users SET<br/>reset_token = ?,<br/>reset_token_expires = ?<br/>WHERE email = ?
|
|
DB->>Flask: Update successful
|
|
|
|
Flask->>EmailSvc: send_password_reset_email(email, token)
|
|
EmailSvc->>MSGraph: POST /users/noreply@nordabiznes.pl/sendMail
|
|
MSGraph->>EmailSvc: 202 Accepted
|
|
|
|
Flask->>Browser: Flash "Sprawdź email"<br/>Redirect to /login
|
|
Browser->>User: Show message
|
|
|
|
else User not found
|
|
Note over Flask: Still show success message<br/>(prevent email enumeration)
|
|
Flask->>Browser: Flash "Sprawdź email"<br/>Redirect to /login
|
|
Browser->>User: Show message
|
|
end
|
|
|
|
Note over User,Browser: Phase 2: Reset Password
|
|
|
|
User->>Browser: Click link in email:<br/>https://nordabiznes.pl/reset-password/<token>
|
|
Browser->>Flask: GET /reset-password/<token>
|
|
|
|
Flask->>DB: SELECT * FROM users<br/>WHERE reset_token = ?<br/>AND reset_token_expires > NOW()
|
|
|
|
alt Token valid
|
|
DB->>Flask: User found
|
|
Flask->>Browser: Render reset_password.html<br/>(password form)
|
|
|
|
User->>Browser: Enter new password (twice)
|
|
Browser->>Flask: POST /reset-password/<token><br/>(new_password, confirm_password)
|
|
|
|
Flask->>Flask: validate_password(new_password)
|
|
Flask->>Flask: Check passwords match
|
|
Flask->>Flask: generate_password_hash(new_password)
|
|
|
|
Flask->>DB: UPDATE users SET<br/>password_hash = ?,<br/>reset_token = NULL,<br/>reset_token_expires = NULL<br/>WHERE reset_token = ?
|
|
DB->>Flask: Update successful
|
|
|
|
Flask->>Browser: Flash "Hasło zostało zmienione"<br/>Redirect to /login
|
|
Browser->>User: Show success message
|
|
|
|
else Token invalid or expired
|
|
Flask->>Browser: Flash "Link resetowania<br/>jest nieprawidłowy lub wygasł"<br/>Redirect to /forgot-password
|
|
Browser->>User: Show error
|
|
end
|
|
```
|
|
|
|
### 6.2 Password Reset Implementation Details
|
|
|
|
**Routes:**
|
|
- `POST /forgot-password` - Request reset
|
|
- `GET /reset-password/<token>` - Show reset form
|
|
- `POST /reset-password/<token>` - Process new password
|
|
|
|
**Files:** `app.py` (lines ~3251-3368)
|
|
**Rate Limit:** 5 requests per hour per IP
|
|
|
|
**Reset Token Properties:**
|
|
- Generated via `secrets.token_urlsafe(32)` (256-bit entropy)
|
|
- Expires after **1 hour**
|
|
- Single-use (cleared after successful reset)
|
|
- Stored in `users.reset_token` column
|
|
|
|
**Database Schema:**
|
|
```sql
|
|
reset_token VARCHAR(255) NULL
|
|
reset_token_expires TIMESTAMP NULL
|
|
```
|
|
|
|
**Security Considerations:**
|
|
- Reset tokens expire after 1 hour
|
|
- Tokens are cleared after use
|
|
- Password strength validation applied
|
|
- Email enumeration prevented (always show success message)
|
|
- Rate limiting prevents brute force
|
|
|
|
---
|
|
|
|
## 7. Authorization & Access Control
|
|
|
|
### 7.1 Authorization Levels
|
|
|
|
```mermaid
|
|
graph TB
|
|
subgraph "Authorization Hierarchy"
|
|
Public[👥 Public<br/>Anonymous Users]
|
|
Auth[🔐 Authenticated<br/>Logged-in Users]
|
|
Member[👔 NORDA Members<br/>is_norda_member=TRUE]
|
|
Admin[👨💼 Administrators<br/>is_admin=TRUE]
|
|
|
|
Public --> Auth
|
|
Auth --> Member
|
|
Member --> Admin
|
|
end
|
|
|
|
subgraph "Access Permissions"
|
|
PublicRoutes["Public Routes:<br/>/, /search, /company/*,<br/>/audit/*, /api/companies"]
|
|
AuthRoutes["Authenticated Routes:<br/>/dashboard, /chat,<br/>/forum/*, /wiadomosci/*,<br/>/kalendarz/*, /tablica/*"]
|
|
AdminRoutes["Admin Routes:<br/>/admin/*, /api/*/audit"]
|
|
end
|
|
|
|
Public --> PublicRoutes
|
|
Auth --> AuthRoutes
|
|
Admin --> AdminRoutes
|
|
```
|
|
|
|
### 7.2 Route Protection Decorators
|
|
|
|
**Public Access (No decorator):**
|
|
```python
|
|
@app.route('/')
|
|
def index():
|
|
"""Public company directory"""
|
|
# No authentication required
|
|
return render_template('index.html')
|
|
```
|
|
|
|
**Authenticated Users Only:**
|
|
```python
|
|
@app.route('/dashboard')
|
|
@login_required
|
|
def dashboard():
|
|
"""User dashboard - requires login"""
|
|
# current_user is automatically available
|
|
return render_template('dashboard.html', user=current_user)
|
|
```
|
|
|
|
**Admin Only (Custom check):**
|
|
```python
|
|
@app.route('/admin/users')
|
|
@login_required
|
|
def admin_users():
|
|
"""Admin user management"""
|
|
if not current_user.is_admin:
|
|
flash('Brak uprawnień administratora.', 'error')
|
|
return redirect(url_for('index'))
|
|
|
|
# Admin logic here
|
|
return render_template('admin/users.html')
|
|
```
|
|
|
|
### 7.3 Access Control Matrix
|
|
|
|
| Route Category | Public | Authenticated | NORDA Member | Admin |
|
|
|---------------|--------|---------------|--------------|-------|
|
|
| `/` (Company directory) | ✅ | ✅ | ✅ | ✅ |
|
|
| `/search` | ✅ | ✅ | ✅ | ✅ |
|
|
| `/company/<slug>` | ✅ | ✅ | ✅ | ✅ |
|
|
| `/audit/*/<slug>` | ✅ | ✅ | ✅ | ✅ |
|
|
| `/api/companies` | ✅ | ✅ | ✅ | ✅ |
|
|
| `/register`, `/login` | ✅ | ❌ | ❌ | ❌ |
|
|
| `/dashboard` | ❌ | ✅ | ✅ | ✅ |
|
|
| `/chat` | ❌ | ✅ | ✅ | ✅ |
|
|
| `/forum/*` | ❌ | ✅ | ✅ | ✅ |
|
|
| `/wiadomosci/*` | ❌ | ✅ | ✅ | ✅ |
|
|
| `/kalendarz/*` | ❌ | ✅ | ✅ | ✅ |
|
|
| `/tablica/*` | ❌ | ✅ | ✅ | ✅ |
|
|
| `/admin/*` | ❌ | ❌ | ❌ | ✅ |
|
|
| `/api/*/audit` | ❌ | ❌ | ❌ | ✅ |
|
|
|
|
**Legend:**
|
|
- ✅ Access granted
|
|
- ❌ Access denied (redirect to login or show error)
|
|
|
|
---
|
|
|
|
## 8. User Model Database Schema
|
|
|
|
### 8.1 Users Table Structure
|
|
|
|
```sql
|
|
CREATE TABLE users (
|
|
-- Primary Key
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
-- Authentication
|
|
email VARCHAR(255) UNIQUE NOT NULL,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
|
|
-- Profile
|
|
name VARCHAR(255),
|
|
phone VARCHAR(50),
|
|
|
|
-- Company Association
|
|
company_nip VARCHAR(10),
|
|
company_id INTEGER REFERENCES companies(id),
|
|
|
|
-- Status Flags
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
is_verified BOOLEAN DEFAULT FALSE,
|
|
is_admin BOOLEAN DEFAULT FALSE,
|
|
is_norda_member BOOLEAN DEFAULT FALSE,
|
|
|
|
-- Timestamps
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
last_login TIMESTAMP,
|
|
verified_at TIMESTAMP,
|
|
|
|
-- Email Verification
|
|
verification_token VARCHAR(255),
|
|
verification_token_expires TIMESTAMP,
|
|
|
|
-- Password Reset
|
|
reset_token VARCHAR(255),
|
|
reset_token_expires TIMESTAMP,
|
|
|
|
-- Indexes
|
|
INDEX idx_users_email (email),
|
|
INDEX idx_users_company_id (company_id)
|
|
);
|
|
```
|
|
|
|
### 8.2 User Model (SQLAlchemy)
|
|
|
|
**File:** `database.py` (lines ~119-158)
|
|
|
|
```python
|
|
class User(Base, UserMixin):
|
|
"""User accounts with Flask-Login integration"""
|
|
__tablename__ = 'users'
|
|
|
|
# Authentication fields
|
|
id = Column(Integer, primary_key=True)
|
|
email = Column(String(255), unique=True, nullable=False, index=True)
|
|
password_hash = Column(String(255), nullable=False)
|
|
|
|
# Profile
|
|
name = Column(String(255))
|
|
company_nip = Column(String(10))
|
|
company_id = Column(Integer, ForeignKey('companies.id'), nullable=True)
|
|
phone = Column(String(50))
|
|
|
|
# Status
|
|
is_active = Column(Boolean, default=True)
|
|
is_verified = Column(Boolean, default=False)
|
|
is_admin = Column(Boolean, default=False)
|
|
is_norda_member = Column(Boolean, default=False)
|
|
|
|
# Timestamps
|
|
created_at = Column(DateTime, default=datetime.now)
|
|
last_login = Column(DateTime)
|
|
verified_at = Column(DateTime)
|
|
|
|
# Verification token
|
|
verification_token = Column(String(255))
|
|
verification_token_expires = Column(DateTime)
|
|
|
|
# Password reset token
|
|
reset_token = Column(String(255))
|
|
reset_token_expires = Column(DateTime)
|
|
|
|
# Relationships
|
|
company = relationship('Company', backref='users', lazy='joined')
|
|
conversations = relationship('AIChatConversation', back_populates='user')
|
|
forum_topics = relationship('ForumTopic', back_populates='author')
|
|
forum_replies = relationship('ForumReply', back_populates='author')
|
|
```
|
|
|
|
**UserMixin Methods (Flask-Login):**
|
|
- `is_authenticated` - Always True for logged-in users
|
|
- `is_active` - Returns `self.is_active`
|
|
- `is_anonymous` - Always False for logged-in users
|
|
- `get_id()` - Returns `str(self.id)` for session storage
|
|
|
|
---
|
|
|
|
## 9. Security Features Summary
|
|
|
|
### 9.1 Security Measures Implemented
|
|
|
|
| Feature | Implementation | Protection Against |
|
|
|---------|----------------|---------------------|
|
|
| **CSRF Protection** | Flask-WTF automatic tokens | Cross-Site Request Forgery |
|
|
| **Password Hashing** | PBKDF2:SHA256 (werkzeug) | Rainbow table attacks |
|
|
| **Secure Cookies** | HttpOnly, Secure, SameSite=Lax | XSS cookie theft, CSRF |
|
|
| **Email Verification** | Required before login | Fake accounts |
|
|
| **Input Sanitization** | `sanitize_input()` function | XSS attacks |
|
|
| **Rate Limiting** | Flask-Limiter (5 req/hour) | Brute force attacks |
|
|
| **Session Regeneration** | Flask-Login automatic | Session fixation |
|
|
| **Token Expiry** | 24h (verify), 1h (reset) | Token replay attacks |
|
|
| **Open Redirect Prevention** | Next URL validation | Phishing attacks |
|
|
| **Password Strength** | 8+ chars, complexity rules | Weak passwords |
|
|
|
|
### 9.2 Security Best Practices
|
|
|
|
**Implemented:**
|
|
✅ HTTPS enforced in production
|
|
✅ Password hashing with secure algorithm
|
|
✅ CSRF protection on all forms
|
|
✅ Session cookies with security flags
|
|
✅ Email verification required
|
|
✅ Rate limiting on auth endpoints
|
|
✅ Input sanitization
|
|
✅ Token expiry
|
|
✅ Open redirect prevention
|
|
|
|
**Potential Improvements:**
|
|
⚠️ Add account lockout after N failed attempts
|
|
⚠️ Implement 2FA (TOTP) for admins
|
|
⚠️ Add password history (prevent reuse)
|
|
⚠️ Log authentication events for auditing
|
|
⚠️ Add CAPTCHA on registration
|
|
⚠️ Implement session timeout (idle)
|
|
⚠️ Add IP-based rate limiting
|
|
|
|
---
|
|
|
|
## 10. Testing Accounts (Production)
|
|
|
|
### 10.1 Test Users
|
|
|
|
**IMPORTANT:** Use only these accounts for testing production features.
|
|
|
|
| Account | Email | Role | Purpose |
|
|
|---------|-------|------|---------|
|
|
| Test User | `test@nordabiznes.pl` | Regular User | Test user-level features |
|
|
| Test Admin | `testadmin@nordabiznes.pl` | Administrator | Test admin features |
|
|
|
|
**Test Account Credentials:**
|
|
- Password stored in CLAUDE.md (do not commit to repository)
|
|
- Accounts are pre-verified (`is_verified = TRUE`)
|
|
- Test Admin has `is_admin = TRUE` flag
|
|
|
|
**Usage:**
|
|
- Always use test accounts for production testing
|
|
- Never modify real user accounts for testing
|
|
- Test authentication flows, authorization, session management
|
|
|
|
---
|
|
|
|
## 11. Related Documentation
|
|
|
|
### 11.1 Architecture Documents
|
|
|
|
- [System Context Diagram](../01-system-context.md) - External actors and integrations
|
|
- [Container Diagram](../02-container-diagram.md) - Flask app and session storage
|
|
- [Flask Components](../04-flask-components.md) - Authentication routes and decorators
|
|
- [Database Schema](../05-database-schema.md) - Users table and relationships
|
|
|
|
### 11.2 Code Files
|
|
|
|
**Authentication Routes:**
|
|
- `app.py` lines ~3077-3500 (register, login, logout, verify, reset)
|
|
|
|
**User Model:**
|
|
- `database.py` lines ~119-158 (User model with Flask-Login)
|
|
|
|
**Email Service:**
|
|
- `email_service.py` (Microsoft Graph email sending)
|
|
|
|
**Security Utilities:**
|
|
- `app.py` - `sanitize_input()`, `validate_email()`, `validate_password()`
|
|
|
|
### 11.3 External Dependencies
|
|
|
|
- **Flask-Login:** User session management
|
|
- **Flask-WTF:** CSRF protection
|
|
- **Flask-Limiter:** Rate limiting
|
|
- **werkzeug.security:** Password hashing
|
|
- **secrets:** Cryptographic token generation
|
|
|
|
---
|
|
|
|
## 12. Maintenance & Updates
|
|
|
|
### 12.1 When to Update This Document
|
|
|
|
Update this document when:
|
|
- Authentication flow changes (new steps, validation)
|
|
- Session management changes (cookie settings, expiry)
|
|
- Security measures are added/modified
|
|
- New authorization levels are introduced
|
|
- User model schema changes
|
|
|
|
### 12.2 Verification Checklist
|
|
|
|
When updating authentication flow:
|
|
- [ ] Test registration with valid/invalid data
|
|
- [ ] Test email verification with valid/expired tokens
|
|
- [ ] Test login with correct/incorrect credentials
|
|
- [ ] Test session persistence (remember me)
|
|
- [ ] Test logout clears session
|
|
- [ ] Test password reset flow end-to-end
|
|
- [ ] Verify CSRF protection is active
|
|
- [ ] Verify rate limiting works
|
|
- [ ] Test authorization levels (public, user, admin)
|
|
- [ ] Verify security headers on cookies
|
|
|
|
### 12.3 Security Audit Checklist
|
|
|
|
Periodic security audit:
|
|
- [ ] Review password hashing algorithm (current: PBKDF2:SHA256)
|
|
- [ ] Check token expiry times (verify: 24h, reset: 1h)
|
|
- [ ] Verify session cookie security flags
|
|
- [ ] Review rate limiting thresholds
|
|
- [ ] Check for SQL injection vulnerabilities
|
|
- [ ] Test XSS prevention in inputs
|
|
- [ ] Verify CSRF protection coverage
|
|
- [ ] Review authentication logs for anomalies
|
|
- [ ] Test open redirect prevention
|
|
- [ ] Verify email verification enforcement
|
|
|
|
---
|
|
|
|
**Document End**
|
|
|
|
*This document is maintained as part of the Norda Biznes Hub architecture documentation. For questions or updates, contact the development team.*
|