ソースを参照

Login phpMailer

Benjamin Harris 2 ヶ月 前
コミット
af9bd378d3

+ 3 - 1
.claude/settings.local.json

@@ -12,7 +12,9 @@
       "WebFetch(domain:github.com)",
       "Bash(grep -A 20 \"CREATE TABLE \\\\`field_sensors\\\\`\" \"f:/GIT_REPO/crop_monitor/cropmonitor.sql\")",
       "Bash(grep -A 20 \"CREATE TABLE \\\\`sensor_id\\\\`\" \"f:/GIT_REPO/crop_monitor/cropmonitor.sql\")",
-      "Bash(grep -A 20 \"CREATE TABLE \\\\`crop_info\\\\`\" \"f:/GIT_REPO/crop_monitor/cropmonitor.sql\")"
+      "Bash(grep -A 20 \"CREATE TABLE \\\\`crop_info\\\\`\" \"f:/GIT_REPO/crop_monitor/cropmonitor.sql\")",
+      "Bash(composer install:*)",
+      "Bash(cmd /c \"where composer 2>nul && where php 2>nul\")"
     ]
   }
 }

+ 1 - 1
client-assets/css/graphPrint.css

@@ -27,7 +27,7 @@
     }
 
     body {
-        font-size: 0.9em;
+        font-size: 0.75em;
     }
     .pagebreak { 
         page-break-before: always;

+ 5 - 5
config/mail.php

@@ -6,11 +6,11 @@
  * Fill in your mail server credentials here (or move to .env).
  */
 
-define('MAIL_HOST',       'smtp.example.com');      // SMTP server
+define('MAIL_HOST',       'mail-au.smtp2go.com');      // SMTP server
 define('MAIL_PORT',       587);                     // 587 = STARTTLS, 465 = SSL
 define('MAIL_ENCRYPTION', 'tls');                   // 'tls' or 'ssl'
-define('MAIL_USERNAME',   'hello@cropmonitor.com.au');
-define('MAIL_PASSWORD',   '');                      // Fill in SMTP password
-define('MAIL_FROM',       'hello@cropmonitor.com.au');
+define('MAIL_USERNAME',   'itadmin@tazz.com.au');
+define('MAIL_PASSWORD',   'UA24yOLlnAViJPJu');                      // Fill in SMTP password
+define('MAIL_FROM',       'itadmin@tazz.com.au');
 define('MAIL_FROM_NAME',  'Crop Monitor');
-define('MAIL_TO',         'hello@cropmonitor.com.au'); // Inbox that receives contact submissions
+define('MAIL_TO',         'itadmin@tazz.com.au'); // Inbox that receives contact submissions

+ 24 - 16
dashboard/crop-analysis/plant-test-data/plant-report.php

@@ -108,7 +108,7 @@ include __DIR__ . '/../../../layouts/header.php';
             <div class="container-fluid px-4">
 
                 <!-- ── Page heading ─────────────────────────────────────────── -->
-                <div class="d-flex align-items-center justify-content-between mt-4 mb-3">
+                <div class="d-flex align-items-center justify-content-between mt-4 mb-3 ">
                     <h1 class="h3 mb-0">Plant Analysis Report</h1>
                     <div class="d-flex gap-2 d-print-none">
                         <a href="/dashboard/crop-analysis/plant-test-data/plant-analysis.php?rid=<?= $recordId ?>&rand=<?= urlencode($randId) ?>&cid=<?= $clientId ?>"
@@ -157,10 +157,12 @@ include __DIR__ . '/../../../layouts/header.php';
                     <div class="card mb-4">
                         <div class="card-header d-flex justify-content-between align-items-center fw-bold">
                             <span>General Comment</span>
-                            <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
-                                    data-section="general" data-target="#general_details">
-                                <i class="fas fa-robot me-1"></i>Generate with AI
-                            </button>
+                            <div class="d-flex d-print-none">
+                                <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
+                                        data-section="general" data-target="#general_details">
+                                    <i class="fas fa-robot me-1"></i>Generate with AI
+                                </button>
+                            </div>
                         </div>
                         <div class="card-body">
                             <textarea id="general_details" name="general_details"
@@ -175,10 +177,12 @@ include __DIR__ . '/../../../layouts/header.php';
                     <div class="card mb-4">
                         <div class="card-header d-flex justify-content-between align-items-center fw-bold">
                             <span>AI Plant Interpretation</span>
-                            <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
+                            <div class="d-flex d-print-none">
+                                <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
                                     data-section="ai_interpretation" data-target="#ai_interpretation">
-                                <i class="fas fa-robot me-1"></i>Interpret with AI
-                            </button>
+                                    <i class="fas fa-robot me-1"></i>Interpret with AI
+                                </button>
+                            </div>
                         </div>
                         <div class="card-body">
                             <p class="text-muted small mb-2">
@@ -196,10 +200,12 @@ include __DIR__ . '/../../../layouts/header.php';
                     <div class="card mb-4">
                         <div class="card-header d-flex justify-content-between align-items-center fw-bold">
                             <span>Recommended Remedial Program</span>
-                            <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
-                                    data-section="recommended" data-target="#recommended_details">
-                                <i class="fas fa-robot me-1"></i>Generate with AI
-                            </button>
+                            <div class="d-flex d-print-none">
+                                <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
+                                        data-section="recommended" data-target="#recommended_details">
+                                    <i class="fas fa-robot me-1"></i>Generate with AI
+                                </button>
+                            </div>
                         </div>
                         <div class="card-body">
                             <textarea id="recommended_details" name="recommended_details"
@@ -214,10 +220,12 @@ include __DIR__ . '/../../../layouts/header.php';
                     <div class="card mb-4">
                         <div class="card-header d-flex justify-content-between align-items-center fw-bold">
                             <span>Foliar Program</span>
-                            <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
-                                    data-section="foliar" data-target="#foliar_details">
-                                <i class="fas fa-robot me-1"></i>Generate with AI
-                            </button>
+                            <div class="d-flex d-print-none">
+                                <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
+                                        data-section="foliar" data-target="#foliar_details">
+                                    <i class="fas fa-robot me-1"></i>Generate with AI
+                                </button>`
+                            </div>
                         </div>
                         <div class="card-body">
                             <textarea id="foliar_details" name="foliar_details"

+ 20 - 0
dashboard/crop-analysis/soil-test-data/soil-report.php

@@ -418,6 +418,26 @@ include __DIR__ . '/../../../layouts/header.php';
         saveReport();
     });
 
+    function autoResize(el) {
+        el.style.height = 'auto';
+        el.style.height = el.scrollHeight + 'px';
+    }
+
+    document.querySelectorAll('.report-textarea').forEach(function (el) {
+
+        // force correct initial height AFTER render
+        setTimeout(function () {
+            autoResize(el);
+        }, 0);
+
+        el.addEventListener('input', function () {
+            autoResize(el);
+
+            clearTimeout(saveTimer);
+            saveTimer = setTimeout(saveReport, 1200);
+        });
+    });
+
     // ── AI generation ────────────────────────────────────────────────────── //
     function generateSection(btn, section, targetSelector) {
         var textarea = document.querySelector(targetSelector);

+ 130 - 0
lib/mailer.php

@@ -0,0 +1,130 @@
+<?php
+/**
+ * lib/mailer.php
+ *
+ * PHPMailer wrapper. All email sending goes through sendMail().
+ *
+ * Requires: vendor/autoload.php (run `composer install`)
+ *           config/mail.php     (SMTP constants)
+ */
+
+use PHPMailer\PHPMailer\PHPMailer;
+use PHPMailer\PHPMailer\SMTP;
+use PHPMailer\PHPMailer\Exception as MailerException;
+
+require_once __DIR__ . '/../config/mail.php';
+
+/**
+ * Send an email via SMTP.
+ *
+ * @param string      $toEmail   Recipient address
+ * @param string      $toName    Recipient display name
+ * @param string      $subject   Email subject
+ * @param string      $htmlBody  HTML message body
+ * @param string|null $textBody  Plain-text fallback (auto-stripped from HTML if null)
+ *
+ * @return array{success: bool, error: string}
+ */
+function sendMail(string $toEmail, string $toName, string $subject, string $htmlBody, ?string $textBody = null): array
+{
+    $mail = new PHPMailer(true);
+
+    try {
+        $mail->isSMTP();
+        $mail->Host       = MAIL_HOST;
+        $mail->SMTPAuth   = true;
+        $mail->Username   = MAIL_USERNAME;
+        $mail->Password   = MAIL_PASSWORD;
+        $mail->SMTPSecure = MAIL_ENCRYPTION === 'ssl' ? PHPMailer::ENCRYPTION_SMTPS : PHPMailer::ENCRYPTION_STARTTLS;
+        $mail->Port       = MAIL_PORT;
+        $mail->CharSet    = 'UTF-8';
+
+        $mail->setFrom(MAIL_FROM, MAIL_FROM_NAME);
+        $mail->addAddress($toEmail, $toName);
+
+        $mail->isHTML(true);
+        $mail->Subject = $subject;
+        $mail->Body    = $htmlBody;
+        $mail->AltBody = $textBody ?? strip_tags($htmlBody);
+
+        $mail->send();
+        return ['success' => true, 'error' => ''];
+
+    } catch (MailerException $e) {
+        error_log('Mailer error to ' . $toEmail . ': ' . $mail->ErrorInfo);
+        return ['success' => false, 'error' => $mail->ErrorInfo];
+    }
+}
+
+/**
+ * Send a password reset email.
+ */
+function sendPasswordResetEmail(string $email, string $token): bool
+{
+    $resetUrl = 'https://cropmonitor.com.au/login/reset-password.php?token=' . urlencode($token);
+
+    $html = '
+<!DOCTYPE html>
+<html>
+<body style="font-family:Arial,sans-serif;background:#f4f4f4;padding:20px;">
+  <div style="max-width:560px;margin:0 auto;background:#fff;border-radius:6px;padding:32px;">
+    <h2 style="color:#2d6a2d;">Crop Monitor — Password Reset</h2>
+    <p>We received a request to reset your password. Click the button below to choose a new one.</p>
+    <p style="margin:28px 0;text-align:center;">
+      <a href="' . htmlspecialchars($resetUrl, ENT_QUOTES, 'UTF-8') . '"
+         style="background:#2d6a2d;color:#fff;padding:12px 28px;border-radius:4px;text-decoration:none;font-size:15px;">
+        Reset My Password
+      </a>
+    </p>
+    <p style="color:#666;font-size:13px;">This link expires in <strong>1 hour</strong>. If you did not request a password reset, you can safely ignore this email.</p>
+    <hr style="border:none;border-top:1px solid #eee;margin:24px 0;">
+    <p style="color:#999;font-size:12px;">Crop Monitor &mdash; Australian Crop Management Platform</p>
+  </div>
+</body>
+</html>';
+
+    $text = "Crop Monitor — Password Reset\n\n"
+          . "Click the link below to reset your password (expires in 1 hour):\n\n"
+          . $resetUrl . "\n\n"
+          . "If you did not request this, ignore this email.";
+
+    $result = sendMail($email, $email, 'Reset your Crop Monitor password', $html, $text);
+    return $result['success'];
+}
+
+/**
+ * Send a welcome email after registration.
+ */
+function sendWelcomeEmail(string $email, string $fullname): bool
+{
+    $loginUrl = 'https://cropmonitor.com.au/login/login.php';
+
+    $html = '
+<!DOCTYPE html>
+<html>
+<body style="font-family:Arial,sans-serif;background:#f4f4f4;padding:20px;">
+  <div style="max-width:560px;margin:0 auto;background:#fff;border-radius:6px;padding:32px;">
+    <h2 style="color:#2d6a2d;">Welcome to Crop Monitor!</h2>
+    <p>Hi ' . htmlspecialchars($fullname, ENT_QUOTES, 'UTF-8') . ',</p>
+    <p>Your account has been created. You can now log in and start managing your crop analysis records.</p>
+    <p style="margin:28px 0;text-align:center;">
+      <a href="' . htmlspecialchars($loginUrl, ENT_QUOTES, 'UTF-8') . '"
+         style="background:#2d6a2d;color:#fff;padding:12px 28px;border-radius:4px;text-decoration:none;font-size:15px;">
+        Go to Crop Monitor
+      </a>
+    </p>
+    <p style="color:#666;font-size:13px;">If you have any questions, reply to this email and we will get back to you.</p>
+    <hr style="border:none;border-top:1px solid #eee;margin:24px 0;">
+    <p style="color:#999;font-size:12px;">Crop Monitor &mdash; Australian Crop Management Platform</p>
+  </div>
+</body>
+</html>';
+
+    $text = "Welcome to Crop Monitor, {$fullname}!\n\n"
+          . "Your account has been created. Log in here:\n\n"
+          . $loginUrl . "\n\n"
+          . "If you have any questions, reply to this email.";
+
+    $result = sendMail($email, $fullname, 'Welcome to Crop Monitor', $html, $text);
+    return $result['success'];
+}

+ 3 - 4
login/forgot-password.php

@@ -2,6 +2,8 @@
 require_once __DIR__ . '/../config/database.php';
 require_once __DIR__ . '/../lib/auth.php';
 require_once __DIR__ . '/../lib/csrf.php';
+require_once __DIR__ . '/../vendor/autoload.php';
+require_once __DIR__ . '/../lib/mailer.php';
 
 if (isLoggedIn()) {
     header('Location: /dashboard/dashboard.php');
@@ -25,10 +27,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
             $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}");
+                sendPasswordResetEmail($email, $token);
             }
 
             $sent = true; // Always show success to prevent email enumeration

+ 3 - 0
login/register.php

@@ -3,6 +3,8 @@ require_once __DIR__ . '/../config/database.php';
 require_once __DIR__ . '/../lib/auth.php';
 require_once __DIR__ . '/../lib/csrf.php';
 require_once __DIR__ . '/../lib/validation.php';
+require_once __DIR__ . '/../vendor/autoload.php';
+require_once __DIR__ . '/../lib/mailer.php';
 
 if (isLoggedIn()) {
     header('Location: /dashboard/dashboard.php');
@@ -66,6 +68,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
             ]);
 
             if ($result['success']) {
+                sendWelcomeEmail($email, $fullname);
                 // Auto-login after registration
                 loginUser($email, $password);
                 header('Location: /dashboard/dashboard.php?registered=1');