CalDAV: Support Basic auth; trim+validate UUID; deprecate query token via headers; ICS end time helper; docs+instructions updated

This commit is contained in:
2025-10-06 17:25:25 +02:00
parent 90029f4b6a
commit 31b007d145
29 changed files with 2311 additions and 321 deletions

View File

@@ -24,12 +24,12 @@ export function AdminAvailability() {
// Neue Queries für wiederkehrende Verfügbarkeiten (mit Authentifizierung)
const { data: recurringRules, refetch: refetchRecurringRules } = useQuery(
queryClient.recurringAvailability.live.adminListRules.experimental_liveOptions({
input: { sessionId: localStorage.getItem("sessionId") || "" }
input: {}
})
);
const { data: timeOffPeriods } = useQuery(
queryClient.recurringAvailability.live.adminListTimeOff.experimental_liveOptions({
input: { sessionId: localStorage.getItem("sessionId") || "" }
input: {}
})
);
@@ -177,14 +177,9 @@ export function AdminAvailability() {
return;
}
const sessionId = localStorage.getItem("sessionId") || "";
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
return;
}
createRule(
{ sessionId, dayOfWeek: selectedDayOfWeek, startTime: ruleStartTime, endTime: ruleEndTime },
{ dayOfWeek: selectedDayOfWeek, startTime: ruleStartTime, endTime: ruleEndTime },
{
onSuccess: () => {
setSuccessMsg(`Regel für ${getDayName(selectedDayOfWeek)} erstellt.`);
@@ -233,13 +228,8 @@ export function AdminAvailability() {
<div className="flex items-center gap-2">
<button
onClick={() => {
const sessionId = localStorage.getItem("sessionId");
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
return;
}
toggleRuleActive(
{ sessionId, id: rule.id },
{ id: rule.id },
{
onSuccess: () => {
setSuccessMsg(`Regel ${rule.isActive ? "deaktiviert" : "aktiviert"}.`);
@@ -256,13 +246,8 @@ export function AdminAvailability() {
</button>
<button
onClick={() => {
const sessionId = localStorage.getItem("sessionId");
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
return;
}
deleteRule(
{ sessionId, id: rule.id },
{ id: rule.id },
{
onSuccess: () => {
setSuccessMsg("Regel gelöscht.");
@@ -348,14 +333,9 @@ export function AdminAvailability() {
return;
}
const sessionId = localStorage.getItem("sessionId") || "";
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
return;
}
createTimeOff(
{ sessionId, startDate: timeOffStartDate, endDate: timeOffEndDate, reason: timeOffReason },
{ startDate: timeOffStartDate, endDate: timeOffEndDate, reason: timeOffReason },
{
onSuccess: () => {
setSuccessMsg("Urlaubszeit hinzugefügt.");
@@ -415,13 +395,8 @@ export function AdminAvailability() {
<div className="flex items-center gap-2">
<button
onClick={() => {
const sessionId = localStorage.getItem("sessionId");
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
return;
}
deleteTimeOff(
{ sessionId, id: period.id },
{ id: period.id },
{
onSuccess: () => {
setSuccessMsg("Urlaubszeit gelöscht.");

View File

@@ -255,7 +255,7 @@ export function AdminBookings() {
{booking.status === "pending" && (
<>
<button
onClick={() => updateBookingStatus({ sessionId: localStorage.getItem("sessionId") || "", id: booking.id, status: "confirmed" })}
onClick={() => updateBookingStatus({ id: booking.id, status: "confirmed" })}
className="text-green-600 hover:text-green-900"
>
Confirm
@@ -271,7 +271,7 @@ export function AdminBookings() {
{booking.status === "confirmed" && (
<>
<button
onClick={() => updateBookingStatus({ sessionId: localStorage.getItem("sessionId") || "", id: booking.id, status: "completed" })}
onClick={() => updateBookingStatus({ id: booking.id, status: "completed" })}
className="text-blue-600 hover:text-blue-900"
>
Complete
@@ -286,7 +286,7 @@ export function AdminBookings() {
)}
{(booking.status === "cancelled" || booking.status === "completed") && (
<button
onClick={() => updateBookingStatus({ sessionId: localStorage.getItem("sessionId") || "", id: booking.id, status: "confirmed" })}
onClick={() => updateBookingStatus({ id: booking.id, status: "confirmed" })}
className="text-green-600 hover:text-green-900"
>
Reactivate
@@ -352,8 +352,7 @@ export function AdminBookings() {
<div className="flex space-x-3">
<button
onClick={() => {
const sessionId = localStorage.getItem("sessionId") || "";
updateBookingStatus({ sessionId, id: showCancelConfirm, status: "cancelled" });
updateBookingStatus({ id: showCancelConfirm, status: "cancelled" });
}}
className="flex-1 bg-red-600 text-white py-2 px-4 rounded-md hover:bg-red-700 transition-colors"
>

View File

@@ -164,24 +164,18 @@ export function AdminCalendar() {
};
const handleStatusUpdate = (bookingId: string, newStatus: string) => {
const sessionId = localStorage.getItem('sessionId');
if (!sessionId) return;
updateBookingStatus({
sessionId,
id: bookingId,
status: newStatus as "pending" | "confirmed" | "cancelled" | "completed"
});
};
const handleDeleteBooking = () => {
const sessionId = localStorage.getItem('sessionId');
if (!sessionId || !showDeleteConfirm) return;
if (!showDeleteConfirm) return;
if (deleteActionType === 'cancel') {
// For cancel action, use updateStatus instead of remove
updateBookingStatus({
sessionId,
id: showDeleteConfirm,
status: "cancelled"
}, {
@@ -197,7 +191,6 @@ export function AdminCalendar() {
} else {
// For delete action, use remove with email option
removeBooking({
sessionId,
id: showDeleteConfirm,
sendEmail: sendDeleteEmail,
}, {
@@ -216,11 +209,8 @@ export function AdminCalendar() {
const today = new Date().toISOString().split('T')[0];
const handleCreateBooking = () => {
const sessionId = localStorage.getItem('sessionId');
if (!sessionId) return;
createManualBooking({
sessionId,
...createFormData
}, {
onSuccess: () => {
@@ -262,13 +252,11 @@ export function AdminCalendar() {
};
const handleRescheduleBooking = () => {
const sessionId = localStorage.getItem('sessionId');
if (!sessionId || !showRescheduleModal) return;
if (!showRescheduleModal) return;
const booking = bookings?.find(b => b.id === showRescheduleModal);
if (!booking) return;
proposeReschedule({
sessionId,
bookingId: booking.id,
proposedDate: rescheduleFormData.appointmentDate,
proposedTime: rescheduleFormData.appointmentTime,
@@ -285,11 +273,8 @@ export function AdminCalendar() {
};
const handleGenerateCalDAVToken = () => {
const sessionId = localStorage.getItem('sessionId');
if (!sessionId) return;
generateCalDAVToken({
sessionId
}, {
onSuccess: (data) => {
setCaldavData(data);

View File

@@ -14,7 +14,7 @@ export function AdminGallery() {
// Data fetching with live query
const { data: photos, refetch: refetchPhotos } = useQuery(
queryClient.gallery.live.adminListPhotos.experimental_liveOptions({
input: { sessionId: localStorage.getItem("sessionId") || "" }
input: {}
})
);
@@ -166,12 +166,6 @@ export function AdminGallery() {
return;
}
const sessionId = localStorage.getItem("sessionId");
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
setDraggedPhotoId(null);
return;
}
// Build a fully reordered list based on the current sorted order
const sorted = [...(photos || [])].sort((a, b) => a.order - b.order);
@@ -191,7 +185,6 @@ export function AdminGallery() {
updatePhotoOrder(
{
sessionId,
photoOrders
},
{
@@ -304,15 +297,9 @@ export function AdminGallery() {
return;
}
const sessionId = localStorage.getItem("sessionId") || "";
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
return;
}
uploadPhoto(
{
sessionId,
base64Data: photoBase64,
title: photoTitle || undefined
},
@@ -396,13 +383,8 @@ export function AdminGallery() {
<button
onClick={() => {
if (confirm("Möchtest du dieses Foto wirklich löschen?")) {
const sessionId = localStorage.getItem("sessionId");
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
return;
}
deletePhoto(
{ sessionId, id: photo.id },
{ id: photo.id },
{
onSuccess: () => {
setSuccessMsg("Foto gelöscht.");
@@ -425,13 +407,8 @@ export function AdminGallery() {
</button>
<button
onClick={() => {
const sessionId = localStorage.getItem("sessionId");
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
return;
}
setCoverPhoto(
{ sessionId, id: photo.id },
{ id: photo.id },
{
onSuccess: () => setSuccessMsg("Als Cover-Bild gesetzt."),
onError: (err: any) => setErrorMsg(err?.message || "Fehler beim Setzen des Cover-Bildes."),

View File

@@ -91,18 +91,17 @@ export function AdminReviews() {
}
}, [successMsg]);
const sessionId = typeof window !== "undefined" ? localStorage.getItem("sessionId") || "" : "";
const { data: reviews } = useQuery(
queryClient.reviews.live.adminListReviews.experimental_liveOptions({
input: { sessionId, statusFilter: activeStatusTab },
input: { statusFilter: activeStatusTab },
})
);
// Separate queries for quick stats calculation
const { data: allReviews } = useQuery(
queryClient.reviews.live.adminListReviews.experimental_liveOptions({
input: { sessionId },
input: {},
})
);
@@ -266,13 +265,13 @@ export function AdminReviews() {
{review.status === "pending" && (
<>
<button
onClick={() => approveReview({ sessionId, id: review.id })}
onClick={() => approveReview({ id: review.id })}
className="bg-green-600 hover:bg-green-700 text-white px-3 py-1.5 rounded-md text-sm"
>
Genehmigen
</button>
<button
onClick={() => rejectReview({ sessionId, id: review.id })}
onClick={() => rejectReview({ id: review.id })}
className="bg-red-600 hover:bg-red-700 text-white px-3 py-1.5 rounded-md text-sm"
>
Ablehnen
@@ -289,7 +288,7 @@ export function AdminReviews() {
{review.status === "approved" && (
<>
<button
onClick={() => rejectReview({ sessionId, id: review.id })}
onClick={() => rejectReview({ id: review.id })}
className="bg-red-600 hover:bg-red-700 text-white px-3 py-1.5 rounded-md text-sm"
>
Ablehnen
@@ -306,7 +305,7 @@ export function AdminReviews() {
{review.status === "rejected" && (
<>
<button
onClick={() => approveReview({ sessionId, id: review.id })}
onClick={() => approveReview({ id: review.id })}
className="bg-green-600 hover:bg-green-700 text-white px-3 py-1.5 rounded-md text-sm"
>
Genehmigen
@@ -334,7 +333,7 @@ export function AdminReviews() {
</p>
<div className="flex space-x-3">
<button
onClick={() => deleteReview({ sessionId, id: showDeleteConfirm })}
onClick={() => deleteReview({ id: showDeleteConfirm })}
className="flex-1 bg-red-600 text-white py-2 px-4 rounded-md hover:bg-red-700 transition-colors"
>
Ja, löschen

View File

@@ -11,7 +11,6 @@ interface User {
interface AuthContextType {
user: User | null;
sessionId: string | null;
isLoading: boolean;
login: (username: string, password: string) => Promise<void>;
logout: () => void;
@@ -34,7 +33,6 @@ interface AuthProviderProps {
export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null);
const [sessionId, setSessionId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const { mutateAsync: loginMutation } = useMutation(
@@ -50,56 +48,45 @@ export function AuthProvider({ children }: AuthProviderProps) {
);
useEffect(() => {
// Check for existing session on app load
const storedSessionId = localStorage.getItem("sessionId");
if (storedSessionId) {
verifySessionMutation(storedSessionId)
.then((result) => {
setUser(result.user);
setSessionId(storedSessionId);
})
.catch(() => {
localStorage.removeItem("sessionId");
})
.finally(() => {
setIsLoading(false);
});
} else {
setIsLoading(false);
}
// Check for existing session on app load - session comes from cookies
verifySessionMutation({})
.then((result) => {
setUser(result.user);
})
.catch(() => {
// Session invalid or expired - user remains null
})
.finally(() => {
setIsLoading(false);
});
}, [verifySessionMutation]);
const login = async (username: string, password: string) => {
try {
const result = await loginMutation({ username, password });
setUser(result.user);
setSessionId(result.sessionId);
localStorage.setItem("sessionId", result.sessionId);
// Cookies are set automatically by the server
} catch (error) {
throw error;
}
};
const logout = async () => {
if (sessionId) {
try {
await logoutMutation(sessionId);
} catch (error) {
// Continue with logout even if server call fails
console.error("Logout error:", error);
}
try {
await logoutMutation({});
// Cookies are cleared automatically by the server
} catch (error) {
// Continue with logout even if server call fails
console.error("Logout error:", error);
}
setUser(null);
setSessionId(null);
localStorage.removeItem("sessionId");
};
const isOwner = user?.role === "owner";
const value: AuthContextType = {
user,
sessionId,
isLoading,
login,
logout,

View File

@@ -4,7 +4,7 @@ import { useAuth } from "@/client/components/auth-provider";
import { queryClient } from "@/client/rpc-client";
export function UserProfile() {
const { user, sessionId, logout } = useAuth();
const { user, logout } = useAuth();
const [showPasswordChange, setShowPasswordChange] = useState(false);
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
@@ -31,13 +31,7 @@ export function UserProfile() {
return;
}
if (!sessionId) {
setError("Keine gültige Sitzung");
return;
}
changePassword({
sessionId,
currentPassword,
newPassword,
}, {