diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0f5b8fe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules +.nuxt +.output +.git +.gitignore +README.md +.env +.env.* +data/*.db diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea1ba16 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Nuxt dev/build outputs +.output +.data +.nuxt +.nitro +.cache +dist + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Misc +.DS_Store +.fleet +.idea + +# Local env files +.env +.env.* +!.env.example + +# Database +data/ +*.db +*.db-shm +*.db-wal diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..210f8b7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +FROM node:20-alpine + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy application files +COPY . . + +# Create data directory for SQLite database +RUN mkdir -p /app/data + +# Build the application +RUN npm run build + +# Expose port +EXPOSE 3000 + +# Set environment variables +ENV NODE_ENV=production +ENV NUXT_HOST=0.0.0.0 +ENV NUXT_PORT=3000 + +# Start the application +CMD ["node", ".output/server/index.mjs"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..262265a --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +# New Life Christian Church - Sermon Itinerary + +A web application for managing and displaying weekly sermons for New Life Christian Church. + +## Features + +- 📝 **Sermon Management**: Create and manage sermon content with a user-friendly form +- 🔐 **Authentication**: Secure admin access with login system +- 📱 **QR Codes**: Generate QR codes for easy sermon sharing +- 📅 **Date-based URLs**: Sermons accessible via `sermon-MMDDYYYY` format +- 🎨 **Modern UI**: Clean, responsive design using Tailwind CSS and Inter font +- 📊 **Three Sections**: Bible References, Personal Appliance, and Pastor's Challenge +- 🗂️ **Smart Organization**: Recent sermons (last 3 months) displayed by default, older sermons in dropdown +- 🐳 **Docker Ready**: Fully containerized for easy deployment + +## Technology Stack + +- **Frontend**: Nuxt 3 (Vue.js) +- **Styling**: Tailwind CSS with Inter font from Google Fonts +- **Database**: SQLite +- **QR Codes**: qrcode library +- **Deployment**: Docker & Docker Compose + +## Getting Started + +### Prerequisites + +- Docker and Docker Compose installed on your system + +### Installation & Deployment + +1. Clone the repository: +```bash +git clone +cd nlcc-itinerary +``` + +2. Update environment variables in `docker-compose.yml`: +```yaml +environment: + - AUTH_SECRET=your-secret-key-change-in-production + - SITE_URL=https://newlife-christian.com +``` + +3. Build and run with Docker Compose: +```bash +docker-compose up -d +``` + +The application will be available at `http://localhost:3000` + +### Default Credentials + +- **Username**: admin +- **Password**: admin123 + +⚠️ **Important**: Change these credentials in production by modifying `server/utils/database.ts` + +## Project Structure + +``` +nlcc-itinerary/ +├── assets/css/ # Global styles +├── components/ # Vue components +│ ├── SermonCard.vue +│ ├── QRCodeButton.vue +│ └── QRCodeModal.vue +├── middleware/ # Route middleware +│ └── auth.ts +├── pages/ # Application pages +│ ├── index.vue # Main sermon listing +│ ├── login.vue # Admin login +│ ├── admin.vue # Sermon creation form +│ └── [slug].vue # Individual sermon page +├── server/ +│ ├── api/ # API endpoints +│ │ ├── auth/ # Authentication endpoints +│ │ └── sermons/ # Sermon CRUD endpoints +│ └── utils/ # Server utilities +│ ├── database.ts # SQLite database functions +│ └── auth.ts # Authentication helpers +├── logos/ # Church logos +├── Dockerfile # Docker configuration +├── docker-compose.yml # Docker Compose configuration +└── nuxt.config.ts # Nuxt configuration +``` + +## Usage + +### Creating a Sermon + +1. Navigate to `/login` and sign in with admin credentials +2. You'll be redirected to `/admin` +3. Fill in the sermon details: + - **Date**: Select the sermon date (URL will be auto-generated as `sermon-MMDDYYYY`) + - **Title**: Enter the sermon title + - **Bible References**: Add one or more Bible verses (use +/- buttons) + - **Personal Appliance**: Enter personal application content + - **Pastor's Challenge**: Enter the pastor's challenge content +4. Click "Create Sermon" + +### Viewing Sermons + +- **Main Page** (`/`): Shows recent sermons (last 3 months) with option to view older ones +- **Individual Sermon** (`/sermon-MMDDYYYY`): Full sermon details with QR code +- **QR Code**: Click the QR code button on any sermon card or page to generate a scannable code + +## Database + +The application uses SQLite with the following schema: + +### Sermons Table +- `id`: Primary key +- `slug`: Unique sermon identifier (e.g., sermon-09282025) +- `title`: Sermon title +- `date`: Sermon date +- `bible_references`: Newline-separated Bible verses +- `personal_appliance`: Personal application content +- `pastors_challenge`: Pastor's challenge content +- `created_at`: Timestamp + +### Users Table +- `id`: Primary key +- `username`: User's username +- `password`: User's password (plain text - should be hashed in production) + +## Security Notes + +⚠️ **For Production Use**: + +1. Change the default admin credentials +2. Implement proper password hashing (bcrypt, argon2, etc.) +3. Use a strong `AUTH_SECRET` in environment variables +4. Enable HTTPS +5. Consider implementing rate limiting +6. Add CSRF protection + +## Docker Commands + +```bash +# Build and start +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop containers +docker-compose down + +# Rebuild after changes +docker-compose up -d --build + +# Access container shell +docker exec -it nlcc-itinerary sh +``` + +## Data Persistence + +The SQLite database is stored in the `./data` directory, which is mounted as a volume in Docker. This ensures sermon data persists across container restarts. + +## License + +This project is created for New Life Christian Church. + +## Support + +For issues or questions, please contact the development team. diff --git a/app.vue b/app.vue new file mode 100644 index 0000000..2b1be09 --- /dev/null +++ b/app.vue @@ -0,0 +1,5 @@ + diff --git a/assets/css/main.css b/assets/css/main.css new file mode 100644 index 0000000..8c5ad1e --- /dev/null +++ b/assets/css/main.css @@ -0,0 +1,11 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +* { + font-family: 'Inter', sans-serif; +} + +body { + @apply bg-gray-50; +} diff --git a/components/QRCodeButton.vue b/components/QRCodeButton.vue new file mode 100644 index 0000000..649c039 --- /dev/null +++ b/components/QRCodeButton.vue @@ -0,0 +1,35 @@ + + + diff --git a/components/QRCodeModal.vue b/components/QRCodeModal.vue new file mode 100644 index 0000000..efbd215 --- /dev/null +++ b/components/QRCodeModal.vue @@ -0,0 +1,63 @@ + + + diff --git a/components/SermonCard.vue b/components/SermonCard.vue new file mode 100644 index 0000000..30e586c --- /dev/null +++ b/components/SermonCard.vue @@ -0,0 +1,44 @@ + + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..de8a7ed --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +services: + nlcc-itinerary: + build: . + container_name: nlcc-itinerary + ports: + - "3002:3000" + volumes: + - ./data:/app/data + environment: + - NODE_ENV=production + - AUTH_SECRET=d8c7c1735fc853b807c1bccce791b054 + - SITE_URL=https://nlcc.rydertech.us + restart: unless-stopped diff --git a/middleware/auth.ts b/middleware/auth.ts new file mode 100644 index 0000000..805379f --- /dev/null +++ b/middleware/auth.ts @@ -0,0 +1,9 @@ +export default defineNuxtRouteMiddleware(async (to, from) => { + if (import.meta.server) return + + const { data } = await useFetch('/api/auth/verify') + + if (!data.value?.authenticated) { + return navigateTo('/login') + } +}) diff --git a/nuxt.config.ts b/nuxt.config.ts new file mode 100644 index 0000000..859a2a3 --- /dev/null +++ b/nuxt.config.ts @@ -0,0 +1,36 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + compatibilityDate: '2024-04-03', + devtools: { enabled: true }, + + modules: [ + '@nuxtjs/tailwindcss', + '@nuxt/ui' + ], + + app: { + head: { + title: 'New Life Christian Church - Sermons', + meta: [ + { charset: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { name: 'description', content: 'Weekly sermons from New Life Christian Church' } + ], + link: [ + { rel: 'icon', type: 'image/png', href: '/logos/favicon.png' }, + { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, + { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' }, + { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap' } + ] + } + }, + + css: ['~/assets/css/main.css'], + + runtimeConfig: { + authSecret: process.env.AUTH_SECRET || 'change-this-secret-in-production', + public: { + siteUrl: process.env.SITE_URL || 'https://newlife-christian.com' + } + } +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..4532180 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "nlcc-itinerary", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "nuxt dev", + "build": "nuxt build", + "generate": "nuxt generate", + "preview": "nuxt preview", + "postinstall": "nuxt prepare" + }, + "dependencies": { + "@nuxt/ui": "^2.18.4", + "better-sqlite3": "^11.3.0", + "nuxt": "^3.13.2", + "qrcode": "^1.5.4", + "vue": "^3.5.5", + "vue-router": "^4.4.5" + }, + "devDependencies": { + "@nuxtjs/tailwindcss": "^6.12.1", + "@types/better-sqlite3": "^7.6.11", + "@types/qrcode": "^1.5.5" + } +} diff --git a/pages/[slug].vue b/pages/[slug].vue new file mode 100644 index 0000000..5f94b4a --- /dev/null +++ b/pages/[slug].vue @@ -0,0 +1,106 @@ + + + diff --git a/pages/admin.vue b/pages/admin.vue new file mode 100644 index 0000000..cb8f187 --- /dev/null +++ b/pages/admin.vue @@ -0,0 +1,212 @@ + + + diff --git a/pages/index.vue b/pages/index.vue new file mode 100644 index 0000000..414e30f --- /dev/null +++ b/pages/index.vue @@ -0,0 +1,99 @@ + + + diff --git a/pages/login.vue b/pages/login.vue new file mode 100644 index 0000000..808d8f4 --- /dev/null +++ b/pages/login.vue @@ -0,0 +1,93 @@ + + + diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts new file mode 100644 index 0000000..828db0f --- /dev/null +++ b/server/api/auth/login.post.ts @@ -0,0 +1,30 @@ +import { getUserByUsername } from '~/server/utils/database' +import { setAuthCookie } from '~/server/utils/auth' + +export default defineEventHandler(async (event) => { + const body = await readBody(event) + const { username, password } = body + + if (!username || !password) { + throw createError({ + statusCode: 400, + message: 'Username and password are required' + }) + } + + const user = getUserByUsername(username) + + if (!user || user.password !== password) { + throw createError({ + statusCode: 401, + message: 'Invalid credentials' + }) + } + + setAuthCookie(event, username) + + return { + success: true, + username: user.username + } +}) diff --git a/server/api/auth/logout.post.ts b/server/api/auth/logout.post.ts new file mode 100644 index 0000000..bc19e71 --- /dev/null +++ b/server/api/auth/logout.post.ts @@ -0,0 +1,9 @@ +import { clearAuthCookie } from '~/server/utils/auth' + +export default defineEventHandler(async (event) => { + clearAuthCookie(event) + + return { + success: true + } +}) diff --git a/server/api/auth/verify.get.ts b/server/api/auth/verify.get.ts new file mode 100644 index 0000000..3bd2145 --- /dev/null +++ b/server/api/auth/verify.get.ts @@ -0,0 +1,9 @@ +import { isAuthenticated } from '~/server/utils/auth' + +export default defineEventHandler(async (event) => { + const authenticated = isAuthenticated(event) + + return { + authenticated + } +}) diff --git a/server/api/sermons/[slug].get.ts b/server/api/sermons/[slug].get.ts new file mode 100644 index 0000000..a9b0e26 --- /dev/null +++ b/server/api/sermons/[slug].get.ts @@ -0,0 +1,23 @@ +import { getSermonBySlug } from '~/server/utils/database' + +export default defineEventHandler(async (event) => { + const slug = getRouterParam(event, 'slug') + + if (!slug) { + throw createError({ + statusCode: 400, + message: 'Slug is required' + }) + } + + const sermon = getSermonBySlug(slug) + + if (!sermon) { + throw createError({ + statusCode: 404, + message: 'Sermon not found' + }) + } + + return sermon +}) diff --git a/server/api/sermons/index.get.ts b/server/api/sermons/index.get.ts new file mode 100644 index 0000000..d910ec5 --- /dev/null +++ b/server/api/sermons/index.get.ts @@ -0,0 +1,10 @@ +import { getAllSermons } from '~/server/utils/database' + +export default defineEventHandler(async (event) => { + const query = getQuery(event) + const limit = query.limit ? parseInt(query.limit as string) : undefined + + const sermons = getAllSermons(limit) + + return sermons +}) diff --git a/server/api/sermons/index.post.ts b/server/api/sermons/index.post.ts new file mode 100644 index 0000000..a1de744 --- /dev/null +++ b/server/api/sermons/index.post.ts @@ -0,0 +1,43 @@ +import { createSermon } from '~/server/utils/database' +import { isAuthenticated } from '~/server/utils/auth' + +export default defineEventHandler(async (event) => { + // Check authentication + if (!isAuthenticated(event)) { + throw createError({ + statusCode: 401, + message: 'Unauthorized' + }) + } + + const body = await readBody(event) + const { slug, title, date, bible_references, personal_appliance, pastors_challenge } = body + + if (!slug || !title || !date || !bible_references || !personal_appliance || !pastors_challenge) { + throw createError({ + statusCode: 400, + message: 'All fields are required' + }) + } + + try { + createSermon({ + slug, + title, + date, + bible_references, + personal_appliance, + pastors_challenge + }) + + return { + success: true, + message: 'Sermon created successfully' + } + } catch (error: any) { + throw createError({ + statusCode: 500, + message: error.message || 'Failed to create sermon' + }) + } +}) diff --git a/server/utils/auth.ts b/server/utils/auth.ts new file mode 100644 index 0000000..59f9e1c --- /dev/null +++ b/server/utils/auth.ts @@ -0,0 +1,23 @@ +import { H3Event } from 'h3' + +export function setAuthCookie(event: H3Event, username: string) { + setCookie(event, 'auth', username, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + maxAge: 60 * 60 * 24 * 7, // 7 days + path: '/' + }) +} + +export function getAuthCookie(event: H3Event) { + return getCookie(event, 'auth') +} + +export function clearAuthCookie(event: H3Event) { + deleteCookie(event, 'auth') +} + +export function isAuthenticated(event: H3Event): boolean { + const auth = getAuthCookie(event) + return !!auth +} diff --git a/server/utils/database.ts b/server/utils/database.ts new file mode 100644 index 0000000..20dbaea --- /dev/null +++ b/server/utils/database.ts @@ -0,0 +1,93 @@ +import Database from 'better-sqlite3' +import { join } from 'path' + +let db: Database.Database | null = null + +export interface Sermon { + id?: number + slug: string + title: string + date: string + bible_references: string + personal_appliance: string + pastors_challenge: string + created_at?: string +} + +export interface User { + id?: number + username: string + password: string +} + +export function getDatabase() { + if (!db) { + const dbPath = join(process.cwd(), 'data', 'sermons.db') + db = new Database(dbPath) + + // Create tables if they don't exist + db.exec(` + CREATE TABLE IF NOT EXISTS sermons ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + slug TEXT UNIQUE NOT NULL, + title TEXT NOT NULL, + date TEXT NOT NULL, + bible_references TEXT NOT NULL, + personal_appliance TEXT NOT NULL, + pastors_challenge TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `) + + db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL + ) + `) + + // Insert default admin user (password: admin123) + // In production, this should be hashed properly + const userExists = db.prepare('SELECT COUNT(*) as count FROM users WHERE username = ?').get('admin') as { count: number } + if (userExists.count === 0) { + db.prepare('INSERT INTO users (username, password) VALUES (?, ?)').run('admin', 'admin123') + } + } + + return db +} + +export function getAllSermons(limit?: number) { + const db = getDatabase() + if (limit) { + return db.prepare('SELECT * FROM sermons ORDER BY date DESC LIMIT ?').all(limit) as Sermon[] + } + return db.prepare('SELECT * FROM sermons ORDER BY date DESC').all() as Sermon[] +} + +export function getSermonBySlug(slug: string) { + const db = getDatabase() + return db.prepare('SELECT * FROM sermons WHERE slug = ?').get(slug) as Sermon | undefined +} + +export function createSermon(sermon: Sermon) { + const db = getDatabase() + const stmt = db.prepare(` + INSERT INTO sermons (slug, title, date, bible_references, personal_appliance, pastors_challenge) + VALUES (?, ?, ?, ?, ?, ?) + `) + return stmt.run( + sermon.slug, + sermon.title, + sermon.date, + sermon.bible_references, + sermon.personal_appliance, + sermon.pastors_challenge + ) +} + +export function getUserByUsername(username: string) { + const db = getDatabase() + return db.prepare('SELECT * FROM users WHERE username = ?').get(username) as User | undefined +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..360846b --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,18 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./components/**/*.{js,vue,ts}", + "./layouts/**/*.vue", + "./pages/**/*.vue", + "./plugins/**/*.{js,ts}", + "./app.vue", + ], + theme: { + extend: { + fontFamily: { + sans: ['Inter', 'sans-serif'], + }, + }, + }, + plugins: [], +}