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
This commit is contained in:
nmemmert committed 2025-10-22 13:33:42 -04:00
1 parent bd6e1eea7c
commit 0f1e8774d7
6 files changed
+204 -4

No files matched your search

+55
View File
@@ -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. 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 ## Getting Started
First, install dependencies: First, install dependencies:
+46
View File
@@ -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
+42
View File
@@ -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
+24
View File
@@ -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;
}
}
}
+1 -1
View File
@@ -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|jLine truncated 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|jLine truncated
+36 -3
View File
@@ -64,14 +64,42 @@ export default function Admin() {
}; };
const saveConfig = async () => { 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); setSaving(true);
setMessage(''); setMessage('');
try { try {
await setDoc(doc(db, 'config', 'smtp'), config); await setDoc(doc(db, 'config', 'smtp'), config);
setMessage('SMTP configuration saved successfully!'); setMessage('SMTP configuration saved successfully!');
} catch (error) { } catch (error: any) {
console.error('Error saving config:', error); 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 { } finally {
setSaving(false); setSaving(false);
} }
@@ -159,8 +187,13 @@ export default function Admin() {
<input <input
type="number" type="number"
value={config.port} value={config.port}
onChange={(e) => 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" className="w-full p-2 bg-gray-700 border border-gray-600 rounded text-white"
min="1"
max="65535"
/> />
</div> </div>