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}
|
||||
- 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
|
||||
|
||||
@ -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
1
src/app.d.ts
vendored
@ -8,6 +8,7 @@ declare global {
|
||||
// interface Platform {}
|
||||
interface Locals {
|
||||
authenticated: boolean;
|
||||
hasAccess: boolean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -21,6 +21,7 @@ export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
|
||||
return {
|
||||
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="flex items-center gap-2">
|
||||
{#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>
|
||||
<SearchBar recipes={data.recipes} />
|
||||
{/if}
|
||||
</div>
|
||||
<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>
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
// 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, '/');
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user