Add AUTH.md documenting authentication implementation
Some checks failed
Deploy Apartment API / deploy (push) Failing after 1s
Some checks failed
Deploy Apartment API / deploy (push) Failing after 1s
- Document completed Google OAuth authentication system - Include implementation details, security measures, and troubleshooting - Track future enhancements (admin dashboard, Apple Sign-In, partial public access)
This commit is contained in:
717
AUTH.md
Normal file
717
AUTH.md
Normal file
@ -0,0 +1,717 @@
|
|||||||
|
# Google OAuth Authentication Implementation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document details the Google OAuth authentication implementation for the Apartment Dashboard application. **All pages and API endpoints require authentication** - unauthenticated users are redirected to a login page.
|
||||||
|
|
||||||
|
> **Status:** Core authentication is complete and deployed. Admin dashboard and future enhancements are documented but not yet implemented.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
0. [Project Context](#0-project-context)
|
||||||
|
1. [Implementation Status](#1-implementation-status)
|
||||||
|
2. [Authentication Scope](#2-authentication-scope)
|
||||||
|
3. [Technical Architecture](#3-technical-architecture)
|
||||||
|
4. [Implementation Details](#4-implementation-details)
|
||||||
|
5. [User Activity Logging](#5-user-activity-logging)
|
||||||
|
6. [Security Measures](#6-security-measures)
|
||||||
|
7. [Deployment Configuration](#7-deployment-configuration)
|
||||||
|
8. [Test Checklist](#8-test-checklist)
|
||||||
|
9. [Future: Admin Dashboard](#9-future-admin-dashboard)
|
||||||
|
10. [Future: Adding Apple Sign-In](#10-future-adding-apple-sign-in)
|
||||||
|
11. [Future: Partial Public Access](#11-future-partial-public-access)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Project Context
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
TowersPriceWebApp/
|
||||||
|
├── apartment-dashboard/ # Frontend (React/Vite)
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── App.jsx # Main app (~1300 lines)
|
||||||
|
│ │ ├── main.jsx # Entry point (wrapped with AuthProvider)
|
||||||
|
│ │ ├── index.css # Tailwind imports
|
||||||
|
│ │ ├── context/
|
||||||
|
│ │ │ └── AuthContext.jsx # Auth state management
|
||||||
|
│ │ ├── hooks/
|
||||||
|
│ │ │ ├── useAuth.js # Auth hook
|
||||||
|
│ │ │ └── useActivityTracker.js # Activity tracking hook
|
||||||
|
│ │ ├── pages/
|
||||||
|
│ │ │ └── Login.jsx # Login page with Google button
|
||||||
|
│ │ └── components/
|
||||||
|
│ │ └── ProtectedRoute.jsx # Route guard component
|
||||||
|
│ ├── vite.config.js
|
||||||
|
│ ├── package.json
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ └── nginx.conf
|
||||||
|
│
|
||||||
|
├── apartment-dashboard-api/ # Backend (Express.js)
|
||||||
|
│ ├── server.js # Main server (~1700 lines)
|
||||||
|
│ ├── config/
|
||||||
|
│ │ └── auth.js # JWT, cookie, OAuth config
|
||||||
|
│ ├── models/
|
||||||
|
│ │ └── user.js # User CRUD operations
|
||||||
|
│ ├── middleware/
|
||||||
|
│ │ ├── passport.js # Google OAuth strategy
|
||||||
|
│ │ └── auth.js # JWT verification middleware
|
||||||
|
│ ├── routes/
|
||||||
|
│ │ ├── auth.js # OAuth routes
|
||||||
|
│ │ └── activity.js # Activity logging routes
|
||||||
|
│ ├── services/
|
||||||
|
│ │ └── activityLogger.js # Activity logging service
|
||||||
|
│ ├── .env.example # Environment template
|
||||||
|
│ ├── .dockerignore # Prevents secrets in image
|
||||||
|
│ ├── package.json
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ └── docker-compose.yml
|
||||||
|
│
|
||||||
|
└── AUTH.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tech Stack
|
||||||
|
|
||||||
|
| Layer | Technology | Version |
|
||||||
|
|-------|------------|---------|
|
||||||
|
| Frontend Framework | React | 19.1.0 |
|
||||||
|
| Frontend Build | Vite | 7.0.4 |
|
||||||
|
| Frontend Routing | React Router DOM | 7.12.0 |
|
||||||
|
| Frontend Styling | Tailwind CSS | 3.4.17 |
|
||||||
|
| Frontend Charts | Recharts | 3.1.0 |
|
||||||
|
| Backend Framework | Express.js | 4.18.2 |
|
||||||
|
| Database | MongoDB | 6.3.0 (driver) |
|
||||||
|
| Auth Library | Passport.js | passport-google-oauth20 |
|
||||||
|
| Token Management | jsonwebtoken | HTTP-only cookies |
|
||||||
|
| Reverse Proxy | Traefik | (with Let's Encrypt SSL) |
|
||||||
|
| Static Server | Nginx | (serves built React app) |
|
||||||
|
|
||||||
|
### URLs and Domains
|
||||||
|
|
||||||
|
| Environment | Frontend URL | API URL |
|
||||||
|
|-------------|--------------|---------|
|
||||||
|
| Production | `https://apartments.maverickapplications.com` | `https://apartments.maverickapplications.com/api` |
|
||||||
|
| Development | `http://localhost:5173` (Vite) | `http://localhost:3000` |
|
||||||
|
|
||||||
|
### MongoDB Details
|
||||||
|
|
||||||
|
| Setting | Value |
|
||||||
|
|---------|-------|
|
||||||
|
| Database name | `apartments` |
|
||||||
|
| Data collections | `units_migration_test`, `unit_prices_migration_test`, `daily_summaries` |
|
||||||
|
| Auth collections | `users`, `user_activity` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Implementation Status
|
||||||
|
|
||||||
|
### Completed
|
||||||
|
|
||||||
|
| Feature | Status | Notes |
|
||||||
|
|---------|--------|-------|
|
||||||
|
| Google OAuth login | Done | Passport.js with state parameter CSRF protection |
|
||||||
|
| JWT in HTTP-only cookies | Done | 7-day expiry with sliding window refresh |
|
||||||
|
| User model with upsert | Done | findOneAndUpdate with $setOnInsert pattern |
|
||||||
|
| Protected API endpoints | Done | All data endpoints require valid JWT |
|
||||||
|
| Activity logging | Done | Configurable levels, TTL indexes for cleanup |
|
||||||
|
| Frontend auth context | Done | React Context with useAuth hook |
|
||||||
|
| Protected routes | Done | ProtectedRoute component with redirect |
|
||||||
|
| Login page | Done | Google Sign-In button with error display |
|
||||||
|
| Security hardening | Done | OAuth state, input validation, .dockerignore |
|
||||||
|
| Docker deployment | Done | env_file configuration, production settings |
|
||||||
|
|
||||||
|
### Not Yet Implemented
|
||||||
|
|
||||||
|
| Feature | Priority | Notes |
|
||||||
|
|---------|----------|-------|
|
||||||
|
| Admin dashboard | Low | User management, activity viewer |
|
||||||
|
| Apple Sign-In | Future | Requires Apple Developer account |
|
||||||
|
| Partial public access | Future | Blurred preview for unauthenticated users |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Authentication Scope
|
||||||
|
|
||||||
|
### What Requires Authentication
|
||||||
|
|
||||||
|
**All pages and API endpoints require authentication.** Unauthenticated users see only the login page.
|
||||||
|
|
||||||
|
| Page/Feature | Unauthenticated | Authenticated |
|
||||||
|
|--------------|-----------------|---------------|
|
||||||
|
| Login Page (`/login`) | Accessible | Redirects to `/` |
|
||||||
|
| Overview Page (`/`) | Redirect to login | Fully accessible |
|
||||||
|
| Units Page (`/units`) | Redirect to login | Fully accessible |
|
||||||
|
| Analytics Page (`/analytics`) | Redirect to login | Fully accessible |
|
||||||
|
| All API Endpoints | 401 Unauthorized | Accessible |
|
||||||
|
|
||||||
|
### Protected API Endpoints
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/health → Public (health check only)
|
||||||
|
GET /api/daily-summary → Protected
|
||||||
|
GET /api/price-history → Protected
|
||||||
|
GET /api/available-units → Protected
|
||||||
|
GET /api/recent-activity → Protected
|
||||||
|
GET /api/plan-stats → Protected
|
||||||
|
GET /api/unit/:code/history → Protected
|
||||||
|
GET /api/analytics → Protected
|
||||||
|
GET /api/best-deals → Protected
|
||||||
|
GET /api/price-drops → Protected
|
||||||
|
GET /api/stale-inventory → Protected
|
||||||
|
GET /api/market-insights → Protected
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth Routes
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /auth/google → Initiates OAuth flow (sets state cookie)
|
||||||
|
GET /auth/google/callback → Handles callback (validates state, sets JWT)
|
||||||
|
GET /auth/me → Returns current user info
|
||||||
|
GET /auth/status → Returns { authenticated: true/false }
|
||||||
|
POST /auth/logout → Clears auth cookie
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Technical Architecture
|
||||||
|
|
||||||
|
### Authentication Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Google OAuth Flow │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 1. User clicks "Sign in with Google" │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 2. Frontend redirects to: /api/auth/google │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 3. Backend generates state parameter, stores in cookie │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 4. Backend redirects to Google's OAuth consent screen │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 5. User authenticates with Google │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 6. Google redirects to: /api/auth/google/callback?code=XXX&state=YYY │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 7. Backend validates state parameter against cookie (CSRF protection) │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 8. Backend exchanges code for tokens, fetches user profile │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 9. Backend creates/updates user in MongoDB (upsert) │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 10. Backend creates JWT, sets HTTP-only cookie │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 11. Backend redirects to frontend │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 12. Frontend AuthContext checks /auth/status, renders authenticated UI │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Token Refresh Strategy (Sliding Window)
|
||||||
|
|
||||||
|
Active users get their tokens refreshed automatically to avoid abrupt logouts.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Sliding Window Refresh │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 1. User makes authenticated request │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 2. Auth middleware validates JWT │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 3. Check: Is user still active? (isActive field) │
|
||||||
|
│ │ │
|
||||||
|
│ ┌────┴────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ No ▼ Yes ▼ │
|
||||||
|
│ Clear cookie 4. Check token age: (now - iat) > 1 day? │
|
||||||
|
│ Return 401 │ │
|
||||||
|
│ ┌────┴────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ No ▼ Yes ▼ │
|
||||||
|
│ Continue 5. Issue new JWT with fresh 7-day expiry │
|
||||||
|
│ normally │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 6. Set new cookie in response │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
| Scenario | Token Age | Action |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| User active daily | < 1 day since last refresh | No refresh |
|
||||||
|
| User returns after 2 days | 2 days old | Refresh token |
|
||||||
|
| User returns after 8 days | Expired | 401, redirect to login |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Implementation Details
|
||||||
|
|
||||||
|
### Auth Configuration (`config/auth.js`)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
module.exports = {
|
||||||
|
google: {
|
||||||
|
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||||
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||||
|
callbackURL: process.env.GOOGLE_CALLBACK_URL,
|
||||||
|
scope: ['profile', 'email']
|
||||||
|
},
|
||||||
|
jwt: {
|
||||||
|
secret: process.env.JWT_SECRET,
|
||||||
|
expiresIn: '7d',
|
||||||
|
refreshThreshold: 24 * 60 * 60 // 1 day in seconds
|
||||||
|
},
|
||||||
|
cookie: {
|
||||||
|
name: 'auth_token',
|
||||||
|
options: {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
||||||
|
domain: process.env.NODE_ENV === 'production'
|
||||||
|
? '.maverickapplications.com'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Model (`models/user.js`)
|
||||||
|
|
||||||
|
Uses MongoDB native driver with upsert pattern:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function findOrCreateUser(db, profile) {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const result = await db.collection('users').findOneAndUpdate(
|
||||||
|
{ googleId: profile.googleId },
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
email: profile.email,
|
||||||
|
name: profile.name,
|
||||||
|
picture: profile.picture || null,
|
||||||
|
lastLoginAt: now
|
||||||
|
},
|
||||||
|
$inc: { loginCount: 1 },
|
||||||
|
$setOnInsert: {
|
||||||
|
googleId: profile.googleId,
|
||||||
|
isActive: true,
|
||||||
|
role: 'user',
|
||||||
|
createdAt: now
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ upsert: true, returnDocument: 'after' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** `loginCount` must NOT be in `$setOnInsert` - it conflicts with `$inc`.
|
||||||
|
|
||||||
|
### Passport Strategy (`middleware/passport.js`)
|
||||||
|
|
||||||
|
Safely extracts profile data from Google:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async (accessToken, refreshToken, profile, done) => {
|
||||||
|
try {
|
||||||
|
const email = profile.emails?.[0]?.value;
|
||||||
|
if (!email) {
|
||||||
|
return done(new Error('No email found in Google profile'), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userProfile = {
|
||||||
|
googleId: profile.id,
|
||||||
|
email: email,
|
||||||
|
name: profile.displayName,
|
||||||
|
picture: profile.photos?.[0]?.value || null
|
||||||
|
};
|
||||||
|
|
||||||
|
const user = await findOrCreateUser(db, userProfile);
|
||||||
|
done(null, user);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Passport strategy error:', error);
|
||||||
|
done(error, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### OAuth State Validation (`routes/auth.js`)
|
||||||
|
|
||||||
|
CSRF protection using state parameter:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Initiate OAuth - generate and store state
|
||||||
|
router.get('/google', (req, res, next) => {
|
||||||
|
const state = crypto.randomBytes(32).toString('hex');
|
||||||
|
res.cookie('oauth_state', state, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
maxAge: 5 * 60 * 1000 // 5 minutes
|
||||||
|
});
|
||||||
|
passport.authenticate('google', {
|
||||||
|
scope: authConfig.google.scope,
|
||||||
|
session: false,
|
||||||
|
state: state
|
||||||
|
})(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback - validate state before processing
|
||||||
|
router.get('/google/callback', (req, res, next) => {
|
||||||
|
const stateFromCookie = req.cookies.oauth_state;
|
||||||
|
const stateFromQuery = req.query.state;
|
||||||
|
res.clearCookie('oauth_state');
|
||||||
|
|
||||||
|
if (!stateFromCookie || !stateFromQuery || stateFromCookie !== stateFromQuery) {
|
||||||
|
return res.redirect(`${process.env.FRONTEND_URL}/login?error=invalid_state`);
|
||||||
|
}
|
||||||
|
// ... proceed with authentication
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Auth Context (`context/AuthContext.jsx`)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const AuthContext = createContext(null);
|
||||||
|
|
||||||
|
export function AuthProvider({ children }) {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkAuthStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const checkAuthStatus = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/auth/status`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.authenticated) {
|
||||||
|
setUser(data.user);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auth check failed:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const login = () => {
|
||||||
|
window.location.href = `${API_BASE}/auth/google`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
await fetch(`${API_BASE}/auth/logout`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
setUser(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={{ user, loading, login, logout }}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Indexes
|
||||||
|
|
||||||
|
Created automatically on server startup:
|
||||||
|
|
||||||
|
**Users Collection:**
|
||||||
|
- `{ googleId: 1 }` - unique
|
||||||
|
- `{ email: 1 }` - unique
|
||||||
|
- `{ isActive: 1, lastLoginAt: -1 }` - compound for queries
|
||||||
|
|
||||||
|
**User Activity Collection:**
|
||||||
|
- `{ userId: 1, timestamp: -1 }` - user activity queries
|
||||||
|
- `{ timestamp: 1 }` - TTL index (90-day auto-cleanup)
|
||||||
|
- `{ action: 1, timestamp: -1 }` - action type queries
|
||||||
|
- `{ sessionId: 1 }` - session grouping
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. User Activity Logging
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Controlled by `ACTIVITY_LOG_LEVEL` environment variable:
|
||||||
|
|
||||||
|
| Level | What's Logged |
|
||||||
|
|-------|---------------|
|
||||||
|
| `all` | All user interactions (default) |
|
||||||
|
| `navigation` | Only page views and route changes |
|
||||||
|
| `none` | Logging disabled |
|
||||||
|
|
||||||
|
### Activity Types
|
||||||
|
|
||||||
|
| Action | When Logged | Metadata |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| `LOGIN` | User completes OAuth | `{ method: 'google' }` |
|
||||||
|
| `LOGOUT` | User logs out | `{}` |
|
||||||
|
| `PAGE_VIEW` | Page load | `{ path }` |
|
||||||
|
| `NAVIGATION` | Route change | `{ from, to }` |
|
||||||
|
|
||||||
|
### Activity Schema
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
_id: ObjectId,
|
||||||
|
userId: ObjectId,
|
||||||
|
sessionId: String, // UUID for grouping
|
||||||
|
action: String,
|
||||||
|
metadata: Object,
|
||||||
|
page: String,
|
||||||
|
timestamp: Date,
|
||||||
|
userAgent: String,
|
||||||
|
ip: String
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auto-Cleanup
|
||||||
|
|
||||||
|
Activity records are automatically deleted after 90 days via MongoDB TTL index.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Security Measures
|
||||||
|
|
||||||
|
### Implemented
|
||||||
|
|
||||||
|
| Security Feature | Implementation |
|
||||||
|
|------------------|----------------|
|
||||||
|
| OAuth state parameter | Random 32-byte hex, stored in cookie, validated on callback |
|
||||||
|
| HTTP-only cookies | JWT not accessible via JavaScript |
|
||||||
|
| Secure cookies (production) | Only sent over HTTPS |
|
||||||
|
| SameSite=Lax | CSRF protection for cookies |
|
||||||
|
| Input validation | Unit codes validated with regex pattern |
|
||||||
|
| isActive check | Disabled users rejected on every request |
|
||||||
|
| .dockerignore | Prevents .env and secrets from being copied into images |
|
||||||
|
| Environment validation | Server exits if MONGO_URI missing in production |
|
||||||
|
|
||||||
|
### Input Validation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const isValidUnitCode = (unitCode) => {
|
||||||
|
return typeof unitCode === 'string' &&
|
||||||
|
unitCode.length > 0 &&
|
||||||
|
unitCode.length <= 20 &&
|
||||||
|
/^[A-Za-z0-9_-]+$/.test(unitCode);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### CORS Configuration
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const corsOptions = {
|
||||||
|
origin: process.env.FRONTEND_URL, // Exact origin, not '*'
|
||||||
|
credentials: true, // Required for cookies
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||||
|
exposedHeaders: ['set-cookie']
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Deployment Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Backend `.env` (see `.env.example` for template):
|
||||||
|
|
||||||
|
```env
|
||||||
|
# MongoDB
|
||||||
|
MONGO_URI=mongodb+srv://...
|
||||||
|
|
||||||
|
# Server
|
||||||
|
PORT=8080
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# Google OAuth
|
||||||
|
GOOGLE_CLIENT_ID=xxx.apps.googleusercontent.com
|
||||||
|
GOOGLE_CLIENT_SECRET=xxx
|
||||||
|
GOOGLE_CALLBACK_URL=https://apartments.maverickapplications.com/api/auth/google/callback
|
||||||
|
|
||||||
|
# JWT (generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")
|
||||||
|
JWT_SECRET=your-secure-random-string
|
||||||
|
|
||||||
|
# App
|
||||||
|
FRONTEND_URL=https://apartments.maverickapplications.com
|
||||||
|
|
||||||
|
# Activity Logging
|
||||||
|
ACTIVITY_LOG_LEVEL=all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
apartment-api:
|
||||||
|
build: .
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
```
|
||||||
|
|
||||||
|
### Google Cloud Console Setup
|
||||||
|
|
||||||
|
Required redirect URIs:
|
||||||
|
```
|
||||||
|
https://apartments.maverickapplications.com/api/auth/google/callback
|
||||||
|
```
|
||||||
|
|
||||||
|
OAuth consent screen:
|
||||||
|
- User type: External
|
||||||
|
- Scopes: `email`, `profile` (non-sensitive)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Test Checklist
|
||||||
|
|
||||||
|
### Completed Tests
|
||||||
|
|
||||||
|
- [x] Google OAuth login flow works end-to-end
|
||||||
|
- [x] JWT cookie is set with correct flags (httpOnly, secure, sameSite)
|
||||||
|
- [x] Protected endpoints return 401 without valid token
|
||||||
|
- [x] User data persists across sessions (cookie survives browser close)
|
||||||
|
- [x] Activity logging captures LOGIN/LOGOUT events
|
||||||
|
- [x] Data loads correctly regardless of server timezone
|
||||||
|
- [x] OAuth state parameter prevents CSRF
|
||||||
|
- [x] User upsert creates new users and updates existing
|
||||||
|
- [x] Sliding window token refresh extends sessions
|
||||||
|
- [x] Logout clears cookie and redirects to login
|
||||||
|
|
||||||
|
### Production Verification
|
||||||
|
|
||||||
|
- [x] OAuth redirect URIs match production domain
|
||||||
|
- [x] HTTPS enforced
|
||||||
|
- [x] Environment variables properly set
|
||||||
|
- [x] MongoDB indexes created on startup
|
||||||
|
- [x] No sensitive data in console logs
|
||||||
|
- [x] .dockerignore prevents secrets in image
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Future: Admin Dashboard
|
||||||
|
|
||||||
|
**Priority: Low**
|
||||||
|
|
||||||
|
### Planned Features
|
||||||
|
|
||||||
|
1. **User Management**
|
||||||
|
- View all registered users
|
||||||
|
- See last login, login count
|
||||||
|
- Enable/disable users
|
||||||
|
|
||||||
|
2. **Activity Viewer**
|
||||||
|
- Recent activity stream
|
||||||
|
- Filter by user, action, date
|
||||||
|
|
||||||
|
3. **Usage Statistics**
|
||||||
|
- Active users (daily/weekly/monthly)
|
||||||
|
- Most common actions
|
||||||
|
- Peak usage times
|
||||||
|
|
||||||
|
### Access Control
|
||||||
|
|
||||||
|
- Add `role: 'admin'` to user document
|
||||||
|
- Create `requireAdmin` middleware
|
||||||
|
- Manually set initial admin via database
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Future: Adding Apple Sign-In
|
||||||
|
|
||||||
|
**Priority: Future**
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
1. Apple Developer account ($99/year)
|
||||||
|
2. Service ID for web authentication
|
||||||
|
3. Private key for token verification
|
||||||
|
|
||||||
|
### Changes Needed
|
||||||
|
|
||||||
|
1. Install `passport-apple`
|
||||||
|
2. Add Apple strategy to passport
|
||||||
|
3. Add routes: `/auth/apple`, `/auth/apple/callback`
|
||||||
|
4. Update user model for multiple providers
|
||||||
|
5. Add "Sign in with Apple" button
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Future: Partial Public Access
|
||||||
|
|
||||||
|
**Priority: Future**
|
||||||
|
|
||||||
|
Allow unauthenticated users to see a blurred preview of the overview page.
|
||||||
|
|
||||||
|
### Behavior
|
||||||
|
|
||||||
|
- Summary cards visible
|
||||||
|
- Charts and detailed data blurred with CSS
|
||||||
|
- Overlay with "Sign in to view" prompt
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
1. Make `/api/daily-summary` public
|
||||||
|
2. Create `AuthBlurOverlay` component
|
||||||
|
3. Wrap protected sections on overview page
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisions Log
|
||||||
|
|
||||||
|
| Decision | Choice | Rationale |
|
||||||
|
|----------|--------|-----------|
|
||||||
|
| Token storage | HTTP-only cookie | More secure than localStorage |
|
||||||
|
| Token expiry | 7 days with sliding refresh | Balance security and UX |
|
||||||
|
| Disabled user handling | Silent logout | No error messaging needed |
|
||||||
|
| State parameter | Random 32-byte hex | Industry standard CSRF protection |
|
||||||
|
| Activity cleanup | 90-day TTL | Automatic via MongoDB index |
|
||||||
|
| Date handling | Use latest database date | Handles server timezone mismatch |
|
||||||
|
| User upsert | findOneAndUpdate | Atomic create/update operation |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
| Issue | Cause | Solution |
|
||||||
|
|-------|-------|----------|
|
||||||
|
| 401 on all endpoints | Missing/invalid JWT | Check cookie is set, not expired |
|
||||||
|
| OAuth callback 500 | Profile field access error | Use optional chaining on profile fields |
|
||||||
|
| Empty data arrays | Timezone mismatch | Server uses latest date from database |
|
||||||
|
| MongoDB conflict error | loginCount in $setOnInsert | Remove from $setOnInsert, use only $inc |
|
||||||
|
| Cookie not set | CORS misconfiguration | Ensure credentials: 'include' on all fetches |
|
||||||
|
| State validation fails | Cookie expired or cleared | State cookie has 5-minute lifetime |
|
||||||
Reference in New Issue
Block a user