Add OpenID Connect authentication and SQLite database with user preferences and favorites system
This commit is contained in:
226
README.md
226
README.md
@@ -1,95 +1,185 @@
|
||||
# ESV Bible Markdown
|
||||
# ESV Bible in Markdown
|
||||
|
||||
A Docker-based web service for serving the ESV Bible in Markdown format with chapter-by-chapter organization.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
esv-bible/
|
||||
├── backend/ # Backend API server
|
||||
│ ├── src/
|
||||
│ │ └── index.js # Express server
|
||||
│ ├── package.json
|
||||
│ └── Dockerfile
|
||||
├── frontend/ # React frontend application
|
||||
│ ├── src/
|
||||
│ ├── public/
|
||||
│ ├── package.json
|
||||
│ └── ...
|
||||
├── bible-data/ # ESV Bible markdown files (auto-downloaded)
|
||||
├── docker-compose.yml
|
||||
└── README.md
|
||||
```
|
||||
A Docker-based ESV Bible application with markdown content, featuring a React frontend and Node.js backend.
|
||||
|
||||
## Features
|
||||
|
||||
- Complete ESV Bible text in Markdown format from [lguenth/mdbible](https://github.com/lguenth/mdbible)
|
||||
- Organized by book and chapter for easy navigation
|
||||
- Docker containerized for easy deployment
|
||||
- Modern React frontend with responsive design
|
||||
- RESTful API for accessing Bible content
|
||||
- Persistent volume storage for Bible data
|
||||
- Optimized for remote hosting
|
||||
- **Complete ESV Bible** in markdown format
|
||||
- **Clean URLs** like `/book/Genesis/chapter/1`
|
||||
- **Mobile responsive** design with adaptive navigation
|
||||
- **Dark mode** support with persistent preferences
|
||||
- **Font size controls** (Small, Medium, Large)
|
||||
- **Chapter navigation** with Previous/Next buttons
|
||||
- **Times New Roman typography** for traditional Bible reading
|
||||
- **Professional favicon** with book icon
|
||||
- **Optional OpenID Connect authentication** for user features
|
||||
- **User preferences** sync (when authenticated)
|
||||
- **Favorites system** for bookmarking verses (when authenticated)
|
||||
|
||||
## Setup
|
||||
## Quick Start
|
||||
|
||||
1. Clone this repository
|
||||
2. Run `docker-compose up` (uses pre-built image from Docker Hub)
|
||||
3. The ESV Bible data is included in the Docker image
|
||||
### Basic Setup (No Authentication)
|
||||
|
||||
## Usage
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone https://github.com/Ryderjj89/esv-bible.git
|
||||
cd esv-bible
|
||||
```
|
||||
|
||||
The service will be available at `http://localhost:3000`
|
||||
2. **Build and run with Docker**
|
||||
```bash
|
||||
docker build -t esv-bible .
|
||||
docker run -p 3000:3000 esv-bible
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
3. **Access the application**
|
||||
- Open http://localhost:3000 in your browser
|
||||
|
||||
- `GET /health` - Health check endpoint
|
||||
- `GET /books` - List all available books
|
||||
- `GET /books/:book` - Get complete book (all chapters combined)
|
||||
- `GET /books/:book/:chapter` - Get specific chapter
|
||||
### Docker Compose Setup
|
||||
|
||||
### Example Usage
|
||||
1. **Run with docker-compose**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
```bash
|
||||
# List all books
|
||||
curl http://localhost:3000/books
|
||||
2. **Access the application**
|
||||
- Open http://localhost:3000 in your browser
|
||||
|
||||
# Get the book of Genesis
|
||||
curl http://localhost:3000/books/Genesis
|
||||
## Authentication Setup (Optional)
|
||||
|
||||
# Get Genesis chapter 1
|
||||
curl http://localhost:3000/books/Genesis/1
|
||||
To enable user authentication and features like favorites and synced preferences, configure OpenID Connect:
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create a `.env` file in the root directory with the following variables:
|
||||
|
||||
```env
|
||||
# OpenID Connect Configuration (Required for authentication)
|
||||
OIDC_ISSUER=https://your-oidc-provider.com
|
||||
OIDC_CLIENT_ID=your-client-id
|
||||
OIDC_CLIENT_SECRET=your-client-secret
|
||||
OIDC_AUTH_URL=https://your-oidc-provider.com/auth
|
||||
OIDC_TOKEN_URL=https://your-oidc-provider.com/token
|
||||
OIDC_USERINFO_URL=https://your-oidc-provider.com/userinfo
|
||||
|
||||
# Optional Configuration
|
||||
OIDC_CALLBACK_URL=/auth/callback
|
||||
SESSION_SECRET=your-session-secret-change-in-production
|
||||
FRONTEND_URL=http://localhost:3000
|
||||
NODE_ENV=production
|
||||
```
|
||||
|
||||
## Features When Authenticated
|
||||
|
||||
### User Preferences
|
||||
- **Font size** synced across devices
|
||||
- **Dark mode** preference synced across devices
|
||||
- **Persistent settings** stored in database
|
||||
|
||||
### Favorites System
|
||||
- **Bookmark verses** for easy reference
|
||||
- **Add notes** to favorite verses
|
||||
- **Organize favorites** by book and chapter
|
||||
- **Quick access** to saved verses
|
||||
|
||||
### API Endpoints (Authenticated)
|
||||
|
||||
```
|
||||
GET /auth/user - Get current user info
|
||||
POST /auth/logout - Logout user
|
||||
GET /api/preferences - Get user preferences
|
||||
PUT /api/preferences - Update user preferences
|
||||
GET /api/favorites - Get user favorites
|
||||
POST /api/favorites - Add favorite verse
|
||||
DELETE /api/favorites/:id - Remove favorite
|
||||
GET /api/favorites/check - Check if verse is favorited
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Backend Development
|
||||
```bash
|
||||
cd backend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
### Prerequisites
|
||||
- Node.js 16+
|
||||
- Docker (optional)
|
||||
|
||||
### Frontend Development
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
### Local Development
|
||||
|
||||
## Docker Deployment
|
||||
1. **Install dependencies**
|
||||
```bash
|
||||
# Backend
|
||||
cd backend
|
||||
npm install
|
||||
|
||||
Run with Docker Compose using pre-built Docker Hub image:
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
# Frontend
|
||||
cd ../frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
The application uses the `ryderjj89/esv-bible:latest` image from Docker Hub. The Bible data is included in the Docker image and stored in a persistent Docker volume named `bible_data` for efficient storage and updates.
|
||||
2. **Start development servers**
|
||||
```bash
|
||||
# Backend (from backend directory)
|
||||
npm run dev
|
||||
|
||||
## Data Source
|
||||
# Frontend (from frontend directory)
|
||||
npm start
|
||||
```
|
||||
|
||||
Bible content is sourced from [lguenth/mdbible](https://github.com/lguenth/mdbible/tree/main/by_chapter), which provides the ESV Bible organized by book and chapter in Markdown format.
|
||||
3. **Access the application**
|
||||
- Frontend: http://localhost:3000
|
||||
- Backend API: http://localhost:3001
|
||||
|
||||
## Database
|
||||
|
||||
The application uses SQLite for user data when authentication is enabled:
|
||||
|
||||
- **Location**: `backend/data/bible.db`
|
||||
- **Tables**: `users`, `user_preferences`, `favorites`
|
||||
- **Automatic setup**: Database and tables created on first run
|
||||
|
||||
## Docker Configuration
|
||||
|
||||
### Dockerfile
|
||||
- Multi-stage build for optimized production image
|
||||
- Node.js backend with React frontend
|
||||
- Persistent volume for database storage
|
||||
|
||||
### docker-compose.yml
|
||||
- Single service configuration
|
||||
- Volume mounting for database persistence
|
||||
- Environment variable support
|
||||
|
||||
## URL Structure
|
||||
|
||||
- **Home**: `/` - Book selection
|
||||
- **Book chapters**: `/book/Genesis` - Chapter selection for Genesis
|
||||
- **Chapter reading**: `/book/Genesis/chapter/1` - Genesis Chapter 1
|
||||
- **Clean URLs**: Professional book names without technical prefixes
|
||||
|
||||
## Browser Support
|
||||
|
||||
- **Modern browsers** with ES6+ support
|
||||
- **Mobile responsive** design
|
||||
- **Touch-friendly** navigation
|
||||
- **Keyboard accessible** controls
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test thoroughly
|
||||
5. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
MIT License - see LICENSE file for details
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions:
|
||||
- Create an issue on GitHub
|
||||
- Check existing documentation
|
||||
- Review environment variable configuration
|
||||
|
||||
---
|
||||
|
||||
**Note**: Authentication is completely optional. The application works fully without any authentication setup, providing a clean Bible reading experience. Authentication only adds user-specific features like favorites and synced preferences.
|
||||
|
||||
@@ -11,9 +11,14 @@
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"express": "^4.18.0",
|
||||
"cors": "^2.8.5",
|
||||
"helmet": "^7.1.0"
|
||||
"helmet": "^6.0.0",
|
||||
"sqlite3": "^5.1.6",
|
||||
"express-session": "^1.17.3",
|
||||
"passport": "^0.6.0",
|
||||
"passport-openidconnect": "^0.1.1",
|
||||
"bcrypt": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
|
||||
156
backend/src/auth.js
Normal file
156
backend/src/auth.js
Normal file
@@ -0,0 +1,156 @@
|
||||
const passport = require('passport');
|
||||
const OpenIDConnectStrategy = require('passport-openidconnect').Strategy;
|
||||
const { userOps } = require('./database');
|
||||
|
||||
// Configure OpenID Connect strategy
|
||||
function configureAuth(app) {
|
||||
// Check if OIDC is configured
|
||||
const isOIDCConfigured = process.env.OIDC_ISSUER &&
|
||||
process.env.OIDC_CLIENT_ID &&
|
||||
process.env.OIDC_CLIENT_SECRET &&
|
||||
process.env.OIDC_AUTH_URL &&
|
||||
process.env.OIDC_TOKEN_URL &&
|
||||
process.env.OIDC_USERINFO_URL;
|
||||
|
||||
if (!isOIDCConfigured) {
|
||||
console.log('OpenID Connect not configured. Authentication features disabled.');
|
||||
console.log('To enable authentication, set the following environment variables:');
|
||||
console.log('- OIDC_ISSUER');
|
||||
console.log('- OIDC_CLIENT_ID');
|
||||
console.log('- OIDC_CLIENT_SECRET');
|
||||
console.log('- OIDC_AUTH_URL');
|
||||
console.log('- OIDC_TOKEN_URL');
|
||||
console.log('- OIDC_USERINFO_URL');
|
||||
console.log('- OIDC_CALLBACK_URL (optional, defaults to /auth/callback)');
|
||||
|
||||
// Add disabled auth routes
|
||||
app.get('/auth/login', (req, res) => {
|
||||
res.status(501).json({ error: 'Authentication not configured' });
|
||||
});
|
||||
|
||||
app.get('/auth/user', (req, res) => {
|
||||
res.status(401).json({ error: 'Authentication not configured' });
|
||||
});
|
||||
|
||||
app.post('/auth/logout', (req, res) => {
|
||||
res.status(501).json({ error: 'Authentication not configured' });
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Session configuration
|
||||
const session = require('express-session');
|
||||
|
||||
app.use(session({
|
||||
secret: process.env.SESSION_SECRET || 'your-secret-key-change-in-production',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
}));
|
||||
|
||||
// Initialize Passport
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
// Configure OpenID Connect strategy
|
||||
passport.use('oidc', new OpenIDConnectStrategy({
|
||||
issuer: process.env.OIDC_ISSUER,
|
||||
authorizationURL: process.env.OIDC_AUTH_URL,
|
||||
tokenURL: process.env.OIDC_TOKEN_URL,
|
||||
userInfoURL: process.env.OIDC_USERINFO_URL,
|
||||
clientID: process.env.OIDC_CLIENT_ID,
|
||||
clientSecret: process.env.OIDC_CLIENT_SECRET,
|
||||
callbackURL: process.env.OIDC_CALLBACK_URL || '/auth/callback',
|
||||
scope: 'openid email profile'
|
||||
}, (issuer, sub, profile, accessToken, refreshToken, done) => {
|
||||
// Extract user info from profile
|
||||
const userProfile = {
|
||||
sub: profile.id,
|
||||
email: profile.emails?.[0]?.value,
|
||||
name: profile.displayName
|
||||
};
|
||||
|
||||
userOps.findOrCreateUser(userProfile, (err, user) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
return done(null, user);
|
||||
});
|
||||
}));
|
||||
|
||||
// Serialize user for session
|
||||
passport.serializeUser((user, done) => {
|
||||
done(null, user.id);
|
||||
});
|
||||
|
||||
// Deserialize user from session
|
||||
passport.deserializeUser((id, done) => {
|
||||
userOps.findById(id, (err, user) => {
|
||||
done(err, user);
|
||||
});
|
||||
});
|
||||
|
||||
// Auth routes
|
||||
app.get('/auth/login', passport.authenticate('oidc'));
|
||||
|
||||
app.get('/auth/callback',
|
||||
passport.authenticate('oidc', { failureRedirect: '/login' }),
|
||||
(req, res) => {
|
||||
// Successful authentication, redirect to frontend
|
||||
res.redirect(process.env.FRONTEND_URL || '/');
|
||||
}
|
||||
);
|
||||
|
||||
app.post('/auth/logout', (req, res) => {
|
||||
req.logout((err) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Logout failed' });
|
||||
}
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Session destruction failed' });
|
||||
}
|
||||
res.json({ message: 'Logged out successfully' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Get current user
|
||||
app.get('/auth/user', (req, res) => {
|
||||
if (req.isAuthenticated()) {
|
||||
res.json({
|
||||
user: {
|
||||
id: req.user.id,
|
||||
email: req.user.email,
|
||||
name: req.user.name
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Middleware to require authentication
|
||||
function requireAuth(req, res, next) {
|
||||
if (req.isAuthenticated()) {
|
||||
return next();
|
||||
}
|
||||
res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
// Middleware to optionally include user info
|
||||
function optionalAuth(req, res, next) {
|
||||
// Always proceed, but user info will be available if authenticated
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
configureAuth,
|
||||
requireAuth,
|
||||
optionalAuth
|
||||
};
|
||||
200
backend/src/database.js
Normal file
200
backend/src/database.js
Normal file
@@ -0,0 +1,200 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
|
||||
// Database file path
|
||||
const DB_PATH = path.join(__dirname, '../data/bible.db');
|
||||
|
||||
// Initialize database
|
||||
const db = new sqlite3.Database(DB_PATH, (err) => {
|
||||
if (err) {
|
||||
console.error('Error opening database:', err.message);
|
||||
} else {
|
||||
console.log('Connected to SQLite database');
|
||||
initializeTables();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize database tables
|
||||
function initializeTables() {
|
||||
// Users table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
openid_sub TEXT UNIQUE NOT NULL,
|
||||
email TEXT,
|
||||
name TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// User preferences table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS user_preferences (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
font_size TEXT DEFAULT 'medium' CHECK(font_size IN ('small', 'medium', 'large')),
|
||||
dark_mode BOOLEAN DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
UNIQUE(user_id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Favorites table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS favorites (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
book TEXT NOT NULL,
|
||||
chapter TEXT NOT NULL,
|
||||
verse_start INTEGER,
|
||||
verse_end INTEGER,
|
||||
note TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
UNIQUE(user_id, book, chapter, verse_start, verse_end)
|
||||
)
|
||||
`);
|
||||
|
||||
console.log('Database tables initialized');
|
||||
}
|
||||
|
||||
// User operations
|
||||
const userOps = {
|
||||
// Find or create user by OpenID Connect subject
|
||||
findOrCreateUser: (profile, callback) => {
|
||||
const { sub, email, name } = profile;
|
||||
|
||||
db.get(
|
||||
'SELECT * FROM users WHERE openid_sub = ?',
|
||||
[sub],
|
||||
(err, user) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (user) {
|
||||
// Update user info
|
||||
db.run(
|
||||
'UPDATE users SET email = ?, name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[email, name, user.id],
|
||||
(err) => {
|
||||
if (err) return callback(err);
|
||||
callback(null, user);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Create new user
|
||||
db.run(
|
||||
'INSERT INTO users (openid_sub, email, name) VALUES (?, ?, ?)',
|
||||
[sub, email, name],
|
||||
function(err) {
|
||||
if (err) return callback(err);
|
||||
|
||||
const newUser = {
|
||||
id: this.lastID,
|
||||
openid_sub: sub,
|
||||
email,
|
||||
name
|
||||
};
|
||||
|
||||
// Create default preferences
|
||||
db.run(
|
||||
'INSERT INTO user_preferences (user_id) VALUES (?)',
|
||||
[newUser.id],
|
||||
(err) => {
|
||||
if (err) console.error('Error creating default preferences:', err);
|
||||
callback(null, newUser);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
// Find user by ID
|
||||
findById: (id, callback) => {
|
||||
db.get('SELECT * FROM users WHERE id = ?', [id], callback);
|
||||
}
|
||||
};
|
||||
|
||||
// Preferences operations
|
||||
const preferencesOps = {
|
||||
// Get user preferences
|
||||
getPreferences: (userId, callback) => {
|
||||
db.get(
|
||||
'SELECT * FROM user_preferences WHERE user_id = ?',
|
||||
[userId],
|
||||
callback
|
||||
);
|
||||
},
|
||||
|
||||
// Update user preferences
|
||||
updatePreferences: (userId, preferences, callback) => {
|
||||
const { font_size, dark_mode } = preferences;
|
||||
|
||||
db.run(
|
||||
`UPDATE user_preferences
|
||||
SET font_size = ?, dark_mode = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE user_id = ?`,
|
||||
[font_size, dark_mode ? 1 : 0, userId],
|
||||
callback
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Favorites operations
|
||||
const favoritesOps = {
|
||||
// Get user favorites
|
||||
getFavorites: (userId, callback) => {
|
||||
db.all(
|
||||
'SELECT * FROM favorites WHERE user_id = ? ORDER BY created_at DESC',
|
||||
[userId],
|
||||
callback
|
||||
);
|
||||
},
|
||||
|
||||
// Add favorite
|
||||
addFavorite: (userId, favorite, callback) => {
|
||||
const { book, chapter, verse_start, verse_end, note } = favorite;
|
||||
|
||||
db.run(
|
||||
`INSERT INTO favorites (user_id, book, chapter, verse_start, verse_end, note)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[userId, book, chapter, verse_start, verse_end, note],
|
||||
callback
|
||||
);
|
||||
},
|
||||
|
||||
// Remove favorite
|
||||
removeFavorite: (userId, favoriteId, callback) => {
|
||||
db.run(
|
||||
'DELETE FROM favorites WHERE id = ? AND user_id = ?',
|
||||
[favoriteId, userId],
|
||||
callback
|
||||
);
|
||||
},
|
||||
|
||||
// Check if verse is favorited
|
||||
isFavorited: (userId, book, chapter, verse_start, verse_end, callback) => {
|
||||
db.get(
|
||||
'SELECT id FROM favorites WHERE user_id = ? AND book = ? AND chapter = ? AND verse_start = ? AND verse_end = ?',
|
||||
[userId, book, chapter, verse_start, verse_end],
|
||||
(err, row) => {
|
||||
if (err) return callback(err);
|
||||
callback(null, !!row);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
db,
|
||||
userOps,
|
||||
preferencesOps,
|
||||
favoritesOps
|
||||
};
|
||||
@@ -3,6 +3,8 @@ const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const { configureAuth, requireAuth, optionalAuth } = require('./auth');
|
||||
const { preferencesOps, favoritesOps } = require('./database');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
@@ -14,9 +16,15 @@ app.use(helmet({
|
||||
crossOriginEmbedderPolicy: false,
|
||||
originAgentCluster: false
|
||||
}));
|
||||
app.use(cors());
|
||||
app.use(cors({
|
||||
origin: process.env.FRONTEND_URL || true,
|
||||
credentials: true
|
||||
}));
|
||||
app.use(express.json());
|
||||
|
||||
// Configure authentication
|
||||
configureAuth(app);
|
||||
|
||||
// Serve static files from the React build
|
||||
app.use(express.static(path.join(__dirname, '../../frontend/build')));
|
||||
|
||||
@@ -120,6 +128,107 @@ app.get('/books/:book/:chapter', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// User preferences routes
|
||||
app.get('/api/preferences', requireAuth, (req, res) => {
|
||||
preferencesOps.getPreferences(req.user.id, (err, preferences) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Failed to get preferences' });
|
||||
}
|
||||
res.json({
|
||||
font_size: preferences?.font_size || 'medium',
|
||||
dark_mode: !!preferences?.dark_mode
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.put('/api/preferences', requireAuth, (req, res) => {
|
||||
const { font_size, dark_mode } = req.body;
|
||||
|
||||
// Validate font_size
|
||||
if (font_size && !['small', 'medium', 'large'].includes(font_size)) {
|
||||
return res.status(400).json({ error: 'Invalid font_size' });
|
||||
}
|
||||
|
||||
preferencesOps.updatePreferences(req.user.id, { font_size, dark_mode }, (err) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Failed to update preferences' });
|
||||
}
|
||||
res.json({ message: 'Preferences updated successfully' });
|
||||
});
|
||||
});
|
||||
|
||||
// Favorites routes
|
||||
app.get('/api/favorites', requireAuth, (req, res) => {
|
||||
favoritesOps.getFavorites(req.user.id, (err, favorites) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Failed to get favorites' });
|
||||
}
|
||||
res.json({ favorites });
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/favorites', requireAuth, (req, res) => {
|
||||
const { book, chapter, verse_start, verse_end, note } = req.body;
|
||||
|
||||
if (!book || !chapter) {
|
||||
return res.status(400).json({ error: 'Book and chapter are required' });
|
||||
}
|
||||
|
||||
const favorite = {
|
||||
book,
|
||||
chapter,
|
||||
verse_start: verse_start || null,
|
||||
verse_end: verse_end || null,
|
||||
note: note || null
|
||||
};
|
||||
|
||||
favoritesOps.addFavorite(req.user.id, favorite, function(err) {
|
||||
if (err) {
|
||||
if (err.code === 'SQLITE_CONSTRAINT') {
|
||||
return res.status(409).json({ error: 'Favorite already exists' });
|
||||
}
|
||||
return res.status(500).json({ error: 'Failed to add favorite' });
|
||||
}
|
||||
res.json({
|
||||
message: 'Favorite added successfully',
|
||||
id: this.lastID
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.delete('/api/favorites/:id', requireAuth, (req, res) => {
|
||||
const favoriteId = req.params.id;
|
||||
|
||||
favoritesOps.removeFavorite(req.user.id, favoriteId, (err) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Failed to remove favorite' });
|
||||
}
|
||||
res.json({ message: 'Favorite removed successfully' });
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/favorites/check', requireAuth, (req, res) => {
|
||||
const { book, chapter, verse_start, verse_end } = req.query;
|
||||
|
||||
if (!book || !chapter) {
|
||||
return res.status(400).json({ error: 'Book and chapter are required' });
|
||||
}
|
||||
|
||||
favoritesOps.isFavorited(
|
||||
req.user.id,
|
||||
book,
|
||||
chapter,
|
||||
verse_start || null,
|
||||
verse_end || null,
|
||||
(err, isFavorited) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Failed to check favorite status' });
|
||||
}
|
||||
res.json({ isFavorited });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Catch-all handler: send back React's index.html for client-side routing
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../../frontend/build/index.html'));
|
||||
|
||||
Reference in New Issue
Block a user