882 lines
44 KiB
TypeScript
882 lines
44 KiB
TypeScript
"use client";
|
|
|
|
/* eslint-disable @next/next/no-img-element */
|
|
import { useEffect, useState, useRef } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import Link from 'next/link';
|
|
import { useAuth } from '@/context/AuthContext';
|
|
import { validateSwedishMobile, normalizeSwedishMobile, validatePersonnummer, validateAddress, lookupSwedishCity, lookupSwedishAddress } from '@/lib/validation';
|
|
import { Settings, X } from 'lucide-react';
|
|
import '@/styles/Auth.css';
|
|
|
|
export default function MePage() {
|
|
const router = useRouter();
|
|
const { user, login, logout, loading: authLoading, isAuthenticated, refreshUser } = useAuth();
|
|
const [loading, setLoading] = useState(true);
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
const [formData, setFormData] = useState({
|
|
name: '',
|
|
personnummer: '',
|
|
mobile: '',
|
|
address: '',
|
|
zip_code: '',
|
|
city: '',
|
|
country: ''
|
|
});
|
|
|
|
// Account Management States
|
|
const [actionLoading, setActionLoading] = useState(false);
|
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
const [showChangePassword, setShowChangePassword] = useState(false);
|
|
const [passwords, setPasswords] = useState({
|
|
current: '',
|
|
new: '',
|
|
confirm: ''
|
|
});
|
|
|
|
// 2FA Setup States
|
|
const [show2FASetup, setShow2FASetup] = useState(false);
|
|
const [qrCode, setQrCode] = useState('');
|
|
const [twoFactorCode, setTwoFactorCode] = useState('');
|
|
const [twoFactorToken, setTwoFactorToken] = useState('');
|
|
const [setupError, setSetupError] = useState('');
|
|
const [setupSuccess, setSetupSuccess] = useState('');
|
|
const successTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (user) {
|
|
setFormData({
|
|
name: user.name || '',
|
|
personnummer: user.personnummer || '',
|
|
mobile: user.mobile || '',
|
|
address: user.address || '',
|
|
zip_code: user.zip_code || '',
|
|
city: user.city || '',
|
|
country: user.country || ''
|
|
});
|
|
}
|
|
}, [user]);
|
|
|
|
// Auto-fetch city when zip code is 5 digits
|
|
useEffect(() => {
|
|
const fetchCity = async () => {
|
|
if (formData.zip_code.length === 5) {
|
|
const city = await lookupSwedishCity(formData.zip_code);
|
|
if (city) {
|
|
setFormData(prev => ({ ...prev, city }));
|
|
}
|
|
}
|
|
};
|
|
fetchCity();
|
|
}, [formData.zip_code]);
|
|
|
|
|
|
useEffect(() => {
|
|
if (!authLoading) {
|
|
if (!isAuthenticated) {
|
|
router.push('/login');
|
|
} else {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
}, [authLoading, isAuthenticated, router]);
|
|
|
|
const handleLogout = () => {
|
|
logout();
|
|
};
|
|
|
|
const initiation2FASetup = async () => {
|
|
setSetupError('');
|
|
setSetupSuccess('');
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
const res = await fetch('/api/auth/2fa/setup', {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Kunde inte initiera 2FA');
|
|
setQrCode(data.qrCodeDataUrl);
|
|
setTwoFactorToken(data.secret);
|
|
setShow2FASetup(true);
|
|
} catch (err: unknown) {
|
|
setSetupError(err instanceof Error ? err.message : 'Ett oväntat fel uppstod');
|
|
}
|
|
};
|
|
|
|
const verifyAndEnable2FA = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setSetupError('');
|
|
setActionLoading(true);
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
const res = await fetch('/api/auth/2fa/verify', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ code: twoFactorCode })
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Felaktig kod');
|
|
|
|
setSetupSuccess('Tvåfaktorsautentisering har aktiverats!');
|
|
setShow2FASetup(false);
|
|
setTwoFactorCode('');
|
|
|
|
if (user) {
|
|
const updatedUser = { ...user, two_factor_enabled: true };
|
|
login(token || '', updatedUser);
|
|
}
|
|
} catch (err: unknown) {
|
|
setSetupError(err instanceof Error ? err.message : 'Ett oväntat fel uppstod');
|
|
} finally {
|
|
setActionLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleDisable2FA = async () => {
|
|
if (!confirm('Är du säker på att du vill inaktivera 2FA? Detta gör ditt konto mindre säkert.')) return;
|
|
|
|
setActionLoading(true);
|
|
setSetupError('');
|
|
setSetupSuccess('');
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
const res = await fetch('/api/auth/2fa/disable', {
|
|
method: 'POST',
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
if (!res.ok) throw new Error('Kunde inte inaktivera 2FA');
|
|
|
|
setSetupSuccess('2FA har inaktiverats.');
|
|
if (user) {
|
|
const updatedUser = { ...user, two_factor_enabled: false };
|
|
login(token || '', updatedUser);
|
|
}
|
|
} catch (err: unknown) {
|
|
setSetupError(err instanceof Error ? err.message : 'Ett fel uppstod');
|
|
} finally {
|
|
setActionLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleNewsletterToggle = async () => {
|
|
const newValue = !user?.newsletter_subscribed;
|
|
setActionLoading(true);
|
|
setSetupError('');
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
const res = await fetch('/api/auth/newsletter/toggle', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ subscribed: newValue })
|
|
});
|
|
if (!res.ok) throw new Error('Kunde inte uppdatera nyhetsbrev');
|
|
|
|
if (user) {
|
|
const updatedUser = { ...user, newsletter_subscribed: newValue };
|
|
login(token || '', updatedUser);
|
|
}
|
|
|
|
if (newValue) {
|
|
setSetupSuccess('Välkommen! Du prenumererar nu på vårt nyhetsbrev.');
|
|
} else {
|
|
setSetupSuccess('Du har avprenumererat dig från vårt nyhetsbrev.');
|
|
}
|
|
|
|
// Reset Timer: Clear existing and start fresh
|
|
if (successTimeoutRef.current) clearTimeout(successTimeoutRef.current);
|
|
successTimeoutRef.current = setTimeout(() => setSetupSuccess(''), 5000);
|
|
} catch (err: unknown) {
|
|
setSetupError(err instanceof Error ? err.message : 'Ett fel uppstod');
|
|
} finally {
|
|
setActionLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleUpdateProfile = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setSetupError('');
|
|
setSetupSuccess('');
|
|
setActionLoading(true);
|
|
|
|
if (formData.name && formData.name.length < 2) {
|
|
setSetupError('Namnet är för kort.');
|
|
setActionLoading(false);
|
|
return;
|
|
}
|
|
|
|
if (formData.personnummer && !validatePersonnummer(formData.personnummer)) {
|
|
setSetupError('Ogiltigt personnummer. Använd formatet YYYYMMDD-XXXX.');
|
|
setActionLoading(false);
|
|
return;
|
|
}
|
|
|
|
if (formData.mobile && !validateSwedishMobile(formData.mobile)) {
|
|
setSetupError('Mobilnummeret är ogiltigt. Det måste vara ett svenskt mobilnummer (t.ex. 070-123 45 67).');
|
|
setActionLoading(false);
|
|
return;
|
|
}
|
|
|
|
if (formData.address && !validateAddress(formData.address)) {
|
|
setSetupError('Ogiltig adress. Ange gatuadress och nummer.');
|
|
setActionLoading(false);
|
|
return;
|
|
}
|
|
|
|
// Verify physical address exists (External API)
|
|
try {
|
|
const addressExists = await lookupSwedishAddress(formData.address, formData.zip_code, formData.city);
|
|
if (!addressExists) {
|
|
setSetupError('Adressen verkar inte existera. Vänligen kontrollera stavning och adressnummer.');
|
|
setActionLoading(false);
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
console.error('Profile address verification error:', err);
|
|
// Fail open on network errors
|
|
}
|
|
|
|
const zipRegex = /^\d{5}$/;
|
|
if (formData.zip_code && !zipRegex.test(formData.zip_code.replace(/\s/g, ''))) {
|
|
setSetupError('Postnummer måste bestå av 5 siffror');
|
|
setActionLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
|
|
const res = await fetch('/api/auth/me', {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
full_name: formData.name,
|
|
personnummer: formData.personnummer,
|
|
mobile: formData.mobile ? normalizeSwedishMobile(formData.mobile) : '',
|
|
address: formData.address,
|
|
zip_code: formData.zip_code,
|
|
city: formData.city
|
|
// Country is locked to 'Sverige'
|
|
})
|
|
});
|
|
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Kunde inte uppdatera profilen');
|
|
|
|
const updatedMobile = formData.mobile ? normalizeSwedishMobile(formData.mobile) : '';
|
|
|
|
if (user) {
|
|
const updatedUser = {
|
|
...user,
|
|
name: formData.name,
|
|
personnummer: formData.personnummer,
|
|
mobile: updatedMobile,
|
|
address: formData.address,
|
|
zip_code: formData.zip_code,
|
|
city: formData.city,
|
|
country: formData.country
|
|
};
|
|
login(token || '', updatedUser);
|
|
}
|
|
|
|
setFormData(prev => ({ ...prev, mobile: updatedMobile }));
|
|
|
|
setSetupSuccess('Profilen har uppdaterats!');
|
|
setIsEditing(false);
|
|
|
|
if (successTimeoutRef.current) clearTimeout(successTimeoutRef.current);
|
|
successTimeoutRef.current = setTimeout(() => setSetupSuccess(''), 5000);
|
|
} catch (err: unknown) {
|
|
setSetupError(err instanceof Error ? err.message : 'Ett fel uppstod');
|
|
} finally {
|
|
setActionLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleChangePassword = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setSetupError('');
|
|
setSetupSuccess('');
|
|
|
|
if (passwords.new !== passwords.confirm) {
|
|
setSetupError('Lösenorden matchar inte');
|
|
return;
|
|
}
|
|
|
|
setActionLoading(true);
|
|
try {
|
|
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
|
|
const res = await fetch('/api/auth/change-password', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
currentPassword: passwords.current,
|
|
newPassword: passwords.new
|
|
})
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Gick inte att byta lösenord');
|
|
|
|
setSetupSuccess('Ditt lösenord har uppdaterats!');
|
|
setShowChangePassword(false);
|
|
setPasswords({ current: '', new: '', confirm: '' });
|
|
|
|
// Update local user state
|
|
if (user) {
|
|
login(token || '', { ...user, has_password: true });
|
|
}
|
|
} catch (err: unknown) {
|
|
const msg = err instanceof Error ? err.message : "Ett fel uppstod";
|
|
setSetupError(msg);
|
|
|
|
if (msg.includes("Nuvarande lösenord krävs") && !user?.has_password) {
|
|
await refreshUser();
|
|
setPasswords({ current: "", new: "", confirm: "" });
|
|
}
|
|
} finally {
|
|
setActionLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleDeleteAccount = async () => {
|
|
setActionLoading(true);
|
|
setSetupError('');
|
|
try {
|
|
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
|
|
const res = await fetch('/api/auth/account', {
|
|
method: 'DELETE',
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
if (!res.ok) throw new Error('Kunde inte ta bort kontot');
|
|
|
|
logout();
|
|
router.push('/register?deleted=true');
|
|
} catch (err: unknown) {
|
|
setSetupError(err instanceof Error ? err.message : 'Ett fel uppstod');
|
|
setActionLoading(false);
|
|
}
|
|
};
|
|
|
|
if (authLoading || loading) {
|
|
return (
|
|
<div className="auth-page-container">
|
|
<div className="text-xl">Laddar...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!user) return null;
|
|
|
|
return (
|
|
<div className="auth-page-container">
|
|
<div className="card-lg">
|
|
<div className="flex items-center justify-between mb-12 border-b border-gray-100 dark:border-white/10 pb-8">
|
|
<div className="flex flex-col gap-2">
|
|
<h2 className="auth-title" style={{ textAlign: 'left', marginBottom: 0, fontSize: '2.5rem' }}>Min Profil</h2>
|
|
{user.email_verified ? (
|
|
<span className="status-badge status-verified" style={{ alignSelf: 'flex-start' }}>Verifierad</span>
|
|
) : (
|
|
<span className="status-badge status-unverified" style={{ alignSelf: 'flex-start' }}>Ej Verifierad</span>
|
|
)}
|
|
</div>
|
|
{user.role === 'admin' && (
|
|
<Link href="/admin" className="auth-button secondary btn-admin">
|
|
ADMIN VIEW
|
|
</Link>
|
|
)}
|
|
</div>
|
|
|
|
{(!user.personnummer || !user.address || !user.mobile) && (
|
|
<div className="auth-error mb-8 flex items-center gap-3" style={{ background: 'oklch(0.962 0.018 272.314/0.1)', color: 'var(--text-secondary)', border: '1px solid var(--border-color)' }}>
|
|
<p className="text-xs font-bold uppercase tracking-wider mb-0">Din profil är inte komplett. Vänligen ange personnummer och adress för att kunna använda alla tjänster.</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="profile-main-grid w-full">
|
|
<div className="profile-card">
|
|
<div className="profile-card-header">
|
|
<div className="header-title-group">
|
|
<h3 className="header-title">Personlig Information</h3>
|
|
</div>
|
|
<div className="flex-shrink-0">
|
|
{!isEditing ? (
|
|
<button
|
|
onClick={() => setIsEditing(true)}
|
|
className="p-2 rounded-full transition-colors text-black dark:text-white hover:bg-black/5 dark:hover:bg-white/5 flex items-center justify-center"
|
|
aria-label="Redigera profil"
|
|
title="Redigera profil"
|
|
>
|
|
<Settings size={24} className="header-icon" />
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={() => setIsEditing(false)}
|
|
className="p-2 rounded-full transition-colors text-gray-400 hover:text-black dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 flex items-center justify-center"
|
|
aria-label="Avbryt redigering"
|
|
title="Avbryt redigering"
|
|
>
|
|
<X size={24} className="header-icon" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<form onSubmit={handleUpdateProfile} className="profile-form-grid">
|
|
<div className="profile-form-row">
|
|
<div className="form-group">
|
|
<label className="form-label">Namn</label>
|
|
{isEditing ? (
|
|
<input
|
|
type="text"
|
|
className="form-input form-input-editable w-full"
|
|
value={formData.name}
|
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
required
|
|
/>
|
|
) : (
|
|
<div className="form-input form-input-readonly">{user.name}</div>
|
|
)}
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="form-label">Roll</label>
|
|
<div className="form-input form-input-readonly">
|
|
<span className={`font-black uppercase ${user.role === 'admin' ? 'text-red-500' : 'text-gray-500'}`}>
|
|
{user.role}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="profile-form-row">
|
|
<div className="form-group">
|
|
<label className="form-label">Användarnamn</label>
|
|
<div className="form-input form-input-readonly">{user.username}</div>
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="form-label">E-postadress</label>
|
|
<div className="form-input form-input-readonly">{user.email}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="profile-form-row">
|
|
<div className="form-group">
|
|
<label className="form-label">Personnummer</label>
|
|
{isEditing ? (
|
|
<input
|
|
type="text"
|
|
className="form-input form-input-editable w-full"
|
|
placeholder="ÅÅÅÅMMDD-XXXX"
|
|
maxLength={13}
|
|
value={formData.personnummer}
|
|
onChange={(e) => {
|
|
let val = e.target.value.replace(/\D/g, '');
|
|
if (val.length > 8) {
|
|
val = val.slice(0, 8) + '-' + val.slice(8, 12);
|
|
}
|
|
setFormData({ ...formData, personnummer: val });
|
|
}}
|
|
/>
|
|
) : (
|
|
<div className="form-input form-input-readonly">{user.personnummer || '-'}</div>
|
|
)}
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="form-label">Mobil</label>
|
|
{isEditing ? (
|
|
<input
|
|
type="tel"
|
|
className="form-input form-input-editable w-full"
|
|
placeholder="+46 70 123 45 67"
|
|
value={formData.mobile}
|
|
onChange={(e) => setFormData({ ...formData, mobile: e.target.value })}
|
|
/>
|
|
) : (
|
|
<div className="form-input form-input-readonly">{user.mobile || '-'}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="form-group">
|
|
<label className="form-label">Adress</label>
|
|
{isEditing ? (
|
|
<input
|
|
type="text"
|
|
className="form-input form-input-editable w-full"
|
|
value={formData.address}
|
|
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
|
|
/>
|
|
) : (
|
|
<div className="form-input form-input-readonly">{user.address || '-'}</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="profile-form-row">
|
|
<div className="form-group">
|
|
<label className="form-label">Postnummer</label>
|
|
{isEditing ? (
|
|
<input
|
|
type="text"
|
|
className="form-input form-input-editable w-full"
|
|
maxLength={5}
|
|
value={formData.zip_code}
|
|
onChange={(e) => setFormData({ ...formData, zip_code: e.target.value.replace(/\D/g, '') })}
|
|
/>
|
|
) : (
|
|
<div className="form-input form-input-readonly">{user.zip_code || '-'}</div>
|
|
)}
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="form-label">Ort</label>
|
|
{isEditing ? (
|
|
<input
|
|
type="text"
|
|
className="form-input form-input-editable w-full"
|
|
value={formData.city}
|
|
onChange={(e) => setFormData({ ...formData, city: e.target.value })}
|
|
required
|
|
/>
|
|
) : (
|
|
<div className="form-input form-input-readonly">{user.city || '-'}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="profile-form-row">
|
|
<div className="form-group">
|
|
<label className="form-label">Land</label>
|
|
<div className="form-input form-input-readonly">Sverige</div>
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="form-label">Medlem sedan</label>
|
|
<div className="form-input form-input-readonly">
|
|
{new Date(user.created_at).toLocaleDateString('sv-SE')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{isEditing && (
|
|
<div className="mt-8">
|
|
<button
|
|
type="submit"
|
|
className="auth-button success"
|
|
disabled={actionLoading}
|
|
>
|
|
{actionLoading ? 'Sparar...' : 'Spara ändringar'}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</form>
|
|
|
|
<div className="logout-section mt-12">
|
|
<button onClick={handleLogout} className="auth-button danger">
|
|
Logga ut Nordic Storium
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* CARD 2: Settings & Security */}
|
|
<div className="profile-card">
|
|
<div className="profile-card-header">
|
|
<div className="header-title-group">
|
|
<h3 className="header-title">Inställningar & Säkerhet</h3>
|
|
</div>
|
|
<div className="flex-shrink-0 invisible pointer-events-none select-none">
|
|
<div className="p-2 flex items-center justify-center">
|
|
<Settings size={24} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="settings-internal-grid">
|
|
{/* Row 1: Password & Newsletter */}
|
|
{/* Password Change */}
|
|
{/* Row 1: Password & Newsletter */}
|
|
{/* Password Change */}
|
|
<div className="bg-white dark:bg-transparent p-6 flex flex-col justify-between">
|
|
<div className="mb-6">
|
|
<h4 className="font-black text-sm uppercase tracking-wider mb-2">
|
|
{user.has_password ? 'Ändra Lösenord' : 'Ställ in Lösenord'}
|
|
</h4>
|
|
<p className="text-xs text-gray-500 leading-relaxed">
|
|
{user.has_password
|
|
? 'Det är rekommenderat att byta lösenord med jämna mellanrum.'
|
|
: 'Ditt konto saknar lösenord då du använder social inloggning. Ställ in ett för att kunna logga in utan Google.'
|
|
}
|
|
</p>
|
|
</div>
|
|
|
|
{!showChangePassword ? (
|
|
<button
|
|
className={user.has_password ? "auth-button danger" : "auth-button success"}
|
|
onClick={() => setShowChangePassword(true)}
|
|
>
|
|
{user.has_password ? 'Ändra' : 'Sätt lösenord'}
|
|
</button>
|
|
) : (
|
|
<div className="animate-fade-in">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<button
|
|
className="auth-button secondary text-sm py-2 px-4"
|
|
onClick={() => setShowChangePassword(false)}
|
|
>
|
|
← Tillbaka
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleChangePassword} className="space-y-4">
|
|
{user.has_password && (
|
|
<div className="form-group">
|
|
<div className="form-label">Nuvarande lösenord</div>
|
|
<input
|
|
type="password"
|
|
required
|
|
className="form-input w-full"
|
|
value={passwords.current}
|
|
onChange={(e) => setPasswords({ ...passwords, current: e.target.value })}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="form-group">
|
|
<div className="form-label">Nytt lösenord</div>
|
|
<input
|
|
type="password"
|
|
required
|
|
className="form-input w-full"
|
|
value={passwords.new}
|
|
onChange={(e) => setPasswords({ ...passwords, new: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="form-group">
|
|
<div className="form-label">Bekräfta nytt lösenord</div>
|
|
<input
|
|
type="password"
|
|
required
|
|
className="form-input w-full"
|
|
value={passwords.confirm}
|
|
onChange={(e) => setPasswords({ ...passwords, confirm: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
className="auth-button success mt-4"
|
|
disabled={actionLoading}
|
|
>
|
|
{actionLoading ? 'Sparar...' : 'Bekräfta'}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Newsletter */}
|
|
<div className="bg-white dark:bg-transparent p-6 flex flex-col justify-between">
|
|
<div className="mb-6">
|
|
<h4 className="font-black text-sm uppercase tracking-wider mb-2">Nyhetsbrev</h4>
|
|
<p className="text-xs text-gray-500 leading-relaxed">Få de senaste nordic-storium erbjudandena direkt i din inkorg.</p>
|
|
</div>
|
|
<button
|
|
className={`auth-button ${user.newsletter_subscribed ? 'danger' : 'success'}`}
|
|
onClick={handleNewsletterToggle}
|
|
disabled={actionLoading}
|
|
>
|
|
{user.newsletter_subscribed ? 'Avprenumerera' : 'Prenumerera'}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Row 2: 2FA & Remove Account */}
|
|
{/* 2FA */}
|
|
<div className="bg-white dark:bg-transparent p-6 flex flex-col justify-between">
|
|
<div className="mb-6">
|
|
<h4 className="font-black text-sm uppercase tracking-wider mb-2">Säker Inloggning</h4>
|
|
<p className="text-xs text-gray-500 leading-relaxed">Säkra ditt konto med tvåfaktorsautentisering vid varje inloggning</p>
|
|
</div>
|
|
|
|
{!show2FASetup ? (
|
|
<button
|
|
className={`auth-button ${user.two_factor_enabled ? 'danger' : 'success'}`}
|
|
onClick={user.two_factor_enabled ? handleDisable2FA : initiation2FASetup}
|
|
disabled={actionLoading}
|
|
>
|
|
{user.two_factor_enabled ? 'Ta bort' : 'Aktivera'}
|
|
</button>
|
|
) : (
|
|
<div className="animate-fade-in">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<button
|
|
className="auth-button secondary text-sm py-2 px-4"
|
|
onClick={() => setShow2FASetup(false)}
|
|
>
|
|
← Avbryt
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-6 w-full">
|
|
{/* 1. QR Code */}
|
|
<div className="flex justify-center w-full">
|
|
<img
|
|
src={qrCode}
|
|
alt="2FA QR"
|
|
className="qr-code-img"
|
|
/>
|
|
</div>
|
|
|
|
{/* 2. Manual Code Copy Section */}
|
|
<div className="w-full text-center space-y-3">
|
|
<p className="text-[10px] uppercase font-black text-gray-500">
|
|
Alternativ kod för manuell inmatning
|
|
</p>
|
|
|
|
<div className="form-group w-full">
|
|
<div
|
|
className="form-input w-full cursor-pointer relative"
|
|
onClick={async (e) => {
|
|
const element = e.currentTarget;
|
|
|
|
try {
|
|
await navigator.clipboard.writeText(twoFactorToken);
|
|
|
|
// Add green flash animation
|
|
element.classList.add('green-flash');
|
|
|
|
// Show compact feedback overlay
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'copy-success-overlay';
|
|
overlay.innerHTML = `
|
|
<div class="success-message-box">
|
|
<svg class="w-3 h-3 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path>
|
|
</svg>
|
|
<span>Kopierad</span>
|
|
</div>
|
|
`;
|
|
element.appendChild(overlay);
|
|
|
|
// Remove after 1.5 seconds
|
|
setTimeout(() => {
|
|
element.classList.remove('green-flash');
|
|
if (overlay.parentNode === element) {
|
|
element.removeChild(overlay);
|
|
}
|
|
}, 1500);
|
|
|
|
} catch (err) {
|
|
console.error('Kunde inte kopiera:', err);
|
|
}
|
|
}}
|
|
>
|
|
<div className="form-label">Klicka & kopiera</div>
|
|
|
|
<div className="flex items-center justify-center">
|
|
<code className="font-mono text-sm font-black tracking-[0.1em] select-all text-center">
|
|
{twoFactorToken.replace(/(.{4})/g, '$1 ').trim()}
|
|
</code>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 3. Verification Form */}
|
|
<form onSubmit={verifyAndEnable2FA} className="w-full space-y-2 mt-4">
|
|
<div className="form-group">
|
|
<div className="form-label">Autentiseringskod</div>
|
|
<input
|
|
type="text"
|
|
className="form-input w-full text-left tracking-[0.2em] text-lg"
|
|
maxLength={6}
|
|
placeholder="000000"
|
|
value={twoFactorCode}
|
|
onChange={(e) => setTwoFactorCode(e.target.value.replace(/\D/g, ''))}
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
className="auth-button success mt-6 w-full"
|
|
disabled={actionLoading || twoFactorCode.length !== 6}
|
|
>
|
|
{actionLoading ? 'Verifierar...' : 'Aktivera Nu'}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Remove Account */}
|
|
<div className="bg-white dark:bg-transparent p-6 flex flex-col justify-between">
|
|
<div className="mb-6">
|
|
<h4 className="font-black text-sm uppercase tracking-wider mb-2 text-red-600">Radera Konto</h4>
|
|
<p className="text-xs text-gray-500 leading-relaxed">Radera ditt konto och all data kopplade till kontot permanent.</p>
|
|
</div>
|
|
|
|
{!showDeleteConfirm ? (
|
|
<button
|
|
className="auth-button danger"
|
|
onClick={() => setShowDeleteConfirm(true)}
|
|
>
|
|
Radera konto
|
|
</button>
|
|
) : (
|
|
<div className="animate-fade-in flex flex-col h-full justify-between">
|
|
<div className="flex justify-end mb-2">
|
|
<button
|
|
className="auth-button secondary text-sm py-2 px-4"
|
|
onClick={() => setShowDeleteConfirm(false)}
|
|
>
|
|
← Tillbaka
|
|
</button>
|
|
</div>
|
|
|
|
<p className="text-sm text-red-600 font-medium mb-4">
|
|
Är du säker? Denna åtgärd går inte att ångra.
|
|
</p>
|
|
|
|
<div className="flex gap-2">
|
|
<button
|
|
className="auth-button danger flex-1"
|
|
onClick={handleDeleteAccount}
|
|
disabled={actionLoading}
|
|
>
|
|
{actionLoading ? 'Raderar...' : 'Ja, Radera'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{setupSuccess && (
|
|
<div className="auth-success mt-8 animate-fade-in">
|
|
{setupSuccess}
|
|
</div>
|
|
)}
|
|
|
|
{setupError && (
|
|
<div className="auth-error mt-8 animate-fade-in">
|
|
{setupError}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|