initial commit: rocket.new export of broadcastbeat
This commit is contained in:
300
src/app/admin/wp-users/page.tsx
Normal file
300
src/app/admin/wp-users/page.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
|
||||
interface LegacyUser {
|
||||
id: string;
|
||||
wp_id: number;
|
||||
wp_username: string;
|
||||
email: string;
|
||||
display_name: string | null;
|
||||
role: string;
|
||||
is_active: boolean;
|
||||
auth_user_id: string | null;
|
||||
password_upgraded: boolean;
|
||||
imported_at: string;
|
||||
last_login_at: string | null;
|
||||
notes: string | null;
|
||||
}
|
||||
|
||||
const ROLE_COLORS: Record<string, string> = {
|
||||
administrator: 'text-red-400 bg-red-400/10',
|
||||
editor: 'text-purple-400 bg-purple-400/10',
|
||||
author: 'text-blue-400 bg-blue-400/10',
|
||||
contributor: 'text-green-400 bg-green-400/10',
|
||||
reader: 'text-gray-400 bg-gray-400/10',
|
||||
};
|
||||
|
||||
export default function WPUsersAdminPage() {
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const supabase = createClient();
|
||||
|
||||
const [users, setUsers] = useState<LegacyUser[]>([]);
|
||||
const [loadingData, setLoadingData] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [roleFilter, setRoleFilter] = useState('all');
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
const [notification, setNotification] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
|
||||
const [processingIds, setProcessingIds] = useState<Set<string>>(new Set());
|
||||
|
||||
const showNotification = (type: 'success' | 'error', message: string) => {
|
||||
setNotification({ type, message });
|
||||
setTimeout(() => setNotification(null), 4000);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) router.push('/login');
|
||||
}, [user, loading, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) fetchUsers();
|
||||
}, [user]);
|
||||
|
||||
const fetchUsers = async () => {
|
||||
setLoadingData(true);
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('wp_legacy_users')
|
||||
.select('*')
|
||||
.order('wp_id', { ascending: true });
|
||||
if (error) throw error;
|
||||
setUsers(data || []);
|
||||
} catch (err: any) {
|
||||
showNotification('error', err.message || 'Failed to load users');
|
||||
} finally {
|
||||
setLoadingData(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleActive = async (userId: string, currentActive: boolean) => {
|
||||
setProcessingIds(prev => new Set(prev).add(userId));
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('wp_legacy_users')
|
||||
.update({ is_active: !currentActive })
|
||||
.eq('id', userId);
|
||||
if (error) throw error;
|
||||
setUsers(prev => prev.map(u => u.id === userId ? { ...u, is_active: !currentActive } : u));
|
||||
showNotification('success', `User ${!currentActive ? 'activated' : 'deactivated'} successfully.`);
|
||||
} catch (err: any) {
|
||||
showNotification('error', err.message);
|
||||
} finally {
|
||||
setProcessingIds(prev => { const s = new Set(prev); s.delete(userId); return s; });
|
||||
}
|
||||
};
|
||||
|
||||
const updateRole = async (userId: string, newRole: string) => {
|
||||
setProcessingIds(prev => new Set(prev).add(userId));
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('wp_legacy_users')
|
||||
.update({ role: newRole })
|
||||
.eq('id', userId);
|
||||
if (error) throw error;
|
||||
setUsers(prev => prev.map(u => u.id === userId ? { ...u, role: newRole } : u));
|
||||
showNotification('success', 'Role updated successfully.');
|
||||
} catch (err: any) {
|
||||
showNotification('error', err.message);
|
||||
} finally {
|
||||
setProcessingIds(prev => { const s = new Set(prev); s.delete(userId); return s; });
|
||||
}
|
||||
};
|
||||
|
||||
const filteredUsers = users.filter(u => {
|
||||
const matchesSearch = !searchQuery ||
|
||||
u.wp_username.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
u.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(u.display_name || '').toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesRole = roleFilter === 'all' || u.role === roleFilter;
|
||||
const matchesStatus = statusFilter === 'all' ||
|
||||
(statusFilter === 'active' && u.is_active) ||
|
||||
(statusFilter === 'inactive' && !u.is_active) ||
|
||||
(statusFilter === 'migrated' && !!u.auth_user_id) ||
|
||||
(statusFilter === 'pending' && !u.auth_user_id);
|
||||
return matchesSearch && matchesRole && matchesStatus;
|
||||
});
|
||||
|
||||
const stats = {
|
||||
total: users.length,
|
||||
active: users.filter(u => u.is_active).length,
|
||||
migrated: users.filter(u => !!u.auth_user_id).length,
|
||||
admins: users.filter(u => u.role === 'administrator').length,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] text-white p-6">
|
||||
{notification && (
|
||||
<div className={`fixed top-4 right-4 z-50 px-5 py-3 rounded-lg text-sm font-medium shadow-lg ${
|
||||
notification.type === 'success' ? 'bg-green-500/20 border border-green-500/40 text-green-300' : 'bg-red-500/20 border border-red-500/40 text-red-300'
|
||||
}`}>
|
||||
{notification.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<Link href="/admin" className="text-[#555] hover:text-white text-sm transition-colors">← Admin</Link>
|
||||
<span className="text-[#333]">/</span>
|
||||
<span className="text-[#888] text-sm">WordPress User Migration</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white">WordPress Migrated Users</h1>
|
||||
<p className="text-[#555] text-sm mt-1">
|
||||
All users imported from the WordPress broadcas_broad database. Passwords validated via phpass on first login.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4 mb-6">
|
||||
{[
|
||||
{ label: 'Total Users', value: stats.total, color: 'text-white' },
|
||||
{ label: 'Active', value: stats.active, color: 'text-green-400' },
|
||||
{ label: 'Migrated to Auth', value: stats.migrated, color: 'text-blue-400' },
|
||||
{ label: 'Administrators', value: stats.admins, color: 'text-red-400' },
|
||||
].map(s => (
|
||||
<div key={s.label} className="bg-[#111] border border-[#222] rounded-xl p-4">
|
||||
<div className={`text-2xl font-bold ${s.color}`}>{s.value}</div>
|
||||
<div className="text-[#555] text-xs mt-1">{s.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex gap-3 mb-5 flex-wrap">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search username, email, name..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
className="flex-1 min-w-48 bg-[#111] border border-[#333] rounded-lg px-4 py-2 text-white placeholder-[#444] focus:outline-none focus:border-[#c9a84c] text-sm"
|
||||
/>
|
||||
<select
|
||||
value={roleFilter}
|
||||
onChange={e => setRoleFilter(e.target.value)}
|
||||
className="bg-[#111] border border-[#333] rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-[#c9a84c]"
|
||||
>
|
||||
<option value="all">All Roles</option>
|
||||
<option value="administrator">Administrator</option>
|
||||
<option value="editor">Editor</option>
|
||||
<option value="author">Author</option>
|
||||
<option value="contributor">Contributor</option>
|
||||
</select>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={e => setStatusFilter(e.target.value)}
|
||||
className="bg-[#111] border border-[#333] rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-[#c9a84c]"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
<option value="migrated">Migrated to Auth</option>
|
||||
<option value="pending">Pending Migration</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
{loadingData ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="w-8 h-8 border-2 border-[#c9a84c] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-[#111] border border-[#222] rounded-xl overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-[#222]">
|
||||
<th className="text-left px-4 py-3 text-[#555] font-medium">WP ID</th>
|
||||
<th className="text-left px-4 py-3 text-[#555] font-medium">Username</th>
|
||||
<th className="text-left px-4 py-3 text-[#555] font-medium">Email</th>
|
||||
<th className="text-left px-4 py-3 text-[#555] font-medium">Display Name</th>
|
||||
<th className="text-left px-4 py-3 text-[#555] font-medium">Role</th>
|
||||
<th className="text-left px-4 py-3 text-[#555] font-medium">Status</th>
|
||||
<th className="text-left px-4 py-3 text-[#555] font-medium">Auth</th>
|
||||
<th className="text-left px-4 py-3 text-[#555] font-medium">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredUsers.map(u => (
|
||||
<tr key={u.id} className="border-b border-[#1a1a1a] hover:bg-[#151515] transition-colors">
|
||||
<td className="px-4 py-3 text-[#555] font-mono text-xs">{u.wp_id}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-white font-medium">{u.wp_username}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-[#888] text-xs">{u.email}</td>
|
||||
<td className="px-4 py-3 text-[#888] text-xs">{u.display_name || '—'}</td>
|
||||
<td className="px-4 py-3">
|
||||
<select
|
||||
value={u.role}
|
||||
onChange={e => updateRole(u.id, e.target.value)}
|
||||
disabled={processingIds.has(u.id)}
|
||||
className={`text-xs px-2 py-1 rounded-full font-medium border-0 focus:outline-none cursor-pointer ${ROLE_COLORS[u.role] || 'text-gray-400 bg-gray-400/10'}`}
|
||||
>
|
||||
<option value="administrator">Administrator</option>
|
||||
<option value="editor">Editor</option>
|
||||
<option value="author">Author</option>
|
||||
<option value="contributor">Contributor</option>
|
||||
<option value="reader">Reader</option>
|
||||
</select>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${u.is_active ? 'text-green-400 bg-green-400/10' : 'text-red-400 bg-red-400/10'}`}>
|
||||
{u.is_active ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{u.auth_user_id ? (
|
||||
<span className="text-xs text-blue-400">✓ Migrated</span>
|
||||
) : (
|
||||
<span className="text-xs text-[#444]">Pending</span>
|
||||
)}
|
||||
{u.password_upgraded && (
|
||||
<span className="text-xs text-green-400 ml-2">🔒 Upgraded</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<button
|
||||
onClick={() => toggleActive(u.id, u.is_active)}
|
||||
disabled={processingIds.has(u.id)}
|
||||
className={`text-xs px-3 py-1 rounded-lg transition-colors disabled:opacity-50 ${
|
||||
u.is_active
|
||||
? 'bg-red-500/10 text-red-400 hover:bg-red-500/20' :'bg-green-500/10 text-green-400 hover:bg-green-500/20'
|
||||
}`}
|
||||
>
|
||||
{u.is_active ? 'Deactivate' : 'Activate'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="px-4 py-3 border-t border-[#222] text-[#555] text-xs">
|
||||
Showing {filteredUsers.length} of {users.length} users
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info box */}
|
||||
<div className="mt-6 bg-[#111] border border-[#333] rounded-xl p-5">
|
||||
<h3 className="text-white font-medium text-sm mb-3">📋 Migration Notes</h3>
|
||||
<ul className="space-y-1.5 text-[#555] text-xs">
|
||||
<li>• Passwords are validated using phpass on first login. Both <code className="text-[#888]">$P$B</code> and <code className="text-[#888]">$wp$2y$10$</code> formats are supported.</li>
|
||||
<li>• On successful first login, a Supabase auth account is created and the password is transparently upgraded.</li>
|
||||
<li>• Test accounts (IDs 757, 760, 761) are set to <strong className="text-red-400">Inactive</strong> and cannot log in until manually reactivated.</li>
|
||||
<li>• ryansalazar (ID 21) has been upgraded to <strong className="text-red-400">Administrator</strong> per site owner instructions.</li>
|
||||
<li>• deanna.jones (ID 728) has been set to <strong className="text-red-400">Administrator</strong> for full access.</li>
|
||||
<li>• The Featured category is locked — Contributors and Authors cannot post to it. The trigger strips it automatically on save.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user