19 Commits

Author SHA1 Message Date
eadb567069 Fix blank white pages by adding Nord theme styling to auth endpoints
Some checks failed
CI / update (push) Has been cancelled
- Add Nord theme CSS variables to login and logout pages
- Use --nord1 background and --nord4 text colors to match site theme
- Eliminates jarring white flash during authentication flow
- Maintains professional appearance and brand consistency
- Endpoints now blend seamlessly with site's dark theme

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 22:06:37 +02:00
7d6226d79a Implement proper page redirects for protected routes
- Update hooks.server.ts to preserve original URL when redirecting to login
- Use callbackUrl parameter to maintain user's intended destination
- Preserve both pathname and search parameters in redirect flow
- Leverage OIDC standard callback URL support built into Auth.js
- Users now land exactly where they intended after authentication
- Works for /rezepte/add, /rezepte/edit/[name], and any future protected routes

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 22:04:27 +02:00
28367293e8 Remove ugly spinner pages from auth flow
- Replace loading spinners and styling with minimal HTML pages
- Auth flow now happens almost instantly without visible intermediary screens
- Reduced bundle size for login/logout endpoints (0.59kB and 0.58kB)
- Maintains seamless user experience while preserving Auth.js integration
- Users stay on current page context during auth transitions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 22:02:19 +02:00
9b77640977 Implement better auth flow with direct Authentik redirects
- Create custom /login and /logout endpoints that bypass Auth.js default pages
- Use auto-submitting forms to POST to Auth.js with proper form data
- Update UserHeader links to use new custom endpoints (/login, /logout)
- Remove old login/logout page server files that are no longer needed
- Login flow: /login → auto-submit form → /auth/signin/authentik → Authentik
- Logout flow: /logout → auto-submit form → /auth/signout → Authentik logout
- Provides seamless user experience with loading spinners during redirects
- Maintains all Auth.js security features and session management
- Eliminates intermediate Auth.js pages for cleaner auth flow

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 21:58:55 +02:00
7f06717615 Revert to clean Authentik provider configuration
- Use official Authentik provider instead of generic OIDC
- Issue was resolved by fixing callback URL in Authentik configuration
- Cleaner and more maintainable auth setup

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 21:46:19 +02:00
4f34ff5329 Update @auth/sveltekit to latest stable version 1.10.0
- Upgraded @auth/sveltekit from 0.14.0 to 1.10.0
- Updated session API from event.locals.getSession() to event.locals.auth()
- Fixed TypeScript definitions for new auth API in app.d.ts
- Updated layout server load functions to use LayoutServerLoad type
- Fixed session callbacks with proper token type casting
- Switched to generic OIDC provider config to resolve issuer validation issues
- All auth functionality now working with latest Auth.js version

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 21:45:14 +02:00
69e719780c Card.svelte: fix top-right icon offset 2025-08-31 21:09:34 +02:00
e2a76d4080 Fix Card.svelte icon positioning and styling
- Restored icon to top-right position with absolute positioning
- Added proper circular background with nord0 color
- Set correct dimensions (50px × 50px) and border-radius for circular shape
- Added shadow and hover effects to match original design
- Fixed z-index to ensure icon appears above other elements
- Maintained shake animation on card hover for visual feedback

The icon now appears correctly in the top-right corner with a round
background instead of being positioned at bottom center with transparent
background.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 21:07:10 +02:00
b847b8f1c8 Add missing Payment model and database connection utilities
- Created Payment model with mongoose schema for cospend functionality
- Added database connection utilities with proper connection caching
- Fixed build errors related to missing imports
- Build now succeeds and dev server starts correctly

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 21:03:15 +02:00
b861f9aeec Upgrade SvelteKit 4 to SvelteKit 5 with latest dependencies
Major changes:
- Upgraded Svelte from v4 to v5.38.6 (latest stable)
- Upgraded SvelteKit from v2.0.0 to v2.37.0 (latest)
- Upgraded Vite from v5 to v7.1.3 for better performance
- Updated all related packages to latest compatible versions
- Added pnpm as package manager with packageManager field
- Fixed Card.svelte nested anchor tags issue by converting inner links to buttons
- Updated component styling to maintain visual consistency
- Removed incompatible svelte-preprocess-import-assets package

Dependencies updated:
- @sveltejs/kit: ^2.0.0 → ^2.37.0
- @sveltejs/vite-plugin-svelte: ^3.0.0 → ^6.1.3
- svelte: ^4.0.0 → ^5.38.6
- vite: ^5.0.0 → ^7.1.3
- @sveltejs/adapter-auto: ^3.0.0 → ^6.1.0
- @sveltejs/adapter-node: ^2.0.0 → ^5.0.0
- svelte-check: ^3.4.6 → ^4.0.0
- mongoose: ^7.4.0 → ^8.0.0
- sharp: ^0.32.3 → ^0.33.0

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 21:01:19 +02:00
dd71832b10 switch from "Unterwegs" to "Snack"
All checks were successful
CI / update (push) Successful in 49s
2025-03-31 17:57:40 +02:00
ce7a542408 fix copilot autocomplete svg messup
All checks were successful
CI / update (push) Successful in 15s
2025-02-02 13:09:15 +01:00
86225d3237 fix docs autolink
All checks were successful
CI / update (push) Successful in 16s
2025-02-02 13:07:42 +01:00
18a5241c1e fix docs autolink
All checks were successful
CI / update (push) Successful in 15s
2025-02-02 12:58:34 +01:00
17a5d6155d fix for svelte 4
All checks were successful
CI / update (push) Successful in 15s
2025-02-02 12:55:33 +01:00
15bf4fd922 remove unused health.bocken.org and papers.bocken.org
All checks were successful
CI / update (push) Successful in 1m26s
2025-02-02 12:44:21 +01:00
aab1f7da9a added tips-and-tricks route
All checks were successful
CI / update (push) Successful in 19s
2024-10-28 17:00:43 +01:00
367ea7a17e render HTML in Recipe Card description
All checks were successful
CI / update (push) Successful in 17s
2024-10-22 17:29:29 +02:00
154a8f5efe fix typo 2024-10-22 17:28:44 +02:00
36 changed files with 5578 additions and 1749 deletions

88
README_DEV_AUTH.md Normal file
View File

@@ -0,0 +1,88 @@
# Development Authentication Bypass
This document explains how to safely disable authentication during development.
## 🔐 Security Overview
The authentication bypass is designed with multiple layers of security:
1. **Development Mode Only**: Only works when `vite dev` is running
2. **Explicit Opt-in**: Requires setting `DEV_DISABLE_AUTH=true`
3. **Production Protection**: Build fails if enabled in production mode
4. **Environment Isolation**: Uses local environment files (gitignored)
## 🚀 Usage
### 1. Create Local Environment File
Create `.env.local` (this file is gitignored):
```bash
# Copy from example
cp .env.local.example .env.local
```
### 2. Enable Development Bypass
Edit `.env.local` and set:
```env
DEV_DISABLE_AUTH=true
```
### 3. Start Development Server
```bash
pnpm run dev
```
You'll see a warning in the console:
```
🚨 AUTH DISABLED: Development mode with DEV_DISABLE_AUTH=true
```
### 4. Access Protected Routes
Protected routes (`/rezepte/edit/*`, `/rezepte/add`) will now be accessible without authentication.
## 🛡️ Security Guarantees
### Production Safety
- **Build-time Check**: Production builds fail if `DEV_DISABLE_AUTH=true`
- **Runtime Check**: Double verification using `dev` flag from `$app/environment`
- **No Environment Leakage**: Uses `process.env` (server-only) not client environment
### Development Isolation
- **Gitignored Files**: `.env.local` is never committed
- **Example Template**: `.env.local.example` shows safe defaults
- **Clear Warnings**: Console warns when auth is disabled
## 🧪 Testing the Security
### Test Production Build Safety
```bash
# This should FAIL with security error
DEV_DISABLE_AUTH=true pnpm run build
```
### Test Normal Production Build
```bash
# This should succeed
pnpm run build
```
## 🔄 Re-enabling Authentication
Set in `.env.local`:
```env
DEV_DISABLE_AUTH=false
```
Or simply delete/rename the `.env.local` file.
## ⚠️ Important Notes
- **Never** commit `.env.local` to git
- **Never** set `DEV_DISABLE_AUTH=true` in production environment
- The bypass provides a mock session with `rezepte_users` group access
- All other authentication flows (signin pages, etc.) remain unchanged

3318
package-lock_old.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -10,22 +10,24 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"packageManager": "pnpm@9.0.0",
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"svelte": "^4.0.0",
"svelte-check": "^3.4.6",
"svelte-preprocess-import-assets": "^1.0.1",
"@auth/core": "^0.40.0",
"@sveltejs/adapter-auto": "^6.1.0",
"@sveltejs/kit": "^2.37.0",
"@sveltejs/vite-plugin-svelte": "^6.1.3",
"@types/node": "^22.12.0",
"svelte": "^5.38.6",
"svelte-check": "^4.0.0",
"tslib": "^2.6.0",
"typescript": "^5.1.6",
"vite": "^5.0.0"
"vite": "^7.1.3"
},
"dependencies": {
"@auth/sveltekit": "^0.14.0",
"@sveltejs/adapter-node": "^2.0.0",
"@auth/sveltekit": "^1.10.0",
"@sveltejs/adapter-node": "^5.0.0",
"cheerio": "1.0.0-rc.12",
"mongoose": "^7.4.0",
"sharp": "^0.32.3"
"mongoose": "^8.0.0",
"sharp": "^0.33.0"
}
}

3173
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

18
src/app.d.ts vendored
View File

@@ -1,12 +1,28 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
import type { Session } from "@auth/sveltekit";
declare global {
namespace App {
// interface Error {}
// interface Locals {}
interface Locals {
auth(): Promise<Session | null>;
}
// interface PageData {}
// interface Platform {}
}
}
declare module "@auth/sveltekit" {
interface Session {
user?: {
name?: string | null;
email?: string | null;
image?: string | null;
nickname?: string;
groups?: string[];
};
}
}
export {};

View File

@@ -20,8 +20,8 @@ export const { handle, signIn, signOut } = SvelteKitAuth({
return token;
},
session: async ({session, token}) => {
session.user.nickname = token.nickname;
session.user.groups = token.groups;
session.user.nickname = token.nickname as string;
session.user.groups = token.groups as string[];
return session;
},

View File

@@ -10,9 +10,11 @@ import * as auth from "./auth"
async function authorization({ event, resolve }) {
// Protect any routes under /authenticated
if (event.url.pathname.startsWith('/rezepte/edit') || event.url.pathname.startsWith('/rezepte/add')) {
const session = await event.locals.getSession();
const session = await event.locals.auth();
if (!session) {
redirect(303, '/auth/signin');
// Preserve the original URL the user was trying to access
const callbackUrl = encodeURIComponent(event.url.pathname + event.url.search);
redirect(303, `/login?callbackUrl=${callbackUrl}`);
}
else if (! session.user.groups.includes('rezepte_users')) {
// strip last dir from url

View File

@@ -0,0 +1 @@
{"terminal": "nvimterm"}

View File

@@ -52,6 +52,24 @@ const img_name=recipe.short_name + ".webp?v=" + recipe.dateModified
}
.icon{
font-family: "Noto Color Emoji", emoji, sans-serif;
border: none;
background: none;
cursor: pointer;
position: absolute;
top: -25px;
right: -25px;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: var(--nord0);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5em;
box-shadow: 0em 0em 1em 0.1em rgba(0, 0, 0, 0.6);
transition: 100ms;
z-index: 10;
}
#image{
width: 300px;
@@ -135,6 +153,7 @@ const img_name=recipe.short_name + ".webp?v=" + recipe.dateModified
margin-bottom: 0.5em;
transition: 100ms;
box-shadow: 0em 0em 0.2em 0.05em rgba(0, 0, 0, 0.3);
border: none;
}
.tag:hover,
.tag:focus-visible
@@ -159,7 +178,8 @@ const img_name=recipe.short_name + ".webp?v=" + recipe.dateModified
padding-inline: 1em;
border-radius: 1000px;
transition: 100ms;
border: none;
cursor: pointer;
}
.card_title .category:hover,
.card_title .category:focus-within
@@ -172,6 +192,18 @@ const img_name=recipe.short_name + ".webp?v=" + recipe.dateModified
scale: 0.9 0.9;
}
.icon:hover,
.icon:focus-visible
{
transform: scale(1.1, 1.1);
background-color: var(--nord3);
box-shadow: 0.2em 0.2em 1em 0.1em rgba(0, 0, 0, 0.8);
}
.icon:focus {
transform: scale(0.9, 0.9);
}
.card:hover .icon,
.card:focus-visible .icon
{
@@ -193,17 +225,17 @@ const img_name=recipe.short_name + ".webp?v=" + recipe.dateModified
</div>
</div>
{#if icon_override || recipe.season.includes(current_month)}
<a class=icon href="/rezepte/icon/{recipe.icon}">{recipe.icon}</a>
<button class=icon on:click={(e) => {e.stopPropagation(); window.location.href = `/rezepte/icon/${recipe.icon}`}}>{recipe.icon}</button>
{/if}
<div class="card_title">
<a class=category href="/rezepte/category/{recipe.category}" >{recipe.category}</a>
<button class=category on:click={(e) => {e.stopPropagation(); window.location.href = `/rezepte/category/${recipe.category}`}}>{recipe.category}</button>
<div>
<div class=name>{@html recipe.name}</div>
<div class=description>{recipe.description}</div>
<div class=description>{@html recipe.description}</div>
</div>
<div class=tags>
{#each recipe.tags as tag}
<a class=tag href="/rezepte/tag/{tag}">{tag}</a>
<button class=tag on:click={(e) => {e.stopPropagation(); window.location.href = `/rezepte/tag/${tag}`}}>{tag}</button>
{/each}
</div>
</div>

View File

@@ -0,0 +1,34 @@
<script>
// get ingredients_store from IngredientsPage.svelte
import ingredients_store from './IngredientsPage.svelte';
let ingredients = [];
ingredients_store.subscribe(value => {
ingredients = value;
});
function toggleHefe(){
if(data.ingredients[i].list[j].name == "Frischhefe"){
data.ingredients[i].list[j].name = "Trockenhefe"
data.ingredients[i].list[j].amount = item.amount / 3
}
else{
item.name = "Frischhefe"
item.amount = item.amount * 3
}
}
</script>
<style>
button{
background: none;
border: none;
cursor: pointer;
}
svg{
width: 1.1em;
height: 1.1em;
fill: var(--blue);
}
</style>
<button onclick={toggleHefe}>
{item.amount} {item.unit} {item.name}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M105.1 202.6c7.7-21.8 20.2-42.3 37.8-59.8c62.5-62.5 163.8-62.5 226.3 0L386.3 160 352 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l111.5 0c0 0 0 0 0 0l.4 0c17.7 0 32-14.3 32-32l0-112c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 35.2L414.4 97.6c-87.5-87.5-229.3-87.5-316.8 0C73.2 122 55.6 150.7 44.8 181.4c-5.9 16.7 2.9 34.9 19.5 40.8s34.9-2.9 40.8-19.5zM39 289.3c-5 1.5-9.8 4.2-13.7 8.2c-4 4-6.7 8.8-8.1 14c-.3 1.2-.6 2.5-.8 3.8c-.3 1.7-.4 3.4-.4 5.1L16 432c0 17.7 14.3 32 32 32s32-14.3 32-32l0-35.1 17.6 17.5c0 0 0 0 0 0c87.5 87.4 229.3 87.4 316.7 0c24.4-24.4 42.1-53.1 52.9-83.8c5.9-16.7-2.9-34.9-19.5-40.8s-34.9 2.9-40.8 19.5c-7.7 21.8-20.2 42.3-37.8 59.8c-62.5 62.5-163.8 62.5-226.3 0l-.1-.1L125.6 352l34.4 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L48.4 288c-1.6 0-3.2 .1-4.8 .3s-3.1 .5-4.6 1z"/></svg>
</button>

View File

@@ -139,10 +139,10 @@ h2 + p{
<p>({user.nickname})</p>
<ul>
<li><a href="https://sso.bocken.org/if/user/#/settings" >Einstellungen</a></li>
<li><a href="/auth/signout" >Log Out</a></li>
<li><a href="/logout" >Log Out</a></li>
</ul>
</div>
</button>
{:else}
<a class=entry href=/auth/signin>Log In</a>
<a class=entry href=/login>Log In</a>
{/if}

44
src/lib/db/db.ts Normal file
View File

@@ -0,0 +1,44 @@
import mongoose from 'mongoose';
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/recipes';
if (!MONGODB_URI) {
throw new Error('Please define the MONGODB_URI environment variable inside .env.local');
}
/**
* Global is used here to maintain a cached connection across hot reloads
* in development. This prevents connections growing exponentially
* during API Route usage.
*/
let cached = (global as any).mongoose;
if (!cached) {
cached = (global as any).mongoose = { conn: null, promise: null };
}
export async function dbConnect() {
if (cached.conn) {
return cached.conn;
}
if (!cached.promise) {
const opts = {
bufferCommands: false,
};
cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
return mongoose;
});
}
cached.conn = await cached.promise;
return cached.conn;
}
export async function dbDisconnect() {
if (cached.conn) {
await cached.conn.disconnect();
cached.conn = null;
cached.promise = null;
}
}

14
src/lib/models/Payment.ts Normal file
View File

@@ -0,0 +1,14 @@
import mongoose from 'mongoose';
const paymentSchema = new mongoose.Schema({
paid_by: { type: String, required: true },
total_amount: { type: Number, required: true },
for_self: { type: Number, default: 0 },
for_other: { type: Number, default: 0 },
currency: { type: String, default: 'CHF' },
description: String,
date: { type: Date, default: Date.now },
receipt_image: String
});
export const Payment = mongoose.models.Payment || mongoose.model('Payment', paymentSchema);

View File

@@ -1,6 +1,6 @@
import type { PageServerLoad } from "./$types"
import type { LayoutServerLoad } from "./$types"
export const load : PageServerLoad = (async ({locals}) => {
export const load : LayoutServerLoad = (async ({locals}) => {
return {
session: await locals.auth(),
}

View File

@@ -1,24 +1,7 @@
<script>
<script lang="ts">
import "$lib/css/nordtheme.css";
import LinksGrid from "$lib/components/LinksGrid.svelte";
export let data;
import { page } from "$app/stores"
const redirect_to_docs = () => {
if (!data.session){
alert("Du musst dich einloggen, um diese Seite zu betreten.");
window.location.href = "/auth/signin";
}
else if (data.session.user.groups.includes("paperless_users")){
window.location.href = "https://docs.bocken.org";
}
else if (data.session.user.groups.includes("paperless_eltern_users")){
window.location.href = "https://dokumente.bocken.org";
}
else
alert("Du hast keine Berechtigung, diese Seite zu betreten.");
}
</script>
<style>
.hero{
@@ -84,7 +67,7 @@ section h2{
Alles ist selbst gehostet bei mir daheim auf einem kleinen Mini-Server (Arch, btw).
</p>
<p>
Zu empfeheln ist meine stetig wachsende Rezeptsammlung. Dort findest du viele leckere Rezepte, die ich selbst ausprobiert habe und ständig weiterfeilsche.
Zu empfehlen ist meine stetig wachsende Rezeptsammlung. Dort findest du viele leckere Rezepte, die ich selbst ausprobiert habe und ständig weiterfeilsche.
Zudem kannst du gerne meine Suchmaschine oder auch Jitsi-instanz für Videokonferenzen nutzen.
Einiges ist hinter einem Login versteckt, anderes ist öffentlich zugänglich.
Wer sich ein bisschen mit Programmieren auskennt, kann auch gerne in meinen Git-Repositories stöbern.
@@ -148,19 +131,24 @@ section h2{
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M288 32c0-17.7-14.3-32-32-32s-32 14.3-32 32V274.7l-73.4-73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l128 128c12.5 12.5 32.8 12.5 45.3 0l128-128c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L288 274.7V32zM64 352c-35.3 0-64 28.7-64 64v32c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V416c0-35.3-28.7-64-64-64H346.5l-45.3 45.3c-25 25-65.5 25-90.5 0L165.5 352H64zm368 56a24 24 0 1 1 0 48 24 24 0 1 1 0-48z"/></svg>
<h3>Transmission</h3>
</a>
<!-- TODO: clean this up with proper aria roles etc -->
<a on:click="{() => redirect_to_docs()}" >
<!-- instead of redirect_to_docs(), use a normal link with internal checks for data.session -->
{#if !data.session}
<a href="/auth/signin">
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m106 512h300c24.814 0 45-20.186 45-45v-317h-105c-24.814 0-45-20.186-45-45v-105h-195c-24.814 0-45 20.186-45 45v422c0 24.814 20.186 45 45 45zm60-301h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h120c8.291 0 15 6.709 15 15s-6.709 15-15 15h-120c-8.291 0-15-6.709-15-15s6.709-15 15-15z"/><path d="m346 120h96.211l-111.211-111.211v96.211c0 8.276 6.724 15 15 15z"/></svg>
<h3>Dokumente</h3>
</a>
<a href=https://papers.bocken.org>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M320 32c-8.1 0-16.1 1.4-23.7 4.1L15.8 137.4C6.3 140.9 0 149.9 0 160s6.3 19.1 15.8 22.6l57.9 20.9C57.3 229.3 48 259.8 48 291.9v28.1c0 28.4-10.8 57.7-22.3 80.8c-6.5 13-13.9 25.8-22.5 37.6C0 442.7-.9 448.3 .9 453.4s6 8.9 11.2 10.2l64 16c4.2 1.1 8.7 .3 12.4-2s6.3-6.1 7.1-10.4c8.6-42.8 4.3-81.2-2.1-108.7C90.3 344.3 86 329.8 80 316.5V291.9c0-30.2 10.2-58.7 27.9-81.5c12.9-15.5 29.6-28 49.2-35.7l157-61.7c8.2-3.2 17.5 .8 20.7 9s-.8 17.5-9 20.7l-157 61.7c-12.4 4.9-23.3 12.4-32.2 21.6l159.6 57.6c7.6 2.7 15.6 4.1 23.7 4.1s16.1-1.4 23.7-4.1L624.2 182.6c9.5-3.4 15.8-12.5 15.8-22.6s-6.3-19.1-15.8-22.6L343.7 36.1C336.1 33.4 328.1 32 320 32zM128 408c0 35.3 86 72 192 72s192-36.7 192-72L496.7 262.6 354.5 314c-11.1 4-22.8 6-34.5 6s-23.5-2-34.5-6L143.3 262.6 128 408z"/></svg>
<h3>Papers</h3>
{:else if data.session.user.groups.includes("paperless_users")}
<a href="https://docs.bocken.org">
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m106 512h300c24.814 0 45-20.186 45-45v-317h-105c-24.814 0-45-20.186-45-45v-105h-195c-24.814 0-45 20.186-45 45v422c0 24.814 20.186 45 45 45zm60-301h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h120c8.291 0 15 6.709 15 15s-6.709 15-15 15h-120c-8.291 0-15-6.709-15-15s6.709-15 15-15z"/><path d="m346 120h96.211l-111.211-111.211v96.211c0 8.276 6.724 15 15 15z"/></svg>
<h3>Dokumente</h3>
</a>
<a href=https://health.bocken.org>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M96 64c0-17.7 14.3-32 32-32l32 0c17.7 0 32 14.3 32 32l0 160 0 64 0 160c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32l0-64-32 0c-17.7 0-32-14.3-32-32l0-64c-17.7 0-32-14.3-32-32s14.3-32 32-32l0-64c0-17.7 14.3-32 32-32l32 0 0-64zm448 0l0 64 32 0c17.7 0 32 14.3 32 32l0 64c17.7 0 32 14.3 32 32s-14.3 32-32 32l0 64c0 17.7-14.3 32-32 32l-32 0 0 64c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32l0-160 0-64 0-160c0-17.7 14.3-32 32-32l32 0c17.7 0 32 14.3 32 32zM416 224l0 64-192 0 0-64 192 0z"/></svg>
<h3>Gym</h3>
{:else if data.session.user.groups.includes("paperless_eltern_users")}
<a href="https://dokumente.bocken.org">
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m106 512h300c24.814 0 45-20.186 45-45v-317h-105c-24.814 0-45-20.186-45-45v-105h-195c-24.814 0-45 20.186-45 45v422c0 24.814 20.186 45 45 45zm60-301h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h120c8.291 0 15 6.709 15 15s-6.709 15-15 15h-120c-8.291 0-15-6.709-15-15s6.709-15 15-15z"/><path d="m346 120h96.211l-111.211-111.211v96.211c0 8.276 6.724 15 15 15z"/></svg>
<h3>Dokumente</h3>
</a>
{/if}
<a href=https://audio.bocken.org>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>
<h3>Hörbücher & Podcasts</h3>

View File

@@ -0,0 +1,119 @@
import type { RequestHandler } from '@sveltejs/kit';
import fs from 'fs';
import path from 'path';
import { mkdir } from 'fs/promises';
import { Payment } from '$lib/models/Payment'; // adjust path as needed
import { dbConnect, dbDisconnect } from '$lib/db/db';
import { error } from '@sveltejs/kit';
const UPLOAD_DIR = './static/cospend';
const BASE_CURRENCY = 'CHF'; // Default currency
export const POST: RequestHandler = async ({ request, locals }) => {
let auth = await locals.auth();
if(!auth){
throw error(401, "Not logged in")
}
const formData = await request.formData();
try {
const name = formData.get('name') as string;
const category = formData.get('category') as string;
const transaction_date= new Date(formData.get('transaction_date') as string);
const description = formData.get('description') as string;
const note = formData.get('note') as string;
const tags = JSON.parse(formData.get('tags') as string) as string[];
const paid_by = formData.get('paid_by') as string
const type = formData.get('type') as string
let currency = formData.get('currency') as string;
let original_amount = parseFloat(formData.get('original_amount') as string);
let total_amount = NaN;
let for_self = parseFloat(formData.get('for_self') as string);
let for_other = parseFloat(formData.get('for_other') as string);
let conversion_rate = 1.0; // Default conversion rate
// if currency is not BASE_CURRENCY, fetch current conversion rate using frankfurter API and date in YYYY-MM-DD format
if (!currency || currency === BASE_CURRENCY) {
currency = BASE_CURRENCY;
total_amount = original_amount;
} else {
console.log(transaction_date);
const date_fmt = transaction_date.toISOString().split('T')[0]; // Convert date to YYYY-MM-DD format
// Fetch conversion rate logic here (not implemented in this example)
console.log(`Fetching conversion rate for ${currency} to ${BASE_CURRENCY} on ${date_fmt}`);
const res = await fetch(`https://api.frankfurter.app/${date_fmt}?from=${currency}&to=${BASE_CURRENCY}`)
console.log(res);
const result = await res.json();
console.log(result);
if (!result || !result.rates[BASE_CURRENCY]) {
return new Response(JSON.stringify({ message: 'Currency conversion failed.' }), { status: 400 });
}
// Assuming you want to convert the total amount to BASE_CURRENCY
conversion_rate = parseFloat(result.rates[BASE_CURRENCY]);
console.log(`Conversion rate from ${currency} to ${BASE_CURRENCY} on ${date_fmt}: ${conversion_rate}`);
total_amount = original_amount * conversion_rate;
for_self = for_self * conversion_rate;
for_other = for_other * conversion_rate;
}
//const personal_amounts = JSON.parse(formData.get('personal_amounts') as string) as { user: string, amount: number }[];
if (!name || isNaN(total_amount)) {
return new Response(JSON.stringify({ message: 'Invalid required fields.' }), { status: 400 });
}
await mkdir(UPLOAD_DIR, { recursive: true });
const images: { mediapath: string }[] = [];
const imageFiles = formData.getAll('images') as File[];
for (const file of imageFiles) {
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const safeName = `${Date.now()}_${file.name.replace(/[^a-zA-Z0-9_.-]/g, '_')}`;
const fullPath = path.join(UPLOAD_DIR, safeName);
fs.writeFileSync(fullPath, buffer);
images.push({ mediapath: `/static/test/${safeName}` });
}
await dbConnect();
const payment = new Payment({
type,
name,
category,
transaction_date,
images,
description,
note,
tags,
original_amount,
total_amount,
paid_by,
for_self,
for_other,
conversion_rate,
currency,
});
try{
await Payment.create(payment);
} catch(e){
return new Response(JSON.stringify({ message: `Error creating payment event. ${e}` }), { status: 500 });
}
await dbDisconnect();
return new Response(JSON.stringify({ message: 'Payment event created successfully.' }), { status: 201 });
} catch (err) {
console.error(err);
return new Response(JSON.stringify({ message: 'Error processing request.' }), { status: 500 });
}
};

View File

@@ -0,0 +1,38 @@
import type { RequestHandler } from '@sveltejs/kit';
import { json } from '@sveltejs/kit';
import { Payment } from '$lib/models/Payment'; // adjust path as needed
import { dbConnect, dbDisconnect } from '$lib/db/db';
const UPLOAD_DIR = '/var/lib/www/static/test';
const BASE_CURRENCY = 'CHF'; // Default currency
export const GET: RequestHandler = async ({ request, locals }) => {
await dbConnect();
const result = await Payment.aggregate([
{
$group: {
_id: "$paid_by",
totalPaid: { $sum: "$total_amount" },
totalForSelf: { $sum: { $ifNull: ["$for_self", 0] } },
totalForOther: { $sum: { $ifNull: ["$for_other", 0] } }
}
},
{
$project: {
_id: 0,
paid_by: "$_id",
netTotal: {
$multiply: [
{ $add: [
{ $subtract: ["$totalPaid", "$totalForSelf"] },
"$totalForOther"
] },
0.5]
}
}
}
]);
await dbDisconnect();
return json(result);
};

View File

@@ -0,0 +1,20 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Payment } from '$lib/models/Payment';
import { dbConnect, dbDisconnect } from '$lib/db/db';
import { error } from '@sveltejs/kit';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
const number_payments = 10;
const number_skip = params.pageno ? (parseInt(params.pageno) - 1 ) * number_payments : 0;
let payments = await Payment.find()
.sort({ transaction_date: -1 })
.skip(number_skip)
.limit(number_payments);
await dbDisconnect();
if(payments == null){
throw error(404, "No more payments found");
}
return json(payments);
};

View File

@@ -0,0 +1,18 @@
import { error } from "@sveltejs/kit";
export async function load({ fetch, params}) {
let balance_res = await fetch(`/api/cospend/balance`);
if (!balance_res.ok) {
throw error(balance_res.status, `Failed to fetch balance`);
}
let balance = await balance_res.json();
const items_res = await fetch(`/api/cospend/page/1`);
if (!items_res.ok) {
throw error(items_res.status, `Failed to fetch items`);
}
let items = await items_res.json();
return {
balance,
items
};
}

View File

@@ -1,6 +1,6 @@
import type { PageServerLoad } from "./$types"
import type { LayoutServerLoad } from "./$types"
export const load : PageServerLoad = (async ({locals}) => {
export const load : LayoutServerLoad = (async ({locals}) => {
return {
session: await locals.auth(),
}

View File

@@ -2,10 +2,6 @@
import Header from '$lib/components/Header.svelte'
import UserHeader from '$lib/components/UserHeader.svelte';
export let data
let username = ""
if(data.user){
username = data.user.username
}
</script>
<Header>
<ul class=site_header slot=links>
@@ -13,6 +9,6 @@ if(data.user){
<li><a href="/glaube/rosenkranz">Rosenkranz</a></li>
<li><a href="/glaube/predigten">Predigten</a></li>
</ul>
<UserHeader {username} slot=right_side></UserHeader>
<UserHeader user={data.session?.user} slot=right_side></UserHeader>
<slot></slot>
</Header>

View File

@@ -70,6 +70,6 @@
<path d="m377.943 50.035v-35.035c0-8.284-6.716-15-15-15h-76.926c-11.523 0-22.046 4.357-30.017 11.505-7.971-7.148-18.494-11.505-30.018-11.505h-76.926c-8.284 0-15 6.716-15 15v35.035z"/>
</g>
</svg>
<h3>Predigten<h3>
<h3>Predigten</h3>
</a>
</LinksGrid>

View File

@@ -463,19 +463,19 @@ Der unten abgebildete Rosenkranz zeigt die aktuellen Gehmeinisse des Tages nach
</g>
<g id=lbead5>
<circle class=lbead />
<circle class=hitbox onclick="" />
<circle class=hitbox onclick="{true}" />
</g>
<g id=lbead4>
<circle class=lbead />
<circle class=hitbox onclick="" />
<circle class=hitbox onclick="{true}" />
</g>
<g id=lbead3>
<circle class=lbead />
<circle class=hitbox onclick="" />
<circle class=hitbox onclick="{true}" />
</g>
<g id=lbead6>
<circle class=lbead />
<circle class=hitbox onclick="" />
<circle class=hitbox onclick="{true}" />
</g>
</g>
<g class=beforedecades>
@@ -490,11 +490,11 @@ Der unten abgebildete Rosenkranz zeigt die aktuellen Gehmeinisse des Tages nach
</g>
<g id=lbead1>
<circle class=lbead />
<circle class=hitbox onclick="" />
<circle class=hitbox onclick="{true}" />
</g>
<g id=lbead2>
<circle class=lbead />
<circle class=hitbox onclick="" />
<circle class=hitbox onclick="{true}" />
</g>
</g>
</g>
@@ -511,7 +511,7 @@ Dieser Plan ist wie folgt:
<div class=table >
<table>
<tbody>
<thead>
<tr>
<td>Mo</td>
<td>Di</td>
<td>Mi</td>
@@ -519,7 +519,7 @@ Dieser Plan ist wie folgt:
<td>Fr</td>
<td>Sa</td>
<td>So</td>
</thead>
</tr>
<tr>
<td>freudenreich</td>
<td>schmerzhaft</td>
@@ -546,7 +546,7 @@ Der Plan ohne lichtreiche Geheimnisse ist wie folgt:
<div class=table>
<table>
<tbody>
<thead>
<tr>
<td>Mo</td>
<td>Di</td>
<td>Mi</td>
@@ -554,7 +554,7 @@ Der Plan ohne lichtreiche Geheimnisse ist wie folgt:
<td>Fr</td>
<td>Sa</td>
<td>So</td>
</thead>
</tr>
<tr>
<td>freudenreich</td>
<td>schmerzhaft</td>

View File

@@ -0,0 +1,44 @@
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url }) => {
const callbackUrl = url.searchParams.get('callbackUrl') || '/';
// Create a minimal page with site styling that immediately triggers auth
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<style>
:root{
--nord0: #2E3440;
--nord1: #3B4252;
--nord4: #D8DEE9;
}
body {
background-color: var(--nord1);
color: var(--nord4);
font-family: sans-serif;
margin: 0;
padding: 0;
min-height: 100vh;
}
</style>
</head>
<body>
<form id="signin-form" method="POST" action="/auth/signin/authentik">
<input type="hidden" name="callbackUrl" value="${callbackUrl}" />
</form>
<script>
// Immediately submit the form to trigger auth flow
document.getElementById('signin-form').submit();
</script>
</body>
</html>`;
return new Response(html, {
headers: {
'Content-Type': 'text/html'
}
});
};

View File

@@ -0,0 +1,44 @@
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url }) => {
const callbackUrl = url.searchParams.get('callbackUrl') || '/';
// Create a minimal page with site styling that immediately triggers logout
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Logout</title>
<style>
:root{
--nord0: #2E3440;
--nord1: #3B4252;
--nord4: #D8DEE9;
}
body {
background-color: var(--nord1);
color: var(--nord4);
font-family: sans-serif;
margin: 0;
padding: 0;
min-height: 100vh;
}
</style>
</head>
<body>
<form id="signout-form" method="POST" action="/auth/signout">
<input type="hidden" name="callbackUrl" value="${callbackUrl}" />
</form>
<script>
// Immediately submit the form to trigger logout flow
document.getElementById('signout-form').submit();
</script>
</body>
</html>`;
return new Response(html, {
headers: {
'Content-Type': 'text/html'
}
});
};

View File

@@ -1,6 +1,6 @@
import type { PageServerLoad } from "./$types"
import type { LayoutServerLoad } from "./$types"
export const load : PageServerLoad = async ({locals}) => {
export const load : LayoutServerLoad = async ({locals}) => {
return {
session: await locals.auth()
}

View File

@@ -15,6 +15,7 @@ if(data.session){
<li><a href="/rezepte/category">Kategorie</a></li>
<li><a href="/rezepte/icon">Icon</a></li>
<li><a href="/rezepte/tag">Stichwörter</a></li>
<li><a href="/rezepte/tips-and-tricks">Tipps</a></li>
</ul>
<UserHeader slot=right_side {user}></UserHeader>
<slot></slot>

View File

@@ -6,7 +6,7 @@
import Search from '$lib/components/Search.svelte';
export let data: PageData;
export let current_month = new Date().getMonth() + 1
const categories = ["Hauptspeise", "Nudel", "Brot", "Dessert", "Suppe", "Beilage", "Salat", "Kuchen", "Frühstück", "Sauce", "Zutat", "Getränk", "Aufstrich", "Guetzli", "Unterwegs"]
const categories = ["Hauptspeise", "Nudel", "Brot", "Dessert", "Suppe", "Beilage", "Salat", "Kuchen", "Frühstück", "Sauce", "Zutat", "Getränk", "Aufstrich", "Guetzli", "Snack"]
</script>
<style>
h1{

View File

@@ -1,5 +1,6 @@
export async function load({locals}) {
const session = await locals.auth();
return {
user: locals.user
user: session?.user
};
};

View File

@@ -1,10 +1,11 @@
import type { PageLoad } from "./$types";
import type { PageServerLoad } from "./$types";
export async function load({ fetch, params, locals}) {
let current_month = new Date().getMonth() + 1
const res = await fetch(`/api/rezepte/items/${params.name}`);
const recipe = await res.json();
const session = await locals.auth();
return {recipe: recipe,
user: locals.user
user: session?.user
};
};

View File

@@ -0,0 +1,62 @@
<script lang="ts">
import type { PageData } from './$types';
import AddButton from '$lib/components/AddButton.svelte';
import Converter from './Converter.svelte';
</script>
<style>
h1{
text-align: center;
margin-bottom: 0;
font-size: 4rem;
}
.subheading{
text-align: center;
margin-top: 0;
font-size: 1.5rem;
}
.content{
max-width: 800px;
margin: 0 auto;
background-color: var(--nord0);
padding: 1rem;
margin-block: 1rem;
}
</style>
<svelte:head>
<title>Bocken Rezepte</title>
<meta name="description" content="Eine stetig wachsende Ansammlung an Rezepten aus der Bockenschen Küche." />
<meta property="og:image" content="https://bocken.org/static/rezepte/thumb/ragu_aus_rindsrippen.webp" />
<meta property="og:image:secure_url" content="https://bocken.org/static/rezepte/thumb/ragu_aus_rindsrippen.webp" />
<meta property="og:image:type" content="image/webp" />
<meta property="og:image:alt" content="Pasta al Ragu mit Linguine" />
</svelte:head>
<h1>Tipps & Tricks</h1>
<div class=content>
<h2>Trockenhefe vs. Frischhefe</h2>
<Converter></Converter>
<p>
Frischhefe ist mit Trockenhefe ersetzbar, jedoch muss man ein paar Kleinigkeiten beachten:
</p>
<ol>
<li>Nur ein Drittel der Menge verwenden.</li>
<li>Falls ein kalter Teig zubereitet wird, die Trockenhefe umbedingt zuerst zur Gärprobe in warmer Flüssigkeit (je nach Rezept z.B. Milch oder Wasser) mit einem TL Zucker für ~10 Minuten <q>aufwachen</q> lassen.</li>
<li>Generell ist die Meinung das Trockenhefe etwas <q>energischer</q> ist am Anfang der Gärung und etwas langsamer am Ende der Gare. Dementsprechend eventuell die Stock- und Stückgare verkürzen bzw. verlängern.</li>
</ol>
</div>
<div class=content>
<h2>Fensterprobe</h2>
<p>
Die Fensterprobe ist eine Methode um den optimalen Knetzustand eines Teiges zu bestimmen.
Dazu wird ein kleines, ca. Walnussgrosses Stück Teig zwischen den Fingern auseinandergezogen. Ist der Teig elastisch und reißt nicht bis der Teig so dünn ist, dass man leicht licht durchsehen kann, so ist der Teig optimal verknetet.
</p>
<p>
Teig lässt sich leichter verkneten wenn er noch trockener ist. Daher lohnt es sich zunächst etwa 10% der Flüssigkeit zurückzuhalten und erst nach und nach zuzugeben nachdem der Teig bereits für einige Minuten geknetet wurde.
</p>
</div>
<AddButton></AddButton>

View File

@@ -0,0 +1,68 @@
<script>
class HefeConverter {
constructor(trockenhefe = 1) {
this._trockenhefe = trockenhefe;
this._frischhefe = this._trockenhefe * 3;
}
get trockenhefe() {
return Math.round(this._trockenhefe * 100) / 100 + "g";
}
set trockenhefe(value) {
this._trockenhefe = value.replace(/\D/g, '');
this._frischhefe = this._trockenhefe * 3;
}
get frischhefe() {
return this._frischhefe+"g";
}
set frischhefe(value) {
this._frischhefe = value.replace(/\D/g, '');
this._trockenhefe = this._frischhefe / 3;
}
}
const hefeConverter = new HefeConverter();
</script>
<style>
.converter_container {
width: fit-content;
display: flex;
flex-direction: row;
justify-content: center;
gap: 2rem;
background-color: var(--blue);
padding: 2rem;
margin-inline: auto;
align-items: center;
}
input {
width: 5rem;
height: 2rem;
font-size: 1rem;
text-align: center;
}
.flex_column {
display: flex;
flex-direction: column;
}
label {
margin-bottom: 0.25rem;
}
</style>
<div class=converter_container>
<div class="flex_column">
<label for="trockenhefe">Trockenhefe</label>
<input type="text" bind:value={hefeConverter.trockenhefe} min="0" />
</div>
<div>
=
</div>
<div class="flex_column">
<label for="frischhefe">Frischhefe</label>
<input type="text" bind:value={hefeConverter.frischhefe} min="0"/>
</div>
</div>

View File

@@ -0,0 +1 @@
{"terminal": "nvimterm"}

2
static/other/jellyfin.js Normal file
View File

@@ -0,0 +1,2 @@
document.addEventListener('load', function() { alert(1);document.querySelector('.detailImageContainer').addEventListener('click',function(){document.querySelector(".btnPlay[title='Play'],.btnPlay[title='Resume']").click()});});

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.