Version 1

This commit is contained in:
George Powell
2025-12-16 10:23:24 -05:00
commit 530291d271
30 changed files with 1662 additions and 0 deletions

BIN
src/lib/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

1
src/lib/index.ts Normal file
View File

@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

81
src/lib/server/auth.ts Normal file
View File

@@ -0,0 +1,81 @@
import type { RequestEvent } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
import { sha256 } from '@oslojs/crypto/sha2';
import { encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding';
import { db } from '$lib/server/db';
import * as table from '$lib/server/db/schema';
const DAY_IN_MS = 1000 * 60 * 60 * 24;
export const sessionCookieName = 'auth-session';
export function generateSessionToken() {
const bytes = crypto.getRandomValues(new Uint8Array(18));
const token = encodeBase64url(bytes);
return token;
}
export async function createSession(token: string, userId: string) {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const session: table.Session = {
id: sessionId,
userId,
expiresAt: new Date(Date.now() + DAY_IN_MS * 30)
};
await db.insert(table.session).values(session);
return session;
}
export async function validateSessionToken(token: string) {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const [result] = await db
.select({
// Adjust user table here to tweak returned data
user: { id: table.user.id, username: table.user.username },
session: table.session
})
.from(table.session)
.innerJoin(table.user, eq(table.session.userId, table.user.id))
.where(eq(table.session.id, sessionId));
if (!result) {
return { session: null, user: null };
}
const { session, user } = result;
const sessionExpired = Date.now() >= session.expiresAt.getTime();
if (sessionExpired) {
await db.delete(table.session).where(eq(table.session.id, session.id));
return { session: null, user: null };
}
const renewSession = Date.now() >= session.expiresAt.getTime() - DAY_IN_MS * 15;
if (renewSession) {
session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30);
await db
.update(table.session)
.set({ expiresAt: session.expiresAt })
.where(eq(table.session.id, session.id));
}
return { session, user };
}
export type SessionValidationResult = Awaited<ReturnType<typeof validateSessionToken>>;
export async function invalidateSession(sessionId: string) {
await db.delete(table.session).where(eq(table.session.id, sessionId));
}
export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date) {
event.cookies.set(sessionCookieName, token, {
expires: expiresAt,
path: '/'
});
}
export function deleteSessionTokenCookie(event: RequestEvent) {
event.cookies.delete(sessionCookieName, {
path: '/'
});
}

View File

@@ -0,0 +1,48 @@
import type { DailyVerse } from '$lib/server/db/schema';
import { getBookById } from './bible';
type ApiVerse = Omit<DailyVerse, 'id' | 'date' | 'createdAt'>;
export async function fetchRandomVerse(): Promise<ApiVerse> {
// Step 1: Fetch random verse to get starting position
const randomRes = await fetch('https://bible-api.com/data/web/random');
if (!randomRes.ok) {
throw new Error(`Failed to fetch random verse: ${randomRes.status}`);
}
const randomData = await randomRes.json() as any;
const randomVerse = randomData.random_verse;
if (!randomVerse || !randomVerse.book_id) {
throw new Error('Invalid random verse data');
}
const bookId = randomVerse.book_id as string;
const chapter = randomVerse.chapter as number;
const verse = randomVerse.verse as number;
const endVerse = verse + 2;
// Step 2: Fetch 3 consecutive verses starting from random
const rangeUrl = `https://bible-api.com/${bookId}${chapter}:${verse}-${endVerse}`;
const rangeRes = await fetch(rangeUrl);
if (!rangeRes.ok) {
throw new Error(`Failed to fetch verse range: ${rangeRes.status}`);
}
const rangeData = await rangeRes.json() as any;
const reference = rangeData.reference as string;
const verses = rangeData.verses as any[];
if (!verses || verses.length === 0) {
throw new Error('No verses in range');
}
const verseText = verses.map((v: any) => v.text).join(' ');
// Validate bookId
if (!getBookById(bookId)) {
throw new Error(`Unknown book ID from API: ${bookId}`);
}
return {
bookId,
reference,
verseText
};
}

23
src/lib/server/bible.ts Normal file
View File

@@ -0,0 +1,23 @@
import type { BibleBook, Testament, BibleSection } from '$lib/types/bible';
import { bibleBooks } from '$lib/types/bible';
export function getBookById(id: string): BibleBook | undefined {
return bibleBooks.find((book) => book.id === id);
}
export function getBooksByTestament(testament: Testament): BibleBook[] {
return bibleBooks.filter((book) => book.testament === testament);
}
export function getBooksBySection(section: BibleSection): BibleBook[] {
return bibleBooks.filter((book) => book.section === section);
}
export function isAdjacent(bookId1: string, bookId2: string): boolean {
const book1 = getBookById(bookId1);
const book2 = getBookById(bookId2);
if (!book1 || !book2) return false;
return Math.abs(book1.order - book2.order) === 1;
}
export { bibleBooks };

View File

@@ -0,0 +1,10 @@
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
import * as schema from './schema';
import { env } from '$env/dynamic/private';
if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
const client = new Database(env.DATABASE_URL);
export const db = drizzle(client, { schema });

View File

@@ -0,0 +1,26 @@
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';
export const user = sqliteTable('user', { id: text('id').primaryKey(), age: integer('age') });
export const session = sqliteTable('session', {
id: text('id').primaryKey(),
userId: text('user_id').notNull().references(() => user.id),
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull()
});
export type Session = typeof session.$inferSelect;
export type User = typeof user.$inferSelect;
export const dailyVerses = sqliteTable('daily_verses', {
id: text('id').primaryKey(),
date: text('date').unique().notNull(),
bookId: text('book_id').notNull(),
verseText: text('verse_text').notNull(),
reference: text('reference').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }),
});
export type DailyVerse = typeof dailyVerses.$inferSelect;

91
src/lib/types/bible.ts Normal file
View File

@@ -0,0 +1,91 @@
export type Testament = 'old' | 'new';
export type BibleSection =
| 'Law'
| 'History'
| 'Wisdom'
| 'Major Prophets'
| 'Minor Prophets'
| 'Gospels'
| 'Pauline Epistles'
| 'General Epistles'
| 'Apocalyptic';
export interface BibleBook {
id: string;
name: string;
testament: Testament;
section: BibleSection;
order: number;
url: string;
popularity: number;
}
export const bibleBooks: BibleBook[] = [
{ id: 'GEN', name: 'Genesis', testament: 'old', section: 'Law', order: 1, url: 'https://bible-api.com/data/web/GEN', popularity: 10 },
{ id: 'EXO', name: 'Exodus', testament: 'old', section: 'Law', order: 2, url: 'https://bible-api.com/data/web/EXO', popularity: 2 },
{ id: 'LEV', name: 'Leviticus', testament: 'old', section: 'Law', order: 3, url: 'https://bible-api.com/data/web/LEV', popularity: 2 },
{ id: 'NUM', name: 'Numbers', testament: 'old', section: 'Law', order: 4, url: 'https://bible-api.com/data/web/NUM', popularity: 2 },
{ id: 'DEU', name: 'Deuteronomy', testament: 'old', section: 'Law', order: 5, url: 'https://bible-api.com/data/web/DEU', popularity: 2 },
{ id: 'JOS', name: 'Joshua', testament: 'old', section: 'History', order: 6, url: 'https://bible-api.com/data/web/JOS', popularity: 2 },
{ id: 'JDG', name: 'Judges', testament: 'old', section: 'History', order: 7, url: 'https://bible-api.com/data/web/JDG', popularity: 2 },
{ id: 'RUT', name: 'Ruth', testament: 'old', section: 'History', order: 8, url: 'https://bible-api.com/data/web/RUT', popularity: 2 },
{ id: '1SA', name: '1 Samuel', testament: 'old', section: 'History', order: 9, url: 'https://bible-api.com/data/web/1SA', popularity: 2 },
{ id: '2SA', name: '2 Samuel', testament: 'old', section: 'History', order: 10, url: 'https://bible-api.com/data/web/2SA', popularity: 2 },
{ id: '1KI', name: '1 Kings', testament: 'old', section: 'History', order: 11, url: 'https://bible-api.com/data/web/1KI', popularity: 2 },
{ id: '2KI', name: '2 Kings', testament: 'old', section: 'History', order: 12, url: 'https://bible-api.com/data/web/2KI', popularity: 2 },
{ id: '1CH', name: '1 Chronicles', testament: 'old', section: 'History', order: 13, url: 'https://bible-api.com/data/web/1CH', popularity: 2 },
{ id: '2CH', name: '2 Chronicles', testament: 'old', section: 'History', order: 14, url: 'https://bible-api.com/data/web/2CH', popularity: 2 },
{ id: 'EZR', name: 'Ezra', testament: 'old', section: 'History', order: 15, url: 'https://bible-api.com/data/web/EZR', popularity: 2 },
{ id: 'NEH', name: 'Nehemiah', testament: 'old', section: 'History', order: 16, url: 'https://bible-api.com/data/web/NEH', popularity: 2 },
{ id: 'EST', name: 'Esther', testament: 'old', section: 'History', order: 17, url: 'https://bible-api.com/data/web/EST', popularity: 2 },
{ id: 'JOB', name: 'Job', testament: 'old', section: 'Wisdom', order: 18, url: 'https://bible-api.com/data/web/JOB', popularity: 2 },
{ id: 'PSA', name: 'Psalms', testament: 'old', section: 'Wisdom', order: 19, url: 'https://bible-api.com/data/web/PSA', popularity: 8 },
{ id: 'PRO', name: 'Proverbs', testament: 'old', section: 'Wisdom', order: 20, url: 'https://bible-api.com/data/web/PRO', popularity: 8 },
{ id: 'ECC', name: 'Ecclesiastes', testament: 'old', section: 'Wisdom', order: 21, url: 'https://bible-api.com/data/web/ECC', popularity: 2 },
{ id: 'SNG', name: 'Song of Solomon', testament: 'old', section: 'Wisdom', order: 22, url: 'https://bible-api.com/data/web/SNG', popularity: 2 },
{ id: 'ISA', name: 'Isaiah', testament: 'old', section: 'Major Prophets', order: 23, url: 'https://bible-api.com/data/web/ISA', popularity: 2 },
{ id: 'JER', name: 'Jeremiah', testament: 'old', section: 'Major Prophets', order: 24, url: 'https://bible-api.com/data/web/JER', popularity: 2 },
{ id: 'LAM', name: 'Lamentations', testament: 'old', section: 'Major Prophets', order: 25, url: 'https://bible-api.com/data/web/LAM', popularity: 2 },
{ id: 'EZK', name: 'Ezekiel', testament: 'old', section: 'Major Prophets', order: 26, url: 'https://bible-api.com/data/web/EZK', popularity: 2 },
{ id: 'DAN', name: 'Daniel', testament: 'old', section: 'Major Prophets', order: 27, url: 'https://bible-api.com/data/web/DAN', popularity: 2 },
{ id: 'HOS', name: 'Hosea', testament: 'old', section: 'Minor Prophets', order: 28, url: 'https://bible-api.com/data/web/HOS', popularity: 2 },
{ id: 'JOL', name: 'Joel', testament: 'old', section: 'Minor Prophets', order: 29, url: 'https://bible-api.com/data/web/JOL', popularity: 2 },
{ id: 'AMO', name: 'Amos', testament: 'old', section: 'Minor Prophets', order: 30, url: 'https://bible-api.com/data/web/AMO', popularity: 2 },
{ id: 'OBA', name: 'Obadiah', testament: 'old', section: 'Minor Prophets', order: 31, url: 'https://bible-api.com/data/web/OBA', popularity: 2 },
{ id: 'JON', name: 'Jonah', testament: 'old', section: 'Minor Prophets', order: 32, url: 'https://bible-api.com/data/web/JON', popularity: 2 },
{ id: 'MIC', name: 'Micah', testament: 'old', section: 'Minor Prophets', order: 33, url: 'https://bible-api.com/data/web/MIC', popularity: 2 },
{ id: 'NAM', name: 'Nahum', testament: 'old', section: 'Minor Prophets', order: 34, url: 'https://bible-api.com/data/web/NAM', popularity: 2 },
{ id: 'HAB', name: 'Habakkuk', testament: 'old', section: 'Minor Prophets', order: 35, url: 'https://bible-api.com/data/web/HAB', popularity: 2 },
{ id: 'ZEP', name: 'Zephaniah', testament: 'old', section: 'Minor Prophets', order: 36, url: 'https://bible-api.com/data/web/ZEP', popularity: 2 },
{ id: 'HAG', name: 'Haggai', testament: 'old', section: 'Minor Prophets', order: 37, url: 'https://bible-api.com/data/web/HAG', popularity: 2 },
{ id: 'ZEC', name: 'Zechariah', testament: 'old', section: 'Minor Prophets', order: 38, url: 'https://bible-api.com/data/web/ZEC', popularity: 2 },
{ id: 'MAL', name: 'Malachi', testament: 'old', section: 'Minor Prophets', order: 39, url: 'https://bible-api.com/data/web/MAL', popularity: 2 },
{ id: 'MAT', name: 'Matthew', testament: 'new', section: 'Gospels', order: 40, url: 'https://bible-api.com/data/web/MAT', popularity: 8 },
{ id: 'MRK', name: 'Mark', testament: 'new', section: 'Gospels', order: 41, url: 'https://bible-api.com/data/web/MRK', popularity: 8 },
{ id: 'LUK', name: 'Luke', testament: 'new', section: 'Gospels', order: 42, url: 'https://bible-api.com/data/web/LUK', popularity: 8 },
{ id: 'JHN', name: 'John', testament: 'new', section: 'Gospels', order: 43, url: 'https://bible-api.com/data/web/JHN', popularity: 8 },
{ id: 'ACT', name: 'Acts', testament: 'new', section: 'History', order: 44, url: 'https://bible-api.com/data/web/ACT', popularity: 2 },
{ id: 'ROM', name: 'Romans', testament: 'new', section: 'Pauline Epistles', order: 45, url: 'https://bible-api.com/data/web/ROM', popularity: 6 },
{ id: '1CO', name: '1 Corinthians', testament: 'new', section: 'Pauline Epistles', order: 46, url: 'https://bible-api.com/data/web/1CO', popularity: 6 },
{ id: '2CO', name: '2 Corinthians', testament: 'new', section: 'Pauline Epistles', order: 47, url: 'https://bible-api.com/data/web/2CO', popularity: 6 },
{ id: 'GAL', name: 'Galatians', testament: 'new', section: 'Pauline Epistles', order: 48, url: 'https://bible-api.com/data/web/GAL', popularity: 6 },
{ id: 'EPH', name: 'Ephesians', testament: 'new', section: 'Pauline Epistles', order: 49, url: 'https://bible-api.com/data/web/EPH', popularity: 6 },
{ id: 'PHP', name: 'Philippians', testament: 'new', section: 'Pauline Epistles', order: 50, url: 'https://bible-api.com/data/web/PHP', popularity: 6 },
{ id: 'COL', name: 'Colossians', testament: 'new', section: 'Pauline Epistles', order: 51, url: 'https://bible-api.com/data/web/COL', popularity: 6 },
{ id: '1TH', name: '1 Thessalonians', testament: 'new', section: 'Pauline Epistles', order: 52, url: 'https://bible-api.com/data/web/1TH', popularity: 6 },
{ id: '2TH', name: '2 Thessalonians', testament: 'new', section: 'Pauline Epistles', order: 53, url: 'https://bible-api.com/data/web/2TH', popularity: 6 },
{ id: '1TI', name: '1 Timothy', testament: 'new', section: 'Pauline Epistles', order: 54, url: 'https://bible-api.com/data/web/1TI', popularity: 6 },
{ id: '2TI', name: '2 Timothy', testament: 'new', section: 'Pauline Epistles', order: 55, url: 'https://bible-api.com/data/web/2TI', popularity: 6 },
{ id: 'TIT', name: 'Titus', testament: 'new', section: 'Pauline Epistles', order: 56, url: 'https://bible-api.com/data/web/TIT', popularity: 6 },
{ id: 'PHM', name: 'Philemon', testament: 'new', section: 'Pauline Epistles', order: 57, url: 'https://bible-api.com/data/web/PHM', popularity: 6 },
{ id: 'HEB', name: 'Hebrews', testament: 'new', section: 'General Epistles', order: 58, url: 'https://bible-api.com/data/web/HEB', popularity: 6 },
{ id: 'JAS', name: 'James', testament: 'new', section: 'General Epistles', order: 59, url: 'https://bible-api.com/data/web/JAS', popularity: 6 },
{ id: '1PE', name: '1 Peter', testament: 'new', section: 'General Epistles', order: 60, url: 'https://bible-api.com/data/web/1PE', popularity: 6 },
{ id: '2PE', name: '2 Peter', testament: 'new', section: 'General Epistles', order: 61, url: 'https://bible-api.com/data/web/2PE', popularity: 6 },
{ id: '1JN', name: '1 John', testament: 'new', section: 'General Epistles', order: 62, url: 'https://bible-api.com/data/web/1JN', popularity: 6 },
{ id: '2JN', name: '2 John', testament: 'new', section: 'General Epistles', order: 63, url: 'https://bible-api.com/data/web/2JN', popularity: 6 },
{ id: '3JN', name: '3 John', testament: 'new', section: 'General Epistles', order: 64, url: 'https://bible-api.com/data/web/3JN', popularity: 6 },
{ id: 'JUD', name: 'Jude', testament: 'new', section: 'General Epistles', order: 65, url: 'https://bible-api.com/data/web/JUD', popularity: 6 },
{ id: 'REV', name: 'Revelation', testament: 'new', section: 'Apocalyptic', order: 66, url: 'https://bible-api.com/data/web/REV', popularity: 2 }
];