Compare commits

...

5 Commits

Author SHA1 Message Date
taogaetz
7a6bd4b92c disable home button if no access 2025-09-14 16:02:59 -04:00
taogaetz
7484646598 disable searchbar when user has no access 2025-09-14 15:52:48 -04:00
taogaetz
05850f3840 update healthcheck to use 127.xx 2025-09-14 15:44:03 -04:00
taogaetz
1fc17a8039 Update layout to include access management and adjust input type for PIN authentication
- Added hasAccess property to layout server load for improved access control.
- Updated layout to check for hasAccess instead of authenticated for rendering the LogoutButton.
- Changed input type for access PIN from password to tel for better user experience and input handling.
2025-09-14 15:10:10 -04:00
taogaetz
fe3f532a25 Add access PIN authentication and manage access cookies
- Introduced ACCESS_PIN in env.example for site access control.
- Updated Locals interface to include hasAccess boolean for access management.
- Implemented access check in hooks.server.ts to redirect users without access to the /access page.
- Cleared access cookie on logout to ensure proper session management.
2025-09-14 15:01:03 -04:00
11 changed files with 153 additions and 14 deletions

4
cookies.txt Normal file
View File

@ -0,0 +1,4 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

View File

@ -13,10 +13,11 @@ services:
- MAGIC_LINK_TOKEN=${MAGIC_LINK_TOKEN}
- CLOUDINARY_URL=${CLOUDINARY_URL}
- ORIGIN=${ORIGIN}
- ACCESS_PIN=${ACCESS_PIN}
volumes:
- /home/chefbible/data:/app/data
healthcheck:
test: [ "CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })" ]
test: [ "CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:3000/health" ]
interval: 30s
timeout: 10s
retries: 3

View File

@ -8,5 +8,8 @@ ORIGIN=https://your-domain.com
# Authentication token (required - generate a secure random token)
MAGIC_LINK_TOKEN=your-secure-token-here
# Access PIN code (required - set a PIN for site access)
ACCESS_PIN=1234
# Cloudinary URL for photo uploads (optional)
CLOUDINARY_URL=cloudinary://api_key:api_secret@cloud_name

1
src/app.d.ts vendored
View File

@ -8,6 +8,7 @@ declare global {
// interface Platform {}
interface Locals {
authenticated: boolean;
hasAccess: boolean;
}
}
}

View File

@ -1,12 +1,24 @@
import type { Handle } from '@sveltejs/kit';
import { redirect } from '@sveltejs/kit';
import { dev } from '$app/environment';
export const handle: Handle = async ({ event, resolve }) => {
// Check if user has access (PIN code authentication)
const accessCookie = event.cookies.get('access_granted');
const hasAccess = !!accessCookie;
// If user doesn't have access and is not on the access page, redirect to /access
if (!hasAccess && event.url.pathname !== '/access') {
throw redirect(302, '/access');
}
// Check if user is authenticated using the chef token cookie
const chefToken = event.cookies.get('chef_token');
const authenticated = !!chefToken;
// Add authentication status to locals for use in load functions
event.locals.authenticated = authenticated;
event.locals.hasAccess = hasAccess;
// Continue with the request
const response = await resolve(event);

View File

@ -4,13 +4,14 @@
export let recipes: any[] = [];
export let placeholder: string = 'Search recipes...';
export let disabled: boolean = false;
let searchTerm = '';
let filteredRecipes: any[] = [];
let showResults = false;
function handleSearch() {
if (!searchTerm.trim()) {
if (disabled || !searchTerm.trim()) {
filteredRecipes = [];
showResults = false;
return;
@ -34,7 +35,7 @@
}
function handleInputFocus() {
if (searchTerm.trim()) {
if (!disabled && searchTerm.trim()) {
showResults = true;
}
}
@ -61,7 +62,9 @@
<div class="relative w-full max-w-lg" bind:this={searchContainer}>
<div
class="relative flex items-center rounded-xl border-2 border-transparent bg-white shadow-sm transition-all duration-200 focus-within:border-primary focus-within:shadow-lg"
class="relative flex items-center rounded-xl border-2 border-transparent bg-white shadow-sm transition-all duration-200 {disabled
? 'cursor-not-allowed opacity-50'
: 'focus-within:border-primary focus-within:shadow-lg'}"
>
<svg
class="pointer-events-none absolute left-4 text-gray-400"
@ -81,9 +84,12 @@
on:input={handleSearch}
on:focus={handleInputFocus}
{placeholder}
class="w-full border-none bg-transparent px-4 py-3.5 pl-12 text-base text-gray-900 outline-none placeholder:text-gray-400"
{disabled}
class="w-full border-none bg-transparent px-4 py-3.5 pl-12 text-base text-gray-900 outline-none placeholder:text-gray-400 {disabled
? 'cursor-not-allowed'
: ''}"
/>
{#if searchTerm}
{#if searchTerm && !disabled}
<button
class="absolute right-3 cursor-pointer rounded border-none bg-none p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
aria-label="Clear search"
@ -106,7 +112,7 @@
{/if}
</div>
{#if showResults && filteredRecipes.length > 0}
{#if showResults && filteredRecipes.length > 0 && !disabled}
<div class="absolute top-full left-1/2 z-50 mt-1 w-screen -translate-x-1/2">
<div class="mx-auto max-w-7xl px-5">
<div class="rounded-xl border border-gray-200 bg-white shadow-2xl">
@ -146,7 +152,7 @@
</div>
</div>
</div>
{:else if showResults && searchTerm.trim()}
{:else if showResults && searchTerm.trim() && !disabled}
<div class="absolute top-full left-1/2 z-50 mt-1 w-screen -translate-x-1/2">
<div class="mx-auto max-w-7xl px-5">
<div class="rounded-xl border border-gray-200 bg-white shadow-2xl">

View File

@ -21,6 +21,7 @@ export const load: LayoutServerLoad = async ({ locals }) => {
return {
recipes,
authenticated: locals.authenticated
authenticated: locals.authenticated,
hasAccess: locals.hasAccess
};
};

View File

@ -19,11 +19,17 @@
>
<div class="mx-auto flex max-w-[1400px] items-center justify-between gap-6 px-5 py-4">
<div class="flex items-center gap-2">
<a href="/" class="flex items-center gap-2">
<img src={logo} alt="Chef Bible" class="w-20" />
</a>
{#if data.hasAccess}
<a href="/" class="flex items-center gap-2">
<img src={logo} alt="Chef Bible" class="w-20" />
</a>
{:else}
<div class="flex cursor-not-allowed items-center gap-2 opacity-50">
<img src={logo} alt="Chef Bible" class="w-20" />
</div>
{/if}
</div>
<SearchBar recipes={data.recipes} />
<SearchBar recipes={data.recipes} disabled={!data.hasAccess} />
<a
href={data.authenticated ? '/recipe/new' : '#'}
class="btn font-extrabold btn-lg btn-primary {!data.authenticated
@ -47,7 +53,7 @@
© 2025 Birbante - Italian Apericena & Dolce Vita
</div>
<div class="flex items-center gap-4">
{#if data.authenticated}
{#if data.hasAccess}
<LogoutButton />
{/if}
</div>

View File

@ -0,0 +1,38 @@
import type { PageServerLoad, Actions } from './$types';
import { fail, redirect } from '@sveltejs/kit';
import { dev } from '$app/environment';
export const load: PageServerLoad = async ({ locals }) => {
// If user already has access, redirect to home
if (locals.hasAccess) {
throw redirect(302, '/');
}
return {};
};
export const actions: Actions = {
default: async ({ request, cookies }) => {
const data = await request.formData();
const pin = data.get('pin') as string;
if (!pin) {
return fail(400, { error: 'PIN is required' });
}
if (pin !== process.env.ACCESS_PIN) {
return fail(400, { error: 'Invalid PIN' });
}
// Set access cookie
cookies.set('access_granted', 'true', {
path: '/',
httpOnly: true,
sameSite: 'lax',
secure: !dev,
maxAge: 60 * 60 * 24 * 30 // 30 days
});
throw redirect(302, '/');
}
};

View File

@ -0,0 +1,49 @@
<script lang="ts">
import { enhance } from '$app/forms';
import type { ActionData } from './$types';
let { form, data }: { form: ActionData; data: any } = $props();
</script>
<svelte:head>
<title>Access Bible</title>
</svelte:head>
<div class="flex min-h-screen items-center justify-center bg-base-200">
<div class="card w-full max-w-md bg-base-100 shadow-xl">
<div class="card-body">
<div class="mb-6 text-center">
<h1 class="text-2xl font-bold text-base-content">Access</h1>
<p class="mt-2 text-base-content/70">Enter your access code to continue</p>
</div>
<form method="POST" use:enhance>
<div class="form-control">
<label class="label" for="pin">
<span class="label-text">Access Code</span>
</label>
<input
type="tel"
inputmode="numeric"
pattern="[0-9]*"
id="pin"
name="pin"
class="input-bordered input w-full"
placeholder="Enter access code"
required
autocomplete="off"
/>
{#if form?.error}
<div class="label">
<span class="label-text-alt text-error">{form.error}</span>
</div>
{/if}
</div>
<div class="form-control mt-6">
<button type="submit" class="btn btn-primary"> Access Site </button>
</div>
</form>
</div>
</div>
</div>

View File

@ -20,6 +20,15 @@ export const POST: RequestHandler = async ({ cookies }) => {
maxAge: 0 // Expire immediately
});
// Clear the access cookie
cookies.set('access_granted', '', {
path: '/',
httpOnly: true,
sameSite: 'lax',
secure: !dev,
maxAge: 0 // Expire immediately
});
// Redirect to home page
throw redirect(302, '/');
};
@ -42,6 +51,15 @@ export const GET: RequestHandler = async ({ cookies }) => {
maxAge: 0 // Expire immediately
});
// Clear the access cookie
cookies.set('access_granted', '', {
path: '/',
httpOnly: true,
sameSite: 'lax',
secure: !dev,
maxAge: 0 // Expire immediately
});
// Redirect to home page
throw redirect(302, '/');
};