Skip to content
Merged
4 changes: 1 addition & 3 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import jwt from 'jsonwebtoken'
const MAX_LOGIN_ATTEMPTS = 5
const LOGIN_ATTEMPT_WINDOW_MS = 15 * 60 * 1000 // 15 minutes
const PUBLIC_PATHS = new Set([
'/api/auth/check',
'/api/auth/login',
'/login',
'/favicon.ico',
'/robots.txt',
'/health',
Expand Down Expand Up @@ -69,7 +68,6 @@ const authMiddleware: Handle = async ({ event, resolve }) => {
// Skip auth for public paths
if (
pathname.startsWith('/login') ||
pathname.startsWith('/api/auth/') ||
pathname.startsWith('/_app/') ||
PUBLIC_PATHS.has(pathname)
) {
Expand Down
23 changes: 18 additions & 5 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,24 @@ export const formatAsUserAndAssistant = (messages: Message[]): ChatMessage[] =>
}

const cleanReplyText = (text: string): string => {
return text
.replace(/^\*+\s*/, '') // Remove leading asterisks and spaces
.replace(/^"/, '') // Remove leading quote
.replace(/"$/, '') // Remove trailing quote
.trim()
let cleaned = text.trim()

// Remove leading asterisks and spaces
while (cleaned.startsWith('*') || cleaned.startsWith(' ')) {
cleaned = cleaned.slice(1)
}

// Remove leading quotes
if (cleaned.startsWith('"') || cleaned.startsWith('"')) {
cleaned = cleaned.slice(1)
}

// Remove trailing quotes
if (cleaned.endsWith('"') || cleaned.endsWith('"')) {
cleaned = cleaned.slice(0, -1)
}

return cleaned.trim()
}

export const extractReplies = (rawOutput: string): string[] => {
Expand Down
33 changes: 33 additions & 0 deletions src/routes/+layout.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { JWT_SECRET } from '$env/static/private'
import { logger } from '$lib/logger'
import type { LayoutServerLoad } from './$types'
import jwt from 'jsonwebtoken'

export const load: LayoutServerLoad = async ({ cookies }) => {
if (!JWT_SECRET) {
logger.error('JWT_SECRET is not defined. Cannot verify JWTs.')
return {
authenticated: false,
}
}

const token = cookies.get('auth_token')

if (!token) {
return {
authenticated: false,
}
}

try {
jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'] })
return {
authenticated: true,
}
} catch (err) {
logger.warn(`JWT verification failed in layout: ${err instanceof Error ? err.message : 'Unknown error'}`)
return {
authenticated: false,
}
}
}
87 changes: 14 additions & 73 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,93 +1,34 @@
<script lang="ts">
import '../app.css'
import { onMount } from 'svelte'
import { browser } from '$app/environment'
import { goto } from '$app/navigation'
import { page } from '$app/state'
import { goto } from '$app/navigation'
import { onMount } from 'svelte'
import type { LayoutData } from './$types'
import type { Snippet } from 'svelte'

// biome-ignore lint/style/useConst: Svelte 5 $props pattern for layout children
let { children } = $props()
let authenticated = $state(false)
let initialCheckLoading = $state(true)

// Custom event type for auth changes
type AuthChangeEvent = CustomEvent<{ authenticated: boolean }>
const AUTH_CHANGE_EVENT = 'authchange'

// Function to dispatch auth change event
export function dispatchAuthChange(authenticated: boolean) {
if (!browser) return

const event = new CustomEvent(AUTH_CHANGE_EVENT, {
detail: { authenticated },
})
window.dispatchEvent(event)
}

async function performAuthCheck() {
if (!browser) return false

try {
const response = await fetch('/api/auth/check')
const isAuthenticated = response.ok
if (authenticated !== isAuthenticated) {
authenticated = isAuthenticated
dispatchAuthChange(isAuthenticated)
}
return isAuthenticated
} catch (err) {
console.error('Auth check failed:', err)
if (authenticated) {
authenticated = false
dispatchAuthChange(false)
}
return false
}
}
const { children, data }: { children: Snippet; data: LayoutData } = $props()

function handleAuthChange(event: AuthChangeEvent) {
authenticated = event.detail.authenticated
initialCheckLoading = false
// Get authentication state from server
const authenticated = $derived(data.authenticated)

// If not authenticated and not on login page, redirect to login
if (!authenticated && page.url.pathname !== '/login') {
goto('/login')
}
}
// Get current page info
const currentPath = $derived(page.url.pathname)

// Handle redirect for unauthenticated users
onMount(() => {
if (!browser) return

// Initial auth check
const checkAuth = async () => {
initialCheckLoading = true
await performAuthCheck()
initialCheckLoading = false
}

checkAuth()

// Listen for auth change events
window.addEventListener(AUTH_CHANGE_EVENT, handleAuthChange as EventListener)

// Cleanup
return () => {
window.removeEventListener(AUTH_CHANGE_EVENT, handleAuthChange as EventListener)
if (!authenticated && currentPath !== '/login') {
goto('/login')
}
})
</script>

{#if page.url.pathname === '/login'}
{#if currentPath === '/login'}
{@render children()}
{:else if initialCheckLoading}
<div style="text-align:center; margin-top: 5rem; padding: 1rem;">
<p>Reticulating splines...</p>
</div>
{:else if authenticated}
{@render children()}
{:else}
<div style="text-align:center; margin-top: 5rem; padding: 1rem;">
<p>Reticulating splines...</p>
<p>Redirecting to login...</p>
</div>
{/if}

Expand Down
41 changes: 5 additions & 36 deletions src/routes/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import type { Message, ToneType } from '$lib/types'
import { fail } from '@sveltejs/kit'
import type { Actions, PageServerLoad } from './$types'

const DEFAULT_TONE = 'gentle'
const DEFAULT_PROVIDER = 'openai'
const ONE_HOUR = 60 * 60 * 1000

export const load: PageServerLoad = async ({ url }) => {
Expand All @@ -22,40 +20,11 @@ export const load: PageServerLoad = async ({ url }) => {
export const actions: Actions = {
generate: async ({ request }) => {
try {
// Check content type
const contentType = request.headers.get('content-type') || ''

// Handle both form data and URL-encoded form data
let context = ''
let messagesString = ''
let tone: ToneType = DEFAULT_TONE
let provider = DEFAULT_PROVIDER

if (
contentType.includes('multipart/form-data') ||
contentType.includes('application/x-www-form-urlencoded')
) {
const formData = await request.formData()
messagesString = formData.get('messages') as string
tone = formData.get('tone') as ToneType
context = formData.get('context') as string
provider = formData.get('provider') as string
} else {
// Try to parse as JSON
try {
const data = await request.json()
messagesString = data.messages ? JSON.stringify(data.messages) : ''
tone = data.tone || DEFAULT_TONE
context = data.context || ''
provider = data.provider || DEFAULT_PROVIDER
} catch (e) {
return fail(400, {
error: 'Unsupported Content-Type',
details:
'Content-Type must be multipart/form-data, application/x-www-form-urlencoded, or application/json',
})
}
}
const formData = await request.formData()
const messagesString = formData.get('messages') as string
const tone = formData.get('tone') as ToneType
const context = formData.get('context') as string
const provider = formData.get('provider') as string

if (!messagesString || !tone) {
return fail(400, { error: 'Invalid request format: Missing messages or tone.' })
Expand Down
4 changes: 2 additions & 2 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ function handleSubmit(event: Event) {
}

// Generate summary and replies
async function onclick() {
async function queryAI() {
formState.ui.loading = true
formState.form.summary = ''
formState.form.suggestedReplies = []
Expand Down Expand Up @@ -180,7 +180,7 @@ async function onclick() {
<ControlBar
bind:lookBackHours={formState.form.lookBackHours}
messageCount={formState.form.messages.length}
onclick={onclick}
onclick={queryAI}
canGenerate={canGenerateReplies}
isLoading={showLoadingIndicators}
/>
Expand Down
65 changes: 0 additions & 65 deletions src/routes/api/auth/check/+server.ts

This file was deleted.

Loading