mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-02-04 10:54:44 -05:00
Version 1
This commit is contained in:
81
src/lib/server/auth.ts
Normal file
81
src/lib/server/auth.ts
Normal 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: '/'
|
||||
});
|
||||
}
|
||||
48
src/lib/server/bible-api.ts
Normal file
48
src/lib/server/bible-api.ts
Normal 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
23
src/lib/server/bible.ts
Normal 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 };
|
||||
10
src/lib/server/db/index.ts
Normal file
10
src/lib/server/db/index.ts
Normal 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 });
|
||||
26
src/lib/server/db/schema.ts
Normal file
26
src/lib/server/db/schema.ts
Normal 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;
|
||||
Reference in New Issue
Block a user