diff --git a/.env.example b/.env.example index 087f929..c168c14 100644 --- a/.env.example +++ b/.env.example @@ -2,15 +2,25 @@ # Copy this file to .env and fill in your values # Database Configuration -DATABASE_URL=postgresql://turftracker:password123@db:5432/turftracker +# You can use either individual fields OR a full DATABASE_URL (DATABASE_URL takes precedence) +DB_HOST=db +DB_PORT=5432 +DB_NAME=turftracker +DB_USER=turftracker +DB_PASSWORD=password123 +# DATABASE_URL=postgresql://turftracker:password123@db:5432/turftracker -# JWT Secret - Change this to a strong random string in production +# JWT Secret - REQUIRED: Used for signing authentication tokens +# Generate a secure random string (see README for generation commands) +# NEVER use the default value in production! JWT_SECRET=your-super-secret-jwt-key-change-this-in-production -# Google OAuth2 Configuration (Optional) -# Get these from Google Cloud Console -GOOGLE_CLIENT_ID=your-google-client-id -GOOGLE_CLIENT_SECRET=your-google-client-secret +# Authentik OAuth2 Configuration (Optional) +# Configure these to enable SSO login through your Authentik instance +AUTHENTIK_CLIENT_ID=your-authentik-client-id +AUTHENTIK_CLIENT_SECRET=your-authentik-client-secret +AUTHENTIK_BASE_URL=https://your-authentik-domain.com +AUTHENTIK_CALLBACK_URL=http://localhost:5000/api/auth/authentik/callback # Weather API Configuration # Get a free API key from https://openweathermap.org/api diff --git a/README.md b/README.md index ba6a3e9..03c708f 100644 --- a/README.md +++ b/README.md @@ -56,17 +56,19 @@ TurfTracker is a comprehensive web application designed for homeowners to track ### 🚧 Planned Features -- **Google Maps Integration** - Enhanced satellite view and area calculation - **GPS Speed Monitoring** - Real-time speed feedback during applications - **Mobile App** - Native iOS/Android applications +- **Advanced Reporting** - PDF reports and data export +- **IoT Integration** - Sensor data integration ## Technology Stack -- **Frontend**: React 18, Tailwind CSS, React Router, React Query +- **Frontend**: React 18, Tailwind CSS, React Router, React Query, Leaflet Maps - **Backend**: Node.js, Express.js, PostgreSQL -- **Authentication**: JWT, OAuth2 (Google) +- **Authentication**: JWT, OAuth2 (Authentik) - **Infrastructure**: Docker, Nginx -- **APIs**: OpenWeatherMap, Google Maps (planned) +- **Maps**: OpenStreetMap via Leaflet, Esri Satellite Imagery +- **APIs**: OpenWeatherMap ## Quick Start @@ -85,23 +87,32 @@ TurfTracker is a comprehensive web application designed for homeowners to track 2. **Environment Configuration** - Create environment files with your API keys: + Copy the example environment file and configure it: + ```bash + cp .env.example .env + ``` - **Backend Environment** (create `.env` in root): + **Required Configuration** - Edit the `.env` file: ```env - # Database - DATABASE_URL=postgresql://turftracker:password123@db:5432/turftracker + # Database - Use separate fields OR full DATABASE_URL + DB_HOST=db + DB_PORT=5432 + DB_NAME=turftracker + DB_USER=turftracker + DB_PASSWORD=password123 - # Authentication - JWT_SECRET=your-super-secret-jwt-key-change-this-in-production - - # Google OAuth2 (optional) - GOOGLE_CLIENT_ID=your-google-client-id - GOOGLE_CLIENT_SECRET=your-google-client-secret + # Authentication - GENERATE A SECURE JWT SECRET + JWT_SECRET=your-generated-jwt-secret-here # Weather API (get free key from OpenWeatherMap) WEATHER_API_KEY=your-openweathermap-api-key + # Authentik OAuth2 (optional - for SSO) + AUTHENTIK_CLIENT_ID=your-authentik-client-id + AUTHENTIK_CLIENT_SECRET=your-authentik-client-secret + AUTHENTIK_BASE_URL=https://your-authentik-domain.com + AUTHENTIK_CALLBACK_URL=http://localhost:5000/api/auth/authentik/callback + # App URLs FRONTEND_URL=http://localhost:3000 ``` @@ -138,30 +149,87 @@ TurfTracker is a comprehensive web application designed for homeowners to track - Add custom products or use the pre-loaded database - Configure application rates -## API Keys Setup +## Environment Configuration Guide -### OpenWeatherMap API Key +### JWT Secret Generation + +The JWT secret is used to sign authentication tokens and **MUST** be secure in production. + +**Generate a secure JWT secret:** + +```bash +# Option 1: Using OpenSSL (recommended) +openssl rand -base64 64 + +# Option 2: Using Node.js +node -e "console.log(require('crypto').randomBytes(64).toString('base64'))" + +# Option 3: Using Python +python3 -c "import secrets; print(secrets.token_urlsafe(64))" + +# Option 4: Online generator +# Visit: https://generate-secret.vercel.app/64 +``` + +**⚠️ Security Warning:** Never use the default JWT secret in production! Always generate a unique, random secret for each environment. + +### Database Configuration + +You can configure the database connection using either: + +**Option 1: Separate Fields (Recommended)** +```env +DB_HOST=db +DB_PORT=5432 +DB_NAME=turftracker +DB_USER=turftracker +DB_PASSWORD=password123 +``` + +**Option 2: Connection URL** +```env +DATABASE_URL=postgresql://username:password@host:port/database +``` + +**Note:** If both are provided, `DATABASE_URL` takes precedence. + +### API Keys Setup + +#### OpenWeatherMap API Key 1. Go to [OpenWeatherMap](https://openweathermap.org/api) -2. Sign up for a free account -3. Get your API key from the dashboard -4. Add it to your `.env` file as `WEATHER_API_KEY` +2. Sign up for a free account (allows 1,000 calls/day) +3. Navigate to "API keys" in your dashboard +4. Copy your API key +5. Add it to your `.env` file as `WEATHER_API_KEY` -### Google OAuth2 (Optional) +#### Authentik OAuth2 Setup (Optional) -1. Go to [Google Cloud Console](https://console.cloud.google.com/) -2. Create a new project or select existing one -3. Enable Google+ API -4. Create OAuth2 credentials -5. Add `http://localhost:5000/api/auth/google/callback` as redirect URI -6. Add client ID and secret to your `.env` file +If you have an Authentik instance for SSO: -### Google Maps API (Future Enhancement) +1. **In Authentik Admin Panel:** + - Go to Applications → Providers + - Create new "OAuth2/OpenID Provider" + - Set Authorization grant type: `authorization-code` + - Set Client type: `confidential` + - Set Redirect URIs: `http://localhost:5000/api/auth/authentik/callback` + - Note the Client ID and Client Secret -1. Go to [Google Cloud Console](https://console.cloud.google.com/) -2. Enable Maps JavaScript API and Geocoding API -3. Create an API key -4. Will be integrated in future updates +2. **In your `.env` file:** + ```env + AUTHENTIK_CLIENT_ID=your-client-id-from-authentik + AUTHENTIK_CLIENT_SECRET=your-client-secret-from-authentik + AUTHENTIK_BASE_URL=https://your-authentik-domain.com + AUTHENTIK_CALLBACK_URL=http://localhost:5000/api/auth/authentik/callback + ``` + +3. **In Authentik Applications:** + - Create new Application + - Set Name: `TurfTracker` + - Set Slug: `turftracker` + - Set Provider: (select the provider created above) + +**Scopes Required:** `openid profile email` ## Application Structure @@ -260,10 +328,12 @@ The backend provides a comprehensive REST API. Key endpoints include: ### Project Roadmap -- [ ] Google Maps integration for enhanced property mapping -- [ ] Mobile application development +- [x] OpenStreetMap integration for property mapping and satellite imagery +- [x] Authentik OAuth2 integration for SSO +- [x] Interactive lawn section drawing and area calculation - [ ] GPS speed monitoring with audio feedback -- [ ] Advanced reporting and analytics +- [ ] Mobile application development +- [ ] Advanced reporting and analytics with PDF export - [ ] Weather-based application recommendations - [ ] Integration with IoT sensors - [ ] Multi-language support diff --git a/backend/package.json b/backend/package.json index 223033a..8322e93 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,7 +18,7 @@ "express-rate-limit": "^7.1.5", "dotenv": "^16.3.1", "passport": "^0.7.0", - "passport-google-oauth20": "^2.0.0", + "passport-oauth2": "^1.7.0", "passport-jwt": "^4.0.1", "multer": "^1.4.5-lts.1", "axios": "^1.6.2", diff --git a/backend/src/config/database.js b/backend/src/config/database.js index 3269869..e61e942 100644 --- a/backend/src/config/database.js +++ b/backend/src/config/database.js @@ -1,12 +1,28 @@ const { Pool } = require('pg'); -const pool = new Pool({ - connectionString: process.env.DATABASE_URL, +// Build connection configuration from environment variables +const dbConfig = { + host: process.env.DB_HOST || 'db', + port: process.env.DB_PORT || 5432, + database: process.env.DB_NAME || 'turftracker', + user: process.env.DB_USER || 'turftracker', + password: process.env.DB_PASSWORD || 'password123', ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, max: 20, idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, -}); +}; + +// Fallback to DATABASE_URL if provided (for backwards compatibility) +const pool = process.env.DATABASE_URL + ? new Pool({ + connectionString: process.env.DATABASE_URL, + ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, + max: 20, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 2000, + }) + : new Pool(dbConfig); // Test the connection pool.on('connect', () => { diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index cdbb503..3c4d52c 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -2,7 +2,7 @@ const express = require('express'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const passport = require('passport'); -const GoogleStrategy = require('passport-google-oauth20').Strategy; +const OAuth2Strategy = require('passport-oauth2').Strategy; const pool = require('../config/database'); const { validateRequest } = require('../utils/validation'); const { registerSchema, loginSchema, changePasswordSchema } = require('../utils/validation'); @@ -10,18 +10,30 @@ const { AppError } = require('../middleware/errorHandler'); const router = express.Router(); -// Configure Google OAuth2 strategy -if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { - passport.use(new GoogleStrategy({ - clientID: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - callbackURL: '/api/auth/google/callback' +// Configure Authentik OAuth2 strategy +if (process.env.AUTHENTIK_CLIENT_ID && process.env.AUTHENTIK_CLIENT_SECRET && process.env.AUTHENTIK_BASE_URL) { + passport.use('authentik', new OAuth2Strategy({ + authorizationURL: `${process.env.AUTHENTIK_BASE_URL}/application/o/authorize/`, + tokenURL: `${process.env.AUTHENTIK_BASE_URL}/application/o/token/`, + clientID: process.env.AUTHENTIK_CLIENT_ID, + clientSecret: process.env.AUTHENTIK_CLIENT_SECRET, + callbackURL: process.env.AUTHENTIK_CALLBACK_URL || '/api/auth/authentik/callback' }, async (accessToken, refreshToken, profile, done) => { try { + // Get user info from Authentik + const axios = require('axios'); + const userInfoResponse = await axios.get(`${process.env.AUTHENTIK_BASE_URL}/application/o/userinfo/`, { + headers: { + 'Authorization': `Bearer ${accessToken}` + } + }); + + const userInfo = userInfoResponse.data; + // Check if user already exists const existingUser = await pool.query( 'SELECT * FROM users WHERE oauth_provider = $1 AND oauth_id = $2', - ['google', profile.id] + ['authentik', userInfo.sub] ); if (existingUser.rows.length > 0) { @@ -31,14 +43,14 @@ if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { // Check if user exists with same email const emailUser = await pool.query( 'SELECT * FROM users WHERE email = $1', - [profile.emails[0].value] + [userInfo.email] ); if (emailUser.rows.length > 0) { - // Link Google account to existing user + // Link Authentik account to existing user await pool.query( 'UPDATE users SET oauth_provider = $1, oauth_id = $2 WHERE id = $3', - ['google', profile.id, emailUser.rows[0].id] + ['authentik', userInfo.sub, emailUser.rows[0].id] ); return done(null, emailUser.rows[0]); } @@ -48,16 +60,17 @@ if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { `INSERT INTO users (email, first_name, last_name, oauth_provider, oauth_id) VALUES ($1, $2, $3, $4, $5) RETURNING *`, [ - profile.emails[0].value, - profile.name.givenName, - profile.name.familyName, - 'google', - profile.id + userInfo.email, + userInfo.given_name || userInfo.name?.split(' ')[0] || 'User', + userInfo.family_name || userInfo.name?.split(' ')[1] || '', + 'authentik', + userInfo.sub ] ); return done(null, newUser.rows[0]); } catch (error) { + console.error('Authentik OAuth error:', error); return done(error, null); } })); @@ -185,18 +198,27 @@ router.post('/login', validateRequest(loginSchema), async (req, res, next) => { } }); -// @route GET /api/auth/google -// @desc Start Google OAuth2 flow +// @route GET /api/auth/authentik +// @desc Start Authentik OAuth2 flow // @access Public -router.get('/google', passport.authenticate('google', { - scope: ['profile', 'email'] -})); +router.get('/authentik', (req, res, next) => { + if (!process.env.AUTHENTIK_CLIENT_ID) { + return res.status(503).json({ + success: false, + message: 'Authentik OAuth not configured' + }); + } + + passport.authenticate('authentik', { + scope: 'openid profile email' + })(req, res, next); +}); -// @route GET /api/auth/google/callback -// @desc Google OAuth2 callback +// @route GET /api/auth/authentik/callback +// @desc Authentik OAuth2 callback // @access Public -router.get('/google/callback', - passport.authenticate('google', { session: false }), +router.get('/authentik/callback', + passport.authenticate('authentik', { session: false }), (req, res) => { const token = generateToken(req.user.id); diff --git a/docker-compose.yml b/docker-compose.yml index 40f6c8d..be23e6b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,11 +23,17 @@ services: - "5000:5000" environment: - NODE_ENV=development - - DATABASE_URL=postgresql://turftracker:password123@db:5432/turftracker + - DB_HOST=db + - DB_PORT=5432 + - DB_NAME=turftracker + - DB_USER=turftracker + - DB_PASSWORD=password123 - JWT_SECRET=your-super-secret-jwt-key - - GOOGLE_CLIENT_ID=your-google-client-id - - GOOGLE_CLIENT_SECRET=your-google-client-secret - - WEATHER_API_KEY=your-weather-api-key + - AUTHENTIK_CLIENT_ID=${AUTHENTIK_CLIENT_ID:-} + - AUTHENTIK_CLIENT_SECRET=${AUTHENTIK_CLIENT_SECRET:-} + - AUTHENTIK_BASE_URL=${AUTHENTIK_BASE_URL:-} + - AUTHENTIK_CALLBACK_URL=${AUTHENTIK_CALLBACK_URL:-} + - WEATHER_API_KEY=${WEATHER_API_KEY:-} volumes: - ./backend:/app - /app/node_modules diff --git a/frontend/package.json b/frontend/package.json index d846941..3430e54 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,8 +12,8 @@ "react-scripts": "5.0.1", "react-router-dom": "^6.8.1", "axios": "^1.6.2", - "@google/maps": "^1.1.3", - "@googlemaps/js-api-loader": "^1.16.2", + "leaflet": "^1.9.4", + "react-leaflet": "^4.2.1", "@headlessui/react": "^1.7.17", "@heroicons/react": "^2.0.18", "react-hook-form": "^7.48.2", @@ -24,8 +24,6 @@ "react-hot-toast": "^2.4.1", "date-fns": "^2.30.0", "recharts": "^2.8.0", - "react-map-gl": "^7.1.7", - "mapbox-gl": "^2.15.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "clsx": "^2.0.0", diff --git a/frontend/src/components/Maps/PropertyMap.js b/frontend/src/components/Maps/PropertyMap.js new file mode 100644 index 0000000..7969fdf --- /dev/null +++ b/frontend/src/components/Maps/PropertyMap.js @@ -0,0 +1,437 @@ +import React, { useState, useCallback, useRef } from 'react'; +import { MapContainer, TileLayer, Polygon, Marker, useMapEvents } from 'react-leaflet'; +import { Icon } from 'leaflet'; +import 'leaflet/dist/leaflet.css'; + +// Fix for default markers in react-leaflet +delete Icon.Default.prototype._getIconUrl; +Icon.Default.mergeOptions({ + iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'), + iconUrl: require('leaflet/dist/images/marker-icon.png'), + shadowUrl: require('leaflet/dist/images/marker-shadow.png'), +}); + +// Custom component to handle map clicks for drawing polygons +const DrawingHandler = ({ isDrawing, onPointAdd, onDrawingComplete }) => { + useMapEvents({ + click: (e) => { + if (isDrawing) { + onPointAdd([e.latlng.lat, e.latlng.lng]); + } + }, + keydown: (e) => { + if (e.originalEvent.key === 'Escape' && isDrawing) { + onDrawingComplete(); + } + } + }); + return null; +}; + +// Calculate polygon area in square feet +const calculatePolygonArea = (coordinates) => { + if (!coordinates || coordinates.length < 3) return 0; + + // Shoelace formula for polygon area + let area = 0; + const n = coordinates.length; + + for (let i = 0; i < n; i++) { + const j = (i + 1) % n; + area += coordinates[i][0] * coordinates[j][1]; + area -= coordinates[j][0] * coordinates[i][1]; + } + + area = Math.abs(area) / 2; + + // Convert from decimal degrees to square feet (approximate) + const avgLat = coordinates.reduce((sum, coord) => sum + coord[0], 0) / n; + const meterToFeet = 3.28084; + const degToMeter = 111320 * Math.cos(avgLat * Math.PI / 180); + + return area * Math.pow(degToMeter * meterToFeet, 2); +}; + +const PropertyMap = ({ + center = [39.8283, -98.5795], // Center of USA as default + zoom = 15, + property, + sections = [], + onSectionCreate, + onSectionUpdate, + onSectionDelete, + onPropertyUpdate, + editable = false, + className = "h-96 w-full" +}) => { + const [isDrawing, setIsDrawing] = useState(false); + const [currentPolygon, setCurrentPolygon] = useState([]); + const [selectedSection, setSelectedSection] = useState(null); + const [showSectionForm, setShowSectionForm] = useState(false); + const [sectionName, setSectionName] = useState(''); + const [grassType, setGrassType] = useState(''); + const [soilType, setSoilType] = useState(''); + const mapRef = useRef(null); + + // Handle adding points to current polygon + const handlePointAdd = useCallback((point) => { + setCurrentPolygon(prev => [...prev, point]); + }, []); + + // Handle completing polygon drawing + const handleDrawingComplete = useCallback(() => { + if (currentPolygon.length >= 3) { + const area = calculatePolygonArea(currentPolygon); + setShowSectionForm(true); + } else { + setCurrentPolygon([]); + } + setIsDrawing(false); + }, [currentPolygon]); + + // Start drawing mode + const startDrawing = () => { + setIsDrawing(true); + setCurrentPolygon([]); + setSelectedSection(null); + }; + + // Cancel drawing + const cancelDrawing = () => { + setIsDrawing(false); + setCurrentPolygon([]); + setShowSectionForm(false); + setSectionName(''); + setGrassType(''); + setSoilType(''); + }; + + // Save new section + const saveSection = async () => { + if (!sectionName.trim() || currentPolygon.length < 3) return; + + const area = calculatePolygonArea(currentPolygon); + const sectionData = { + name: sectionName, + area, + polygonData: { + type: 'Polygon', + coordinates: [currentPolygon.map(([lat, lng]) => [lng, lat])] // GeoJSON format + }, + grassType: grassType || null, + soilType: soilType || null + }; + + try { + if (onSectionCreate) { + await onSectionCreate(sectionData); + } + + // Reset form + cancelDrawing(); + } catch (error) { + console.error('Error creating section:', error); + } + }; + + // Handle section click + const handleSectionClick = (section) => { + setSelectedSection(selectedSection?.id === section.id ? null : section); + }; + + // Delete selected section + const deleteSelectedSection = async () => { + if (!selectedSection || !onSectionDelete) return; + + try { + await onSectionDelete(selectedSection.id); + setSelectedSection(null); + } catch (error) { + console.error('Error deleting section:', error); + } + }; + + // Get section color based on grass type + const getSectionColor = (section) => { + const colors = { + 'bermuda': '#10b981', + 'fescue': '#059669', + 'kentucky bluegrass': '#047857', + 'zoysia': '#065f46', + 'st augustine': '#064e3b', + 'centipede': '#6ee7b7', + 'default': '#3b82f6' + }; + + return colors[section.grassType?.toLowerCase()] || colors.default; + }; + + return ( +
+ {/* Map Controls */} + {editable && ( +
+
+ {!isDrawing ? ( + + ) : ( +
+ + +
+ )} +
+ + {selectedSection && ( +
+

{selectedSection.name}

+

+ {Math.round(selectedSection.area).toLocaleString()} sq ft +

+ {selectedSection.grassType && ( +

+ Grass: {selectedSection.grassType} +

+ )} + {selectedSection.soilType && ( +

+ Soil: {selectedSection.soilType} +

+ )} + +
+ )} +
+ )} + + {/* Section Creation Form Modal */} + {showSectionForm && ( +
+
+

+ New Lawn Section +

+ +
+
+ + setSectionName(e.target.value)} + className="input" + placeholder="e.g., Front Yard, Back Lawn" + autoFocus + /> +
+ +
+ + +
+ +
+ + +
+ +
+

+ Area: {Math.round(calculatePolygonArea(currentPolygon)).toLocaleString()} sq ft +

+

+ {currentPolygon.length} points drawn +

+
+
+ +
+ + +
+
+
+ )} + + {/* Map */} + + + + {/* Satellite imagery option */} + + + {/* Drawing handler */} + + + {/* Property marker */} + {property && property.latitude && property.longitude && ( + + )} + + {/* Existing sections */} + {sections.map((section) => { + if (!section.polygonData?.coordinates?.[0]) return null; + + const coordinates = section.polygonData.coordinates[0].map(([lng, lat]) => [lat, lng]); + const isSelected = selectedSection?.id === section.id; + + return ( + handleSectionClick(section) + }} + /> + ); + })} + + {/* Current polygon being drawn */} + {currentPolygon.length > 0 && ( + <> + {/* Show markers for each point */} + {currentPolygon.map((point, index) => ( + + )} + + )} + + + {/* Drawing instructions */} + {isDrawing && ( +
+

+ Drawing Mode: +

+

+ Click to add points. Press Escape or + click "Finish" when done. +

+

+ Need at least 3 points to create a section. +

+
+ )} + + {/* Section stats */} + {sections.length > 0 && !isDrawing && ( +
+

+ {sections.length} Section{sections.length !== 1 ? 's' : ''} +

+

+ Total: {sections.reduce((sum, section) => sum + (section.area || 0), 0).toLocaleString()} sq ft +

+
+ )} +
+ ); +}; + +export default PropertyMap; \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index b3f0a1d..2e977e1 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -2,6 +2,9 @@ @tailwind components; @tailwind utilities; +/* Import Leaflet CSS for maps */ +@import 'leaflet/dist/leaflet.css'; + /* Base styles */ @layer base { html { diff --git a/frontend/src/pages/Auth/Login.js b/frontend/src/pages/Auth/Login.js index 50ae365..8c95005 100644 --- a/frontend/src/pages/Auth/Login.js +++ b/frontend/src/pages/Auth/Login.js @@ -32,9 +32,9 @@ const Login = () => { } }; - const handleGoogleLogin = () => { - // Redirect to Google OAuth endpoint - window.location.href = `${process.env.REACT_APP_API_URL || 'http://localhost:5000'}/api/auth/google`; + const handleAuthentikLogin = () => { + // Redirect to Authentik OAuth endpoint + window.location.href = `${process.env.REACT_APP_API_URL || 'http://localhost:5000'}/api/auth/authentik`; }; return ( @@ -170,28 +170,14 @@ const Login = () => {
diff --git a/nginx/nginx.conf b/nginx/nginx.conf index fa71d18..cb8cb74 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -48,7 +48,7 @@ http { add_header X-Frame-Options DENY; add_header X-XSS-Protection "1; mode=block"; add_header Referrer-Policy "strict-origin-when-cross-origin"; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://maps.googleapis.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://maps.googleapis.com https://maps.gstatic.com; connect-src 'self' https://api.openweathermap.org;"; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://*.openstreetmap.org https://server.arcgisonline.com; connect-src 'self' https://api.openweathermap.org https://*.openstreetmap.org https://server.arcgisonline.com;"; upstream backend { server backend:5000;