Benjamin Harris 2 mesi fa
parent
commit
c900478fa1

+ 7 - 0
.claude/settings.local.json

@@ -0,0 +1,7 @@
+{
+  "permissions": {
+    "allow": [
+      "WebFetch(domain:raw.githubusercontent.com)"
+    ]
+  }
+}

+ 302 - 100
CLAUDE.md

@@ -2,106 +2,308 @@
 
 ## Project Overview
 
-Crop Management Platform (CMS -> PHP migration)
-- Purpose: Centralize records for irrigation, weather, and soil moisture; supports real-time monitoring for Australian conditions.
-- Current stack: PHP 8.4, MySQL; originally built on modX CMS (template tags and resource IDs present).
-- Repository root: `f:\GIT_REPO\crop_monitor`
-
-## Identified Modules & Paths
-
-- Front controllers: `index.php`, `post.php`, `newClientDetails.php`
-- API: `api/api.php`, `api/Rest.inc.php`, `api/updateweatherstation.php`
-- Dashboard UI: `dashboard/*.php`, in particular `dashboard/crop-analysis/*` for soil analysis reports
-- Login management: `login/*.php`
-- Static assets: `client-assets/`, `books/`, `uploads/`, etc.
-
-## Database Schema Available
-
-- **Schema File**: `cropmonitor.sql` added to root folder
-- **Key Tables Identified**:
-  - `soil_records`: Main soil analysis data with all nutrient values, base saturations, and specifications
-  - `client_records`: Client information with weather station API keys
-  - `soil_specifications`: Soil type specifications for different crops
-  - `animal_records`: Animal feed analysis data
-  - `plant_records`: Plant tissue analysis data
-  - `fertiliser_specifications`: Fertilizer composition data
-
-## Database Validation
-
-- **Soil Records Fields**: All fields used in `lib/soil_calculations.php` confirmed present (BS_ca_ppm, ca_ppm_min/max, etc.)
-- **Data Types**: Most nutrient values stored as VARCHAR(10), need to handle numeric conversion in PHP
-- **Primary Keys**: All tables have proper primary key indexes
-- **Relationships**: `modx_user_id` field links records to users (legacy modX integration)
-
-## modX remnants to refactor
-
-- Template markers like `[[*longtitle]]`, `[[++site_name]]`, `[[!++site_url]]` need replacement with PHP-based logic.
-- Resource URL helpers such as `[[~41~]]` from modX should resolve to real PHP route URLs in migrated implementation.
-- Includes like `[[!Profile]]` are modX snippets; replace with traditional include/require and controller logic.
-
-## Immediate Actions (High Priority)
-
-1. Inventory all modX markers across `.php` files (grep for `\[\[.*\]\]`) and catalog them.
-2. Implement configuration layer for DB credentials, environment-based.
-3. Replace direct `mysqli_*` calls with PDO (prepared statements) for security and maintainability.
-4. Build PHP routing (`index.php` + `GET`/`POST` handling) and template system (Twig/Blade/manual) for consistent page output.
-5. Migrate each page one-by-one preserving functionality: login, dashboard, soil analysis, reports.
-
-## File-specific findings (example in `dashboard/crop-analysis/soil-analysis-pdf.php`)
-
-- Uses `$_GET` keys `cid`, `rid`, `rand`, `stid` and query, but no sanitization.
-- Performs `SELECT * FROM soil_records WHERE id = '$record_id' AND rand = '$rand_id'`.
-- Uses a modX resource reference mechanism for flow buttons; in pure PHP, build URLs manually.
-- Styles and assets loaded via `<link>` tags; safe to reuse.
-
-## Migration strategy recommendations
-
-- Step 1: Set up a global config `config.php` with database credentials and site constants.
-- Step 2: Create `lib/db.php` for database operations (PDO). Add `catch` error logging.
-- Step 3: Add `lib/helper.php` for URL generation, escaping, and date formatting.
-- Step 4: Create test data and verify with `phpunit` (?) if tests added later.
-
-## Next file audits planned
-
-- `dashboard/crop-analysis/soil-analysis.php`
-- `dashboard/crop-analysis/soil-report.php`, `soil-report-pdf.php`
-- `login/login.php`, `login/register.php`, `login/change-password.php`
-- `api/api.php` and REST API endpoints
-
-## Recent Progress (2026-03-27)
-
-### ✅ Completed Components
-- **Layout System**: Created reusable `layouts/header.php`, `layouts/footer.php`, `layouts/navbar.php`, `layouts/sidebar.php`
-- **Client Details Form**: Converted `[[!clientDetailsFORM]]` → `components/clientDetailsForm.php` (PDO + validation)
-- **New Client Modal**: Converted `[[!newClientDetails]]` → `components/newClientModal.php` + `controllers/newClientSubmit.php` (AJAX + validation)
-- **Soil Analysis Form**: Created `components/soilAnalysisForm.php` with comprehensive field validation
-- **Secure Controller**: Converted `[[!soilformSubmit]]` → `controllers/soilTestSubmit.php` (PDO + CSRF + auth)
-- **Database Config**: `config/database.php` with PDO connection
-- **Security Libraries**: `lib/auth.php`, `lib/csrf.php`, `lib/validation.php`
-- **Navigation System**: Converted `[[!Personalize?]]` and `[[Wayfinder?]]` → `components/navigation.php` (authentication-based navigation)- **Soil Calculations**: Created `lib/soil_calculations.php` with `soilAnalysisReportCalcs()` and `soilProgramCalcs()` functions
-- **Page Migration**: `soil-test-data.php` now uses include-based layout system
-- **Soil Analysis Migration**: `soil-analysis.php` migrated from modX to secure PHP with PDO, Bootstrap 5, and calculation functions
-### 🔧 Security Improvements Made
-- Replaced mysqli with PDO prepared statements
-- Added CSRF token protection
-- Input validation and sanitization
-- Session-based authentication checks
-- Proper error handling and logging
-- Removed SQL injection vulnerabilities
-
-### 📋 Remaining Tasks
-1. Migrate `soil-analysis.php` (data display page) - **COMPLETED**
-2. Update `soil-analysis-pdf.php` to use secure queries
-3. Convert remaining modX placeholders across all files
-4. Add proper user authentication system
-5. Test form submission and data flow
-
-## Questions for you
-
-- Do you prefer retaining the current page structure (`dashboard/crop-analysis/*`) or migrating to a MVC-style folder layout?
-- Are we required to keep URL slugs like existing modX IDs (e.g., page 41, 66, 37) for compatibility with external links?
-- Do you have existing MySQL schema docs or dumps to validate field mappings?
+**Crop Management Platform** — modX CMS → standalone PHP migration
+Purpose: Centralise records for irrigation, weather, and soil moisture; supports real-time monitoring for Australian conditions.
+Stack: **PHP 8.4**, **MySQL**, Bootstrap 5, jQuery, Chart.js
+Repository root: `f:\GIT_REPO\crop_monitor`
 
 ---
 
-*Generated on 2026-03-27*
+## Directory Structure
+
+```
+crop_monitor/
+├── api/                          # REST API endpoints (legacy)
+├── client-assets/                # CSS, JS, images, uploads
+│   ├── css/                      # Bootstrap 4 + custom styles
+│   ├── js/                       # jQuery, Chart.js, custom scripts
+│   ├── table/                    # DataTables AJAX helpers (legacy mysqli)
+│   ├── FredTemplate/             # Old template assets
+│   └── fullcalendar/             # Calendar widget
+├── components/                   # Reusable UI components (migrated)
+├── config/                       # Database config (PDO)
+├── controllers/                  # Form POST handlers (migrated)
+├── dashboard/                    # Main application pages
+│   ├── crop-analysis/
+│   │   ├── soil-test-data/       # Soil analysis entry + display
+│   │   ├── plant-test-data/      # Plant tissue analysis
+│   │   ├── animal-dietary-balance/
+│   │   └── water-test-data/
+│   ├── client-settings/
+│   ├── crop-cards/
+│   ├── inbox.php
+│   └── planning-calendar.php
+├── layouts/                      # Page templates (header, footer, nav, sidebar)
+├── lib/                          # Utility libraries
+├── login/                        # Auth pages (still modX templates)
+├── cropmonitor.sql               # Full database schema dump
+├── index.php                     # Empty — routing via .htaccess
+├── .htaccess                     # Front controller rewrite rules
+└── .env                          # Empty — credentials currently in config/database.php
+```
+
+---
+
+## Database Schema
+
+Schema file: `cropmonitor.sql`
+
+| Table | Purpose |
+|-------|---------|
+| `client_records` | Client info + weather station API keys (`modx_user_id` FK) |
+| `soil_records` | Soil analysis data — 80+ columns (nutrients, base saturations, calculations) |
+| `soil_specifications` | Min/max nutrient ranges by soil type (used for recommendations) |
+| `soil_comments` | Element-specific recommendation text |
+| `plant_records` | Plant tissue analysis |
+| `plant_specifications` | Nutrient ranges by crop/growth stage |
+| `animal_records` | Animal dietary balance |
+| `animal_specifications` | Nutrient requirements by species |
+| `water_records` | Water quality analysis |
+| `weather_station` | Weather station data ingestion |
+| `fertiliser_specifications` | Fertilizer product database |
+| `block_info` | Field/block metadata |
+| `crop_info` | Crop definitions |
+| `calendar_events` | User scheduling |
+| `reports` | Comments/notes on analyses |
+| `field_sensors` | IoT sensor readings |
+| `sensor_id` | Sensor registration |
+
+**Important data notes:**
+- Most nutrient values stored as `VARCHAR(10)` — always cast to float in PHP
+- `modx_user_id` field links all records to users (legacy modX user system)
+- `rand` field on `soil_records` is a secondary verification token for URL access
+
+---
+
+## Migration Status
+
+### Completed (PDO + secure)
+| Original modX | Migrated to |
+|---------------|------------|
+| `[[!clientDetailsFORM]]` | `components/clientDetailsForm.php` |
+| `[[!newClientDetails]]` | `components/newClientModal.php` + `controllers/newClientSubmit.php` |
+| `[[!soilformSubmit]]` | `controllers/soilTestSubmit.php` |
+| `[[!Personalize?]]` + `[[Wayfinder?]]` | `components/navigation.php` |
+| `[[$dash-header]]` + `[[$dash-footer]]` | `layouts/header.php`, `layouts/footer.php`, `layouts/navbar.php`, `layouts/sidebar.php` |
+| `soil-test-data.php` | Uses include-based layout system |
+| `soil-analysis.php` | PDO + Bootstrap 5 + calc functions |
+| `[[!Login]]` | `login/login.php` (session auth, CSRF, PDO) |
+| `[[!Register]]` | `login/register.php` (validation, auto-login, CSRF) |
+| `[[!ForgotPassword]]` | `login/forgot-password.php` + `login/reset-password.php` (token-based, 1hr expiry) |
+
+### Still Needs Migration (priority order)
+1. ~~**`login/login.php`**~~ — **DONE** (2026-03-27)
+2. ~~**`login/register.php`**~~ — **DONE** (2026-03-27)
+3. ~~**`login/forgot-password.php`**~~ — **DONE** (2026-03-27)
+4. **`login/change-password.php`** — modX `[[!ChangePassword]]` → rewrite using `changePassword()` in `lib/auth.php`
+5. **`dashboard/crop-analysis/soil-test-data/soil-analysis-pdf.php`** — mysqli → PDO, SQL injection fix
+6. **`dashboard/crop-analysis/soil-test-data/soil-report.php`** — display page audit
+7. **`dashboard/crop-analysis/soil-test-data/soil-report-pdf.php`** — mysqli → PDO
+8. **`dashboard/crop-analysis/plant-test-data/`** — full migration
+9. **`dashboard/crop-analysis/animal-dietary-balance/`** — full migration
+10. **`dashboard/crop-analysis/water-test-data/`** — full migration
+11. **`dashboard/client-settings/product-list.php`** — mysqli → PDO
+12. **`dashboard/client-settings/updateproduct.php`** — SQL injection fix
+13. **`dashboard/crop-cards/index.php`** — mysqli → PDO
+14. **`dashboard/dashboard.php`** — replace `[[$widget]]` chunks with PHP components
+15. **`api/updateweatherstation.php`** — critical SQL injection fix
+16. **`api/api.php`** — remove deprecated `mysql_connect()`, use PDO
+17. **`client-assets/table/gettable.php`** — AJAX DataTable endpoint, mysqli → PDO
+
+### Legacy files to delete after migration
+- `post.php` (replaced by `components/clientDetailsForm.php`)
+- `newClientDetails.php` (replaced by `components/newClientModal.php`)
+- `soilAnalysisCalcs.php` (dev/debug file)
+- `test-analysis.php` (dev/debug file)
+- `dashboard/crop-analysis/soil-test-data/soil-submit.php` (replaced by `controllers/soilTestSubmit.php`)
+
+---
+
+## Security Audit
+
+### Safe (PDO prepared statements)
+- `config/database.php`
+- `lib/soil_calculations.php`
+- `components/clientDetailsForm.php`
+- `controllers/newClientSubmit.php`
+- `controllers/soilTestSubmit.php`
+- `dashboard/crop-analysis/soil-test-data/soil-analysis.php`
+- `dashboard/crop-analysis/soil-test-data/soil-report.php`
+
+### Critical vulnerabilities (SQL injection via unescaped params)
+| File | Issue |
+|------|-------|
+| `api/updateweatherstation.php` | 40+ unescaped GET params in INSERT |
+| `newClientDetails.php` | POST params concatenated in INSERT |
+| `post.php` | `$modx_user` directly in SELECT WHERE |
+| `soilAnalysisCalcs.php` | GET `id` directly in SELECT WHERE |
+| `dashboard/crop-analysis/soil-test-data/soil-submit.php` | POST params in INSERT |
+| `dashboard/crop-analysis/soil-test-data/soil-analysis-pdf.php` | GET params in SELECT |
+| `dashboard/crop-analysis/soil-test-data/soil-report-pdf.php` | GET params in SELECT |
+| `dashboard/crop-analysis/soil-test-data/base-saturation-pie.php` | GET params in SELECT |
+| `dashboard/crop-analysis/animal-dietary-balance/animal-submit.php` | POST params in INSERT |
+| `dashboard/crop-analysis/plant-test-data/generating-plant-analysis.php` | POST params in INSERT |
+| `dashboard/client-settings/updateproduct.php` | String concatenation in INSERT |
+| `dashboard/crop-cards/index.php` | `$client_id` in SELECT |
+| `client-assets/table/gettable.php` | Dynamic query from POST params |
+| `api/api.php` | Uses removed `mysql_connect()` (PHP 7+: fatal error) |
+
+### Other security issues
+- Hardcoded credentials in multiple legacy files (`root`/`R3M0T31` and `cropmonitor`/`brvnCcaEYxlPCS3`)
+- `.env` file is empty — credentials should move there
+- `lib/auth.php` `hasPermission()` always returns `true` — RBAC not yet implemented
+
+---
+
+## Key Files Reference
+
+### Config & Libraries
+| File | Purpose |
+|------|---------|
+| `config/database.php` | PDO connection, `getDBConnection()` function |
+| `lib/auth.php` | `isLoggedIn()`, `requireLogin()`, `getCurrentUserId()` |
+| `lib/csrf.php` | `generateCsrfToken()`, `verifyCsrfToken()`, `regenerateCsrfToken()` |
+| `lib/validation.php` | `sanitizeString()`, `validateNumeric()`, `ValidationException` |
+| `lib/soil_calculations.php` | `soilAnalysisReportCalcs()`, `soilProgramCalcs()` |
+| `lib/db.php` | **Empty** — intended for shared DB helpers |
+| `lib/flash.php` | **Empty** — intended for flash messages |
+
+### Layouts
+| File | Purpose |
+|------|---------|
+| `layouts/header.php` | HTML `<head>`, Bootstrap 5 CDN, CSS includes. Expects `$pageTitle`, `$siteName` |
+| `layouts/navbar.php` | Top navigation bar |
+| `layouts/sidebar.php` | Left sidebar navigation |
+| `layouts/footer.php` | Closing tags + JS includes |
+
+### Components
+| File | Purpose |
+|------|---------|
+| `components/clientDetailsForm.php` | Client dropdown + auto-fill (PDO, session user filter) |
+| `components/newClientModal.php` | Bootstrap modal to add client (CSRF protected) |
+| `components/soilAnalysisForm.php` | Full soil test entry form (~50 fields, 7 sections) |
+| `components/navigation.php` | Auth-aware navigation |
+
+### Controllers
+| File | Purpose |
+|------|---------|
+| `controllers/newClientSubmit.php` | AJAX POST handler: creates client in `client_records` |
+| `controllers/soilTestSubmit.php` | POST handler: validates + inserts soil test, runs calculations, redirects to analysis |
+
+---
+
+## modX Tags Reference
+
+### Replaced
+| Tag | Replacement |
+|-----|------------|
+| `[[!clientDetailsFORM]]` | `components/clientDetailsForm.php` |
+| `[[!newClientDetails]]` | `components/newClientModal.php` |
+| `[[!soilformSubmit]]` | `controllers/soilTestSubmit.php` |
+| `[[!Personalize?]]` + `[[Wayfinder?]]` | `components/navigation.php` |
+| `[[$dash-header]]` | `layouts/header.php` + `layouts/navbar.php` |
+| `[[$dash-footer]]` | `layouts/footer.php` |
+
+### Still present in codebase (needs replacement)
+| Tag | Location | Replace with |
+|-----|----------|-------------|
+| `[[*longtitle]]` | `login/*.php` | PHP variable `$pageTitle` |
+| `[[++site_name]]` | `login/*.php` | `config/constants.php` constant |
+| `[[!++site_url]]` | `login/*.php` | Base URL constant |
+| `[[++modx_charset]]` | `login/*.php` | `'UTF-8'` |
+| `[[*introtext]]`, `[[*description]]` | `login/*.php` | Page-level PHP vars |
+| `[[!Login?...]]` | `login/login.php` | Custom PHP session login |
+| `[[!Register?...]]` | `login/register.php` | Custom PHP registration |
+| `[[!ForgotPassword?...]]` | `login/forgot-password.php` | Custom PHP password reset |
+| `[[!ChangePassword?...]]` | `login/change-password.php` | Custom PHP password update |
+| `[[!Profile]]` | `login/login.php` | Session user data |
+| `[[$test-widget]]`, `[[$client-widget]]`, etc. | `dashboard/dashboard.php` | PHP components |
+| `[[~4]]`, `[[~5]]`, `[[~10]]` | `login/*.php` | Hardcoded URL constants |
+
+---
+
+## Coding Patterns to Follow
+
+### Page template pattern
+```php
+<?php
+require_once __DIR__ . '/../../config/database.php';
+require_once __DIR__ . '/../../lib/auth.php';
+require_once __DIR__ . '/../../lib/csrf.php';
+
+requireLogin();
+
+$pageTitle = 'Page Title';
+$siteName  = 'Crop Monitor';
+
+include __DIR__ . '/../../layouts/header.php';
+include __DIR__ . '/../../layouts/navbar.php';
+include __DIR__ . '/../../layouts/sidebar.php';
+?>
+<!-- page content -->
+<?php include __DIR__ . '/../../layouts/footer.php'; ?>
+```
+
+### PDO query pattern
+```php
+$pdo  = getDBConnection();
+$stmt = $pdo->prepare('SELECT * FROM soil_records WHERE id = ? AND rand = ?');
+$stmt->execute([$recordId, $randId]);
+$row  = $stmt->fetch();
+```
+
+### Output escaping
+Always use `htmlspecialchars($value, ENT_QUOTES, 'UTF-8')` when echoing user or DB data into HTML.
+
+### CSRF in forms
+```php
+<input type="hidden" name="csrf_token" value="<?= generateCsrfToken() ?>">
+```
+
+### URL parameters used by soil analysis pages
+| Param | Type | Meaning |
+|-------|------|---------|
+| `cid` | int | client_records.id |
+| `rid` | int | soil_records.id |
+| `rand` | float | soil_records.rand (secondary auth token) |
+| `stid` | string | crop/soil type |
+
+---
+
+## Architecture Decisions
+
+- **No framework** — plain PHP, manual includes, no routing library
+- **Layout system** — `layouts/` includes (not a templating engine)
+- **Folder structure** — keep existing `dashboard/crop-analysis/` paths (no MVC rename needed unless requested)
+- **Authentication** — session-based (`$_SESSION['user_id']`); RBAC via `hasPermission()` not yet implemented
+- **Asset pipeline** — Bootstrap/jQuery via CDN; custom CSS/JS in `client-assets/`
+- **PDF generation** — `pdfchrome.php` (headless Chrome); also FPDF/mPDF patterns in some pages
+
+---
+
+## New auth files (2026-03-27)
+
+| File | Purpose |
+|------|---------|
+| `database/migrations/001_create_users.sql` | Run once — creates `users` + `password_resets` tables |
+| `lib/auth.php` | `loginUser()`, `logoutUser()`, `registerUser()`, `createPasswordResetToken()`, `validatePasswordResetToken()`, `resetPassword()`, `changePassword()` |
+| `login/_head.php` | Shared minimal HTML head for auth pages (Bootstrap 5, no sidebar) |
+| `login/_foot.php` | Closing tags + Bootstrap JS |
+| `login/login.php` | Email + password login with CSRF |
+| `login/register.php` | Full registration form with server-side validation |
+| `login/forgot-password.php` | Email submission for reset token |
+| `login/reset-password.php` | Token-validated password reset |
+| `login/logout.php` | Session destruction + redirect |
+
+**Note on email sending:** `forgot-password.php` generates the token and logs it via `error_log()`. No email is sent until SMTP is configured. The reset link format is `/login/reset-password.php?token={token}`.
+
+## Open Questions
+
+1. **URL compatibility**: Must URL slugs matching modX resource IDs (e.g., page 41, 66) be preserved for external links?
+2. **Credentials**: Move DB password to `.env` file and load with `vlucas/phpdotenv` or manual `parse_ini_file()`?
+3. **Email / SMTP**: Configure SMTP (e.g., PHPMailer + SMTP credentials) to enable password reset emails.
+4. **Role-based access**: `hasPermission()` is a stub — what roles/permissions are needed?
+
+---
+
+*Last updated: 2026-03-27*

+ 409 - 0
albrecht-soil-analysis.php

@@ -0,0 +1,409 @@
+<!doctype html>
+<html lang="en">
+    <head>
+        <title>[[*longtitle]] | [[++site_name]]</title>
+        <base href="[[!++site_url]]" >
+        <meta charset="[[++modx_charset]]" >
+        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" >
+        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" >
+        <meta name="keywords" content="[[*introtext]]" >
+        <meta name="description" content="[[*description]]" >
+        
+        <link rel="icon" href="client-assets/images/favicon.ico?v=2" type="image/x-icon" >
+        [[$dash-header]]
+        <!-- Custom styles for this template -->
+        <link rel="stylesheet" href="client-assets/css/greyscale.css" >
+    
+    </header>
+    
+    <body>
+        
+        <style>
+            .carousel-item {
+                height: 65vh;
+                min-height: 250px;
+                background: no-repeat center center scroll;
+                -webkit-background-size: cover;
+                -moz-background-size: cover;
+                -o-background-size: cover;
+                background-size: cover;
+            }
+            .nav-link {
+                /* font-size: 2em; */
+                text-align: center;
+                font-weight: bold;
+            }
+        </style>
+        
+        <!-- Navigation -->
+        <nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top" id="mainNav">
+        <div class="container">
+
+            <a class="navbar-brand js-scroll-trigger" href="#">
+                <img src="client-assets/images/favicon.ico" width="30" height="30" class="d-inline-block align-top" alt="">
+                Crop monitor
+            </a>
+            
+            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
+                <span class="navbar-toggler-icon"></span>
+            </button>
+            
+            <div class="collapse navbar-collapse" id="navbarResponsive">
+                
+                <form class="form-inline mx-auto justify-content-center d-none d-md-block">
+                    <button class="btn btn-sm btn-outline-success" type="button">Try a [[*longtitle]] Free</button>
+                </form>
+                
+                <ul class="navbar-nav ml-auto">
+                    <li class="nav-item">
+                        <a class="nav-link js-scroll-trigger" href="[[~18]]">About</a>
+                    </li>
+                    <li class="nav-item">
+                        <a class="nav-link js-scroll-trigger" href="[[~71]]">Blog</a>
+                    </li>
+                    <li class="nav-item">
+                        <a class="nav-link js-scroll-trigger" href="[[~17]]">Contact Us</a>
+                    </li>
+                    <li class="nav-item">
+                        <a class="nav-link js-scroll-trigger" href="[[~4]]">Login</a>
+                    </li>
+                    <div class="row">
+                        <li class="col pr-0 nav-item"><a class="nav-link text-dark" href="#"><i class="fab fa-facebook-f"></i></a></li>
+                        <li class="col px-0 nav-item"><a class="nav-link text-dark" href="#"><i class="fab fa-twitter"></i></a></li>
+                        <li class="col pl-0 nav-item"><a class="nav-link text-dark" href="#"><i class="fab fa-instagram"></i></a></li>
+                    </div>
+                </ul>
+            </div>
+        </div>
+        </nav>
+
+        <header>
+          <div id="carouselLandingPage" class="carousel slide" data-ride="carousel">
+            <ol class="carousel-indicators">
+              <li data-target="#carouselLandingPage" data-slide-to="0" class="active"></li>
+              <li data-target="#carouselLandingPage" data-slide-to="1"></li>
+              <li data-target="#carouselLandingPage" data-slide-to="2"></li>
+            </ol>
+            <div class="carousel-inner" role="listbox">
+                
+                [[!Gallery? 
+                    &album=`Albrech Landing Page`
+                    &thumbTpl=`slideshow_landing_page`
+                ]]
+                
+                <style>
+                    .slideOne {
+                        background-image: linear-gradient(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.25) ), url('client-assets/FredTemplate/images/work.png'), url('client-assets/FredTemplate/images/g6.jpg');
+                        background-size: cover, auto 300px;
+                        background-position: center, center right 25%;
+                    }
+                    .slideTwo {
+                        background-image: linear-gradient(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.25) ), url('client-assets/FredTemplate/images/work.png'), url('client-assets/FredTemplate/images/g7.jpg');
+                        background-size: cover, auto 300px;
+                        background-position: center, center right 25%;
+                    }
+                    .slideThree {
+                        background-image: linear-gradient(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.25) ), url('client-assets/FredTemplate/images/work.png'), url('client-assets/FredTemplate/images/g3.jpg');
+                        background-size: cover, auto 300px;
+                        background-position: center, center right 25%;
+                    }
+                </style>
+                
+                <!-- Slide One - Set the background image for this slide in the line below -->
+                <div class="carousel-item active slideOne" style=" ">
+                    <div class="carousel-caption text-left">
+                        <div class="row">
+                            <div class="col-md-9">
+                                <h2 class="text-bold">[[*longtitle]]</h2>
+                                <p class="lead col-12 col-md-6 ">[[*introtext]]</p>
+                                <p class="col-12 col-md-6 font-italic d-none d-md-block">[[*description]]</p>
+                                <a href="#" class="btn btn-outline-success">Get a Free Soil Report now!</a>
+                            </div>
+                            <div class="col-md-3 d-none d-md-block align-middle">
+                                <!-- <img src="client-assets/FredTemplate/images/work.png" class="img-fluid mx-auto" alt="Responsive image"> -->
+                            </div>
+                        </div>
+                    </div>
+                </div>
+              
+                <!-- Slide Two - Set the background image for this slide in the line below -->
+                <div class="carousel-item slideTwo" style="">
+                    <div class="carousel-caption text-left">
+                        <div class="row">
+                            <div class="col-md-8">
+                              <h2 class="text-bold">Second Slide</h2>
+                              <p class="lead col-12 col-md-6 ">This is a description for the second slide.</p>
+                              <p class="col-12 col-md-6 font-italic d-none d-md-block"></p>
+                              <a href="#" class="btn btn-outline-success">Get a Free Soil Report now!</a>
+                            </div>
+                            <div class="col-md-4 d-none d-md-block">
+                                <!-- <img src="client-assets/FredTemplate/images/work.png" class="img-fluid mx-auto align-middle" alt="Responsive image"> -->
+                            </div>
+                        </div>
+                    </div>
+                </div>
+              
+              <!-- Slide Three - Set the background image for this slide in the line below -->
+              <div class="carousel-item slideThree" style="">
+                <div class="carousel-caption text-left">
+                    <div class="row">
+                        <div class="col-md-8">
+                          <h2 class="text-bold">Third Slide</h2>
+                          <p class="lead col-12 col-md-6 ">This is a description for the third slide.</p>
+                          <p class="col-12 col-md-6 font-italic d-none d-md-block"></p>
+                          <a href="#" class="btn btn-outline-success">Get a Free Soil Report now!</a>
+                        </div>
+                        <div class="col-md-4 d-none d-md-block">
+                            <!-- <img src="client-assets/FredTemplate/images/work.png" class="img-fluid mx-auto align-middle" alt="Responsive image"> -->
+                        </div>
+                    </div>
+                </div>
+              </div>
+            </div>
+            
+            <a class="carousel-control-prev" href="#carouselLandingPage" role="button" data-slide="prev">
+                  <span class="carousel-control-prev-icon" aria-hidden="true"></span>
+                  <span class="sr-only">Previous</span>
+                </a>
+            <a class="carousel-control-next" href="#carouselLandingPage" role="button" data-slide="next">
+                  <span class="carousel-control-next-icon" aria-hidden="true"></span>
+                  <span class="sr-only">Next</span>
+                </a>
+          </div>
+        </header>
+
+        <!-- Page Content --> 
+<section id="about" class="py-5"> 
+  <div class="container"> 
+    <h2 class="text-success">Albrecht Soil Analysis</h2> 
+     
+    <hr>
+    
+    <blockquote class="blockquote text-center"> 
+        <p class="mb-0">The soil is the ‘<i>creative material</i>’ of most of the basic needs of life. Creation starts with a handful of dust.</p> 
+        <footer class="blockquote-footer">Dr. William A. Abrecht. <cite title="Source Title">University of Missouri</cite></footer> 
+    </blockquote> 
+    
+    <hr>
+    
+    <div class="row card-deck"> 
+        <div class="card border-success text-center mb-3"> 
+            <div class="card-body">
+                <h2 class="card-title text-center">Physical</h2> 
+                <i class="fas fa-atom fa-3x"></i> 
+                <p class="card-text">When you correct cation balance, you have addressed soil chemistry, which improves the physical structure of the soil.</p> 
+            </div>
+        </div> 
+         
+        <div class="card border-success text-center mb-3"> 
+            <div class="card-body">
+                <h2 class="card-title text-center">Chemical</h2> 
+                <i class="fas fa-flask fa-3x"></i> 
+                <p class="card-text">The correction of cation balance and the provision of minimum levels of micronutrients tkatesk care of the chemical part of soil productivity and health.</p>
+            </div>
+        </div> 
+         
+        <div class="card border-success text-center mb-3"> 
+            <div class="card-body">
+                <h2 class="card-title text-center">Biological</h2> 
+                <i class="fas fa-bug fa-3x"></i> 
+                <p class="card-text">The essential understanding involves a recognition that the purpose of cation balancing is to stimulate soil biology, and much of the beneficial response relates to firing up this workforce</p> 
+            </div>
+        </div> 
+    </div> 
+     
+    <hr> 
+
+  </div> 
+</section> 
+
+<!-- Projects Section -->
+<section id="services" class="projects-section bg-light pt-2">
+    <div class="container">
+         [[!getResources? 
+           &parents=`[[*id]]`
+           &level=`0`
+           &includeTVs=`1` 
+           &processTVs=`1`
+           &includeContent=`1`
+           &tplFirst=`projectsSectionFIRST`
+           &tpl=`projectsSectionEVEN`
+           &tplOdd=`projectsSectionODD`
+           &sortby=`FIELD(modResource.id, 95 )`
+           &sortdir=`ASC`
+           &limit=`0`
+         ]]
+    </div>
+</section>
+          
+<section class="bg-dark text-light py-5" id="blog" > 
+    <div class="container"> 
+        <p class="lead">
+            
+        </p> 
+    </div> 
+</section> 
+
+<section class="py-5" id="contact" > 
+    <div class="container"> 
+        <p class="lead">
+            Dr. Albrecht saw a direct link between soil quality and food quality, drawing direct connection between poor quality forage crops, and ill health in livestock.
+        </p> 
+    </div> 
+</section> 
+
+<section class="bg-dark text-light py-5" id="login" > 
+    <div class="container"> 
+        <p class="lead">
+            Feed the soil to feed the plant is another vital concept of the Albrecht Model of soil building.
+        </p> 
+    </div> 
+</section> 
+
+<section class="py-5" id="" > 
+    <div class="container"> 
+        <p class="lead">
+            Feed the soil to feed the plant is another vital concept of the Albrecht Model of soil building.
+        </p> 
+    </div> 
+</section>
+        
+
+<!-- Signup Section -->
+<section id="signup" class="signup-section">
+    <div class="container">
+      <div class="row">
+        <div class="col-md-10 col-lg-8 mx-auto text-center">
+
+          <i class="far fa-paper-plane fa-2x mb-2 text-white"></i>
+          <h2 class="text-white mb-5">Subscribe to receive updates!</h2>
+                [[!FormIt?
+                    &hooks=`MailChimpSubscribe`
+                    &validate=`email:email:required,name:required`
+                    &validationErrorMessage=`true`
+                    &clearFieldsOnSuccess=`1`
+                    &submitVar=`newsletter-submit`
+                    &mailchimpListId=`04e221f3bc`
+                    &mailchimpFields=`name=FNAME,email=EMAIL`
+                    &mailchimpSubscribeField=`newsletter`
+                    &mailchimpSubscribeFieldValue=`1`
+                    &successMessage=`Thankyou for subscribing`
+                ]]
+          <span>[[!+fi.successMessage:notempty=`<h2 class="text-white mb-5">[[!+fi.successMessage]]</h2>`]]
+          [[!+fi.validation_error_message:notempty=`<h2 class="text-white mb-5">[[+errors]] [[!+fi.validation_error_message]]</h2>`]]</span>
+          <form action="[[~[[*id]]]]#signup" method="post" class="form-inline d-flex">
+            <input type="hidden" name="nospam" value="" />
+            <input class="form-control flex-fill mr-0 mr-sm-2 mb-3 mb-sm-0" type="email" name="email" id="email" value="[[!+fi.email]]" placeholder="Email Address" >
+            <input class="form-control flex-fill mr-0 mr-sm-2 mb-3 mb-sm-0" type="text" name="name" id="name" value="[[!+fi.name]]" placeholder="Full Name">
+            <input type="submit" name="newsletter-submit" class="btn btn-success mx-auto" value="Subscribe" />
+          </form>
+          
+        </div>
+      </div>
+    </div>
+</section>
+
+
+<!-- Contact Section -->
+<section id="contact"  class="contact-section bg-black">
+    <div class="container">
+        
+        <div class="row">
+            <div class="col-md-4 mb-3 mb-md-0">
+              <div class="card py-4 h-100">
+                <div class="card-body text-center">
+                  <i class="fas fa-map-marked-alt text-primary mb-2"></i>
+                  <h4 class="text-uppercase m-0">Address</h4>
+                  <hr class="my-4">
+                  <div class="small text-black-50">34 Coplestone Street,<br> Scottsdale, Tasmania 7260</div>
+                </div>
+              </div>
+            </div>
+    
+            <div class="col-md-4 mb-3 mb-md-0">
+              <div class="card py-4 h-100">
+                <div class="card-body text-center">
+                  <i class="fas fa-envelope text-primary mb-2"></i>
+                  <h4 class="text-uppercase m-0">Email</h4>
+                  <hr class="my-4">
+                  <div class="small text-black-50">
+                    <a href="#">enquiry@cropmonitor.info</a>
+                  </div>
+                </div>
+              </div>
+            </div>
+    
+            <div class="col-md-4 mb-3 mb-md-0">
+              <div class="card py-4 h-100">
+                <div class="card-body text-center">
+                  <i class="fas fa-mobile-alt text-primary mb-2"></i>
+                  <h4 class="text-uppercase m-0">Phone</h4>
+                  <hr class="my-4">
+                  <div class="small text-black-50">0417 728 061</div>
+                </div>
+              </div>
+            </div>
+        </div>
+    
+        <div class="row">
+            <div class="col mb-3 mt-3 mb-md-0">
+              <div class="card py-4 h-100">
+                <div class="card-body text-center">
+                  <i class="fas fa-mobile-alt text-primary mb-2"></i>
+                  <h4 class="text-uppercase m-0">Contact Us</h4>
+                  <hr class="my-4">
+                        [[!FormIt?
+                            &hooks=`spam,email`
+                            &submitVar=`enquiry-submit`
+                            &emailTpl=`CMenquiryEmailTpl`
+                            &emailTo=`enquiries@cropmonitor.info`
+                            &successMessage=`Thankyou for contacting us, we will be in touch soon.`
+                            &validationErrorMessage=`true`
+                            &validate=`nospam:blank,
+                                name:required,
+                                email:email:required,
+                                text:required:stripTags`
+                        ]]  
+                    <span>
+                        [[!+fi.successMessage:notempty=`<div class="small text-black-50">[[!+fi.successMessage]]</div>`]]
+                        [[!+fi.validation_error_message:notempty=`<div class="small text-black-50">[[+errors]] [[!+fi.validation_error_message]]</div>`]]
+                    </span>
+                    <form action="[[~[[*id]]]]#contact" method="post" class="form-inline d-flex">
+                        <input type="hidden" name="nospam" value="" />
+                        <input class="form-control form-control-sm flex-fill mr-0 mr-sm-2 mb-3 mb-sm-0" type="email" name="email" id="email" value="[[!+fi.email]]" placeholder="Email Address" >
+                        <input class="form-control form-control-sm flex-fill mr-0 mr-sm-2 mb-3 mb-sm-0" type="text" name="name" id="name" value="[[!+fi.name]]" placeholder="Full Name">
+                        <textarea class="form-control form-control-sm flex-fill mr-0 mr-sm-2 mb-3 mb-sm-0" name="text" id="text" rows="1" value="[[!+fi.text]]" placeholder="Your enquiry is about...." ></textarea>
+                        <input type="submit" name="enquiry-submit" class="btn-sm btn btn-success" value="Submit" />
+                    </form>
+                </div>
+              </div>
+            </div>
+        </div>
+    
+        <!--
+        <div class="social d-flex justify-content-center">
+            <a href="#" class="mx-2">
+              <i class="fab fa-youtube"></i>
+            </a>
+            <a href="#" class="mx-2">
+              <i class="fab fa-facebook-f"></i>
+            </a>
+            <a href="#" class="mx-2">
+              <i class="fab fa-linkedin"></i>
+            </a>
+        </div>
+        -->
+    </div>
+</section>        
+        <!-- Footer -->
+        <footer class="footer bg-dark py-2 text-center text-white-50">
+            <div class="container">
+              [[SimpleCopyright? &startYear=`2005`]]. All Rights Reserved<a href="[[~1]]"></a>
+            </div>
+        </footer>
+        
+    </body>
+
+    [[$dash-footer]]
+
+</html>

+ 3 - 3
config/database.php

@@ -7,9 +7,9 @@
 
 // Database configuration
 define('DB_HOST', 'localhost');
-define('DB_NAME', 'cropmonitor');
-define('DB_USER', 'cropmonitor');
-define('DB_PASS', 'brvnCcaEYxlPCS3');
+define('DB_NAME', 'devcrop');
+define('DB_USER', 'devcrop');
+define('DB_PASS', 'PGrspJH7NBbNiJYMbEYRHIJHw');
 define('DB_CHARSET', 'utf8');
 
 // Create PDO instance

+ 42 - 0
database/migrations/001_create_users.sql

@@ -0,0 +1,42 @@
+-- ============================================================
+-- Migration 001: User authentication tables
+-- Run once against the cropmonitor database
+-- ============================================================
+
+-- Users table (replaces modX user management)
+CREATE TABLE IF NOT EXISTS `users` (
+    `id`          INT UNSIGNED NOT NULL AUTO_INCREMENT,
+    `fullname`    VARCHAR(255) NOT NULL,
+    `email`       VARCHAR(255) NOT NULL,
+    `password`    VARCHAR(255) NOT NULL,          -- bcrypt hash
+    `company`     VARCHAR(255) DEFAULT '',
+    `mobilephone` VARCHAR(50)  DEFAULT '',
+    `industry`    VARCHAR(100) DEFAULT '',
+    `role`        VARCHAR(100) DEFAULT '',
+    `city`        VARCHAR(100) DEFAULT '',
+    `state`       VARCHAR(100) DEFAULT '',
+    `postcode`    VARCHAR(20)  DEFAULT '',
+    `country`     VARCHAR(100) DEFAULT 'Australia',
+    `active`      TINYINT(1)   NOT NULL DEFAULT 1,
+    `created_at`  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `updated_at`  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uq_users_email` (`email`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- Password reset tokens
+CREATE TABLE IF NOT EXISTS `password_resets` (
+    `id`         INT UNSIGNED NOT NULL AUTO_INCREMENT,
+    `email`      VARCHAR(255) NOT NULL,
+    `token`      VARCHAR(64)  NOT NULL,           -- bin2hex(random_bytes(32))
+    `created_at` DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `expires_at` DATETIME     NOT NULL,            -- created_at + 1 hour
+    PRIMARY KEY (`id`),
+    KEY `idx_resets_email` (`email`),
+    KEY `idx_resets_token` (`token`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- Link existing client_records rows to the new users table.
+-- The old modx_user_id (always 1 in legacy data) is replaced by users.id.
+-- No structural change needed: client_records.modx_user_id stays as the FK column;
+-- just populate users first, then update client_records rows as users are created.

+ 201 - 17
lib/auth.php

@@ -2,28 +2,40 @@
 /**
  * lib/auth.php
  *
- * Authentication and authorization functions.
+ * Authentication and authorisation functions.
+ * Requires config/database.php to be included before use.
  */
 
-/**
- * Check if user is logged in
- */
+if (session_status() === PHP_SESSION_NONE) {
+    session_start();
+}
+
+// ---------------------------------------------------------------------------
+// Session helpers
+// ---------------------------------------------------------------------------
+
 function isLoggedIn(): bool
 {
     return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
 }
 
-/**
- * Get current user ID
- */
 function getCurrentUserId(): ?int
 {
-    return $_SESSION['user_id'] ?? null;
+    return isset($_SESSION['user_id']) ? (int) $_SESSION['user_id'] : null;
+}
+
+function getCurrentUser(): ?array
+{
+    if (!isLoggedIn()) {
+        return null;
+    }
+    return [
+        'id'       => (int) $_SESSION['user_id'],
+        'fullname' => $_SESSION['user_name']  ?? '',
+        'email'    => $_SESSION['user_email'] ?? '',
+    ];
 }
 
-/**
- * Require user to be logged in, redirect if not
- */
 function requireLogin(): void
 {
     if (!isLoggedIn()) {
@@ -32,17 +44,189 @@ function requireLogin(): void
     }
 }
 
+function hasPermission(string $permission): bool
+{
+    // Stub — extend with role checks when roles are introduced
+    return isLoggedIn();
+}
+
+// ---------------------------------------------------------------------------
+// Login / Logout
+// ---------------------------------------------------------------------------
+
 /**
- * Check if user has specific role/permission
+ * Attempt login with email + plain-text password.
+ * Returns user row array on success, null on failure.
  */
-function hasPermission(string $permission): bool
+function loginUser(string $email, string $password): ?array
 {
-    if (!isLoggedIn()) {
+    $pdo  = getDBConnection();
+    $stmt = $pdo->prepare(
+        'SELECT id, fullname, email, password FROM users WHERE email = ? AND active = 1 LIMIT 1'
+    );
+    $stmt->execute([strtolower(trim($email))]);
+    $user = $stmt->fetch();
+
+    if (!$user || !password_verify($password, $user['password'])) {
+        return null;
+    }
+
+    // Rehash on cost/algorithm upgrade
+    if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
+        $pdo->prepare('UPDATE users SET password = ? WHERE id = ?')
+            ->execute([password_hash($password, PASSWORD_DEFAULT), $user['id']]);
+    }
+
+    session_regenerate_id(true);
+    $_SESSION['user_id']    = $user['id'];
+    $_SESSION['user_name']  = $user['fullname'];
+    $_SESSION['user_email'] = $user['email'];
+
+    return $user;
+}
+
+/**
+ * Destroy session completely and clear the session cookie.
+ */
+function logoutUser(): void
+{
+    $_SESSION = [];
+    if (ini_get('session.use_cookies')) {
+        $p = session_get_cookie_params();
+        setcookie(
+            session_name(), '', time() - 42000,
+            $p['path'], $p['domain'], $p['secure'], $p['httponly']
+        );
+    }
+    session_destroy();
+}
+
+// ---------------------------------------------------------------------------
+// Registration
+// ---------------------------------------------------------------------------
+
+/**
+ * Register a new user.
+ * Returns ['success' => true, 'user_id' => int]
+ *      or ['success' => false, 'error' => string]
+ */
+function registerUser(array $data): array
+{
+    $pdo   = getDBConnection();
+    $email = strtolower(trim($data['email'] ?? ''));
+
+    // Duplicate email check
+    $stmt = $pdo->prepare('SELECT id FROM users WHERE email = ? LIMIT 1');
+    $stmt->execute([$email]);
+    if ($stmt->fetch()) {
+        return ['success' => false, 'error' => 'An account with that email already exists.'];
+    }
+
+    $stmt = $pdo->prepare('
+        INSERT INTO users
+            (fullname, email, password, company, mobilephone, industry, role, city, state, postcode, country, active, created_at)
+        VALUES
+            (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, NOW())
+    ');
+    $stmt->execute([
+        trim($data['fullname']    ?? ''),
+        $email,
+        password_hash($data['password'], PASSWORD_DEFAULT),
+        trim($data['company']     ?? ''),
+        trim($data['mobilephone'] ?? ''),
+        $data['industry'] ?? '',
+        $data['role']     ?? '',
+        trim($data['city']     ?? ''),
+        $data['state']    ?? '',
+        trim($data['postcode'] ?? ''),
+        $data['country']  ?? 'Australia',
+    ]);
+
+    return ['success' => true, 'user_id' => (int) $pdo->lastInsertId()];
+}
+
+// ---------------------------------------------------------------------------
+// Password reset
+// ---------------------------------------------------------------------------
+
+/**
+ * Create a password-reset token for $email (1-hour expiry).
+ * Returns the raw token string, or null if the email doesn't exist.
+ */
+function createPasswordResetToken(string $email): ?string
+{
+    $pdo   = getDBConnection();
+    $email = strtolower(trim($email));
+
+    $stmt = $pdo->prepare('SELECT id FROM users WHERE email = ? AND active = 1 LIMIT 1');
+    $stmt->execute([$email]);
+    if (!$stmt->fetch()) {
+        return null;
+    }
+
+    // Remove any previous tokens for this email
+    $pdo->prepare('DELETE FROM password_resets WHERE email = ?')->execute([$email]);
+
+    $token = bin2hex(random_bytes(32));
+    $pdo->prepare(
+        'INSERT INTO password_resets (email, token, created_at, expires_at)
+         VALUES (?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 1 HOUR))'
+    )->execute([$email, $token]);
+
+    return $token;
+}
+
+/**
+ * Validate a reset token. Returns the associated email on success, null if invalid/expired.
+ */
+function validatePasswordResetToken(string $token): ?string
+{
+    $pdo  = getDBConnection();
+    $stmt = $pdo->prepare(
+        'SELECT email FROM password_resets WHERE token = ? AND expires_at > NOW() LIMIT 1'
+    );
+    $stmt->execute([$token]);
+    $row = $stmt->fetch();
+    return $row ? $row['email'] : null;
+}
+
+/**
+ * Update the user's password and delete the reset token.
+ */
+function resetPassword(string $token, string $newPassword): bool
+{
+    $pdo   = getDBConnection();
+    $email = validatePasswordResetToken($token);
+
+    if (!$email) {
         return false;
     }
 
-    // TODO: Implement proper role-based permissions
-    // For now, just check if user is logged in
+    $pdo->prepare('UPDATE users SET password = ? WHERE email = ?')
+        ->execute([password_hash($newPassword, PASSWORD_DEFAULT), $email]);
+
+    $pdo->prepare('DELETE FROM password_resets WHERE email = ?')->execute([$email]);
+
+    return true;
+}
+
+/**
+ * Change password for currently logged-in user after verifying old password.
+ * Returns true on success, false if old password is wrong.
+ */
+function changePassword(int $userId, string $oldPassword, string $newPassword): bool
+{
+    $pdo  = getDBConnection();
+    $stmt = $pdo->prepare('SELECT password FROM users WHERE id = ? LIMIT 1');
+    $stmt->execute([$userId]);
+    $user = $stmt->fetch();
+
+    if (!$user || !password_verify($oldPassword, $user['password'])) {
+        return false;
+    }
+
+    $pdo->prepare('UPDATE users SET password = ? WHERE id = ?')
+        ->execute([password_hash($newPassword, PASSWORD_DEFAULT), $userId]);
+
     return true;
 }
-?>

+ 3 - 0
login/_foot.php

@@ -0,0 +1,3 @@
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
+</body>
+</html>

+ 23 - 0
login/_head.php

@@ -0,0 +1,23 @@
+<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title><?= htmlspecialchars($pageTitle ?? 'Crop Monitor', ENT_QUOTES, 'UTF-8') ?> | Crop Monitor</title>
+
+    <link rel="icon" href="/client-assets/images/favicon.ico?v=2" type="image/x-icon">
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
+    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.9.0/css/all.css" integrity="sha256-PF6MatZtiJ8/c9O9HQ8uSUXr++R9KBYu4gbNG5511WE=" crossorigin="anonymous" rel="stylesheet">
+    <script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
+    <style>
+        body { background: linear-gradient(180deg, #1cc88a 10%, #17a673 100%); }
+        .bg-login-image  { background: url('/client-assets/images/water-droplet-on-leaf.jpg') center/cover no-repeat; }
+        .bg-register-image { background: url('/client-assets/images/water-droplet-on-leaf.jpg') center/cover no-repeat; }
+        .form-control-user { border-radius: 10rem; padding: .75rem 1rem; font-size: .85rem; }
+        .btn-user { border-radius: 10rem; padding: .75rem 1rem; font-size: .85rem; }
+        .error { color: #e74a3b; font-size: .8rem; display: block; margin-top: .25rem; }
+        .alert { border-radius: .5rem; }
+    </style>
+</head>
+<body>

+ 91 - 43
login/forgot-password.php

@@ -1,47 +1,95 @@
-<style>
-    .bg-password-image {
-        background: url(https://cropmonitor.info/client-assets/FredTemplate/images/g8.jpg); /* https://source.unsplash.com/K4mSJ7kc0As/600x800 */
-        background-position: center;
-        background-size: cover;
+<?php
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../lib/auth.php';
+require_once __DIR__ . '/../lib/csrf.php';
+
+if (isLoggedIn()) {
+    header('Location: /dashboard/dashboard.php');
+    exit;
+}
+
+$sent  = false;
+$error = '';
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+    if (!verifyCsrfToken($_POST['csrf_token'] ?? '')) {
+        $error = 'Invalid request. Please try again.';
+    } else {
+        $email = trim($_POST['email'] ?? '');
+
+        if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
+            $error = 'Please enter a valid email address.';
+        } else {
+            // createPasswordResetToken returns null if email not found,
+            // but we show the same success message to avoid email enumeration.
+            $token = createPasswordResetToken($email);
+
+            if ($token !== null) {
+                // TODO: send email with reset link.
+                // Reset link: /login/reset-password.php?token={$token}
+                // Until SMTP is configured, the token is logged for development.
+                error_log("Password reset token for {$email}: {$token}");
+            }
+
+            $sent = true; // Always show success to prevent email enumeration
+        }
     }
-</style>
+}
 
-<body class="bg-gradient-success" >
-      
-    <div class="container">
-    <!-- Outer Row -->
+$pageTitle = 'Forgot Password';
+include __DIR__ . '/_head.php';
+?>
+
+<div class="container">
     <div class="row justify-content-center">
-    <div class="col-xl-10 col-lg-12 col-md-9">
-    	<div class="card o-hidden border-0 shadow-lg my-5">
-    		<div class="card-body p-0">
-    			<!-- Nested Row within Card Body -->
-    			<div class="row">
-    				<div class="col-lg-6 d-none d-lg-block bg-password-image"></div>
-    				<div class="col-lg-6">
-    					<div class="p-5">
-    						<div class="text-center">
-    							<h1 class="h4 text-gray-900 mb-2">Forgot Your Password?</h1>
-    							<p class="mb-4">We get it, stuff happens. Just enter your email address below and we'll send you a link to reset your password!</p>
-    						</div>
-    						
-    						[[!ForgotPassword? 
-                                &resetResourceId=`3`
-                                &tpl=`CMlgnForgotPassTpl`
-                                &loginResourceId=`4`
-                            ]]
-    						
-    						<hr>
-    						<div class="text-center">
-    							<a class="small" href="[[~5]]">Create an Account!</a>
-    						</div>
-    						<div class="text-center">
-    							<a class="small" href="[[~4]]">Already have an account? Login!</a>
-    						</div>
-    					</div>
-    				</div>
-    			</div>
-    		</div>
-    	</div>
+        <div class="col-xl-10 col-lg-12 col-md-9">
+            <div class="card o-hidden border-0 shadow-lg my-5">
+                <div class="card-body p-0">
+                    <div class="row">
+                        <div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
+                        <div class="col-lg-6">
+                            <div class="p-5">
+                                <div class="text-center mb-4">
+                                    <h1 class="h4 text-gray-900">Forgot Your Password?</h1>
+                                    <p class="text-muted small">Enter your email and we'll send you a reset link.</p>
+                                </div>
+
+                                <?php if ($sent): ?>
+                                    <div class="alert alert-success" role="alert">
+                                        If that email is registered, a reset link has been sent. Please check your inbox.
+                                    </div>
+                                <?php else: ?>
+                                    <?php if ($error !== ''): ?>
+                                        <div class="alert alert-danger"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div>
+                                    <?php endif; ?>
+
+                                    <form method="POST" action="/login/forgot-password.php" novalidate>
+                                        <input type="hidden" name="csrf_token" value="<?= generateCsrfToken() ?>">
+                                        <div class="mb-3">
+                                            <input type="email" name="email" class="form-control form-control-user"
+                                                   placeholder="Email Address" required autofocus
+                                                   value="<?= htmlspecialchars($_POST['email'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
+                                        </div>
+                                        <button type="submit" class="btn btn-success btn-user btn-block w-100 mb-3">
+                                            Send Reset Link
+                                        </button>
+                                    </form>
+                                <?php endif; ?>
+
+                                <hr>
+                                <div class="text-center">
+                                    <a class="small" href="/login/register.php">Create an Account!</a>
+                                </div>
+                                <div class="text-center">
+                                    <a class="small" href="/login/login.php">Already have an account? Login!</a>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
     </div>
-</body>
-`
+</div>
+
+<?php include __DIR__ . '/_foot.php'; ?>

+ 101 - 77
login/login.php

@@ -1,85 +1,109 @@
-<!doctype html>
-<html lang="en">
-	<head>
-		<title>[[*longtitle]] | [[++site_name]]</title>
-		<base href="[[!++site_url]]" >
-		<meta charset="[[++modx_charset]]" >
-		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" >
-		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" >
-		<meta name="keywords" content="[[*introtext]]" >
-		<meta name="description" content="[[*description]]" >
+<?php
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../lib/auth.php';
+require_once __DIR__ . '/../lib/csrf.php';
 
-		[[!Profile]]
+// Already logged in → go to dashboard
+if (isLoggedIn()) {
+    header('Location: /dashboard/dashboard.php');
+    exit;
+}
 
-		[[$dash-header]]
-		
-		
-		<link rel='stylesheet' type='text/css' href="/client-assets/css/dashboard.css" />
-		<script src="https://unpkg.com/gijgo@1.9.11/js/gijgo.min.js" type="text/javascript"></script>
-		<link href="https://unpkg.com/gijgo@1.9.11/css/gijgo.min.css" rel="stylesheet" type="text/css" />
-		<script src="client-assets/js/skycons.js"></script>
-    
-        <link rel="stylesheet" href="client-assets/home/css/graphing.css" media="screen">
-        <link rel="stylesheet" href="client-assets/home/css/alux.min.css" media="screen">
+$error = '';
 
-    </head>
-	
-<style>
-	.bg-login-image {
-	background: url(client-assets/images/water-droplet-on-leaf.jpg);
-	background-position: center;
-	background-size: cover;
-	}
-</style>
-	
-<body class="bg-gradient-success" >
-    
-    <div class="container">
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+    if (!verifyCsrfToken($_POST['csrf_token'] ?? '')) {
+        $error = 'Invalid request. Please try again.';
+    } else {
+        $email    = trim($_POST['email']    ?? '');
+        $password = trim($_POST['password'] ?? '');
 
-        <!-- Outer Row -->
-        <div class="row justify-content-center">
-           <div class="col-xl-10 col-lg-12 col-md-9">
-              <div class="card o-hidden border-0 shadow-lg my-5">
-                 <div class="card-body p-0">
-                    <!-- Nested Row within Card Body -->
-                    <div class="row"> 
-                       <div class="col-lg-6 d-none d-lg-block bg-login-image">
-                           
-                       </div>
-                       <div class="col-lg-6">
-                          <div class="p-5">
-                             <div class="text-center">
-                                <h1 class="h4 text-gray-900 mb-4">Welcome Back!</h1>
-                             </div>
-                                [[!Login? 
-                                    &loginTpl=`CMlgnLoginTpl`
-                                    &loginResourceId=`9`
-                                    &loginViaEmail=`true`
-                                    &errTpl=`lgnErrTpl`
-                                    &logoutTpl=`CMlgnLogoutTpl`
-                                    &logoutResourceId=`4`
-                                    &tpl=`CMlgnForgotPassTpl`
-                                    &resetResourceId=`10`
-                                ]]
-                             <hr>
-                             <div class="text-center">
-                                <a class="small" href="[[~10]]">Forgot Password?</a>
-                             </div>
-                             <div class="text-center">
-                                <a class="small" href="[[~5]]">Create an Account!</a>
-                             </div>
-                          </div>
-                       </div>
-                    </div>
-                 </div>
-              </div>
-           </div>
-        </div>
+        if ($email === '' || $password === '') {
+            $error = 'Please enter your email and password.';
+        } else {
+            $user = loginUser($email, $password);
+            if ($user) {
+                $redirect = $_GET['redirect'] ?? '/dashboard/dashboard.php';
+                // Sanitise redirect to prevent open redirect
+                if (!str_starts_with($redirect, '/')) {
+                    $redirect = '/dashboard/dashboard.php';
+                }
+                header('Location: ' . $redirect);
+                exit;
+            } else {
+                $error = 'Invalid email or password.';
+            }
+        }
+    }
+}
 
-    </div>
+$pageTitle = 'Login';
+include __DIR__ . '/_head.php';
+?>
+
+<div class="container">
+    <div class="row justify-content-center">
+        <div class="col-xl-10 col-lg-12 col-md-9">
+            <div class="card o-hidden border-0 shadow-lg my-5">
+                <div class="card-body p-0">
+                    <div class="row">
+                        <div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
+                        <div class="col-lg-6">
+                            <div class="p-5">
+                                <div class="text-center mb-4">
+                                    <h1 class="h4 text-gray-900">Welcome Back!</h1>
+                                </div>
+
+                                <?php if ($error !== ''): ?>
+                                    <div class="alert alert-danger" role="alert">
+                                        <?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?>
+                                    </div>
+                                <?php endif; ?>
+
+                                <?php if (isset($_GET['registered'])): ?>
+                                    <div class="alert alert-success" role="alert">
+                                        Account created! You can now log in.
+                                    </div>
+                                <?php endif; ?>
 
-</body>
+                                <?php if (isset($_GET['reset'])): ?>
+                                    <div class="alert alert-success" role="alert">
+                                        Password reset successfully. Please log in.
+                                    </div>
+                                <?php endif; ?>
 
-[[$dash-footer]]
+                                <form method="POST" action="/login/login.php" novalidate>
+                                    <input type="hidden" name="csrf_token" value="<?= generateCsrfToken() ?>">
+
+                                    <div class="mb-3">
+                                        <input type="email" name="email" class="form-control form-control-user"
+                                               placeholder="Email Address"
+                                               value="<?= htmlspecialchars($_POST['email'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
+                                               required autofocus>
+                                    </div>
+                                    <div class="mb-3">
+                                        <input type="password" name="password" class="form-control form-control-user"
+                                               placeholder="Password" required>
+                                    </div>
+                                    <button type="submit" class="btn btn-success btn-user btn-block w-100 mb-3">
+                                        Log In
+                                    </button>
+                                </form>
+
+                                <hr>
+                                <div class="text-center">
+                                    <a class="small" href="/login/forgot-password.php">Forgot Password?</a>
+                                </div>
+                                <div class="text-center">
+                                    <a class="small" href="/login/register.php">Create an Account!</a>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
 
-</html>
+<?php include __DIR__ . '/_foot.php'; ?>

+ 8 - 0
login/logout.php

@@ -0,0 +1,8 @@
+<?php
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../lib/auth.php';
+
+logoutUser();
+
+header('Location: /login/login.php');
+exit;

+ 246 - 151
login/register.php

@@ -1,151 +1,246 @@
-<style>
-	.bg-register-image {
-	background: url(https://cropmonitor.info/client-assets/images/water-droplet-on-leaf.jpg); /* https://source.unsplash.com/K4mSJ7kc0As/600x800 */
-	background-position: center;
-	background-size: cover;
-	}
-</style>
-
-<body class="bg-gradient-success">
-	<div class="container">
-	<div class="card o-hidden border-0 shadow-lg my-5">
-		<div class="card-body p-0">
-			<!-- Nested Row within Card Body -->
-			<div class="row">
-				<div class="col-lg-5 d-none d-lg-block bg-register-image"></div>
-				<div class="col-lg-7">
-					<div class="p-5">
-						<div class="text-center">
-							<h1 class="h4 text-gray-900 mb-4">Create an Account!</h1>
-							<div class="registerMessage">[[!+reg.error.message]]</div>
-						</div>
-						[[!Register?
-    						&submitVar=`registerbtn`
-    						&usernameField=`email`
-    						&activationEmailTpl=`CMlgnActivateEmailTpl`
-    						&activationEmailSubject=`Please activate your account!`
-    						&activationResourceId=`6`
-    						&submittedResourceId=`6`
-    						&usergroups=`Members`
-    						&validate=`nospam:blank,
-    						password:required:minLength=^6^,
-    						password_confirm:password_confirm=^password^,
-    						fullname:required,
-    						email:required,
-    						mobilephone:required`
-    						    &placeholderPrefix=`reg.`
-    						    &postHooks=`notifyAdmin`
-						]]
-						<form class="user" action="[[~[[*id]]]]" method="post">
-							<input type="hidden" name="nospam" value="[[!+reg.nospam]]" /> 
-							<div class="form-group row">
-								<div class="col-md-6 mb-3 mb-md-0">
-									<input type="text" class="form-control form-control-user" placeholder="Full Name" name="fullname" id="fullname" value="[[!+reg.fullname]]" required autofocus>
-									<span class="invalid-feedback error">[[!+reg.error.fullname]]</span>
-								</div>
-								<div class="col-md-6">
-									<input type="text" class="form-control form-control-user" placeholder="Company Name" name="company" id="company" value="[[!+reg.fax]]">
-									<span class="invalid-feedback error">[[!+reg.error.fax]]</span>
-								</div>
-							</div>
-							<div class="form-group">
-								<input type="email" class="form-control form-control-user" placeholder="Email" name="email" id="email" value="[[!+reg.email]]" required>
-								<span class="invalid-feedback error">[[!+reg.error.email]]</span>
-							</div>
-							<div class="form-group">
-								<input type="text" class="form-control form-control-user" placeholder="Mobile Phone" name="mobilephone" id="mobilephone" value="[[!+reg.mobilephone]]" required>
-								<span class="invalid-feedback error">[[!+reg.error.mobilephone]]</span>
-							</div>
-							
-							<div class="form-group row">
-								<div class="col-md-6 mb-3 mb-md-0">
-									<select class="form-control" name="industry" name="industry" id="industry" value="[[!+reg.website]]">
-										<option selected>Choose your industry</option>
-										<option name="broadacre">Broadacre</option>
-										<option name="viticulture">Viticulture</option>
-										<option name="horticulture">Horticulture</option>
-										<option name="permaculture">Permaculture</option>
-										<option name="dairy">Dairy</option>
-									</select>
-									<span class="invalid-feedback error">[[!+reg.error.website]]</span>
-								</div>
-								<div class="col-md-6">
-									<select class="form-control" name="role" name="role" id="role" value="[[!+reg.phone]]">
-										<option selected>Choose your role</option>
-										<option name="manager">Manager</option>
-										<option name="viticulturist">Viticulturist</option>
-										<option name="horticulturist">Horticulturist</option>
-										<option name="permaculturist">Permaculturist</option>
-										<option name="irrigation-manager">Irrigation Manager</option>
-									</select>
-									<span class="invalid-feedback error">[[!+reg.error.phone]]</span>
-								</div>
-							</div>
-							
-							<div class="form-group row">
-								<div class="col-md-6 mb-3 mb-md-0">
-									<input type="text" class="form-control form-control-user" placeholder="City" name="city" id="city" value="[[!+reg.city]]" >
-									<span class="invalid-feedback error">[[!+reg.error.city]]</span>
-								</div>
-								<div class="col-md-6">
-									<select class="form-control" name="state" name="state" id="state" value="[[!+reg.state]]">
-										<option selected>State</option>
-										<option name="nsw">New South Wales</option>
-										<option name="vic">Victoria</option>
-										<option name="qld">Queensland</option>
-										<option name="wa">Western Australia</option>
-										<option name="sa">South Australia</option>
-										<option name="tas">Tasmania</option>
-										<option name="act">Australian Capital Territory</option>
-										<option name="nt">Northern Territory</option>
-										<option name="other">Other</option>
-									</select>
-									<span class="invalid-feedback error">[[!+reg.error.state]]</span>
-								</div>
-							</div>
-							<div class="form-group row">
-								<div class="col-md-6 mb-3 mb-md-0">
-									<input type="text" class="form-control form-control-user" placeholder="Post Code" name="postcode" id="postcode" value="[[!+reg.zip]]">
-									<span class="error">[[!+reg.error.zip]]</span>
-								</div>
-								<div class="col-md-6">
-									<select type="hidden" id="country" name="country:required" class="form-control" value="[[!+reg.country]]">
-										<option selected>Country</option>
-										<option value="Australia">Australia</option>
-										<option value="New Zealand">New Zealand</option>
-									</select>
-									<span class="error">[[!+reg.error.country]]</span>
-								</div>
-							</div>
-							<div class="form-group row">
-								<div class="col-md-6 mb-3 mb-md-0">
-									<input type="password" class="form-control form-control-user" placeholder="Password" name="password" id="password" value="[[!+reg.password]]" required>
-								</div>
-								<div class="col-md-6">
-									<input type="password" class="form-control form-control-user" placeholder="Repeat password" name="password_confirm" id="password_confirm" required="" value="[[!+reg.password_confirm]]" required>
-								</div>
-							</div>
-							<input class="loginLoginValue" type="hidden" name="service" value="login" />
-							<!-- <a href="login.html" class="btn btn-lg btn-success btn-block btn-user text-uppercase" type="submit" name="registerbtn" >Register Account</a> -->
-							<input class="btn btn-lg btn-success btn-block text-uppercase" type="submit" name="registerbtn" value="Register Account" > 
-							<hr>
-							<a href="index.html" class="btn btn-danger btn-google btn-user btn-block">
-							<i class="fab fa-google fa-fw"></i> Register with Google
-							</a>
-							<a href="index.html" class="btn btn-primary btn-facebook btn-user btn-block">
-							<i class="fab fa-facebook-f fa-fw"></i> Register with Facebook
-							</a>
-						</form>
-						<hr>
-						<div class="text-center">
-							<a class="small" href="[[~10]]">Forgot Password?</a>
-						</div>
-						<div class="text-center">
-							<a class="small" href="[[~4]]">Already have an account? Login!</a>
-						</div>
-					</div>
-				</div>
-			</div>
-		</div>
-	</div>
-</body>
+<?php
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../lib/auth.php';
+require_once __DIR__ . '/../lib/csrf.php';
+require_once __DIR__ . '/../lib/validation.php';
+
+if (isLoggedIn()) {
+    header('Location: /dashboard/dashboard.php');
+    exit;
+}
+
+$errors = [];
+$old    = []; // repopulate form fields on error
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+    if (!verifyCsrfToken($_POST['csrf_token'] ?? '')) {
+        $errors['general'] = 'Invalid request. Please try again.';
+    } else {
+        $old = $_POST;
+
+        // --- Validation ---
+        $fullname    = sanitizeString($_POST['fullname']    ?? '', 255);
+        $email       = sanitizeString($_POST['email']       ?? '', 255);
+        $company     = sanitizeString($_POST['company']     ?? '', 255);
+        $mobilephone = sanitizeString($_POST['mobilephone'] ?? '', 50);
+        $industry    = $_POST['industry']  ?? '';
+        $role        = $_POST['role']      ?? '';
+        $city        = sanitizeString($_POST['city']     ?? '', 100);
+        $state       = $_POST['state']     ?? '';
+        $postcode    = sanitizeString($_POST['postcode'] ?? '', 20);
+        $country     = $_POST['country']   ?? 'Australia';
+        $password    = $_POST['password']         ?? '';
+        $password2   = $_POST['password_confirm'] ?? '';
+
+        if ($fullname === '')    $errors['fullname']    = 'Full name is required.';
+        if ($email === '')       $errors['email']       = 'Email is required.';
+        elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors['email'] = 'Please enter a valid email.';
+        if ($mobilephone === '') $errors['mobilephone'] = 'Mobile phone is required.';
+        if ($password === '')    $errors['password']    = 'Password is required.';
+        elseif (strlen($password) < 8) $errors['password'] = 'Password must be at least 8 characters.';
+        if ($password !== $password2) $errors['password_confirm'] = 'Passwords do not match.';
+
+        $allowedIndustries = ['Broadacre','Viticulture','Horticulture','Permaculture','Dairy'];
+        $allowedRoles      = ['Manager','Viticulturist','Horticulturist','Permaculturist','Irrigation Manager'];
+        $allowedStates     = ['New South Wales','Victoria','Queensland','Western Australia','South Australia','Tasmania','Australian Capital Territory','Northern Territory','Other'];
+        $allowedCountries  = ['Australia','New Zealand'];
+
+        if (!in_array($industry, $allowedIndustries, true)) $errors['industry'] = 'Please select an industry.';
+        if (!in_array($role,     $allowedRoles,      true)) $errors['role']     = 'Please select a role.';
+        if (!in_array($state,    $allowedStates,     true)) $errors['state']    = 'Please select a state.';
+        if (!in_array($country,  $allowedCountries,  true)) $country = 'Australia';
+
+        if (empty($errors)) {
+            $result = registerUser([
+                'fullname'    => $fullname,
+                'email'       => $email,
+                'company'     => $company,
+                'mobilephone' => $mobilephone,
+                'industry'    => $industry,
+                'role'        => $role,
+                'city'        => $city,
+                'state'       => $state,
+                'postcode'    => $postcode,
+                'country'     => $country,
+                'password'    => $password,
+            ]);
+
+            if ($result['success']) {
+                // Auto-login after registration
+                loginUser($email, $password);
+                header('Location: /dashboard/dashboard.php?registered=1');
+                exit;
+            } else {
+                $errors['email'] = $result['error'];
+            }
+        }
+    }
+}
+
+$pageTitle = 'Create an Account';
+include __DIR__ . '/_head.php';
+
+// Helper: old value for text inputs
+$v = fn(string $key) => htmlspecialchars($old[$key] ?? '', ENT_QUOTES, 'UTF-8');
+// Helper: re-select dropdown option
+$sel = fn(string $key, string $val) => (($old[$key] ?? '') === $val) ? 'selected' : '';
+?>
+
+<div class="container">
+    <div class="card o-hidden border-0 shadow-lg my-5">
+        <div class="card-body p-0">
+            <div class="row">
+                <div class="col-lg-5 d-none d-lg-block bg-register-image"></div>
+                <div class="col-lg-7">
+                    <div class="p-5">
+                        <div class="text-center mb-4">
+                            <h1 class="h4 text-gray-900">Create an Account</h1>
+                        </div>
+
+                        <?php if (!empty($errors['general'])): ?>
+                            <div class="alert alert-danger"><?= htmlspecialchars($errors['general'], ENT_QUOTES, 'UTF-8') ?></div>
+                        <?php endif; ?>
+
+                        <form method="POST" action="/login/register.php" novalidate>
+                            <input type="hidden" name="csrf_token" value="<?= generateCsrfToken() ?>">
+
+                            <!-- Name + Company -->
+                            <div class="row mb-3">
+                                <div class="col-md-6 mb-2 mb-md-0">
+                                    <input type="text" name="fullname" class="form-control form-control-user <?= isset($errors['fullname']) ? 'is-invalid' : '' ?>"
+                                           placeholder="Full Name *" value="<?= $v('fullname') ?>" required>
+                                    <?php if (isset($errors['fullname'])): ?>
+                                        <span class="error"><?= htmlspecialchars($errors['fullname'], ENT_QUOTES, 'UTF-8') ?></span>
+                                    <?php endif; ?>
+                                </div>
+                                <div class="col-md-6">
+                                    <input type="text" name="company" class="form-control form-control-user"
+                                           placeholder="Company Name" value="<?= $v('company') ?>">
+                                </div>
+                            </div>
+
+                            <!-- Email -->
+                            <div class="mb-3">
+                                <input type="email" name="email" class="form-control form-control-user <?= isset($errors['email']) ? 'is-invalid' : '' ?>"
+                                       placeholder="Email Address *" value="<?= $v('email') ?>" required>
+                                <?php if (isset($errors['email'])): ?>
+                                    <span class="error"><?= htmlspecialchars($errors['email'], ENT_QUOTES, 'UTF-8') ?></span>
+                                <?php endif; ?>
+                            </div>
+
+                            <!-- Mobile -->
+                            <div class="mb-3">
+                                <input type="tel" name="mobilephone" class="form-control form-control-user <?= isset($errors['mobilephone']) ? 'is-invalid' : '' ?>"
+                                       placeholder="Mobile Phone *" value="<?= $v('mobilephone') ?>">
+                                <?php if (isset($errors['mobilephone'])): ?>
+                                    <span class="error"><?= htmlspecialchars($errors['mobilephone'], ENT_QUOTES, 'UTF-8') ?></span>
+                                <?php endif; ?>
+                            </div>
+
+                            <!-- Industry + Role -->
+                            <div class="row mb-3">
+                                <div class="col-md-6 mb-2 mb-md-0">
+                                    <select name="industry" class="form-control form-control-user <?= isset($errors['industry']) ? 'is-invalid' : '' ?>">
+                                        <option value="">Choose your industry *</option>
+                                        <option value="Broadacre"    <?= $sel('industry','Broadacre') ?>>Broadacre</option>
+                                        <option value="Viticulture"  <?= $sel('industry','Viticulture') ?>>Viticulture</option>
+                                        <option value="Horticulture" <?= $sel('industry','Horticulture') ?>>Horticulture</option>
+                                        <option value="Permaculture" <?= $sel('industry','Permaculture') ?>>Permaculture</option>
+                                        <option value="Dairy"        <?= $sel('industry','Dairy') ?>>Dairy</option>
+                                    </select>
+                                    <?php if (isset($errors['industry'])): ?>
+                                        <span class="error"><?= htmlspecialchars($errors['industry'], ENT_QUOTES, 'UTF-8') ?></span>
+                                    <?php endif; ?>
+                                </div>
+                                <div class="col-md-6">
+                                    <select name="role" class="form-control form-control-user <?= isset($errors['role']) ? 'is-invalid' : '' ?>">
+                                        <option value="">Choose your role *</option>
+                                        <option value="Manager"            <?= $sel('role','Manager') ?>>Manager</option>
+                                        <option value="Viticulturist"      <?= $sel('role','Viticulturist') ?>>Viticulturist</option>
+                                        <option value="Horticulturist"     <?= $sel('role','Horticulturist') ?>>Horticulturist</option>
+                                        <option value="Permaculturist"     <?= $sel('role','Permaculturist') ?>>Permaculturist</option>
+                                        <option value="Irrigation Manager" <?= $sel('role','Irrigation Manager') ?>>Irrigation Manager</option>
+                                    </select>
+                                    <?php if (isset($errors['role'])): ?>
+                                        <span class="error"><?= htmlspecialchars($errors['role'], ENT_QUOTES, 'UTF-8') ?></span>
+                                    <?php endif; ?>
+                                </div>
+                            </div>
+
+                            <!-- City + State -->
+                            <div class="row mb-3">
+                                <div class="col-md-6 mb-2 mb-md-0">
+                                    <input type="text" name="city" class="form-control form-control-user"
+                                           placeholder="City" value="<?= $v('city') ?>">
+                                </div>
+                                <div class="col-md-6">
+                                    <select name="state" class="form-control form-control-user <?= isset($errors['state']) ? 'is-invalid' : '' ?>">
+                                        <option value="">State *</option>
+                                        <option value="New South Wales"          <?= $sel('state','New South Wales') ?>>New South Wales</option>
+                                        <option value="Victoria"                 <?= $sel('state','Victoria') ?>>Victoria</option>
+                                        <option value="Queensland"               <?= $sel('state','Queensland') ?>>Queensland</option>
+                                        <option value="Western Australia"        <?= $sel('state','Western Australia') ?>>Western Australia</option>
+                                        <option value="South Australia"          <?= $sel('state','South Australia') ?>>South Australia</option>
+                                        <option value="Tasmania"                 <?= $sel('state','Tasmania') ?>>Tasmania</option>
+                                        <option value="Australian Capital Territory" <?= $sel('state','Australian Capital Territory') ?>>ACT</option>
+                                        <option value="Northern Territory"       <?= $sel('state','Northern Territory') ?>>Northern Territory</option>
+                                        <option value="Other"                    <?= $sel('state','Other') ?>>Other</option>
+                                    </select>
+                                    <?php if (isset($errors['state'])): ?>
+                                        <span class="error"><?= htmlspecialchars($errors['state'], ENT_QUOTES, 'UTF-8') ?></span>
+                                    <?php endif; ?>
+                                </div>
+                            </div>
+
+                            <!-- Postcode + Country -->
+                            <div class="row mb-3">
+                                <div class="col-md-6 mb-2 mb-md-0">
+                                    <input type="text" name="postcode" class="form-control form-control-user"
+                                           placeholder="Post Code" value="<?= $v('postcode') ?>">
+                                </div>
+                                <div class="col-md-6">
+                                    <select name="country" class="form-control form-control-user">
+                                        <option value="Australia"    <?= $sel('country','Australia') ?>>Australia</option>
+                                        <option value="New Zealand"  <?= $sel('country','New Zealand') ?>>New Zealand</option>
+                                    </select>
+                                </div>
+                            </div>
+
+                            <!-- Password -->
+                            <div class="row mb-3">
+                                <div class="col-md-6 mb-2 mb-md-0">
+                                    <input type="password" name="password" class="form-control form-control-user <?= isset($errors['password']) ? 'is-invalid' : '' ?>"
+                                           placeholder="Password * (min 8 chars)" required>
+                                    <?php if (isset($errors['password'])): ?>
+                                        <span class="error"><?= htmlspecialchars($errors['password'], ENT_QUOTES, 'UTF-8') ?></span>
+                                    <?php endif; ?>
+                                </div>
+                                <div class="col-md-6">
+                                    <input type="password" name="password_confirm" class="form-control form-control-user <?= isset($errors['password_confirm']) ? 'is-invalid' : '' ?>"
+                                           placeholder="Repeat Password *" required>
+                                    <?php if (isset($errors['password_confirm'])): ?>
+                                        <span class="error"><?= htmlspecialchars($errors['password_confirm'], ENT_QUOTES, 'UTF-8') ?></span>
+                                    <?php endif; ?>
+                                </div>
+                            </div>
+
+                            <button type="submit" class="btn btn-success btn-user btn-block w-100 mb-3">
+                                Register Account
+                            </button>
+                        </form>
+
+                        <hr>
+                        <div class="text-center">
+                            <a class="small" href="/login/forgot-password.php">Forgot Password?</a>
+                        </div>
+                        <div class="text-center">
+                            <a class="small" href="/login/login.php">Already have an account? Login!</a>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+
+<?php include __DIR__ . '/_foot.php'; ?>

+ 111 - 0
login/reset-password.php

@@ -0,0 +1,111 @@
+<?php
+require_once __DIR__ . '/../config/database.php';
+require_once __DIR__ . '/../lib/auth.php';
+require_once __DIR__ . '/../lib/csrf.php';
+
+if (isLoggedIn()) {
+    header('Location: /dashboard/dashboard.php');
+    exit;
+}
+
+$token = trim($_GET['token'] ?? '');
+$error = '';
+$done  = false;
+
+// Validate token on page load
+if ($token === '') {
+    header('Location: /login/forgot-password.php');
+    exit;
+}
+
+$tokenEmail = validatePasswordResetToken($token);
+if ($tokenEmail === null) {
+    $error = 'This reset link is invalid or has expired. Please request a new one.';
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && $tokenEmail !== null) {
+    if (!verifyCsrfToken($_POST['csrf_token'] ?? '')) {
+        $error = 'Invalid request. Please try again.';
+    } else {
+        $password  = $_POST['password']         ?? '';
+        $password2 = $_POST['password_confirm'] ?? '';
+
+        if (strlen($password) < 8) {
+            $error = 'Password must be at least 8 characters.';
+        } elseif ($password !== $password2) {
+            $error = 'Passwords do not match.';
+        } else {
+            if (resetPassword($token, $password)) {
+                $done = true;
+            } else {
+                $error = 'This reset link is invalid or has expired. Please request a new one.';
+            }
+        }
+    }
+}
+
+$pageTitle = 'Reset Password';
+include __DIR__ . '/_head.php';
+?>
+
+<div class="container">
+    <div class="row justify-content-center">
+        <div class="col-xl-10 col-lg-12 col-md-9">
+            <div class="card o-hidden border-0 shadow-lg my-5">
+                <div class="card-body p-0">
+                    <div class="row">
+                        <div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
+                        <div class="col-lg-6">
+                            <div class="p-5">
+                                <div class="text-center mb-4">
+                                    <h1 class="h4 text-gray-900">Reset Your Password</h1>
+                                </div>
+
+                                <?php if ($done): ?>
+                                    <div class="alert alert-success" role="alert">
+                                        Your password has been reset.
+                                        <a href="/login/login.php?reset=1">Click here to log in.</a>
+                                    </div>
+
+                                <?php elseif ($error !== '' && $tokenEmail === null): ?>
+                                    <div class="alert alert-danger"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div>
+                                    <div class="text-center">
+                                        <a href="/login/forgot-password.php">Request a new reset link</a>
+                                    </div>
+
+                                <?php else: ?>
+                                    <?php if ($error !== ''): ?>
+                                        <div class="alert alert-danger"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div>
+                                    <?php endif; ?>
+
+                                    <form method="POST" action="/login/reset-password.php?token=<?= urlencode($token) ?>" novalidate>
+                                        <input type="hidden" name="csrf_token" value="<?= generateCsrfToken() ?>">
+
+                                        <div class="mb-3">
+                                            <input type="password" name="password" class="form-control form-control-user"
+                                                   placeholder="New Password (min 8 chars)" required autofocus>
+                                        </div>
+                                        <div class="mb-3">
+                                            <input type="password" name="password_confirm" class="form-control form-control-user"
+                                                   placeholder="Confirm New Password" required>
+                                        </div>
+                                        <button type="submit" class="btn btn-success btn-user btn-block w-100 mb-3">
+                                            Set New Password
+                                        </button>
+                                    </form>
+                                <?php endif; ?>
+
+                                <hr>
+                                <div class="text-center">
+                                    <a class="small" href="/login/login.php">Back to Login</a>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+
+<?php include __DIR__ . '/_foot.php'; ?>