mirror of
https://github.com/taogaetz/chefbible.git
synced 2025-12-06 11:47:24 -05:00
Compare commits
5 Commits
ec24b39d1a
...
7a6bd4b92c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a6bd4b92c | ||
|
|
7484646598 | ||
|
|
05850f3840 | ||
|
|
1fc17a8039 | ||
|
|
fe3f532a25 |
4
cookies.txt
Normal file
4
cookies.txt
Normal 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.
|
||||||
|
|
||||||
@ -13,10 +13,11 @@ services:
|
|||||||
- MAGIC_LINK_TOKEN=${MAGIC_LINK_TOKEN}
|
- MAGIC_LINK_TOKEN=${MAGIC_LINK_TOKEN}
|
||||||
- CLOUDINARY_URL=${CLOUDINARY_URL}
|
- CLOUDINARY_URL=${CLOUDINARY_URL}
|
||||||
- ORIGIN=${ORIGIN}
|
- ORIGIN=${ORIGIN}
|
||||||
|
- ACCESS_PIN=${ACCESS_PIN}
|
||||||
volumes:
|
volumes:
|
||||||
- /home/chefbible/data:/app/data
|
- /home/chefbible/data:/app/data
|
||||||
healthcheck:
|
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
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@ -8,5 +8,8 @@ ORIGIN=https://your-domain.com
|
|||||||
# Authentication token (required - generate a secure random token)
|
# Authentication token (required - generate a secure random token)
|
||||||
MAGIC_LINK_TOKEN=your-secure-token-here
|
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 for photo uploads (optional)
|
||||||
CLOUDINARY_URL=cloudinary://api_key:api_secret@cloud_name
|
CLOUDINARY_URL=cloudinary://api_key:api_secret@cloud_name
|
||||||
|
|||||||
1
src/app.d.ts
vendored
1
src/app.d.ts
vendored
@ -8,6 +8,7 @@ declare global {
|
|||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
interface Locals {
|
interface Locals {
|
||||||
authenticated: boolean;
|
authenticated: boolean;
|
||||||
|
hasAccess: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,24 @@
|
|||||||
import type { Handle } from '@sveltejs/kit';
|
import type { Handle } from '@sveltejs/kit';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import { dev } from '$app/environment';
|
||||||
|
|
||||||
export const handle: Handle = async ({ event, resolve }) => {
|
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
|
// Check if user is authenticated using the chef token cookie
|
||||||
const chefToken = event.cookies.get('chef_token');
|
const chefToken = event.cookies.get('chef_token');
|
||||||
const authenticated = !!chefToken;
|
const authenticated = !!chefToken;
|
||||||
|
|
||||||
// Add authentication status to locals for use in load functions
|
// Add authentication status to locals for use in load functions
|
||||||
event.locals.authenticated = authenticated;
|
event.locals.authenticated = authenticated;
|
||||||
|
event.locals.hasAccess = hasAccess;
|
||||||
|
|
||||||
// Continue with the request
|
// Continue with the request
|
||||||
const response = await resolve(event);
|
const response = await resolve(event);
|
||||||
|
|||||||
@ -4,13 +4,14 @@
|
|||||||
|
|
||||||
export let recipes: any[] = [];
|
export let recipes: any[] = [];
|
||||||
export let placeholder: string = 'Search recipes...';
|
export let placeholder: string = 'Search recipes...';
|
||||||
|
export let disabled: boolean = false;
|
||||||
|
|
||||||
let searchTerm = '';
|
let searchTerm = '';
|
||||||
let filteredRecipes: any[] = [];
|
let filteredRecipes: any[] = [];
|
||||||
let showResults = false;
|
let showResults = false;
|
||||||
|
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
if (!searchTerm.trim()) {
|
if (disabled || !searchTerm.trim()) {
|
||||||
filteredRecipes = [];
|
filteredRecipes = [];
|
||||||
showResults = false;
|
showResults = false;
|
||||||
return;
|
return;
|
||||||
@ -34,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleInputFocus() {
|
function handleInputFocus() {
|
||||||
if (searchTerm.trim()) {
|
if (!disabled && searchTerm.trim()) {
|
||||||
showResults = true;
|
showResults = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,7 +62,9 @@
|
|||||||
|
|
||||||
<div class="relative w-full max-w-lg" bind:this={searchContainer}>
|
<div class="relative w-full max-w-lg" bind:this={searchContainer}>
|
||||||
<div
|
<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
|
<svg
|
||||||
class="pointer-events-none absolute left-4 text-gray-400"
|
class="pointer-events-none absolute left-4 text-gray-400"
|
||||||
@ -81,9 +84,12 @@
|
|||||||
on:input={handleSearch}
|
on:input={handleSearch}
|
||||||
on:focus={handleInputFocus}
|
on:focus={handleInputFocus}
|
||||||
{placeholder}
|
{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
|
<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"
|
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"
|
aria-label="Clear search"
|
||||||
@ -106,7 +112,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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="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="mx-auto max-w-7xl px-5">
|
||||||
<div class="rounded-xl border border-gray-200 bg-white shadow-2xl">
|
<div class="rounded-xl border border-gray-200 bg-white shadow-2xl">
|
||||||
@ -146,7 +152,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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="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="mx-auto max-w-7xl px-5">
|
||||||
<div class="rounded-xl border border-gray-200 bg-white shadow-2xl">
|
<div class="rounded-xl border border-gray-200 bg-white shadow-2xl">
|
||||||
|
|||||||
@ -21,6 +21,7 @@ export const load: LayoutServerLoad = async ({ locals }) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
recipes,
|
recipes,
|
||||||
authenticated: locals.authenticated
|
authenticated: locals.authenticated,
|
||||||
|
hasAccess: locals.hasAccess
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -19,11 +19,17 @@
|
|||||||
>
|
>
|
||||||
<div class="mx-auto flex max-w-[1400px] items-center justify-between gap-6 px-5 py-4">
|
<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">
|
<div class="flex items-center gap-2">
|
||||||
<a href="/" class="flex items-center gap-2">
|
{#if data.hasAccess}
|
||||||
<img src={logo} alt="Chef Bible" class="w-20" />
|
<a href="/" class="flex items-center gap-2">
|
||||||
</a>
|
<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>
|
</div>
|
||||||
<SearchBar recipes={data.recipes} />
|
<SearchBar recipes={data.recipes} disabled={!data.hasAccess} />
|
||||||
<a
|
<a
|
||||||
href={data.authenticated ? '/recipe/new' : '#'}
|
href={data.authenticated ? '/recipe/new' : '#'}
|
||||||
class="btn font-extrabold btn-lg btn-primary {!data.authenticated
|
class="btn font-extrabold btn-lg btn-primary {!data.authenticated
|
||||||
@ -47,7 +53,7 @@
|
|||||||
© 2025 Birbante - Italian Apericena & Dolce Vita
|
© 2025 Birbante - Italian Apericena & Dolce Vita
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
{#if data.authenticated}
|
{#if data.hasAccess}
|
||||||
<LogoutButton />
|
<LogoutButton />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
38
src/routes/access/+page.server.ts
Normal file
38
src/routes/access/+page.server.ts
Normal 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, '/');
|
||||||
|
}
|
||||||
|
};
|
||||||
49
src/routes/access/+page.svelte
Normal file
49
src/routes/access/+page.svelte
Normal 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>
|
||||||
@ -20,6 +20,15 @@ export const POST: RequestHandler = async ({ cookies }) => {
|
|||||||
maxAge: 0 // Expire immediately
|
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
|
// Redirect to home page
|
||||||
throw redirect(302, '/');
|
throw redirect(302, '/');
|
||||||
};
|
};
|
||||||
@ -42,6 +51,15 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
|||||||
maxAge: 0 // Expire immediately
|
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
|
// Redirect to home page
|
||||||
throw redirect(302, '/');
|
throw redirect(302, '/');
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user