Compare commits
6 Commits
feature-co
...
svelte5
Author | SHA1 | Date | |
---|---|---|---|
7f06717615
|
|||
4f34ff5329
|
|||
69e719780c
|
|||
e2a76d4080
|
|||
b847b8f1c8
|
|||
b861f9aeec
|
88
README_DEV_AUTH.md
Normal file
88
README_DEV_AUTH.md
Normal 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
3318
package-lock_old.json
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -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
3211
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
18
src/app.d.ts
vendored
18
src/app.d.ts
vendored
@@ -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 {};
|
||||
|
@@ -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;
|
||||
},
|
||||
|
||||
|
@@ -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');
|
||||
}
|
||||
|
1
src/lib/components/.jukit/.jukit_info.json
Normal file
1
src/lib/components/.jukit/.jukit_info.json
Normal file
@@ -0,0 +1 @@
|
||||
{"terminal": "nvimterm"}
|
@@ -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>
|
||||
|
34
src/lib/components/HefeSwapper.svelte
Normal file
34
src/lib/components/HefeSwapper.svelte
Normal 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>
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
@@ -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(),
|
||||
}
|
||||
|
3
src/routes/(main)/login/+page.server.ts
Normal file
3
src/routes/(main)/login/+page.server.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { signIn } from "../../../auth"
|
||||
import type { Actions } from "./$types"
|
||||
export const actions: Actions = { default: signIn }
|
3
src/routes/(main)/logout/+page.server.ts
Normal file
3
src/routes/(main)/logout/+page.server.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { signIn } from "../../../auth"
|
||||
import type { Actions } from "./$types"
|
||||
export const actions: Actions = { default: signIn }
|
@@ -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 });
|
38
src/routes/api/cospend/balance/+server.ts
Normal file
38
src/routes/api/cospend/balance/+server.ts
Normal 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);
|
||||
};
|
20
src/routes/api/cospend/page/[pageno]/+server.ts
Normal file
20
src/routes/api/cospend/page/[pageno]/+server.ts
Normal 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);
|
||||
};
|
@@ -1,7 +0,0 @@
|
||||
import type { PageServerLoad } from "./$types"
|
||||
|
||||
export const load : PageServerLoad = (async ({locals}) => {
|
||||
return {
|
||||
session: await locals.auth(),
|
||||
}
|
||||
});
|
@@ -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>
|
@@ -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>
|
18
src/routes/cospend/+page.ts
Normal file
18
src/routes/cospend/+page.ts
Normal 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
|
||||
};
|
||||
}
|
@@ -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(),
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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()
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
export async function load({locals}) {
|
||||
const session = await locals.auth();
|
||||
return {
|
||||
user: locals.user
|
||||
user: session?.user
|
||||
};
|
||||
};
|
||||
|
@@ -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
|
||||
};
|
||||
};
|
||||
|
1
static/other/.jukit/.jukit_info.json
Normal file
1
static/other/.jukit/.jukit_info.json
Normal file
@@ -0,0 +1 @@
|
||||
{"terminal": "nvimterm"}
|
2
static/other/jellyfin.js
Normal file
2
static/other/jellyfin.js
Normal 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()});});
|
BIN
static/test/1749501265645_angelus0001.jpg
Normal file
BIN
static/test/1749501265645_angelus0001.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
BIN
static/test/1749501265648_Anna_ID.pdf
Normal file
BIN
static/test/1749501265648_Anna_ID.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user