diff --git a/backend/middleware/betaAuth.js b/backend/middleware/betaAuth.js new file mode 100644 index 0000000..fe84175 --- /dev/null +++ b/backend/middleware/betaAuth.js @@ -0,0 +1,21 @@ +const verifyBetaPassword = (req, res, next) => { + const betaPassword = req.headers['x-beta-password']; + const configuredPassword = process.env.BETA_PASSWORD; + + if (!configuredPassword) { + console.error('BETA_PASSWORD environment variable is not set'); + return res.status(500).json({ error: 'Beta password not configured on server' }); + } + + if (!betaPassword) { + return res.status(401).json({ error: 'Beta password required' }); + } + + if (betaPassword !== configuredPassword) { + return res.status(403).json({ error: 'Invalid beta password' }); + } + + next(); +}; + +module.exports = { verifyBetaPassword }; \ No newline at end of file diff --git a/backend/routes/beta.js b/backend/routes/beta.js new file mode 100644 index 0000000..adfcc64 --- /dev/null +++ b/backend/routes/beta.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router(); +const { verifyBetaPassword } = require('../middleware/betaAuth'); + +// Beta verification endpoint +router.get('/verify', verifyBetaPassword, (req, res) => { + res.json({ success: true, message: 'Beta access granted' }); +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 67c792d..a9051d2 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,50 +1,57 @@ // Load environment-specific config -const env = process.env.NODE_ENV; +const env = process.env.NODE_ENV || "dev"; const envFile = `.env.${env}`; -require('dotenv').config({ - path: process.env.DOTENV_CONFIG_PATH +require("dotenv").config({ + path: envFile, }); -const express = require('express'); -const cors = require('cors'); -const bodyParser = require('body-parser'); -const path = require('path'); -const { sequelize } = require('./models'); // Import from models/index.js to ensure associations are loaded +const express = require("express"); +const cors = require("cors"); +const bodyParser = require("body-parser"); +const path = require("path"); +const { sequelize } = require("./models"); // Import from models/index.js to ensure associations are loaded -const authRoutes = require('./routes/auth'); -const phoneAuthRoutes = require('./routes/phone-auth'); -const userRoutes = require('./routes/users'); -const itemRoutes = require('./routes/items'); -const rentalRoutes = require('./routes/rentals'); -const messageRoutes = require('./routes/messages'); +const authRoutes = require("./routes/auth"); +const phoneAuthRoutes = require("./routes/phone-auth"); +const userRoutes = require("./routes/users"); +const itemRoutes = require("./routes/items"); +const rentalRoutes = require("./routes/rentals"); +const messageRoutes = require("./routes/messages"); +const betaRoutes = require("./routes/beta"); const app = express(); app.use(cors()); -app.use(bodyParser.json({ limit: '5mb' })); -app.use(bodyParser.urlencoded({ extended: true, limit: '5mb' })); +app.use(bodyParser.json({ limit: "5mb" })); +app.use(bodyParser.urlencoded({ extended: true, limit: "5mb" })); // Serve static files from uploads directory -app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); +app.use("/uploads", express.static(path.join(__dirname, "uploads"))); -app.use('/api/auth', authRoutes); -app.use('/api/auth/phone', phoneAuthRoutes); -app.use('/api/users', userRoutes); -app.use('/api/items', itemRoutes); -app.use('/api/rentals', rentalRoutes); -app.use('/api/messages', messageRoutes); +// Beta verification route (doesn't require auth) +app.use("/api/beta", betaRoutes); -app.get('/', (req, res) => { - res.json({ message: 'CommunityRentals.App API is running!' }); +app.use("/api/auth", authRoutes); +app.use("/api/auth/phone", phoneAuthRoutes); +app.use("/api/users", userRoutes); +app.use("/api/items", itemRoutes); +app.use("/api/rentals", rentalRoutes); +app.use("/api/messages", messageRoutes); + +app.get("/", (req, res) => { + res.json({ message: "CommunityRentals.App API is running!" }); }); const PORT = process.env.PORT || 5000; -sequelize.sync({ alter: true }).then(() => { - console.log('Database synced'); - app.listen(PORT, () => { - console.log(`Server is running on port ${PORT}`); +sequelize + .sync({ alter: true }) + .then(() => { + console.log("Database synced"); + app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); + }); + }) + .catch((err) => { + console.error("Unable to sync database:", err); }); -}).catch(err => { - console.error('Unable to sync database:', err); -}); \ No newline at end of file diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt index e9e57dc..b21f088 100644 --- a/frontend/public/robots.txt +++ b/frontend/public/robots.txt @@ -1,3 +1,3 @@ # https://www.robotstxt.org/robotstxt.html User-agent: * -Disallow: +Disallow: / diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c685a08..002a682 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { AuthProvider } from './contexts/AuthContext'; +import BetaPasswordProtection from './components/BetaPasswordProtection'; import Navbar from './components/Navbar'; import Footer from './components/Footer'; import Home from './pages/Home'; @@ -22,12 +23,13 @@ import './App.css'; function App() { return ( - - -
- -
- + + + +
+ +
+ } /> } /> } /> @@ -104,6 +106,7 @@ function App() {
+
); } diff --git a/frontend/src/components/BetaPasswordProtection.tsx b/frontend/src/components/BetaPasswordProtection.tsx new file mode 100644 index 0000000..ef22828 --- /dev/null +++ b/frontend/src/components/BetaPasswordProtection.tsx @@ -0,0 +1,135 @@ +import React, { useState, useEffect } from "react"; + +interface BetaPasswordProtectionProps { + children: React.ReactNode; +} + +const BetaPasswordProtection: React.FC = ({ + children, +}) => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Check if user already has valid beta access + const betaToken = localStorage.getItem("betaAccess"); + if (betaToken) { + // Verify the stored token is still valid + verifyBetaAccess(betaToken); + } else { + setLoading(false); + } + }, []); + + const verifyBetaAccess = async (token: string) => { + try { + const response = await fetch( + `${process.env.REACT_APP_API_URL}/beta/verify`, + { + headers: { + "X-Beta-Password": token, + }, + } + ); + + if (response.ok) { + setIsAuthenticated(true); + } else { + localStorage.removeItem("betaAccess"); + } + } catch (error) { + localStorage.removeItem("betaAccess"); + } finally { + setLoading(false); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + + if (!password) { + setError("Please enter a password"); + return; + } + + try { + const response = await fetch( + `${process.env.REACT_APP_API_URL}/beta/verify`, + { + headers: { + "X-Beta-Password": password, + }, + } + ); + + if (response.ok) { + localStorage.setItem("betaAccess", password); + setIsAuthenticated(true); + } else { + setError("Invalid beta password"); + } + } catch (error) { + setError("Failed to verify beta password"); + } + }; + + if (loading) { + return ( +
+
+ Loading... +
+
+ ); + } + + if (!isAuthenticated) { + return ( +
+
+
+

Beta Access Required

+

+ This site is currently in beta testing. Please enter the beta + password to continue. +

+
+
+ + setPassword(e.target.value)} + placeholder="Enter beta password" + autoFocus + /> +
+ {error && ( +
+ {error} +
+ )} + +
+
+
+
+ ); + } + + return <>{children}; +}; + +export default BetaPasswordProtection;