feat: enforce 12-hour limit for active tasting sessions
This commit is contained in:
@@ -8,6 +8,7 @@ import TastingNoteForm from '@/components/TastingNoteForm';
|
|||||||
import StatusSwitcher from '@/components/StatusSwitcher';
|
import StatusSwitcher from '@/components/StatusSwitcher';
|
||||||
import TastingList from '@/components/TastingList';
|
import TastingList from '@/components/TastingList';
|
||||||
import DeleteBottleButton from '@/components/DeleteBottleButton';
|
import DeleteBottleButton from '@/components/DeleteBottleButton';
|
||||||
|
import { validateSession } from '@/services/validate-session';
|
||||||
|
|
||||||
export default async function BottlePage({
|
export default async function BottlePage({
|
||||||
params,
|
params,
|
||||||
@@ -16,7 +17,15 @@ export default async function BottlePage({
|
|||||||
params: { id: string },
|
params: { id: string },
|
||||||
searchParams: { session_id?: string }
|
searchParams: { session_id?: string }
|
||||||
}) {
|
}) {
|
||||||
const sessionId = searchParams.session_id;
|
let sessionId = searchParams.session_id;
|
||||||
|
|
||||||
|
// Validate Session Age (12 hour limit)
|
||||||
|
if (sessionId) {
|
||||||
|
const isValid = await validateSession(sessionId);
|
||||||
|
if (!isValid) {
|
||||||
|
sessionId = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = createServerComponentClient({ cookies });
|
||||||
|
|
||||||
const { data: bottle } = await supabase
|
const { data: bottle } = await supabase
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Link from 'next/link';
|
|||||||
import { Search, Filter, X, Calendar, Clock, Package, Lock, Unlock, Ghost, FlaskConical, AlertCircle, Trash2, AlertTriangle, PlusCircle } from 'lucide-react';
|
import { Search, Filter, X, Calendar, Clock, Package, Lock, Unlock, Ghost, FlaskConical, AlertCircle, Trash2, AlertTriangle, PlusCircle } from 'lucide-react';
|
||||||
import { getStorageUrl } from '@/lib/supabase';
|
import { getStorageUrl } from '@/lib/supabase';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import { validateSession } from '@/services/validate-session';
|
||||||
|
|
||||||
interface Bottle {
|
interface Bottle {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -115,6 +116,19 @@ interface BottleGridProps {
|
|||||||
export default function BottleGrid({ bottles }: BottleGridProps) {
|
export default function BottleGrid({ bottles }: BottleGridProps) {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const sessionId = searchParams.get('session_id');
|
const sessionId = searchParams.get('session_id');
|
||||||
|
const [validatedSessionId, setValidatedSessionId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const checkSession = async () => {
|
||||||
|
if (sessionId) {
|
||||||
|
const isValid = await validateSession(sessionId);
|
||||||
|
setValidatedSessionId(isValid ? sessionId : null);
|
||||||
|
} else {
|
||||||
|
setValidatedSessionId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
checkSession();
|
||||||
|
}, [sessionId]);
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||||
@@ -284,7 +298,7 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
|
|||||||
{filteredBottles.length > 0 ? (
|
{filteredBottles.length > 0 ? (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 w-full max-w-6xl mx-auto px-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 w-full max-w-6xl mx-auto px-4">
|
||||||
{filteredBottles.map((bottle) => (
|
{filteredBottles.map((bottle) => (
|
||||||
<BottleCard key={bottle.id} bottle={bottle} sessionId={sessionId} />
|
<BottleCard key={bottle.id} bottle={bottle} sessionId={validatedSessionId} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { BottleMetadata } from '@/types/whisky';
|
|||||||
import { savePendingBottle } from '@/lib/offline-db';
|
import { savePendingBottle } from '@/lib/offline-db';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { findMatchingBottle } from '@/services/find-matching-bottle';
|
import { findMatchingBottle } from '@/services/find-matching-bottle';
|
||||||
|
import { validateSession } from '@/services/validate-session';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
interface CameraCaptureProps {
|
interface CameraCaptureProps {
|
||||||
@@ -23,6 +24,19 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const sessionId = searchParams.get('session_id');
|
const sessionId = searchParams.get('session_id');
|
||||||
|
const [validatedSessionId, setValidatedSessionId] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const checkSession = async () => {
|
||||||
|
if (sessionId) {
|
||||||
|
const isValid = await validateSession(sessionId);
|
||||||
|
setValidatedSessionId(isValid ? sessionId : null);
|
||||||
|
} else {
|
||||||
|
setValidatedSessionId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
checkSession();
|
||||||
|
}, [sessionId]);
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
@@ -222,7 +236,7 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const url = `/bottles/${lastSavedId}${sessionId ? `?session_id=${sessionId}` : ''}`;
|
const url = `/bottles/${lastSavedId}${validatedSessionId ? `?session_id=${validatedSessionId}` : ''}`;
|
||||||
router.push(url);
|
router.push(url);
|
||||||
}}
|
}}
|
||||||
className="w-full py-4 px-6 bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 rounded-2xl font-black uppercase tracking-widest flex items-center justify-center gap-2 hover:bg-zinc-800 dark:hover:bg-white transition-all shadow-xl"
|
className="w-full py-4 px-6 bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 rounded-2xl font-black uppercase tracking-widest flex items-center justify-center gap-2 hover:bg-zinc-800 dark:hover:bg-white transition-all shadow-xl"
|
||||||
@@ -244,7 +258,7 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
|
|||||||
</div>
|
</div>
|
||||||
) : matchingBottle ? (
|
) : matchingBottle ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/bottles/${matchingBottle.id}${sessionId ? `?session_id=${sessionId}` : ''}`}
|
href={`/bottles/${matchingBottle.id}${validatedSessionId ? `?session_id=${validatedSessionId}` : ''}`}
|
||||||
className="w-full py-4 px-6 bg-amber-600 hover:bg-amber-700 text-white rounded-xl font-semibold flex items-center justify-center gap-2 transition-all active:scale-[0.98] shadow-lg shadow-amber-600/20"
|
className="w-full py-4 px-6 bg-amber-600 hover:bg-amber-700 text-white rounded-xl font-semibold flex items-center justify-center gap-2 transition-all active:scale-[0.98] shadow-lg shadow-amber-600/20"
|
||||||
>
|
>
|
||||||
<ExternalLink size={20} />
|
<ExternalLink size={20} />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
|
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
|
import { validateSession } from './validate-session';
|
||||||
|
|
||||||
export async function saveTasting(data: {
|
export async function saveTasting(data: {
|
||||||
bottle_id: string;
|
bottle_id: string;
|
||||||
@@ -20,6 +21,14 @@ export async function saveTasting(data: {
|
|||||||
const { data: { session } } = await supabase.auth.getSession();
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
if (!session) throw new Error('Nicht autorisiert');
|
if (!session) throw new Error('Nicht autorisiert');
|
||||||
|
|
||||||
|
// Validate Session Age (12 hour limit)
|
||||||
|
if (data.session_id) {
|
||||||
|
const isValid = await validateSession(data.session_id);
|
||||||
|
if (!isValid) {
|
||||||
|
throw new Error('Dieses Tasting Session ist bereits abgelaufen (Limit: 12 Stunden).');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { data: tasting, error } = await supabase
|
const { data: tasting, error } = await supabase
|
||||||
.from('tastings')
|
.from('tastings')
|
||||||
.insert({
|
.insert({
|
||||||
|
|||||||
33
src/services/validate-session.ts
Normal file
33
src/services/validate-session.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if a session is still "active" based on its age.
|
||||||
|
* Returns true if the session is less than 12 hours old.
|
||||||
|
*/
|
||||||
|
export async function validateSession(sessionId: string | null): Promise<boolean> {
|
||||||
|
if (!sessionId) return false;
|
||||||
|
|
||||||
|
const supabase = createServerActionClient({ cookies });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: session, error } = await supabase
|
||||||
|
.from('tasting_sessions')
|
||||||
|
.select('created_at')
|
||||||
|
.eq('id', sessionId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error || !session) return false;
|
||||||
|
|
||||||
|
const createdAt = new Date(session.created_at).getTime();
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const twelveHoursInMs = 12 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
return (now - createdAt) < twelveHoursInMs;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Session validation error:', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user