Update environment configuration and enhance user management features
- Changed VITE_SUPABASE_URL in .env.example for deployment consistency. - Added new user management functionality to reset user passwords in UserManagement component. - Updated supabase.ts to include first and last name fields in user profiles and added password reset functionality. - Enhanced DashboardLayout to include a user profile view and improved user display in TopNavbar. - Updated seed.sql to create additional users with roles for testing purposes.
This commit is contained in:
@@ -31,3 +31,5 @@ export function CameraRoute({ cameraNumber }: CameraRouteProps) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DataEntry } from './DataEntry'
|
||||
import { VisionSystem } from './VisionSystem'
|
||||
import { Scheduling } from './Scheduling'
|
||||
import { VideoStreamingPage } from '../features/video-streaming'
|
||||
import { UserProfile } from './UserProfile'
|
||||
import { userManagement, type User } from '../lib/supabase'
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
@@ -25,7 +26,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
|
||||
// Valid dashboard views
|
||||
const validViews = ['dashboard', 'user-management', 'experiments', 'analytics', 'data-entry', 'vision-system', 'scheduling', 'video-library']
|
||||
const validViews = ['dashboard', 'user-management', 'experiments', 'analytics', 'data-entry', 'vision-system', 'scheduling', 'video-library', 'profile']
|
||||
|
||||
// Save current view to localStorage
|
||||
const saveCurrentView = (view: string) => {
|
||||
@@ -194,6 +195,8 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
|
||||
return <Scheduling user={user} currentRoute={currentRoute} />
|
||||
case 'video-library':
|
||||
return <VideoStreamingPage />
|
||||
case 'profile':
|
||||
return <UserProfile user={user} />
|
||||
default:
|
||||
return <DashboardHome user={user} />
|
||||
}
|
||||
@@ -274,6 +277,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
|
||||
currentView={currentView}
|
||||
onToggleSidebar={handleToggleSidebar}
|
||||
isSidebarOpen={isMobileOpen}
|
||||
onNavigateToProfile={() => handleViewChange('profile')}
|
||||
/>
|
||||
<div className="p-4 mx-auto max-w-(--breakpoint-2xl) md:p-6">
|
||||
{renderCurrentView()}
|
||||
|
||||
@@ -140,3 +140,5 @@ export function LiveCameraView({ cameraName }: LiveCameraViewProps) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ interface TopNavbarProps {
|
||||
currentView?: string
|
||||
onToggleSidebar?: () => void
|
||||
isSidebarOpen?: boolean
|
||||
onNavigateToProfile?: () => void
|
||||
}
|
||||
|
||||
export function TopNavbar({
|
||||
@@ -14,7 +15,8 @@ export function TopNavbar({
|
||||
onLogout,
|
||||
currentView = 'dashboard',
|
||||
onToggleSidebar,
|
||||
isSidebarOpen = false
|
||||
isSidebarOpen = false,
|
||||
onNavigateToProfile
|
||||
}: TopNavbarProps) {
|
||||
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false)
|
||||
|
||||
@@ -145,11 +147,16 @@ export function TopNavbar({
|
||||
>
|
||||
<span className="mr-3 overflow-hidden rounded-full h-11 w-11">
|
||||
<div className="w-11 h-11 bg-brand-500 rounded-full flex items-center justify-center text-white text-sm font-medium">
|
||||
{user.email.charAt(0).toUpperCase()}
|
||||
{(user.first_name || user.email).charAt(0).toUpperCase()}
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<span className="block mr-1 font-medium text-theme-sm">{user.email.split('@')[0]}</span>
|
||||
<span className="block mr-1 font-medium text-theme-sm">
|
||||
{user.first_name && user.last_name
|
||||
? `${user.first_name} ${user.last_name}`
|
||||
: user.email.split('@')[0]
|
||||
}
|
||||
</span>
|
||||
<svg
|
||||
className={`stroke-gray-500 dark:stroke-gray-400 transition-transform duration-200 ${isUserMenuOpen ? "rotate-180" : ""
|
||||
}`}
|
||||
@@ -174,7 +181,10 @@ export function TopNavbar({
|
||||
<div className="absolute right-0 mt-[17px] flex w-[260px] flex-col rounded-2xl border border-gray-200 bg-white p-3 shadow-theme-lg dark:border-gray-800 dark:bg-gray-dark z-50">
|
||||
<div>
|
||||
<span className="block font-medium text-gray-700 text-theme-sm dark:text-gray-400">
|
||||
{user.email.split('@')[0]}
|
||||
{user.first_name && user.last_name
|
||||
? `${user.first_name} ${user.last_name}`
|
||||
: user.email.split('@')[0]
|
||||
}
|
||||
</span>
|
||||
<span className="mt-0.5 block text-theme-xs text-gray-500 dark:text-gray-400">
|
||||
{user.email}
|
||||
@@ -183,7 +193,13 @@ export function TopNavbar({
|
||||
|
||||
<ul className="flex flex-col gap-1 pt-4 pb-3 border-b border-gray-200 dark:border-gray-800">
|
||||
<li>
|
||||
<div className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg text-theme-sm dark:text-gray-400">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsUserMenuOpen(false)
|
||||
onNavigateToProfile?.()
|
||||
}}
|
||||
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg text-theme-sm hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300 w-full text-left"
|
||||
>
|
||||
<svg
|
||||
className="fill-gray-500 dark:fill-gray-400"
|
||||
width="24"
|
||||
@@ -200,7 +216,7 @@ export function TopNavbar({
|
||||
/>
|
||||
</svg>
|
||||
Profile
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<div className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg text-theme-sm dark:text-gray-400">
|
||||
|
||||
@@ -81,6 +81,20 @@ export function UserManagement() {
|
||||
}
|
||||
}
|
||||
|
||||
const handlePasswordReset = async (userId: string, userEmail: string) => {
|
||||
if (!confirm(`Are you sure you want to reset the password for ${userEmail}? The password will be reset to "password123".`)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await userManagement.resetUserPassword(userId)
|
||||
alert(`Password reset successfully for ${result.email}. New password: ${result.new_password}`)
|
||||
} catch (err) {
|
||||
console.error('Password reset error:', err)
|
||||
alert('Failed to reset user password')
|
||||
}
|
||||
}
|
||||
|
||||
const handleUserCreated = (newUser: User) => {
|
||||
setUsers([...users, newUser])
|
||||
setShowCreateModal(false)
|
||||
@@ -260,6 +274,7 @@ export function UserManagement() {
|
||||
onStatusToggle={handleStatusToggle}
|
||||
onRoleUpdate={handleRoleUpdate}
|
||||
onEmailUpdate={handleEmailUpdate}
|
||||
onPasswordReset={handlePasswordReset}
|
||||
getRoleBadgeColor={getRoleBadgeColor}
|
||||
/>
|
||||
))}
|
||||
@@ -290,6 +305,7 @@ interface UserRowProps {
|
||||
onStatusToggle: (userId: string, currentStatus: UserStatus) => void
|
||||
onRoleUpdate: (userId: string, newRoles: RoleName[]) => void
|
||||
onEmailUpdate: (userId: string, newEmail: string) => void
|
||||
onPasswordReset: (userId: string, userEmail: string) => void
|
||||
getRoleBadgeColor: (role: string) => string
|
||||
}
|
||||
|
||||
@@ -302,6 +318,7 @@ function UserRow({
|
||||
onStatusToggle,
|
||||
onRoleUpdate,
|
||||
onEmailUpdate,
|
||||
onPasswordReset,
|
||||
getRoleBadgeColor
|
||||
}: UserRowProps) {
|
||||
const [editEmail, setEditEmail] = useState(user.email)
|
||||
@@ -381,8 +398,8 @@ function UserRow({
|
||||
<button
|
||||
onClick={() => onStatusToggle(user.id, user.status)}
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${user.status === 'active'
|
||||
? 'bg-green-100 text-green-800 hover:bg-green-200'
|
||||
: 'bg-red-100 text-red-800 hover:bg-red-200'
|
||||
? 'bg-green-100 text-green-800 hover:bg-green-200'
|
||||
: 'bg-red-100 text-red-800 hover:bg-red-200'
|
||||
}`}
|
||||
>
|
||||
{user.status.charAt(0).toUpperCase() + user.status.slice(1)}
|
||||
@@ -408,12 +425,26 @@ function UserRow({
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="text-blue-600 hover:text-blue-900"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="p-1 text-blue-600 hover:text-blue-900 hover:bg-blue-50 rounded"
|
||||
title="Edit user"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onPasswordReset(user.id, user.email)}
|
||||
className="p-1 text-orange-600 hover:text-orange-900 hover:bg-orange-50 rounded"
|
||||
title="Reset password to 'password123'"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
312
management-dashboard-web-app/src/components/UserProfile.tsx
Normal file
312
management-dashboard-web-app/src/components/UserProfile.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
import { useState } from 'react'
|
||||
import { userManagement, type User } from '../lib/supabase'
|
||||
|
||||
interface UserProfileProps {
|
||||
user: User
|
||||
}
|
||||
|
||||
interface ChangePasswordModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
}
|
||||
|
||||
function ChangePasswordModal({ isOpen, onClose, onSuccess }: ChangePasswordModalProps) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [currentPassword, setCurrentPassword] = useState('')
|
||||
const [newPassword, setNewPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
|
||||
const handlePasswordChange = async () => {
|
||||
// Validation
|
||||
if (!currentPassword || !newPassword || !confirmPassword) {
|
||||
setError('All fields are required')
|
||||
return
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
setError('New passwords do not match')
|
||||
return
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
setError('New password must be at least 6 characters long')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
const result = await userManagement.changeUserPassword(currentPassword, newPassword)
|
||||
alert('Password changed successfully!')
|
||||
onSuccess()
|
||||
onClose()
|
||||
// Clear form
|
||||
setCurrentPassword('')
|
||||
setNewPassword('')
|
||||
setConfirmPassword('')
|
||||
} catch (err: any) {
|
||||
console.error('Password change error:', err)
|
||||
if (err.message?.includes('Current password is incorrect')) {
|
||||
setError('Current password is incorrect')
|
||||
} else {
|
||||
setError('Failed to change password. Please try again.')
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onClick={onClose}></div>
|
||||
|
||||
<div className="relative bg-white rounded-lg shadow-xl max-w-md w-full p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">Change Password</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Current Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Enter your current password"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
New Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Enter your new password"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Confirm New Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Confirm your new password"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-md">
|
||||
<p className="text-sm text-red-700">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end space-x-3 mt-6">
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={isLoading}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePasswordChange}
|
||||
disabled={isLoading}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? 'Changing...' : 'Change Password'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function UserProfile({ user }: UserProfileProps) {
|
||||
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false)
|
||||
|
||||
const getRoleBadgeColor = (role: string) => {
|
||||
switch (role) {
|
||||
case 'admin':
|
||||
return 'bg-red-100 text-red-800'
|
||||
case 'conductor':
|
||||
return 'bg-blue-100 text-blue-800'
|
||||
case 'analyst':
|
||||
return 'bg-green-100 text-green-800'
|
||||
case 'data recorder':
|
||||
return 'bg-purple-100 text-purple-800'
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800'
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="bg-white shadow rounded-lg">
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">User Profile</h1>
|
||||
<p className="text-sm text-gray-600">Manage your account information and settings</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-16 h-16 bg-brand-500 rounded-full flex items-center justify-center text-white text-xl font-medium">
|
||||
{(user.first_name || user.email).charAt(0).toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Profile Information */}
|
||||
<div className="px-6 py-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Basic Information */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">Basic Information</h3>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Full Name
|
||||
</label>
|
||||
<div className="text-sm text-gray-900 bg-gray-50 px-3 py-2 rounded-md border">
|
||||
{user.first_name && user.last_name
|
||||
? `${user.first_name} ${user.last_name}`
|
||||
: 'Name not set'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email Address
|
||||
</label>
|
||||
<div className="text-sm text-gray-900 bg-gray-50 px-3 py-2 rounded-md border">
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Account Status
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${user.status === 'active'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{user.status.charAt(0).toUpperCase() + user.status.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Member Since
|
||||
</label>
|
||||
<div className="text-sm text-gray-900">
|
||||
{formatDate(user.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Roles and Permissions */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">Roles & Permissions</h3>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Assigned Roles
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{user.roles.map((role) => (
|
||||
<span
|
||||
key={role}
|
||||
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${getRoleBadgeColor(role)}`}
|
||||
>
|
||||
{role.charAt(0).toUpperCase() + role.slice(1)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Last Updated
|
||||
</label>
|
||||
<div className="text-sm text-gray-900">
|
||||
{formatDate(user.updated_at)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="mt-8 pt-6 border-t border-gray-200">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Account Actions</h3>
|
||||
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<button
|
||||
onClick={() => setShowChangePasswordModal(true)}
|
||||
className="inline-flex items-center px-4 py-2 border border-blue-300 text-sm font-medium rounded-md text-blue-700 bg-blue-50 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
Change Password
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Change Password Modal */}
|
||||
<ChangePasswordModal
|
||||
isOpen={showChangePasswordModal}
|
||||
onClose={() => setShowChangePasswordModal(false)}
|
||||
onSuccess={() => {
|
||||
// Could add success notification here
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -19,6 +19,8 @@ export type ResultsStatus = 'valid' | 'invalid'
|
||||
export interface User {
|
||||
id: string
|
||||
email: string
|
||||
first_name?: string
|
||||
last_name?: string
|
||||
roles: RoleName[]
|
||||
status: UserStatus
|
||||
created_at: string
|
||||
@@ -257,6 +259,8 @@ export const userManagement = {
|
||||
.select(`
|
||||
id,
|
||||
email,
|
||||
first_name,
|
||||
last_name,
|
||||
status,
|
||||
created_at,
|
||||
updated_at
|
||||
@@ -373,6 +377,8 @@ export const userManagement = {
|
||||
.select(`
|
||||
id,
|
||||
email,
|
||||
first_name,
|
||||
last_name,
|
||||
status,
|
||||
created_at,
|
||||
updated_at
|
||||
@@ -397,6 +403,27 @@ export const userManagement = {
|
||||
...profile,
|
||||
roles: userRoles.map(ur => (ur.roles as any).name as RoleName)
|
||||
}
|
||||
},
|
||||
|
||||
// Reset user password to default (admin only)
|
||||
async resetUserPassword(userId: string): Promise<{ user_id: string; email: string; new_password: string; reset_at: string }> {
|
||||
const { data, error } = await supabase.rpc('reset_user_password', {
|
||||
target_user_id: userId
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
},
|
||||
|
||||
// Change user password (user can only change their own password)
|
||||
async changeUserPassword(currentPassword: string, newPassword: string): Promise<{ user_id: string; email: string; password_changed_at: string }> {
|
||||
const { data, error } = await supabase.rpc('change_user_password', {
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user