diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 54ff657..0000000 --- a/Dockerfile +++ /dev/null @@ -1,53 +0,0 @@ -# Stage 1: Builder -FROM node:24-alpine AS builder - -# Install dumb-init, Python, and build tools for native builds -RUN apk add --no-cache dumb-init python3 build-base - -# Set working directory -WORKDIR /app - -# Copy package files -COPY package*.json ./ - -# Install all dependencies (including dev dependencies for building) -RUN npm install - -# Copy application code -COPY . . - -# Build the application -RUN npm run build - -# Stage 2: Runner -FROM node:24-alpine - -# Install dumb-init, wget for healthcheck, and build tools for native modules -RUN apk add --no-cache dumb-init wget python3 build-base - -# Set working directory -WORKDIR /app - -# Copy package files for rebuilding native modules -COPY --from=builder /app/package*.json ./ - -# Copy node_modules from builder -COPY --from=builder /app/node_modules ./node_modules - -# Rebuild native modules for production environment -RUN npm rebuild better-sqlite3 - -# Copy the built application from builder stage -COPY --from=builder /app/.output ./.output - -# Create data directory (don't pre-create database file) -RUN mkdir -p /app/data - -# Expose port -EXPOSE 3000 - -# Use dumb-init to handle signals properly -ENTRYPOINT ["/usr/bin/dumb-init", "--"] - -# Start the application -CMD ["node", ".output/server/index.mjs"] diff --git a/README.md b/README.md deleted file mode 100644 index c93c811..0000000 --- a/README.md +++ /dev/null @@ -1,101 +0,0 @@ -# New Life Christian Church Sermon Management System - -A Nuxt 4 application for managing and displaying church sermons with authentication and QR code generation. - -## Features - -- **Sermon Management**: Create, view, and manage sermons with Bible references, personal applications, and pastor's challenges -- **Authentication**: Secure admin login for sermon creation -- **Responsive Design**: Built with NuxtUI for a modern, mobile-friendly interface -- **QR Codes**: Auto-generated QR codes for each sermon linking to the sermon page -- **SQLite Database**: Lightweight, file-based database for sermon storage -- **Docker Ready**: Complete containerization for easy deployment - -## Quick Start - -1. **Clone and setup**: - ```bash - git clone - cd nlcc-sermons - npm install - ``` - -2. **Run locally**: - ```bash - npm run dev - ``` - -3. **Build for production**: - ```bash - npm run build - ``` - -4. **Deploy with Docker**: - ```bash - docker-compose up -d - ``` - -## Environment Variables - -Create a `.env` file or set these environment variables: - -- `JWT_SECRET`: Secret key for JWT token generation (change in production!) -- `ADMIN_PASSWORD`: Default admin password (default: admin123) - -## Default Login - -- **Username**: admin -- **Password**: admin123 (or set via ADMIN_PASSWORD) - -## Project Structure - -``` -├── server/ -│ ├── api/ # API routes -│ │ ├── auth/ # Authentication endpoints -│ │ └── sermons/ # Sermon management endpoints -│ └── utils/ # Database and auth utilities -├── components/ # Vue components -├── pages/ # Nuxt pages -├── public/ # Static assets -└── data/ # SQLite database location -``` - -## API Endpoints - -- `POST /api/auth/login` - User authentication -- `GET /api/sermons` - List sermons (with time filtering) -- `POST /api/sermons` - Create new sermon (admin only) -- `GET /api/sermons/[slug]` - Get specific sermon - -## Docker Deployment - -The application is fully containerized and ready for deployment: - -```bash -# Build and run -docker-compose up -d - -# View logs -docker-compose logs -f - -# Stop -docker-compose down -``` - -The SQLite database will persist in a Docker volume named `sermon_data`. - -## Development - -- Uses Nuxt 4 with NuxtUI components -- TypeScript for type safety -- Tailwind CSS for styling -- QR code generation for sermon sharing -- Responsive design for all devices - -## Security Notes - -- Change default passwords before production deployment -- Use strong JWT secrets in production -- Consider implementing rate limiting for API endpoints -- Database is stored in `/data/sermons.db` in the container diff --git a/app.vue b/app.vue deleted file mode 100644 index f5e850d..0000000 --- a/app.vue +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/assets/css/main.css b/assets/css/main.css deleted file mode 100644 index 22acd89..0000000 --- a/assets/css/main.css +++ /dev/null @@ -1,5 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); diff --git a/components/QRCodeButton.vue b/components/QRCodeButton.vue deleted file mode 100644 index 672eea6..0000000 --- a/components/QRCodeButton.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - diff --git a/components/QRCodeModal.vue b/components/QRCodeModal.vue deleted file mode 100644 index 919051a..0000000 --- a/components/QRCodeModal.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - diff --git a/components/SermonCard.vue b/components/SermonCard.vue deleted file mode 100644 index 200846b..0000000 --- a/components/SermonCard.vue +++ /dev/null @@ -1,93 +0,0 @@ - - - diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 66b6d6c..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,22 +0,0 @@ -services: - sermons: - build: . - ports: - - "3002:3000" - environment: - - NODE_ENV=production - - JWT_SECRET=d8c7c1735fc853b807c1bccce791b054 - - ADMIN_PASSWORD=admin123 - volumes: - - sermon_data:/app/data - restart: unless-stopped - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000"] - interval: 30s - retries: 3 - start_period: 40s - timeout: 10s - -volumes: - sermon_data: - driver: local diff --git a/middleware/auth.ts b/middleware/auth.ts deleted file mode 100644 index 4eafd33..0000000 --- a/middleware/auth.ts +++ /dev/null @@ -1,21 +0,0 @@ -export default defineNuxtRouteMiddleware(async (to, from) => { - const token = useCookie('auth_token') - - if (!token.value && to.path === '/admin') { - // Redirect to login page if not authenticated and trying to access admin - return navigateTo('/login') - } - - if (token.value) { - try { - // Verify token with server - await $fetch('/api/auth/verify') - } catch { - // If token is invalid, clear it and redirect to login - token.value = '' - if (to.path === '/admin') { - return navigateTo('/login') - } - } - } -}) diff --git a/nuxt.config.ts b/nuxt.config.ts deleted file mode 100644 index 722aeab..0000000 --- a/nuxt.config.ts +++ /dev/null @@ -1,47 +0,0 @@ -export default defineNuxtConfig({ - compatibilityDate: '2024-04-03', - devtools: { enabled: false }, - css: ['~/assets/css/main.css'], - modules: [ - '@nuxt/ui' - ], - colorMode: { - preference: 'light' - }, - runtimeConfig: { - jwtSecret: process.env.JWT_SECRET || 'your-secret-key', - adminPassword: process.env.ADMIN_PASSWORD || 'admin123' - }, - nitro: { - experimental: { - wasm: true - }, - externals: { - inline: ['better-sqlite3'] - }, - moduleSideEffects: ['better-sqlite3'], - alias: { - 'tailwindcss/colors': 'tailwindcss/colors.js' - } - }, - app: { - head: { - 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@400;500;600;700&display=swap' - } - ] - } - } -}) diff --git a/package.json b/package.json deleted file mode 100644 index d68e522..0000000 --- a/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "nlcc-sermons", - "version": "1.0.0", - "description": "New Life Christian Church Sermon Management System", - "type": "module", - "scripts": { - "build": "nuxt build", - "dev": "nuxt dev", - "generate": "nuxt generate", - "preview": "nuxt preview", - "postinstall": "nuxt prepare", - "start": "nuxt start" - }, - "dependencies": { - "nuxt": "^4.0.0", - "@nuxt/ui": "^4.0.0", - "better-sqlite3": "^11.3.0", - "qrcode": "^1.5.4", - "jose": "^5.3.0", - "date-fns": "^4.1.0", - "bcryptjs": "^2.4.3", - "tailwindcss": "^3.0.0", - "postcss": "^8.0.0", - "autoprefixer": "^10.0.0" - }, - "devDependencies": { - "typescript": "^5.4.0", - "@types/node": "^20.0.0", - "@types/better-sqlite3": "^7.6.8", - "@types/qrcode": "^1.5.5" - } -} diff --git a/pages/[slug].vue b/pages/[slug].vue deleted file mode 100644 index f241fc2..0000000 --- a/pages/[slug].vue +++ /dev/null @@ -1,154 +0,0 @@ - - - diff --git a/pages/admin.vue b/pages/admin.vue deleted file mode 100644 index e2cc84b..0000000 --- a/pages/admin.vue +++ /dev/null @@ -1,244 +0,0 @@ - - - diff --git a/pages/index.vue b/pages/index.vue deleted file mode 100644 index a3e9275..0000000 --- a/pages/index.vue +++ /dev/null @@ -1,130 +0,0 @@ - - - diff --git a/pages/login.vue b/pages/login.vue deleted file mode 100644 index 643fffd..0000000 --- a/pages/login.vue +++ /dev/null @@ -1,89 +0,0 @@ - - - diff --git a/pages/logos/favicon.png b/pages/logos/favicon.png deleted file mode 100644 index c180239..0000000 Binary files a/pages/logos/favicon.png and /dev/null differ diff --git a/pages/logos/logo.png b/pages/logos/logo.png deleted file mode 100644 index c9840e1..0000000 Binary files a/pages/logos/logo.png and /dev/null differ diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index 2e7af2b..0000000 --- a/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/public/fonts/README.md b/public/fonts/README.md deleted file mode 100644 index c169cd0..0000000 --- a/public/fonts/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Neue Haas Grotesk Font Setup - -If you have the Neue Haas Grotesk font files, place them in this directory with the following naming convention: - -## Required Font Files: - -- `neue-haas-grotesk-regular.woff2` (and `.woff`) -- `neue-haas-grotesk-medium.woff2` (and `.woff`) - for 500 weight -- `neue-haas-grotesk-bold.woff2` (and `.woff`) - for 700 weight - -## Alternative Setup: - -If you don't have the Neue Haas Grotesk font files, the application will automatically fall back to using Inter font from Google Fonts, which is a similar clean, modern sans-serif font. - -## Current Font Stack: - -The application uses the following font fallback order: -1. Inter (Google Fonts) - Primary fallback -2. Neue Haas Grotesk (if font files are present) -3. System UI fonts (system-ui, -apple-system, etc.) -4. Generic sans-serif - -## To Enable Neue Haas Grotesk: - -1. Add your font files to this `/public/fonts/` directory -2. Uncomment the `@font-face` declarations in `assets/css/main.css` -3. The font will automatically be used throughout the application - -## Font Weights Used: - -- Regular (400): Body text, general content -- Medium (500): Subheadings, important information -- Bold (700): Headings, buttons, emphasis diff --git a/public/logos/logo.png b/public/logos/logo.png deleted file mode 100644 index c9840e1..0000000 Binary files a/public/logos/logo.png and /dev/null differ diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts deleted file mode 100644 index eb76d19..0000000 --- a/server/api/auth/login.post.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { authenticateUser, createJWT } 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, - statusMessage: 'Username and password are required' - }) - } - - const user = await authenticateUser(username, password) - if (!user) { - throw createError({ - statusCode: 401, - statusMessage: 'Invalid credentials' - }) - } - - const token = await createJWT(user) - - setCookie(event, 'auth_token', token, { - httpOnly: false, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - maxAge: 7 * 24 * 60 * 60 // 7 days - }) - - return { - user: { - id: user.id, - username: user.username - } - } -}) diff --git a/server/api/auth/logout.post.ts b/server/api/auth/logout.post.ts deleted file mode 100644 index 0029bad..0000000 --- a/server/api/auth/logout.post.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default defineEventHandler(async (event) => { - // Clear the auth cookie - deleteCookie(event, 'auth_token') - - return { success: true } -}) diff --git a/server/api/auth/verify.get.ts b/server/api/auth/verify.get.ts deleted file mode 100644 index cd1d416..0000000 --- a/server/api/auth/verify.get.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { verifyJWT } from '~/server/utils/auth' - -export default defineEventHandler(async (event) => { - const token = getCookie(event, 'auth_token') - - if (!token) { - throw createError({ - statusCode: 401, - statusMessage: 'No authentication token provided' - }) - } - - const payload = await verifyJWT(token) - if (!payload) { - throw createError({ - statusCode: 401, - statusMessage: 'Invalid authentication token' - }) - } - - return { - user: { - id: payload.userId, - username: payload.username - } - } -}) diff --git a/server/api/sermons/[slug].get.ts b/server/api/sermons/[slug].get.ts deleted file mode 100644 index 556ee6a..0000000 --- a/server/api/sermons/[slug].get.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { getDatabase } from '~/server/utils/database.server' -import { getRouterParam, createError } from 'h3' - -export default defineEventHandler(async (event) => { - try { - const db = await getDatabase() - const slug = getRouterParam(event, 'slug') - - if (!slug) { - throw createError({ - statusCode: 400, - statusMessage: 'Sermon slug is required' - }) - } - - const sermon = db.prepare('SELECT * FROM sermons WHERE slug = ?').get(slug) as any - - if (!sermon) { - throw createError({ - statusCode: 404, - statusMessage: 'Sermon not found' - }) - } - - // Parse JSON fields - return { - ...sermon, - bibleReferences: sermon.bible_references ? JSON.parse(sermon.bible_references) : [] - } - } catch (error) { - console.error('Error loading sermon:', error) - throw createError({ - statusCode: 500, - statusMessage: 'Failed to load sermon' - }) - } -}) diff --git a/server/api/sermons/index.get.ts b/server/api/sermons/index.get.ts deleted file mode 100644 index 50b10e6..0000000 --- a/server/api/sermons/index.get.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { getDatabase } from '~/server/utils/database.server' -import { verifyJWT } from '~/server/utils/auth' -import { getQuery } from 'h3' - -export default defineEventHandler(async (event) => { - try { - const db = await getDatabase() - - // Check for time filter - const query = getQuery(event) - const timeFilter = query.time as string || '3months' - - let dateFilter = '1970-01-01T00:00:00.000Z' - const now = new Date() - - switch (timeFilter) { - case '3months': - const threeMonthsAgo = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate()) - dateFilter = threeMonthsAgo.toISOString() - break - case '6months': - const sixMonthsAgo = new Date(now.getFullYear(), now.getMonth() - 6, now.getDate()) - dateFilter = sixMonthsAgo.toISOString() - break - case '1year': - const oneYearAgo = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate()) - dateFilter = oneYearAgo.toISOString() - break - case 'all': - dateFilter = '1970-01-01T00:00:00.000Z' - break - } - - const sermons = db.prepare(` - SELECT * FROM sermons - WHERE date >= ? - ORDER BY date DESC - `).all(dateFilter) as any[] - - return { data: sermons || [] } - } catch (error) { - console.error('Error loading sermons:', error) - return { data: [] } - } -}) diff --git a/server/api/sermons/index.post.ts b/server/api/sermons/index.post.ts deleted file mode 100644 index 6ef08c0..0000000 --- a/server/api/sermons/index.post.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { getDatabase } from '~/server/utils/database.server' -import { verifyJWT } from '~/server/utils/auth' - -const generateSlug = (title: string, date: string) => { - if (!title || !date) return '' - - const formattedTitle = title - .toLowerCase() - .replace(/[^a-z0-9\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-') - .trim() - - const dateObj = new Date(date) - const month = String(dateObj.getMonth() + 1).padStart(2, '0') - const day = String(dateObj.getDate()).padStart(2, '0') - const year = dateObj.getFullYear() - - return `sermon-${month}${day}${year}` -} - -export default defineEventHandler(async (event) => { - const db = await getDatabase() - - const body = await readBody(event) - const { title, date, bibleReferences, personalApplication, pastorChallenge } = body - - if (!title || !date) { - throw createError({ - statusCode: 400, - statusMessage: 'Title and date are required' - }) - } - - const slug = generateSlug(title, date) - - try { - const result = db.prepare(` - INSERT INTO sermons (title, date, slug, bible_references, personal_application, pastor_challenge) - VALUES (?, ?, ?, ?, ?, ?) - `).run(title, date, slug, JSON.stringify(bibleReferences || []), personalApplication || '', pastorChallenge || '') - - return { - id: result.lastInsertRowid, - title, - date, - slug, - bibleReferences: bibleReferences || [], - personalApplication: personalApplication || '', - pastorChallenge: pastorChallenge || '' - } - } catch (error: any) { - if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') { - throw createError({ - statusCode: 409, - statusMessage: 'A sermon with this date already exists' - }) - } - throw error - } -}) diff --git a/server/utils/auth.ts b/server/utils/auth.ts deleted file mode 100644 index 32e11a0..0000000 --- a/server/utils/auth.ts +++ /dev/null @@ -1,48 +0,0 @@ -import bcrypt from 'bcryptjs' -import { SignJWT, jwtVerify } from 'jose' -import { getDatabase } from './database.server' - -export interface User { - id: number - username: string - password_hash: string - created_at: string -} - -export async function authenticateUser(username: string, password: string): Promise { - const db = await getDatabase() - const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username) as User | undefined - - if (!user) return null - - const isValid = await bcrypt.compare(password, user.password_hash) - if (!isValid) return null - - return user -} - -export async function createJWT(user: User): Promise { - const config = useRuntimeConfig() - const secret = new TextEncoder().encode(config.jwtSecret) - - return await new SignJWT({ userId: user.id, username: user.username }) - .setProtectedHeader({ alg: 'HS256' }) - .setIssuedAt() - .setExpirationTime('7d') - .sign(secret) -} - -export async function verifyJWT(token: string): Promise<{ userId: number; username: string } | null> { - try { - const config = useRuntimeConfig() - const secret = new TextEncoder().encode(config.jwtSecret) - - const { payload } = await jwtVerify(token, secret) - return { - userId: payload.userId as number, - username: payload.username as string - } - } catch { - return null - } -} diff --git a/server/utils/database.server.ts b/server/utils/database.server.ts deleted file mode 100644 index e60f1f6..0000000 --- a/server/utils/database.server.ts +++ /dev/null @@ -1,71 +0,0 @@ -import Database from 'better-sqlite3' -import { join } from 'path' -import { existsSync, mkdirSync } from 'fs' -import { dirname } from 'path' -import bcrypt from 'bcryptjs' - -let db: Database.Database - -export async function getDatabase() { - if (!db) { - // Use absolute path in production (Docker), relative path in development - const isProduction = process.env.NODE_ENV === 'production' - const dbPath = isProduction - ? '/app/data/sermons.db' - : './data/sermons.db' - - // Ensure directory exists - const dir = dirname(dbPath) - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }) - } - - db = new Database(dbPath) - await initializeDatabase(db) - } - return db -} - -async function initializeDatabase(db: Database.Database) { - // Create sermons table - db.exec(` - CREATE TABLE IF NOT EXISTS sermons ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT NOT NULL, - date TEXT NOT NULL, - slug TEXT UNIQUE NOT NULL, - bible_references TEXT, - personal_application TEXT, - pastor_challenge TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) - `) - - // Create users table for authentication - db.exec(` - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) - `) - - // Create default admin user if it doesn't exist - const config = useRuntimeConfig() - const saltRounds = 10 - const passwordHash = await bcrypt.hash(config.adminPassword, saltRounds) - - const existingAdmin = db.prepare('SELECT id FROM users WHERE username = ?').get('admin') - if (!existingAdmin) { - db.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)').run('admin', passwordHash) - } -} - -export function closeDatabase() { - if (db) { - db.close() - db = null - } -} diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index 2d3d73f..0000000 --- a/tailwind.config.js +++ /dev/null @@ -1,34 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: [ - "./components/**/*.{js,vue,ts}", - "./layouts/**/*.vue", - "./pages/**/*.vue", - "./plugins/**/*.{js,ts}", - "./nuxt.config.{js,ts}", - "./app.vue" - ], - theme: { - extend: { - fontFamily: { - 'sans': ['Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', '"Noto Sans"', 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', '"Segoe UI Symbol"', '"Noto Color Emoji"'] - }, - colors: { - primary: { - 50: '#fef2f2', - 100: '#fee2e2', - 200: '#fecaca', - 300: '#fca5a5', - 400: '#f87171', - 500: '#ef4444', - 600: '#dc2626', - 700: '#b91c1c', - 800: '#991b1b', - 900: '#7f1d1d', - 950: '#450a0a', - } - } - }, - }, - plugins: [], -}