feat: update AUTH_SECRET handling and improve session management
Build and Push Docker Image / build (push) Successful in 1m23s
Build and Push Docker Image / build (push) Successful in 1m23s
fix: add credentials to POST request in NewCompanyPage fix: update Docker image pull policy for app service
This commit is contained in:
+5
-2
@@ -1,8 +1,11 @@
|
|||||||
# Datenbank (für lokale Entwicklung)
|
# Datenbank (für lokale Entwicklung)
|
||||||
DATABASE_URL="mysql://user:password@localhost:3306/annas_rechnungsmanager"
|
DATABASE_URL="mysql://user:password@localhost:3306/annas_rechnungsmanager"
|
||||||
|
|
||||||
# Session-Secret – zufälligen Wert generieren: openssl rand -base64 32
|
# Session-Secret – optional
|
||||||
AUTH_SECRET="HIER_ZUFAELLIGEN_WERT_EINSETZEN"
|
# Wenn nicht gesetzt, wird ein zufälliger Wert generiert (empfohlen für Docker/Development)
|
||||||
|
# Bei Containerneustarts werden alle Sessions dann automatisch ungültig
|
||||||
|
# Für Production mit persistenter Session: openssl rand -base64 32
|
||||||
|
AUTH_SECRET=""
|
||||||
|
|
||||||
# Docker-Compose: Datenbank-Credentials
|
# Docker-Compose: Datenbank-Credentials
|
||||||
DB_ROOT_PASSWORD="sicheres_root_passwort"
|
DB_ROOT_PASSWORD="sicheres_root_passwort"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { calcItemAmounts, calcItemAmountsKleinunternehmer, calcInvoiceTotals, formatCurrency, TAX_RATES } from "@/lib/tax";
|
import { calcItemAmounts, calcItemAmountsKleinunternehmer, calcInvoiceTotals, formatCurrency, TAX_RATES } from "@/lib/tax";
|
||||||
import { Plus, Trash2 } from "lucide-react";
|
import { Plus, Trash2 } from "lucide-react";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
|
|
||||||
interface Customer {
|
interface Customer {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -301,7 +300,7 @@ export function InvoiceForm({ customers, companyId, onSubmit, defaultValues, def
|
|||||||
value={descValue}
|
value={descValue}
|
||||||
onChange={(e) => setValue(`items.${index}.description`, e.target.value)}
|
onChange={(e) => setValue(`items.${index}.description`, e.target.value)}
|
||||||
onFocus={() => setOpenDropdown(index)}
|
onFocus={() => setOpenDropdown(index)}
|
||||||
onBlur={() => setOpenDropdown(null)}
|
onBlur={() => setTimeout(() => setOpenDropdown(null), 100)}
|
||||||
placeholder="Leistungsbeschreibung"
|
placeholder="Leistungsbeschreibung"
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@@ -315,7 +314,7 @@ export function InvoiceForm({ customers, companyId, onSubmit, defaultValues, def
|
|||||||
className="w-full text-left px-3 py-2 text-sm hover:bg-gray-50 flex flex-col"
|
className="w-full text-left px-3 py-2 text-sm hover:bg-gray-50 flex flex-col"
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setValue(`items.${index}.description`, s.description ?? s.name);
|
setValue(`items.${index}.description`, s.name);
|
||||||
setValue(`items.${index}.unit`, s.unit ?? "Stück");
|
setValue(`items.${index}.unit`, s.unit ?? "Stück");
|
||||||
setValue(`items.${index}.unitPrice`, String(s.unitPrice));
|
setValue(`items.${index}.unitPrice`, String(s.unitPrice));
|
||||||
setValue(`items.${index}.taxRate`, String(s.taxRate));
|
setValue(`items.${index}.taxRate`, String(s.taxRate));
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export default function NewCompanyPage() {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
|
credentials: "include",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|||||||
+21
-3
@@ -1,10 +1,18 @@
|
|||||||
import { createCookieSessionStorage, redirect } from "react-router";
|
import { createCookieSessionStorage, redirect } from "react-router";
|
||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
|
import { randomBytes } from "crypto";
|
||||||
import prisma from "@/lib/prisma.server";
|
import prisma from "@/lib/prisma.server";
|
||||||
import { log } from "@/lib/logger.server";
|
import { log } from "@/lib/logger.server";
|
||||||
|
|
||||||
if (!process.env.AUTH_SECRET) {
|
/**
|
||||||
throw new Error("AUTH_SECRET environment variable is required");
|
* AUTH_SECRET wird nur aus .env gelesen, falls die Umgebungsvariable nicht existiert.
|
||||||
|
* Falls nicht gesetzt, wird eine zufällige generiert.
|
||||||
|
* Bei jedem Containerstart mit ephemerem Secret werden alle bestehenden Sessions invalidiert.
|
||||||
|
*/
|
||||||
|
const AUTH_SECRET = process.env.AUTH_SECRET || randomBytes(32).toString("base64");
|
||||||
|
|
||||||
|
if (!AUTH_SECRET) {
|
||||||
|
throw new Error("AUTH_SECRET could not be generated");
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionStorage = createCookieSessionStorage({
|
const sessionStorage = createCookieSessionStorage({
|
||||||
@@ -14,7 +22,7 @@ const sessionStorage = createCookieSessionStorage({
|
|||||||
maxAge: 60 * 60 * 4, // 4 Stunden
|
maxAge: 60 * 60 * 4, // 4 Stunden
|
||||||
path: "/",
|
path: "/",
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
secrets: [process.env.AUTH_SECRET],
|
secrets: [AUTH_SECRET],
|
||||||
secure: process.env.NODE_ENV === "production",
|
secure: process.env.NODE_ENV === "production",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -64,6 +72,7 @@ export async function createUserSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserSession(request: Request) {
|
export async function getUserSession(request: Request) {
|
||||||
|
try {
|
||||||
const session = await sessionStorage.getSession(
|
const session = await sessionStorage.getSession(
|
||||||
request.headers.get("Cookie")
|
request.headers.get("Cookie")
|
||||||
);
|
);
|
||||||
@@ -72,6 +81,15 @@ export async function getUserSession(request: Request) {
|
|||||||
userName: session.get("userName") as string | undefined,
|
userName: session.get("userName") as string | undefined,
|
||||||
userRole: session.get("userRole") as string | undefined,
|
userRole: session.get("userRole") as string | undefined,
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// Session-Cookie ist ungültig (z.B. nach Neustart mit neuem AUTH_SECRET)
|
||||||
|
// Gib eine leere Session zurück, damit der Nutzer zum Login weitergeleitet wird
|
||||||
|
return {
|
||||||
|
userId: undefined,
|
||||||
|
userName: undefined,
|
||||||
|
userRole: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requireUser(request: Request) {
|
export async function requireUser(request: Request) {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ services:
|
|||||||
# registry.henryathome.home64.de/henry/annasrechnungsmanager:latest
|
# registry.henryathome.home64.de/henry/annasrechnungsmanager:latest
|
||||||
image: git.henryathome.home64.de/henry/annasrechnungsmanager:latest
|
image: git.henryathome.home64.de/henry/annasrechnungsmanager:latest
|
||||||
container_name: annas_app
|
container_name: annas_app
|
||||||
|
pull_policy: always
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
|
|||||||
Reference in New Issue
Block a user