From 0f1e8774d71b3c604a48fc26a786b308c4fffe4e Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Wed, 22 Oct 2025 13:33:42 -0400 Subject: [PATCH] Add Firestore security rules and deployment scripts - Add firestore.rules for SMTP config permissions - Add deployment scripts for Firestore rules - Update Admin component with better error handling - Update README with deployment instructions --- README.md | 55 ++++++++++++++++++++++++++++++++++++++ deploy-firestore-rules.bat | 46 +++++++++++++++++++++++++++++++ deploy-firestore-rules.sh | 42 +++++++++++++++++++++++++++++ firestore.rules | 24 +++++++++++++++++ public/sw.js | 2 +- src/components/Admin.tsx | 39 ++++++++++++++++++++++++--- 6 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 deploy-firestore-rules.bat create mode 100644 deploy-firestore-rules.sh create mode 100644 firestore.rules diff --git a/README.md b/README.md index 06837c2..0120428 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,61 @@ Admin users can access the admin panel by clicking the "Admin" button in the hea The admin panel saves settings to Firestore for persistent configuration. +## Firestore Security Rules + +To enable SMTP configuration saving and other admin features, you need to deploy Firestore security rules: + +1. Install Firebase CLI if you haven't already: + ```bash + npm install -g firebase-tools + ``` + +2. Login to Firebase: + ```bash + firebase login + ``` + +3. Initialize or connect to your Firebase project: + ```bash + firebase use --add + ``` + Select your prayer-app project. + +4. Deploy the security rules using the provided script: + ```bash + # On Linux/Mac + ./deploy-firestore-rules.sh + + # On Windows + deploy-firestore-rules.bat + ``` + + Or deploy manually: + ```bash + firebase deploy --only firestore:rules + ``` + +The `firestore.rules` file in this repository allows authenticated users to read/write configuration data needed for the admin panel. + +## SMTP Configuration + +After deploying Firestore rules, you can configure SMTP settings through the admin panel: + +1. Log in as an admin user +2. Click the "Admin" button +3. Fill in your SMTP provider details (Gmail, Outlook, etc.) +4. Save the configuration + +**Gmail Setup:** +- Host: `smtp.gmail.com` +- Port: `587` (or `465` for SSL) +- Username: Your Gmail address +- Password: App password (not your regular password) +- From: Your Gmail address +- Use SSL/TLS: Checked for port 465, unchecked for port 587 + +**Note:** For Gmail, you'll need to generate an "App Password" in your Google Account settings. + ## Getting Started First, install dependencies: diff --git a/deploy-firestore-rules.bat b/deploy-firestore-rules.bat new file mode 100644 index 0000000..9ac2a56 --- /dev/null +++ b/deploy-firestore-rules.bat @@ -0,0 +1,46 @@ +@echo off +echo 🚀 Deploying Firestore Security Rules... +echo. + +REM Check if Firebase CLI is installed +firebase --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo ❌ Firebase CLI is not installed. Please install it first: + echo npm install -g firebase-tools + pause + exit /b 1 +) + +REM Check if user is logged in +firebase projects:list >nul 2>&1 +if %errorlevel% neq 0 ( + echo 🔐 Please login to Firebase first: + echo firebase login + pause + exit /b 1 +) + +echo 📋 Available Firebase projects: +firebase projects:list +echo. +echo 🔗 Make sure you're using the correct project (prayer-app-6701f) +echo If not, run: firebase use prayer-app-6701f +echo. +pause + +REM Deploy the rules +echo 📤 Deploying Firestore security rules... +firebase deploy --only firestore:rules + +if %errorlevel% equ 0 ( + echo. + echo ✅ Firestore security rules deployed successfully! + echo You can now save SMTP configuration in the admin panel. +) else ( + echo. + echo ❌ Failed to deploy Firestore rules. Please check the error messages above. + pause + exit /b 1 +) + +pause \ No newline at end of file diff --git a/deploy-firestore-rules.sh b/deploy-firestore-rules.sh new file mode 100644 index 0000000..76e773e --- /dev/null +++ b/deploy-firestore-rules.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +echo "🚀 Deploying Firestore Security Rules..." +echo "" + +# Check if Firebase CLI is installed +if ! command -v firebase &> /dev/null; then + echo "❌ Firebase CLI is not installed. Please install it first:" + echo "npm install -g firebase-tools" + exit 1 +fi + +# Check if user is logged in +if ! firebase projects:list &> /dev/null; then + echo "🔐 Please login to Firebase first:" + echo "firebase login" + exit 1 +fi + +echo "📋 Available Firebase projects:" +firebase projects:list + +echo "" +echo "🔗 Make sure you're using the correct project (prayer-app-6701f)" +echo "If not, run: firebase use prayer-app-6701f" +echo "" + +read -p "Press Enter to deploy Firestore rules..." + +# Deploy the rules +echo "📤 Deploying Firestore security rules..." +firebase deploy --only firestore:rules + +if [ $? -eq 0 ]; then + echo "" + echo "✅ Firestore security rules deployed successfully!" + echo "You can now save SMTP configuration in the admin panel." +else + echo "" + echo "❌ Failed to deploy Firestore rules. Please check the error messages above." + exit 1 +fi \ No newline at end of file diff --git a/firestore.rules b/firestore.rules new file mode 100644 index 0000000..0bd1910 --- /dev/null +++ b/firestore.rules @@ -0,0 +1,24 @@ +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + // Allow authenticated users to read/write their own data + match /users/{userId} { + allow read, write: if request.auth != null && request.auth.uid == userId; + } + + // Allow authenticated users to read/write prayers + match /prayers/{prayerId} { + allow read, write: if request.auth != null; + } + + // Allow authenticated users to read/write config (for admin settings) + match /config/{configId} { + allow read, write: if request.auth != null; + } + + // Allow anyone to read public data (if needed in future) + match /public/{document} { + allow read: if true; + } + } +} \ No newline at end of file diff --git a/public/sw.js b/public/sw.js index fdb7520..9ff8e98 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1 +1 @@ -if(!self.define){let e,s={};const a=(a,n)=>(a=new URL(a+".js",n).href,s[a]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=a,e.onload=s,document.head.appendChild(e)}else e=a,importScripts(a),s()}).then(()=>{let e=s[a];if(!e)throw new Error(`Module ${a} didn’t register its module`);return e}));self.define=(n,t)=>{const i=e||("document"in self?document.currentScript.src:"")||location.href;if(s[i])return;let c={};const r=e=>a(e,i),o={module:{uri:i},exports:c,require:r};s[i]=Promise.all(n.map(e=>o[e]||r(e))).then(e=>(t(...e),c))}}define(["./workbox-4754cb34"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/chunks/139.678015000317a168.js",revision:"678015000317a168"},{url:"/_next/static/chunks/240-754372c10c8766c9.js",revision:"754372c10c8766c9"},{url:"/_next/static/chunks/4bd1b696-b7529f9b306ab05d.js",revision:"b7529f9b306ab05d"},{url:"/_next/static/chunks/512-8d2b9de6fda25175.js",revision:"8d2b9de6fda25175"},{url:"/_next/static/chunks/646.30443a55a43a3bfb.js",revision:"30443a55a43a3bfb"},{url:"/_next/static/chunks/7508b87c-1ab0c72b2a9e57da.js",revision:"1ab0c72b2a9e57da"},{url:"/_next/static/chunks/app/_global-error/page-7a99a1a9afe03223.js",revision:"7a99a1a9afe03223"},{url:"/_next/static/chunks/app/_not-found/page-1123cfe416c748d0.js",revision:"1123cfe416c748d0"},{url:"/_next/static/chunks/app/api/send-email/route-7a99a1a9afe03223.js",revision:"7a99a1a9afe03223"},{url:"/_next/static/chunks/app/layout-dc9d7c3da9cf59ef.js",revision:"dc9d7c3da9cf59ef"},{url:"/_next/static/chunks/app/page-d9fbb10ee3eb8a72.js",revision:"d9fbb10ee3eb8a72"},{url:"/_next/static/chunks/dd8162e8-b0853216adf21a86.js",revision:"b0853216adf21a86"},{url:"/_next/static/chunks/framework-292291387d6b2e39.js",revision:"292291387d6b2e39"},{url:"/_next/static/chunks/main-7ddb690de3906442.js",revision:"7ddb690de3906442"},{url:"/_next/static/chunks/main-app-4f19d69ed4295336.js",revision:"4f19d69ed4295336"},{url:"/_next/static/chunks/next/dist/client/components/builtin/app-error-7a99a1a9afe03223.js",revision:"7a99a1a9afe03223"},{url:"/_next/static/chunks/next/dist/client/components/builtin/forbidden-7a99a1a9afe03223.js",revision:"7a99a1a9afe03223"},{url:"/_next/static/chunks/next/dist/client/components/builtin/global-error-bcba0732671665d2.js",revision:"bcba0732671665d2"},{url:"/_next/static/chunks/next/dist/client/components/builtin/not-found-7a99a1a9afe03223.js",revision:"7a99a1a9afe03223"},{url:"/_next/static/chunks/next/dist/client/components/builtin/unauthorized-7a99a1a9afe03223.js",revision:"7a99a1a9afe03223"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-167d4419e1b22999.js",revision:"167d4419e1b22999"},{url:"/_next/static/css/0e9b93944341831f.css",revision:"0e9b93944341831f"},{url:"/_next/static/hS4u_kF5nc-qn-M9O01uW/_buildManifest.js",revision:"009de90521464c9665092678c4f82fe9"},{url:"/_next/static/hS4u_kF5nc-qn-M9O01uW/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/media/4cf2300e9c8272f7-s.p.woff2",revision:"18bae71b1e1b2bb25321090a3b563103"},{url:"/_next/static/media/747892c23ea88013-s.woff2",revision:"a0761690ccf4441ace5cec893b82d4ab"},{url:"/_next/static/media/8d697b304b401681-s.woff2",revision:"cc728f6c0adb04da0dfcb0fc436a8ae5"},{url:"/_next/static/media/93f479601ee12b01-s.p.woff2",revision:"da83d5f06d825c5ae65b7cca706cb312"},{url:"/_next/static/media/9610d9e46709d722-s.woff2",revision:"7b7c0ef93df188a852344fc272fc096b"},{url:"/_next/static/media/ba015fad6dcf6784-s.woff2",revision:"8ea4f719af3312a055caf09f34c89a77"},{url:"/file.svg",revision:"d09f95206c3fa0bb9bd9fefabfd0ea71"},{url:"/globe.svg",revision:"2aaafa6a49b6563925fe440891e32717"},{url:"/manifest.json",revision:"785fbc5941efd3caa27ade80c67bb5ef"},{url:"/next.svg",revision:"8e061864f388b47f33a1c3780831193e"},{url:"/vercel.svg",revision:"c0af2f507b369b085b35ef4bbe3bcf1e"},{url:"/window.svg",revision:"a2760511c65806022ad20adf74370ff3"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:a,state:n})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;const s=e.pathname;return!s.startsWith("/api/auth/")&&!!s.startsWith("/api/")},new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;return!e.pathname.startsWith("/api/")},new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>!(self.origin===e.origin),new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET")}); +if(!self.define){let e,s={};const a=(a,n)=>(a=new URL(a+".js",n).href,s[a]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=a,e.onload=s,document.head.appendChild(e)}else e=a,importScripts(a),s()}).then(()=>{let e=s[a];if(!e)throw new Error(`Module ${a} didn’t register its module`);return e}));self.define=(n,t)=>{const i=e||("document"in self?document.currentScript.src:"")||location.href;if(s[i])return;let c={};const r=e=>a(e,i),o={module:{uri:i},exports:c,require:r};s[i]=Promise.all(n.map(e=>o[e]||r(e))).then(e=>(t(...e),c))}}define(["./workbox-4754cb34"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/84QbTf9EqwkRW6ZuK_UKV/_buildManifest.js",revision:"009de90521464c9665092678c4f82fe9"},{url:"/_next/static/84QbTf9EqwkRW6ZuK_UKV/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/139.678015000317a168.js",revision:"678015000317a168"},{url:"/_next/static/chunks/240-754372c10c8766c9.js",revision:"754372c10c8766c9"},{url:"/_next/static/chunks/4bd1b696-b7529f9b306ab05d.js",revision:"b7529f9b306ab05d"},{url:"/_next/static/chunks/512-8d2b9de6fda25175.js",revision:"8d2b9de6fda25175"},{url:"/_next/static/chunks/646.30443a55a43a3bfb.js",revision:"30443a55a43a3bfb"},{url:"/_next/static/chunks/7508b87c-1ab0c72b2a9e57da.js",revision:"1ab0c72b2a9e57da"},{url:"/_next/static/chunks/app/_global-error/page-7a99a1a9afe03223.js",revision:"7a99a1a9afe03223"},{url:"/_next/static/chunks/app/_not-found/page-1123cfe416c748d0.js",revision:"1123cfe416c748d0"},{url:"/_next/static/chunks/app/api/send-email/route-7a99a1a9afe03223.js",revision:"7a99a1a9afe03223"},{url:"/_next/static/chunks/app/layout-dc9d7c3da9cf59ef.js",revision:"dc9d7c3da9cf59ef"},{url:"/_next/static/chunks/app/page-5ebd182a478eec74.js",revision:"5ebd182a478eec74"},{url:"/_next/static/chunks/dd8162e8-b0853216adf21a86.js",revision:"b0853216adf21a86"},{url:"/_next/static/chunks/framework-292291387d6b2e39.js",revision:"292291387d6b2e39"},{url:"/_next/static/chunks/main-7ddb690de3906442.js",revision:"7ddb690de3906442"},{url:"/_next/static/chunks/main-app-4f19d69ed4295336.js",revision:"4f19d69ed4295336"},{url:"/_next/static/chunks/next/dist/client/components/builtin/app-error-7a99a1a9afe03223.js",revision:"7a99a1a9afe03223"},{url:"/_next/static/chunks/next/dist/client/components/builtin/forbidden-7a99a1a9afe03223.js",revision:"7a99a1a9afe03223"},{url:"/_next/static/chunks/next/dist/client/components/builtin/global-error-bcba0732671665d2.js",revision:"bcba0732671665d2"},{url:"/_next/static/chunks/next/dist/client/components/builtin/not-found-7a99a1a9afe03223.js",revision:"7a99a1a9afe03223"},{url:"/_next/static/chunks/next/dist/client/components/builtin/unauthorized-7a99a1a9afe03223.js",revision:"7a99a1a9afe03223"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-167d4419e1b22999.js",revision:"167d4419e1b22999"},{url:"/_next/static/css/8a0dbbd40cc7c3db.css",revision:"8a0dbbd40cc7c3db"},{url:"/_next/static/media/4cf2300e9c8272f7-s.p.woff2",revision:"18bae71b1e1b2bb25321090a3b563103"},{url:"/_next/static/media/747892c23ea88013-s.woff2",revision:"a0761690ccf4441ace5cec893b82d4ab"},{url:"/_next/static/media/8d697b304b401681-s.woff2",revision:"cc728f6c0adb04da0dfcb0fc436a8ae5"},{url:"/_next/static/media/93f479601ee12b01-s.p.woff2",revision:"da83d5f06d825c5ae65b7cca706cb312"},{url:"/_next/static/media/9610d9e46709d722-s.woff2",revision:"7b7c0ef93df188a852344fc272fc096b"},{url:"/_next/static/media/ba015fad6dcf6784-s.woff2",revision:"8ea4f719af3312a055caf09f34c89a77"},{url:"/file.svg",revision:"d09f95206c3fa0bb9bd9fefabfd0ea71"},{url:"/globe.svg",revision:"2aaafa6a49b6563925fe440891e32717"},{url:"/manifest.json",revision:"785fbc5941efd3caa27ade80c67bb5ef"},{url:"/next.svg",revision:"8e061864f388b47f33a1c3780831193e"},{url:"/vercel.svg",revision:"c0af2f507b369b085b35ef4bbe3bcf1e"},{url:"/window.svg",revision:"a2760511c65806022ad20adf74370ff3"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:a,state:n})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;const s=e.pathname;return!s.startsWith("/api/auth/")&&!!s.startsWith("/api/")},new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;return!e.pathname.startsWith("/api/")},new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>!(self.origin===e.origin),new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET")}); diff --git a/src/components/Admin.tsx b/src/components/Admin.tsx index a22f7f8..7ab9b48 100644 --- a/src/components/Admin.tsx +++ b/src/components/Admin.tsx @@ -64,14 +64,42 @@ export default function Admin() { }; const saveConfig = async () => { + // Validate required fields + if (!config.host.trim()) { + setMessage('SMTP host is required.'); + return; + } + if (!config.user.trim()) { + setMessage('SMTP username is required.'); + return; + } + if (!config.pass.trim()) { + setMessage('SMTP password is required.'); + return; + } + if (!config.from.trim()) { + setMessage('From email is required.'); + return; + } + if (isNaN(config.port) || config.port < 1 || config.port > 65535) { + setMessage('Port must be a valid number between 1 and 65535.'); + return; + } + setSaving(true); setMessage(''); try { await setDoc(doc(db, 'config', 'smtp'), config); setMessage('SMTP configuration saved successfully!'); - } catch (error) { + } catch (error: any) { console.error('Error saving config:', error); - setMessage('Error saving configuration.'); + if (error.code === 'permission-denied') { + setMessage('Permission denied. Please check your authentication and Firestore security rules.'); + } else if (error.code === 'unavailable') { + setMessage('Firestore is currently unavailable. Please try again later.'); + } else { + setMessage(`Error saving configuration: ${error.message || 'Unknown error'}`); + } } finally { setSaving(false); } @@ -159,8 +187,13 @@ export default function Admin() { setConfig(prev => ({ ...prev, port: parseInt(e.target.value) }))} + onChange={(e) => { + const value = parseInt(e.target.value); + setConfig(prev => ({ ...prev, port: isNaN(value) ? 587 : value })); + }} className="w-full p-2 bg-gray-700 border border-gray-600 rounded text-white" + min="1" + max="65535" />