Merge branch 'staging' of https://gitea.hephex.de/Daniel/webshop into staging
All checks were successful
Staging Build / build (push) Successful in 3m39s
All checks were successful
Staging Build / build (push) Successful in 3m39s
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
--card-foreground: 220 40% 2%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 220 40% 2%;
|
||||
--primary: 220 90% 56%;
|
||||
--primary: 199 100% 43%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary: 220 14% 90%;
|
||||
--secondary-foreground: 220 40% 2%;
|
||||
|
||||
@@ -121,7 +121,7 @@ export function HomeClient({ initialUser }: HomeClientProps) {
|
||||
>
|
||||
<Zap className="w-3.5 h-3.5 text-blue-400 fill-blue-400/20" />
|
||||
</motion.div>
|
||||
CASPOS Lizenz-Order-Portal
|
||||
CASPOS Lizenz-Portal
|
||||
</motion.div>
|
||||
|
||||
{/* Headline */}
|
||||
@@ -237,7 +237,7 @@ export function HomeClient({ initialUser }: HomeClientProps) {
|
||||
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{/* Card 1 */}
|
||||
<motion.div
|
||||
<a href="https://support.teamzone.softengine.de" target="_blank" rel="noopener noreferrer"><motion.div
|
||||
whileHover={{ y: -6 }}
|
||||
className="group relative flex flex-col p-6 rounded-2xl border border-slate-900 bg-slate-950/70 hover:bg-slate-900/40 hover:border-slate-800/80 transition-all duration-300 shadow-xl overflow-hidden"
|
||||
>
|
||||
@@ -249,13 +249,13 @@ export function HomeClient({ initialUser }: HomeClientProps) {
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-slate-200 group-hover:text-white transition-colors">Zulip</h3>
|
||||
<p className="text-slate-400 text-sm leading-relaxed">
|
||||
Sie möchten sich mit anderen CASPOS Partnern austauschen, Fragen stellen oder Feedback geben? Treten Sie unserem Zulip-Chat bei.
|
||||
Treten Sie unserem Partner-Chat bei! tauschen Sie sich mit anderen CASPOS Partnern aus.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div></a>
|
||||
|
||||
{/* Card 2 */}
|
||||
<motion.div
|
||||
<a href="https://wiki.caspos.de" target="_blank" rel="noopener noreferrer"><motion.div
|
||||
whileHover={{ y: -6 }}
|
||||
className="group relative flex flex-col p-6 rounded-2xl border border-slate-900 bg-slate-950/70 hover:bg-slate-900/40 hover:border-slate-800/80 transition-all duration-300 shadow-xl overflow-hidden"
|
||||
>
|
||||
@@ -270,10 +270,10 @@ export function HomeClient({ initialUser }: HomeClientProps) {
|
||||
Ihr Wissens-Portal für die CASPOS Produktfamilie.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div></a>
|
||||
|
||||
{/* Card 3 */}
|
||||
<motion.div
|
||||
<a href="https://support.caspos.de" target="_blank" rel="noopener noreferrer"><motion.div
|
||||
whileHover={{ y: -6 }}
|
||||
className="group relative flex flex-col p-6 rounded-2xl border border-slate-900 bg-slate-950/70 hover:bg-slate-900/40 hover:border-slate-800/80 transition-all duration-300 shadow-xl overflow-hidden"
|
||||
>
|
||||
@@ -285,10 +285,10 @@ export function HomeClient({ initialUser }: HomeClientProps) {
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-slate-200 group-hover:text-white transition-colors">Support</h3>
|
||||
<p className="text-slate-400 text-sm leading-relaxed">
|
||||
Sie haben ein Problem in der CASPOS? Wir helfen Ihnen gerne weiter! Erstellen Sie jetzt ein Ticket über unser Ticketsystem.
|
||||
Sie haben ein technisches Problem in der CASPOS? Erstellen Sie jetzt ein Ticket!
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div></a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -98,7 +98,7 @@ export function NavbarClient({ user, role = "partner" }: NavbarClientProps) {
|
||||
<>
|
||||
<header className="px-4 lg:px-6 h-16 flex items-center justify-between border-b border-white/5 backdrop-blur-md sticky top-0 z-50 bg-[#020617]/85">
|
||||
<Link className="flex items-center justify-center gap-2 group" href="/">
|
||||
<span className="font-bold text-xl tracking-tighter text-white">CASPOS Shop</span>
|
||||
<img src="/assets/CASPOS-logo.webp" alt="CASPOS Logo" className="h-8" />
|
||||
<DemoWrapper>
|
||||
<span className="text-[10px] bg-amber-500/20 text-amber-400 px-2 py-0.5 rounded-full font-semibold uppercase tracking-wider border border-amber-500/30">
|
||||
Demo
|
||||
|
||||
@@ -75,67 +75,61 @@ export function AdminRecentOrders() {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
|
||||
<Card className="col-span-4 glass-dark border-white/5">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7 bg-[#020617]/85 p-4 rounded-xl">
|
||||
<div className="rounded-xl border border-white/5 bg-[#020617]/85 overflow-hidden backdrop-blur-xl col-span-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Übersicht (letzte 6 Monate)</CardTitle>
|
||||
<CardTitle className="text-white">Übersicht (letzte 6 Monate)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pl-2">
|
||||
<RevenueChart data={chartData} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="col-span-3 glass-dark border-white/5">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-3">
|
||||
<CardTitle>Neue Bestellungen</CardTitle>
|
||||
<div className="flex flex-col gap-4 bg-[#020617]/85 p-4 rounded-xl border border-white/5 col-span-3">
|
||||
<div className="flex flex-row items-center justify-between pb-3">
|
||||
<h3 className="font-semibold text-lg text-white">Neue Bestellungen</h3>
|
||||
<span className="text-[10px] text-slate-500">
|
||||
Aktualisiert: {lastChecked.toLocaleTimeString('de-DE')}
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
</div>
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{orders.length === 0 ? (
|
||||
<p className="text-slate-500 text-sm italic text-center py-8">Noch keine Bestellungen</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{orders.map(order => {
|
||||
const isNew = newOrderIds.has(order.id)
|
||||
const customerName =
|
||||
order.customer_data?.company_name ||
|
||||
`${order.customer_data?.first_name ?? ''} ${order.customer_data?.last_name ?? ''}`.trim() ||
|
||||
'Unbekannt'
|
||||
|
||||
return (
|
||||
<Link key={order.id} href="/admin/orders" className="block">
|
||||
<div
|
||||
className={`flex items-center gap-3 p-3 rounded-lg transition-all duration-500 ${
|
||||
isNew
|
||||
? 'bg-primary/10 border border-primary/30 shadow-[0_0_12px_rgba(59,130,246,0.2)]'
|
||||
: 'bg-white/5 hover:bg-white/10 border border-white/5'
|
||||
orders.slice(0, 3).map(order => {
|
||||
const isNew = newOrderIds.has(order.id)
|
||||
const customerName =
|
||||
order.customer_data?.company_name ||
|
||||
`${order.customer_data?.first_name ?? ''} ${order.customer_data?.last_name ?? ''}`.trim() ||
|
||||
'Unbekannt'
|
||||
return (
|
||||
<Link key={order.id} href="/admin/orders" className="block">
|
||||
<div
|
||||
className={`flex items-center gap-3 p-3 rounded-lg transition-all duration-500 ${isNew
|
||||
? 'bg-primary/10 border border-primary/30 shadow-[0_0_12px_rgba(59,130,246,0.2)]'
|
||||
: 'bg-[#020617]/20 hover:bg-[#020617]/30 border border-white/5'
|
||||
}`}
|
||||
>
|
||||
<div className="w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center text-primary text-xs font-bold shrink-0">
|
||||
{customerName.slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">{customerName}</p>
|
||||
<p className="text-xs text-slate-500 font-mono">#{order.order_number}</p>
|
||||
</div>
|
||||
<div className="text-right shrink-0 space-y-1">
|
||||
<p className="font-medium text-white text-sm">
|
||||
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(order.total_price)}
|
||||
</p>
|
||||
<Badge className={`text-[10px] ${statusClass[order.status] ?? ''}`}>
|
||||
{statusLabel[order.status] ?? order.status}
|
||||
</Badge>
|
||||
</div>
|
||||
>
|
||||
<div className="w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center text-primary text-xs font-bold shrink-0">
|
||||
{customerName.slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">{customerName}</p>
|
||||
<p className="text-xs text-slate-500 font-mono">#{order.order_number}</p>
|
||||
</div>
|
||||
<div className="text-right shrink-0 space-y-1">
|
||||
<p className="font-medium text-white text-sm">
|
||||
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(order.total_price)}
|
||||
</p>
|
||||
<Badge className={`text-[10px] ${statusClass[order.status] ?? ''}`}>{statusLabel[order.status] ?? order.status}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { createAdminClient } from '@/lib/supabase/admin'
|
||||
import { headers } from 'next/headers'
|
||||
import { sendLockoutEmail } from '@/lib/utils/email'
|
||||
|
||||
export async function signIn(email: string, password: string) {
|
||||
try {
|
||||
@@ -12,6 +13,32 @@ export async function signIn(email: string, password: string) {
|
||||
password,
|
||||
})
|
||||
if (error) {
|
||||
// Track failed attempts
|
||||
const { data: usr, error: usrError } = await supabase
|
||||
.from('users')
|
||||
.select('failed_attempts, email')
|
||||
.eq('email', email)
|
||||
.single()
|
||||
|
||||
const attempts = (usr?.failed_attempts ?? 0) + 1
|
||||
|
||||
// Update attempts count
|
||||
await supabase
|
||||
.from('users')
|
||||
.update({ failed_attempts: attempts })
|
||||
.eq('email', email)
|
||||
|
||||
// Lock account on 5th failure
|
||||
if (attempts >= 5) {
|
||||
await supabase
|
||||
.from('users')
|
||||
.update({ role: 'gesperrt' })
|
||||
.eq('email', email)
|
||||
// Notify admin
|
||||
await sendLockoutEmail('info@hephex.de')
|
||||
return { success: false, error: 'Konto gesperrt nach 5 Fehlversuchen.' }
|
||||
}
|
||||
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
|
||||
|
||||
25
shop/lib/utils/email.ts
Normal file
25
shop/lib/utils/email.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
'use server'
|
||||
|
||||
import nodemailer from 'nodemailer'
|
||||
|
||||
export async function sendLockoutEmail(to: string) {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT),
|
||||
secure: process.env.SMTP_SECURE === 'true',
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
})
|
||||
|
||||
const message = {
|
||||
from: `"Admin" <${process.env.SMTP_FROM}>`,
|
||||
to,
|
||||
subject: 'Konto gesperrt',
|
||||
text: 'Ihr Konto wurde wegen zu vieler falscher Passwortversuche gesperrt.',
|
||||
html: `<p>Ihr Konto wurde gesperrt, weil das Passwort zu oft falsch eingegeben wurde.</p>`,
|
||||
}
|
||||
|
||||
await transporter.sendMail(message)
|
||||
}
|
||||
BIN
shop/public/assets/CASPOS-logo.webp
Normal file
BIN
shop/public/assets/CASPOS-logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
Reference in New Issue
Block a user