Add mail verification and use .env insteads of environment in compose
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
This commit is contained in:
15
.env.example
15
.env.example
@@ -12,9 +12,24 @@ JWT_SECRET=your-super-secret-jwt-key-change-this
|
||||
# DATABASE_URL=postgres://user:password@localhost:5432/redlight
|
||||
DATABASE_URL=
|
||||
|
||||
POSTGRES_USER=redlight
|
||||
POSTGRES_PASSWORD=redlight
|
||||
POSTGRES_DB=redlight
|
||||
|
||||
# SQLite file path (only used when DATABASE_URL is not set)
|
||||
# SQLITE_PATH=./redlight.db
|
||||
|
||||
# Default Admin Account (created on first run)
|
||||
ADMIN_EMAIL=admin@example.com
|
||||
ADMIN_PASSWORD=admin123
|
||||
|
||||
# SMTP Configuration (for email verification)
|
||||
# If not set, registration works without email verification
|
||||
SMTP_HOST=smtp.example.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=noreply@example.com
|
||||
SMTP_PASS=your-smtp-password
|
||||
SMTP_FROM=noreply@example.com
|
||||
|
||||
# App URL (used for verification links, auto-detected if not set)
|
||||
# APP_URL=https://your-domain.com
|
||||
|
||||
13
compose.yml
13
compose.yml
@@ -4,13 +4,7 @@ services:
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3001:3001"
|
||||
environment:
|
||||
DATABASE_URL: postgres://redlight:redlight@postgres:5432/redlight
|
||||
BBB_URL: https://your-bbb-server.com/bigbluebutton/api/
|
||||
BBB_SECRET: your-bbb-shared-secret
|
||||
JWT_SECRET: change-me-to-a-random-secret
|
||||
ADMIN_EMAIL: admin@example.com
|
||||
ADMIN_PASSWORD: admin123
|
||||
env_file: ".env"
|
||||
volumes:
|
||||
- uploads:/app/uploads
|
||||
depends_on:
|
||||
@@ -20,10 +14,7 @@ services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: redlight
|
||||
POSTGRES_PASSWORD: redlight
|
||||
POSTGRES_DB: redlight
|
||||
env_file: ".env"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
|
||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -18,11 +18,13 @@
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"lucide-react": "^0.460.0",
|
||||
"multer": "^2.0.2",
|
||||
"nodemailer": "^8.0.1",
|
||||
"pg": "^8.18.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"uuid": "^13.0.0",
|
||||
"xml2js": "^0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -3180,6 +3182,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz",
|
||||
"integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
@@ -4607,6 +4618,19 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist-node/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
|
||||
@@ -22,11 +22,13 @@
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"lucide-react": "^0.460.0",
|
||||
"multer": "^2.0.2",
|
||||
"nodemailer": "^8.0.1",
|
||||
"pg": "^8.18.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"uuid": "^13.0.0",
|
||||
"xml2js": "^0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -134,6 +134,9 @@ export async function initDatabase() {
|
||||
theme TEXT DEFAULT 'dark',
|
||||
avatar_color TEXT DEFAULT '#6366f1',
|
||||
avatar_image TEXT DEFAULT NULL,
|
||||
email_verified INTEGER DEFAULT 0,
|
||||
verification_token TEXT,
|
||||
verification_token_expires TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
@@ -189,6 +192,9 @@ export async function initDatabase() {
|
||||
theme TEXT DEFAULT 'dark',
|
||||
avatar_color TEXT DEFAULT '#6366f1',
|
||||
avatar_image TEXT DEFAULT NULL,
|
||||
email_verified INTEGER DEFAULT 0,
|
||||
verification_token TEXT,
|
||||
verification_token_expires DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@@ -247,6 +253,19 @@ export async function initDatabase() {
|
||||
if (!(await db.columnExists('rooms', 'moderator_code'))) {
|
||||
await db.exec('ALTER TABLE rooms ADD COLUMN moderator_code TEXT');
|
||||
}
|
||||
if (!(await db.columnExists('users', 'email_verified'))) {
|
||||
await db.exec('ALTER TABLE users ADD COLUMN email_verified INTEGER DEFAULT 0');
|
||||
}
|
||||
if (!(await db.columnExists('users', 'verification_token'))) {
|
||||
await db.exec('ALTER TABLE users ADD COLUMN verification_token TEXT');
|
||||
}
|
||||
if (!(await db.columnExists('users', 'verification_token_expires'))) {
|
||||
if (isPostgres) {
|
||||
await db.exec('ALTER TABLE users ADD COLUMN verification_token_expires TIMESTAMP');
|
||||
} else {
|
||||
await db.exec('ALTER TABLE users ADD COLUMN verification_token_expires DATETIME');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Default admin ───────────────────────────────────────────────────────
|
||||
const adminEmail = process.env.ADMIN_EMAIL || 'admin@example.com';
|
||||
@@ -256,7 +275,7 @@ export async function initDatabase() {
|
||||
if (!existingAdmin) {
|
||||
const hash = bcrypt.hashSync(adminPassword, 12);
|
||||
await db.run(
|
||||
'INSERT INTO users (name, email, password_hash, role) VALUES (?, ?, ?, ?)',
|
||||
'INSERT INTO users (name, email, password_hash, role, email_verified) VALUES (?, ?, ?, ?, 1)',
|
||||
['Administrator', adminEmail, hash, 'admin']
|
||||
);
|
||||
console.log(`✅ Default admin created: ${adminEmail}`);
|
||||
|
||||
70
server/config/mailer.js
Normal file
70
server/config/mailer.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
let transporter;
|
||||
|
||||
export function initMailer() {
|
||||
const host = process.env.SMTP_HOST;
|
||||
const port = parseInt(process.env.SMTP_PORT || '587', 10);
|
||||
const user = process.env.SMTP_USER;
|
||||
const pass = process.env.SMTP_PASS;
|
||||
|
||||
if (!host || !user || !pass) {
|
||||
console.warn('⚠️ SMTP not configured – email verification disabled');
|
||||
return false;
|
||||
}
|
||||
|
||||
transporter = nodemailer.createTransport({
|
||||
host,
|
||||
port,
|
||||
secure: port === 465,
|
||||
auth: { user, pass },
|
||||
});
|
||||
|
||||
console.log('✅ SMTP mailer configured');
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isMailerConfigured() {
|
||||
return !!transporter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the verification email with a clickable link.
|
||||
* @param {string} to – recipient email
|
||||
* @param {string} name – user's display name
|
||||
* @param {string} verifyUrl – full verification URL
|
||||
* @param {string} appName – branding app name (default "Redlight")
|
||||
*/
|
||||
export async function sendVerificationEmail(to, name, verifyUrl, appName = 'Redlight') {
|
||||
if (!transporter) {
|
||||
throw new Error('SMTP not configured');
|
||||
}
|
||||
|
||||
const from = process.env.SMTP_FROM || process.env.SMTP_USER;
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `"${appName}" <${from}>`,
|
||||
to,
|
||||
subject: `${appName} – E-Mail bestätigen / Verify your email`,
|
||||
html: `
|
||||
<div style="font-family:Arial,sans-serif;max-width:520px;margin:0 auto;padding:32px;background:#1e1e2e;color:#cdd6f4;border-radius:12px;">
|
||||
<h2 style="color:#cba6f7;margin-top:0;">Hey ${name} 👋</h2>
|
||||
<p>Bitte bestätige deine E-Mail-Adresse, indem du auf den folgenden Button klickst:</p>
|
||||
<p style="text-align:center;margin:28px 0;">
|
||||
<a href="${verifyUrl}"
|
||||
style="display:inline-block;background:#cba6f7;color:#1e1e2e;padding:12px 32px;border-radius:8px;text-decoration:none;font-weight:bold;">
|
||||
E-Mail bestätigen
|
||||
</a>
|
||||
</p>
|
||||
<p style="font-size:13px;color:#7f849c;">
|
||||
Oder kopiere diesen Link in deinen Browser:<br/>
|
||||
<a href="${verifyUrl}" style="color:#89b4fa;word-break:break-all;">${verifyUrl}</a>
|
||||
</p>
|
||||
<p style="font-size:13px;color:#7f849c;">Der Link ist 24 Stunden gültig.</p>
|
||||
<hr style="border:none;border-top:1px solid #313244;margin:24px 0;"/>
|
||||
<p style="font-size:12px;color:#585b70;">Falls du dich nicht registriert hast, ignoriere diese E-Mail.</p>
|
||||
</div>
|
||||
`,
|
||||
text: `Hey ${name},\n\nBitte bestätige deine E-Mail: ${verifyUrl}\n\nDer Link ist 24 Stunden gültig.\n\n– ${appName}`,
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import cors from 'cors';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { initDatabase } from './config/database.js';
|
||||
import { initMailer } from './config/mailer.js';
|
||||
import authRoutes from './routes/auth.js';
|
||||
import roomRoutes from './routes/rooms.js';
|
||||
import recordingRoutes from './routes/recordings.js';
|
||||
@@ -26,6 +27,7 @@ app.use(express.json());
|
||||
// Initialize database & start server
|
||||
async function start() {
|
||||
await initDatabase();
|
||||
initMailer();
|
||||
|
||||
// API Routes
|
||||
app.use('/api/auth', authRoutes);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { getDb } from '../config/database.js';
|
||||
import { authenticateToken, generateToken } from '../middleware/auth.js';
|
||||
import { isMailerConfigured, sendVerificationEmail } from '../config/mailer.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -37,8 +39,36 @@ router.post('/register', async (req, res) => {
|
||||
}
|
||||
|
||||
const hash = bcrypt.hashSync(password, 12);
|
||||
|
||||
// If SMTP is configured, require email verification
|
||||
if (isMailerConfigured()) {
|
||||
const verificationToken = uuidv4();
|
||||
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
await db.run(
|
||||
'INSERT INTO users (name, email, password_hash, email_verified, verification_token, verification_token_expires) VALUES (?, ?, ?, 0, ?, ?)',
|
||||
[name, email.toLowerCase(), hash, verificationToken, expires]
|
||||
);
|
||||
|
||||
// Build verification URL
|
||||
const baseUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const verifyUrl = `${baseUrl}/verify-email?token=${verificationToken}`;
|
||||
|
||||
// Load app name from branding settings
|
||||
const brandingSetting = await db.get("SELECT value FROM settings WHERE key = 'branding'");
|
||||
let appName = 'Redlight';
|
||||
if (brandingSetting?.value) {
|
||||
try { appName = JSON.parse(brandingSetting.value).appName || appName; } catch {}
|
||||
}
|
||||
|
||||
await sendVerificationEmail(email.toLowerCase(), name, verifyUrl, appName);
|
||||
|
||||
return res.status(201).json({ needsVerification: true, message: 'Verifizierungs-E-Mail wurde gesendet' });
|
||||
}
|
||||
|
||||
// No SMTP configured – register and login immediately (legacy behaviour)
|
||||
const result = await db.run(
|
||||
'INSERT INTO users (name, email, password_hash) VALUES (?, ?, ?)',
|
||||
'INSERT INTO users (name, email, password_hash, email_verified) VALUES (?, ?, ?, 1)',
|
||||
[name, email.toLowerCase(), hash]
|
||||
);
|
||||
|
||||
@@ -52,6 +82,86 @@ router.post('/register', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/auth/verify-email?token=...
|
||||
router.get('/verify-email', async (req, res) => {
|
||||
try {
|
||||
const { token } = req.query;
|
||||
if (!token) {
|
||||
return res.status(400).json({ error: 'Token fehlt' });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const user = await db.get(
|
||||
'SELECT id, verification_token_expires FROM users WHERE verification_token = ? AND email_verified = 0',
|
||||
[token]
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
return res.status(400).json({ error: 'Ungültiger oder bereits verwendeter Token' });
|
||||
}
|
||||
|
||||
if (new Date(user.verification_token_expires) < new Date()) {
|
||||
return res.status(400).json({ error: 'Token ist abgelaufen. Bitte registriere dich erneut.' });
|
||||
}
|
||||
|
||||
await db.run(
|
||||
'UPDATE users SET email_verified = 1, verification_token = NULL, verification_token_expires = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[user.id]
|
||||
);
|
||||
|
||||
res.json({ verified: true, message: 'E-Mail erfolgreich verifiziert' });
|
||||
} catch (err) {
|
||||
console.error('Verify email error:', err);
|
||||
res.status(500).json({ error: 'Verifizierung fehlgeschlagen' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/auth/resend-verification
|
||||
router.post('/resend-verification', async (req, res) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
if (!email) {
|
||||
return res.status(400).json({ error: 'E-Mail ist erforderlich' });
|
||||
}
|
||||
|
||||
if (!isMailerConfigured()) {
|
||||
return res.status(400).json({ error: 'SMTP ist nicht konfiguriert' });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const user = await db.get('SELECT id, name, email_verified FROM users WHERE email = ?', [email.toLowerCase()]);
|
||||
|
||||
if (!user || user.email_verified) {
|
||||
// Don't reveal whether account exists
|
||||
return res.json({ message: 'Falls ein Konto existiert, wurde eine neue E-Mail gesendet.' });
|
||||
}
|
||||
|
||||
const verificationToken = uuidv4();
|
||||
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
await db.run(
|
||||
'UPDATE users SET verification_token = ?, verification_token_expires = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[verificationToken, expires, user.id]
|
||||
);
|
||||
|
||||
const baseUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const verifyUrl = `${baseUrl}/verify-email?token=${verificationToken}`;
|
||||
|
||||
const brandingSetting = await db.get("SELECT value FROM settings WHERE key = 'branding'");
|
||||
let appName = 'Redlight';
|
||||
if (brandingSetting?.value) {
|
||||
try { appName = JSON.parse(brandingSetting.value).appName || appName; } catch {}
|
||||
}
|
||||
|
||||
await sendVerificationEmail(email.toLowerCase(), user.name, verifyUrl, appName);
|
||||
|
||||
res.json({ message: 'Falls ein Konto existiert, wurde eine neue E-Mail gesendet.' });
|
||||
} catch (err) {
|
||||
console.error('Resend verification error:', err);
|
||||
res.status(500).json({ error: 'E-Mail konnte nicht gesendet werden' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/auth/login
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
@@ -68,6 +178,10 @@ router.post('/login', async (req, res) => {
|
||||
return res.status(401).json({ error: 'Ungültige Anmeldedaten' });
|
||||
}
|
||||
|
||||
if (!user.email_verified && isMailerConfigured()) {
|
||||
return res.status(403).json({ error: 'E-Mail-Adresse noch nicht verifiziert. Bitte prüfe dein Postfach.', needsVerification: true });
|
||||
}
|
||||
|
||||
const token = generateToken(user.id);
|
||||
const { password_hash, ...safeUser } = user;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import ProtectedRoute from './components/ProtectedRoute';
|
||||
import Home from './pages/Home';
|
||||
import Login from './pages/Login';
|
||||
import Register from './pages/Register';
|
||||
import VerifyEmail from './pages/VerifyEmail';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import RoomDetail from './pages/RoomDetail';
|
||||
import Settings from './pages/Settings';
|
||||
@@ -45,6 +46,7 @@ export default function App() {
|
||||
<Route path="/" element={user ? <Navigate to="/dashboard" /> : <Home />} />
|
||||
<Route path="/login" element={user ? <Navigate to="/dashboard" /> : <Login />} />
|
||||
<Route path="/register" element={user ? <Navigate to="/dashboard" /> : <Register />} />
|
||||
<Route path="/verify-email" element={<VerifyEmail />} />
|
||||
<Route path="/join/:uid" element={<GuestJoin />} />
|
||||
|
||||
{/* Protected routes */}
|
||||
|
||||
@@ -30,6 +30,9 @@ export function AuthProvider({ children }) {
|
||||
|
||||
const register = useCallback(async (name, email, password) => {
|
||||
const res = await api.post('/auth/register', { name, email, password });
|
||||
if (res.data.needsVerification) {
|
||||
return { needsVerification: true };
|
||||
}
|
||||
localStorage.setItem('token', res.data.token);
|
||||
setUser(res.data.user);
|
||||
return res.data.user;
|
||||
|
||||
@@ -61,7 +61,17 @@
|
||||
"registerSuccess": "Registrierung erfolgreich!",
|
||||
"loginFailed": "Anmeldung fehlgeschlagen",
|
||||
"registerFailed": "Registrierung fehlgeschlagen",
|
||||
"allFieldsRequired": "Alle Felder sind erforderlich"
|
||||
"allFieldsRequired": "Alle Felder sind erforderlich",
|
||||
"verificationSent": "Verifizierungs-E-Mail wurde gesendet!",
|
||||
"verificationSentDesc": "Wir haben dir eine E-Mail mit einem Bestätigungslink geschickt. Bitte klicke auf den Link, um dein Konto zu aktivieren.",
|
||||
"checkYourEmail": "Prüfe dein Postfach",
|
||||
"verifying": "E-Mail wird verifiziert...",
|
||||
"verifySuccess": "Deine E-Mail-Adresse wurde erfolgreich bestätigt. Du kannst dich jetzt anmelden.",
|
||||
"verifySuccessTitle": "E-Mail bestätigt!",
|
||||
"verifyFailed": "Verifizierung fehlgeschlagen",
|
||||
"verifyFailedTitle": "Verifizierung fehlgeschlagen",
|
||||
"verifyTokenMissing": "Kein Verifizierungstoken vorhanden.",
|
||||
"emailNotVerified": "E-Mail-Adresse noch nicht verifiziert. Bitte prüfe dein Postfach."
|
||||
},
|
||||
"home": {
|
||||
"poweredBy": "Powered by BigBlueButton",
|
||||
|
||||
@@ -61,7 +61,17 @@
|
||||
"registerSuccess": "Registration successful!",
|
||||
"loginFailed": "Login failed",
|
||||
"registerFailed": "Registration failed",
|
||||
"allFieldsRequired": "All fields are required"
|
||||
"allFieldsRequired": "All fields are required",
|
||||
"verificationSent": "Verification email sent!",
|
||||
"verificationSentDesc": "We've sent you an email with a verification link. Please click the link to activate your account.",
|
||||
"checkYourEmail": "Check your inbox",
|
||||
"verifying": "Verifying your email...",
|
||||
"verifySuccess": "Your email has been verified successfully. You can now sign in.",
|
||||
"verifySuccessTitle": "Email verified!",
|
||||
"verifyFailed": "Verification failed",
|
||||
"verifyFailedTitle": "Verification failed",
|
||||
"verifyTokenMissing": "No verification token provided.",
|
||||
"emailNotVerified": "Email not yet verified. Please check your inbox."
|
||||
},
|
||||
"home": {
|
||||
"poweredBy": "Powered by BigBlueButton",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { Mail, Lock, User, ArrowRight, Loader2 } from 'lucide-react';
|
||||
import { Mail, Lock, User, ArrowRight, Loader2, CheckCircle } from 'lucide-react';
|
||||
import BrandLogo from '../components/BrandLogo';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -12,6 +12,7 @@ export default function Register() {
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [needsVerification, setNeedsVerification] = useState(false);
|
||||
const { register } = useAuth();
|
||||
const { t } = useLanguage();
|
||||
const navigate = useNavigate();
|
||||
@@ -31,9 +32,14 @@ export default function Register() {
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await register(name, email, password);
|
||||
toast.success(t('auth.registerSuccess'));
|
||||
navigate('/dashboard');
|
||||
const result = await register(name, email, password);
|
||||
if (result?.needsVerification) {
|
||||
setNeedsVerification(true);
|
||||
toast.success(t('auth.verificationSent'));
|
||||
} else {
|
||||
toast.success(t('auth.registerSuccess'));
|
||||
navigate('/dashboard');
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(err.response?.data?.error || t('auth.registerFailed'));
|
||||
} finally {
|
||||
@@ -60,6 +66,18 @@ export default function Register() {
|
||||
<BrandLogo size="lg" />
|
||||
</div>
|
||||
|
||||
{needsVerification ? (
|
||||
<div className="text-center space-y-4">
|
||||
<CheckCircle size={48} className="mx-auto text-green-400" />
|
||||
<h2 className="text-2xl font-bold text-th-text">{t('auth.checkYourEmail')}</h2>
|
||||
<p className="text-th-text-s">{t('auth.verificationSentDesc')}</p>
|
||||
<p className="text-sm text-th-text-s font-medium">{email}</p>
|
||||
<Link to="/login" className="btn-primary inline-flex items-center gap-2 mt-4">
|
||||
{t('auth.login')}
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-th-text mb-2">{t('auth.createAccount')}</h2>
|
||||
<p className="text-th-text-s">
|
||||
@@ -156,6 +174,8 @@ export default function Register() {
|
||||
<Link to="/" className="block mt-4 text-center text-sm text-th-text-s hover:text-th-text transition-colors">
|
||||
{t('auth.backToHome')}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
87
src/pages/VerifyEmail.jsx
Normal file
87
src/pages/VerifyEmail.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { CheckCircle, XCircle, Loader2, Mail } from 'lucide-react';
|
||||
import BrandLogo from '../components/BrandLogo';
|
||||
import api from '../services/api';
|
||||
|
||||
export default function VerifyEmail() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const token = searchParams.get('token');
|
||||
const { t } = useLanguage();
|
||||
|
||||
const [status, setStatus] = useState('loading'); // loading | success | error
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
setStatus('error');
|
||||
setMessage(t('auth.verifyTokenMissing'));
|
||||
return;
|
||||
}
|
||||
|
||||
api.get(`/auth/verify-email?token=${token}`)
|
||||
.then(() => {
|
||||
setStatus('success');
|
||||
setMessage(t('auth.verifySuccess'));
|
||||
})
|
||||
.catch(err => {
|
||||
setStatus('error');
|
||||
setMessage(err.response?.data?.error || t('auth.verifyFailed'));
|
||||
});
|
||||
}, [token]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-6 relative overflow-hidden">
|
||||
{/* Animated background */}
|
||||
<div className="absolute inset-0 bg-th-bg">
|
||||
<div className="absolute inset-0 opacity-30">
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-th-accent rounded-full blur-[128px] animate-pulse" />
|
||||
<div className="absolute bottom-1/4 right-1/4 w-80 h-80 bg-purple-500 rounded-full blur-[128px] animate-pulse" style={{ animationDelay: '2s' }} />
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-64 h-64 bg-pink-500 rounded-full blur-[128px] animate-pulse" style={{ animationDelay: '4s' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full max-w-md">
|
||||
<div className="card p-8 backdrop-blur-xl bg-th-card/80 border border-th-border shadow-2xl rounded-2xl text-center">
|
||||
<div className="flex justify-center mb-8">
|
||||
<BrandLogo size="lg" />
|
||||
</div>
|
||||
|
||||
{status === 'loading' && (
|
||||
<div className="space-y-4">
|
||||
<Loader2 size={48} className="mx-auto animate-spin text-th-accent" />
|
||||
<p className="text-th-text">{t('auth.verifying')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'success' && (
|
||||
<div className="space-y-4">
|
||||
<CheckCircle size={48} className="mx-auto text-green-400" />
|
||||
<h2 className="text-2xl font-bold text-th-text">{t('auth.verifySuccessTitle')}</h2>
|
||||
<p className="text-th-text-s">{message}</p>
|
||||
<Link to="/login" className="btn-primary inline-flex items-center gap-2 mt-4">
|
||||
{t('auth.login')}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<div className="space-y-4">
|
||||
<XCircle size={48} className="mx-auto text-red-400" />
|
||||
<h2 className="text-2xl font-bold text-th-text">{t('auth.verifyFailedTitle')}</h2>
|
||||
<p className="text-th-text-s">{message}</p>
|
||||
<Link to="/register" className="btn-primary inline-flex items-center gap-2 mt-4">
|
||||
{t('auth.register')}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Link to="/" className="block mt-6 text-center text-sm text-th-text-s hover:text-th-text transition-colors">
|
||||
{t('auth.backToHome')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user