6 Commits

Author SHA1 Message Date
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
31 changed files with 5243 additions and 1933 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"
}
}

3211
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,7 +10,7 @@ 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');
}

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>{@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

@@ -1,37 +1,44 @@
import mongoose from 'mongoose';
import { MONGO_URL } from '$env/static/private';
/*
0 - disconnected
1 - connected
2 - connecting
3 - disconnecting
4 - uninitialized
*/
const mongoConnection = {
isConnected: 0,
};
export const dbConnect = async () => {
if (mongoConnection.isConnected === 1) {
return;
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 (mongoose.connections.length > 0) {
mongoConnection.isConnected = mongoose.connections[0].readyState;
if (mongoConnection.isConnected === 1) {
return;
}
if (!cached.promise) {
const opts = {
bufferCommands: false,
};
await mongoose.disconnect();
cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
return mongoose;
});
}
await mongoose.connect(MONGO_URL ?? '');
mongoConnection.isConnected = 1;
};
cached.conn = await cached.promise;
return cached.conn;
}
export const dbDisconnect = async () => {
if (process.env.NODE_ENV === 'development') return;
if (mongoConnection.isConnected === 0) return;
await mongoose.disconnect();
mongoConnection.isConnected = 0;
};
export async function dbDisconnect() {
if (cached.conn) {
await cached.conn.disconnect();
cached.conn = null;
cached.promise = null;
}
}

View File

@@ -1,25 +1,14 @@
import mongoose from 'mongoose';
const PaymentSchema = new mongoose.Schema(
{
type: {type: String, required: true, enum: ['payment', 'reimbursement']},
name: {type: String, required: true},
category : {type: String, required: false,},
date: {type: Date, default: Date.now},
images: [ {
mediapath: {type: String, required: false},
}],
description: {type: String, required: false},
note: {type: String, required: false},
tags : [String],
original_amount: {type: Number, required: true},
total_amount: {type: Number, required: true},
personal_amounts: [{
user: {type:String, required: true},
amount: {type: Number, required: true, default:0}
}],
currency: {type: String, required: true, default: 'CHF'},
}, {timestamps: true}
);
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.model("Payment", PaymentSchema);
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

@@ -0,0 +1,3 @@
import { signIn } from "../../../auth"
import type { Actions } from "./$types"
export const actions: Actions = { default: signIn }

View File

@@ -0,0 +1,3 @@
import { signIn } from "../../../auth"
import type { Actions } from "./$types"
export const actions: Actions = { default: signIn }

View File

@@ -4,50 +4,68 @@ 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 = '/var/lib/www/static/test';
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 date= new Date(formData.get('date') 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 = parseFloat(formData.get('total_amount') as string);
total_amount = original_amount;
} else {
const date_fmt = date.toISOString().split('T')[0]; // Convert date to YYYY-MM-DD format
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}`)
const { result } = await res.json();
if (!result || !result[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
const conversionRate = parseFloat(result.rates[BASE_CURRENCY]);
alert(`Conversion rate from ${currency} to ${BASE_CURRENCY} on ${date_fmt}: ${conversionRate}`);
total_amount = original_amount * conversionRate;
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 }[];
//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 });
await mkdir(UPLOAD_DIR, { recursive: true });
const images: { mediapath: string }[] = [];
const imageFiles = formData.getAll('images') as File[];
@@ -57,35 +75,40 @@ export const POST: RequestHandler = async ({ request, locals }) => {
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);
fs.writeFileSync(fullPath, buffer);
images.push({ mediapath: `/static/test/${safeName}` });
}
await dbConnect();
const payment = new Payment({
type,
name,
category,
date,
transaction_date,
images,
description,
note,
tags,
total_amount,
original_amount,
total_amount,
paid_by,
for_self,
for_other,
conversion_rate,
currency,
personal_amounts,
images
});
// let auth = await locals.auth();
// // if(!auth){
// throw error(401, "Not logged in")
// }
try{
await Payment.create(payment);
} catch(e){
throw error(400, 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 });

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

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

View File

@@ -1,18 +0,0 @@
<script>
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>
<li><a href="/glaube/gebete">Gebete</a></li>
<li><a href="/glaube/rosenkranz">Rosenkranz</a></li>
<li><a href="/glaube/predigten">Predigten</a></li>
</ul>
<UserHeader {username} slot=right_side></UserHeader>
<slot></slot>
</Header>

View File

@@ -1,133 +0,0 @@
<style>
h1{
text-align: center;
font-size: 3em;
}
p{
max-width: 600px;
margin: 0 auto;
font-size: 1.1em;
}
</style>
<h1>Settlement Plan</h1>
<script lang="ts">
import { onMount } from 'svelte';
let name = '';
let category = '';
let date = new Date().toISOString().split('T')[0]; // format as yyyy-mm-dd
let images: File[] = [];
let description = '';
let note = '';
let tags = '';
let original_amount = 0;
let currency = 'CHF';
let payment_method = '';
let personal_amounts = [
{ user: 'alexander', amount: 0 },
{ user: 'anna', amount: 0 }
];
const handleSubmit = async () => {
const formData = new FormData();
formData.append('name', name);
formData.append('category', category);
formData.append('dateCreated', date);
formData.append('description', description);
formData.append('note', note);
formData.append('tags', JSON.stringify(tags.split(',').map(tag => tag.trim())));
formData.append('total_amount', total_amount.toString());
formData.append('currency', currency);
formData.append('payment_method', payment_method);
formData.append('personal_amounts', JSON.stringify(personal_amounts));
images.forEach((file, index) => {
formData.append('images', file);
});
const res = await fetch('/api/cospend/add', {
method: 'POST',
body: formData
});
const result = await res.json();
alert(result.message);
};
</script>
<form on:submit|preventDefault={handleSubmit} class="flex flex-col gap-4 max-w-xl">
<label>
Name:
<input type="text" bind:value={name} required />
</label>
<label>
Category:
<input type="text" bind:value={category} />
</label>
<label>
Date Created:
<input type="date" bind:value={date} />
</label>
<label>
Images:
<input type="file" multiple accept="image/*" on:change={(e) => images = Array.from(e.target.files)} />
</label>
<label>
Description:
<textarea bind:value={description}></textarea>
</label>
<label>
Note:
<textarea bind:value={note}></textarea>
</label>
<label>
Tags (comma separated):
<input type="text" bind:value={tags} />
</label>
<label>
Total Amount:
<input type="number" bind:value={original_amount} step="0.01" required />
</label>
<fieldset>
<legend>Personal Amounts</legend>
{#each personal_amounts as entry, i}
<div class="flex gap-2 items-center">
<label>{entry.user}</label>
<input type="number" bind:value={personal_amounts[i].amount} step="0.01" required />
</div>
{/each}
</fieldset>
<label>
Currency:
<select bind:value={currency}>
<option value="CHF">CHF</option>
<option value="EUR">EUR</option>
<option value="USD">USD</option>
</select>
</label>
<label>
Payment Method:
<select bind:value={payment_method}>
<option value="">-- Select --</option>
<option value="cash">Cash</option>
<option value="bank_transfer">Bank Transfer</option>
<option value="credit_card">Credit Card</option>
<option value="twint">Twint</option>
</select>
</label>
<button type="submit">Save Payment</button>
</form>

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

@@ -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,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 @@
{"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.