feat: auth service + security audit fixes + cleanup legacy services
Major changes:
- Add auth-svc: JWT auth, register/login/refresh, password reset
- Add auth UI: modals, pages (/login, /register, /forgot-password)
- Add usage tracking (usage_metrics table, daily limits)
- Add tiered rate limiting (free/pro/business)
- Add LLM usage limits per tier
Security fixes:
- All repos now require userID for Update/Delete operations
- JWT middleware in chat-svc, llm-svc, agent-svc, discover-svc
- ErrNotFound/ErrForbidden errors for proper access control
Cleanup:
- Remove legacy TypeScript services/ directory
- Remove computer-svc (to be reimplemented)
- Remove old deploy/docker configs
New files:
- backend/cmd/auth-svc/main.go
- backend/internal/auth/{types,repository}.go
- backend/internal/usage/{types,repository}.go
- backend/pkg/middleware/{llm_limits,ratelimit_tiered}.go
- backend/webui/src/components/auth/*
- backend/webui/src/app/(auth)/*
Made-with: Cursor
This commit is contained in:
316
backend/webui/src/app/(main)/travel/page.tsx
Normal file
316
backend/webui/src/app/(main)/travel/page.tsx
Normal file
@@ -0,0 +1,316 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Plane,
|
||||
MapPin,
|
||||
Calendar,
|
||||
Users,
|
||||
Search,
|
||||
Star,
|
||||
Clock,
|
||||
Loader2,
|
||||
Sparkles,
|
||||
Globe,
|
||||
Hotel,
|
||||
Car,
|
||||
Palmtree,
|
||||
Mountain,
|
||||
Building2,
|
||||
Waves,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface Destination {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
image: string;
|
||||
rating: number;
|
||||
price: string;
|
||||
category: 'beach' | 'city' | 'nature' | 'mountain';
|
||||
description: string;
|
||||
}
|
||||
|
||||
const popularDestinations: Destination[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Сочи',
|
||||
country: 'Россия',
|
||||
image: '🏖️',
|
||||
rating: 4.7,
|
||||
price: 'от 15 000 ₽',
|
||||
category: 'beach',
|
||||
description: 'Черноморский курорт с горами и морем',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Санкт-Петербург',
|
||||
country: 'Россия',
|
||||
image: '🏛️',
|
||||
rating: 4.9,
|
||||
price: 'от 8 000 ₽',
|
||||
category: 'city',
|
||||
description: 'Культурная столица России',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Казань',
|
||||
country: 'Россия',
|
||||
image: '🕌',
|
||||
rating: 4.6,
|
||||
price: 'от 7 500 ₽',
|
||||
category: 'city',
|
||||
description: 'Древний город на Волге',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Байкал',
|
||||
country: 'Россия',
|
||||
image: '🏔️',
|
||||
rating: 4.8,
|
||||
price: 'от 25 000 ₽',
|
||||
category: 'nature',
|
||||
description: 'Самое глубокое озеро в мире',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Калининград',
|
||||
country: 'Россия',
|
||||
image: '⚓',
|
||||
rating: 4.5,
|
||||
price: 'от 12 000 ₽',
|
||||
category: 'city',
|
||||
description: 'Европейский колорит на Балтике',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'Алтай',
|
||||
country: 'Россия',
|
||||
image: '🗻',
|
||||
rating: 4.7,
|
||||
price: 'от 20 000 ₽',
|
||||
category: 'mountain',
|
||||
description: 'Горные пейзажи и чистый воздух',
|
||||
},
|
||||
];
|
||||
|
||||
const categories = [
|
||||
{ id: 'all', label: 'Все', icon: Globe },
|
||||
{ id: 'beach', label: 'Пляж', icon: Waves },
|
||||
{ id: 'city', label: 'Города', icon: Building2 },
|
||||
{ id: 'nature', label: 'Природа', icon: Palmtree },
|
||||
{ id: 'mountain', label: 'Горы', icon: Mountain },
|
||||
];
|
||||
|
||||
const quickActions = [
|
||||
{ icon: Plane, label: 'Авиабилеты', color: 'bg-blue-500/10 text-blue-400' },
|
||||
{ icon: Hotel, label: 'Отели', color: 'bg-purple-500/10 text-purple-400' },
|
||||
{ icon: Car, label: 'Авто', color: 'bg-green-500/10 text-green-400' },
|
||||
{ icon: Sparkles, label: 'AI Планировщик', color: 'active-gradient text-gradient' },
|
||||
];
|
||||
|
||||
function DestinationCard({ destination, delay }: { destination: Destination; delay: number }) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay }}
|
||||
className="group bg-elevated/40 border border-border/40 rounded-xl overflow-hidden hover:border-border transition-all cursor-pointer"
|
||||
>
|
||||
<div className="h-28 sm:h-32 bg-surface/60 flex items-center justify-center text-4xl sm:text-5xl">
|
||||
{destination.image}
|
||||
</div>
|
||||
<div className="p-3 sm:p-4">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-primary group-hover:text-gradient transition-colors">
|
||||
{destination.name}
|
||||
</h3>
|
||||
<p className="text-xs text-muted flex items-center gap-1">
|
||||
<MapPin className="w-3 h-3" />
|
||||
{destination.country}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<Star className="w-3 h-3 text-amber-400 fill-amber-400" />
|
||||
<span className="text-secondary">{destination.rating}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted mb-3 line-clamp-2">{destination.description}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-gradient">{destination.price}</span>
|
||||
<button className="text-xs text-muted hover:text-primary transition-colors">
|
||||
Подробнее →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TravelPage() {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [travelers, setTravelers] = useState(2);
|
||||
const [departureDate, setDepartureDate] = useState('');
|
||||
|
||||
const handleAISearch = useCallback(async () => {
|
||||
if (!searchQuery.trim()) return;
|
||||
setIsLoading(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
setIsLoading(false);
|
||||
}, [searchQuery]);
|
||||
|
||||
const filteredDestinations = popularDestinations.filter((d) => {
|
||||
if (selectedCategory !== 'all' && d.category !== selectedCategory) return false;
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
return (
|
||||
d.name.toLowerCase().includes(query) ||
|
||||
d.country.toLowerCase().includes(query) ||
|
||||
d.description.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="max-w-2xl mx-auto px-4 sm:px-6 py-6 sm:py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<h1 className="text-xl sm:text-2xl font-semibold text-primary mb-1 sm:mb-2">Путешествия</h1>
|
||||
<p className="text-sm text-secondary">Планируйте поездки с помощью AI</p>
|
||||
</div>
|
||||
|
||||
{/* AI Search */}
|
||||
<div className="bg-elevated/40 border border-border/40 rounded-xl p-4 mb-6">
|
||||
<div className="relative mb-4">
|
||||
<Sparkles className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gradient" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleAISearch()}
|
||||
placeholder="Куда хотите поехать? Опишите своё идеальное путешествие..."
|
||||
className="w-full h-12 pl-12 pr-4 bg-surface/50 border border-border/50 rounded-xl text-sm text-primary placeholder:text-muted focus:outline-none focus:border-accent/50 focus:ring-1 focus:ring-accent/20 transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3 mb-4">
|
||||
<div className="flex items-center gap-2 px-3 py-2 bg-surface/50 border border-border/40 rounded-lg">
|
||||
<Calendar className="w-4 h-4 text-muted" />
|
||||
<input
|
||||
type="date"
|
||||
value={departureDate}
|
||||
onChange={(e) => setDepartureDate(e.target.value)}
|
||||
className="bg-transparent text-sm text-secondary outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-2 bg-surface/50 border border-border/40 rounded-lg">
|
||||
<Users className="w-4 h-4 text-muted" />
|
||||
<select
|
||||
value={travelers}
|
||||
onChange={(e) => setTravelers(Number(e.target.value))}
|
||||
className="bg-transparent text-sm text-secondary outline-none"
|
||||
>
|
||||
<option value={1}>1 человек</option>
|
||||
<option value={2}>2 человека</option>
|
||||
<option value={3}>3 человека</option>
|
||||
<option value={4}>4 человека</option>
|
||||
<option value={5}>5+ человек</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleAISearch}
|
||||
disabled={isLoading || !searchQuery.trim()}
|
||||
className="w-full h-11 flex items-center justify-center gap-2 active-gradient text-gradient font-medium text-sm rounded-xl disabled:opacity-50 disabled:cursor-not-allowed transition-opacity"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
AI планирует маршрут...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Search className="w-4 h-4" />
|
||||
Найти с помощью AI
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-4 gap-2 sm:gap-3 mb-6 sm:mb-8">
|
||||
{quickActions.map((action) => (
|
||||
<button
|
||||
key={action.label}
|
||||
className={`flex flex-col items-center gap-2 p-3 sm:p-4 rounded-xl border border-border/40 hover:border-border transition-all ${action.color}`}
|
||||
>
|
||||
<action.icon className="w-5 h-5 sm:w-6 sm:h-6" />
|
||||
<span className="text-[10px] sm:text-xs font-medium">{action.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="flex gap-2 mb-6 overflow-x-auto pb-1 -mx-4 px-4 sm:mx-0 sm:px-0">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => setSelectedCategory(cat.id)}
|
||||
className={`flex items-center gap-2 px-4 py-2.5 text-sm rounded-xl border transition-all whitespace-nowrap ${
|
||||
selectedCategory === cat.id
|
||||
? 'active-gradient text-primary border-transparent'
|
||||
: 'bg-surface/30 border-border/30 text-muted hover:text-secondary hover:border-border'
|
||||
}`}
|
||||
>
|
||||
<cat.icon className="w-4 h-4" />
|
||||
{cat.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Popular Destinations */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-9 h-9 rounded-xl active-gradient flex items-center justify-center">
|
||||
<Plane className="w-4 h-4" />
|
||||
</div>
|
||||
<h2 className="text-sm font-medium text-primary">Популярные направления</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 sm:gap-4">
|
||||
{filteredDestinations.map((dest, i) => (
|
||||
<DestinationCard key={dest.id} destination={dest} delay={i * 0.05} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredDestinations.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<MapPin className="w-12 h-12 mx-auto mb-4 text-muted" />
|
||||
<p className="text-secondary">Направления не найдены</p>
|
||||
<p className="text-sm text-muted mt-1">Попробуйте изменить фильтры</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* AI Tips */}
|
||||
<div className="bg-surface/30 border border-border/30 rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Clock className="w-4 h-4 text-muted" />
|
||||
<span className="text-xs font-medium text-secondary">Совет AI</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted">
|
||||
Опишите ваши предпочтения в поиске: бюджет, тип отдыха, даты — и AI составит
|
||||
оптимальный маршрут с учётом всех пожеланий.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user