diff --git a/pages/admin/users.tsx b/pages/admin/users.tsx new file mode 100644 index 0000000..bb0cbcb --- /dev/null +++ b/pages/admin/users.tsx @@ -0,0 +1,176 @@ +import { useState, useEffect } from "react" +import type { NextPage } from "next" +import Head from "next/head" +import Link from "next/link" + +interface User { + id: number + email: string + role: string + is_active: boolean + created_at: string + last_login: string | null + sources_moderated: number +} + +const UsersManagement: NextPage = () => { + const [users, setUsers] = useState([]) + const [loading, setLoading] = useState(true) + const [showAddForm, setShowAddForm] = useState(false) + const [newUser, setNewUser] = useState({ email: '', password: '', role: 'moderator' }) + + useEffect(() => { + fetchUsers() + }, []) + + const fetchUsers = async () => { + try { + const response = await fetch('/api/admin/users') + const data = await response.json() + setUsers(data.users || []) + } catch (error) { + console.error('Error fetching users:', error) + } + setLoading(false) + } + + const handleAddUser = async (e: React.FormEvent) => { + e.preventDefault() + if (!newUser.email || !newUser.password) return + + try { + const response = await fetch('/api/admin/users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newUser) + }) + + if (response.ok) { + setNewUser({ email: '', password: '', role: 'moderator' }) + setShowAddForm(false) + fetchUsers() + } else { + const error = await response.json() + alert('Error: ' + error.error) + } + } catch (error) { + alert('Failed to add user') + } + } + + if (loading) return
Loading...
+ + return ( +
+ + Users Management - Infohliadka + + +
+
+ ← Back to Admin +
+ +
+

Users Management

+ +
+ + {showAddForm && ( +
+

Add New User

+
+
+ setNewUser({...newUser, email: e.target.value})} + required + style={{ width: '200px', padding: '5px', marginRight: '10px' }} + /> + setNewUser({...newUser, password: e.target.value})} + required + style={{ width: '200px', padding: '5px', marginRight: '10px' }} + /> + + +
+
+
+ )} + + + + + + + + + + + + + + {users.map((user) => ( + + + + + + + + + ))} + +
EmailRoleStatusSources ModeratedCreatedLast Login
{user.email} + + {user.role} + + + + {user.is_active ? 'Active' : 'Inactive'} + + {user.sources_moderated} + {new Date(user.created_at).toLocaleDateString()} + + {user.last_login ? new Date(user.last_login).toLocaleDateString() : 'Never'} +
+ + {users.length === 0 && ( +

+ No users found. +

+ )} +
+
+ ) +} + +export default UsersManagement \ No newline at end of file diff --git a/pages/api/admin/users.ts b/pages/api/admin/users.ts new file mode 100644 index 0000000..ff0aeb4 --- /dev/null +++ b/pages/api/admin/users.ts @@ -0,0 +1,81 @@ +import type { NextApiRequest, NextApiResponse } from "next" +import sqlite3 from "sqlite3" +import path from "path" +import crypto from "crypto" + +function hashPassword(password: string): { hash: string, salt: string } { + const salt = crypto.randomBytes(32).toString('hex') + const hash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha256').toString('hex') + return { hash, salt } +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const dbPath = path.join(process.cwd(), "database", "antihoax.db") + const db = new sqlite3.Database(dbPath) + + try { + if (req.method === "GET") { + const users = await new Promise((resolve, reject) => { + db.all( + `SELECT id, email, role, is_active, created_at, last_login, + (SELECT COUNT(*) FROM sources WHERE moderator_id = users.id) as sources_moderated + FROM users ORDER BY created_at DESC`, + (err, rows) => { + if (err) reject(err) + else resolve(rows) + } + ) + }) + + res.json({ users }) + + } else if (req.method === "POST") { + const { email, password, role } = req.body + + if (!email || !password || !role) { + return res.status(400).json({ error: "Email, password and role required" }) + } + + if (!['admin', 'moderator'].includes(role)) { + return res.status(400).json({ error: "Invalid role" }) + } + + const { hash, salt } = hashPassword(password) + + const result = await new Promise((resolve, reject) => { + db.run( + `INSERT INTO users (email, password_hash, salt, role, is_active, created_at) + VALUES (?, ?, ?, ?, 1, datetime('now'))`, + [email, hash, salt, role], + function(err) { + if (err) reject(err) + else resolve({ id: this.lastID }) + } + ) + }) + + res.json({ + success: true, + user: { + id: result.id, + email, + role, + is_active: true + } + }) + + } else { + res.status(405).json({ error: "Method not allowed" }) + } + + } catch (error) { + console.error('Users API error:', error) + if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') { + res.status(400).json({ error: "User already exists" }) + } else { + res.status(500).json({ error: "Operation failed" }) + } + } finally { + db.close() + } +} \ No newline at end of file diff --git a/pages/api/auth/login.ts b/pages/api/auth/login.ts new file mode 100644 index 0000000..cd99b08 --- /dev/null +++ b/pages/api/auth/login.ts @@ -0,0 +1,75 @@ +import type { NextApiRequest, NextApiResponse } from "next" +import sqlite3 from "sqlite3" +import path from "path" +import crypto from "crypto" + +function hashPassword(password: string, salt: string): string { + return crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha256').toString('hex') +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST") return res.status(405).json({ error: "Method not allowed" }) + + const { email, password } = req.body + + if (!email || !password) { + return res.status(400).json({ error: "Email and password required" }) + } + + const dbPath = path.join(process.cwd(), "database", "antihoax.db") + const db = new sqlite3.Database(dbPath) + + try { + const user = await new Promise((resolve, reject) => { + db.get( + "SELECT id, email, password_hash, salt, role, is_active FROM users WHERE email = ?", + [email], + (err, row) => { + if (err) reject(err) + else resolve(row) + } + ) + }) + + if (!user) { + return res.status(401).json({ error: "Invalid credentials" }) + } + + if (!user.is_active) { + return res.status(401).json({ error: "Account is disabled" }) + } + + const hashedPassword = hashPassword(password, user.salt) + if (hashedPassword !== user.password_hash) { + return res.status(401).json({ error: "Invalid credentials" }) + } + + // Update last login + await new Promise((resolve, reject) => { + db.run( + "UPDATE users SET last_login = datetime('now') WHERE id = ?", + [user.id], + (err) => { + if (err) reject(err) + else resolve() + } + ) + }) + + res.json({ + success: true, + user: { + id: user.id, + email: user.email, + role: user.role + }, + token: Buffer.from(`${user.id}:${Date.now()}`).toString('base64') + }) + + } catch (error) { + console.error('Login error:', error) + res.status(500).json({ error: "Login failed" }) + } finally { + db.close() + } +} \ No newline at end of file