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:
salirezav
2025-09-22 11:20:15 -04:00
parent 0ba385eebc
commit 44c8c3f6dd
23 changed files with 1398 additions and 31 deletions

View File

@@ -1,5 +1,5 @@
# Web environment (Vite)
VITE_SUPABASE_URL=http://localhost:54321
VITE_SUPABASE_URL=http://exp-dash:54321
VITE_SUPABASE_ANON_KEY=[REDACTED]
# API config (optional)

1
.gitignore vendored
View File

@@ -31,3 +31,4 @@ camera-management-api/usda_vision_system.log
camera-management-api/camera_sdk/
camera-management-api/core
management-dashboard-web-app/users.txt

View File

@@ -474,11 +474,11 @@ const apiConfig = {
### Hostname Setup Guide
```bash
# Option 1: Add to /etc/hosts (Linux/Mac)
echo "127.0.0.1 vision" | sudo tee -a /etc/hosts
echo "exp-dash vision" | sudo tee -a /etc/hosts
# Option 2: Add to hosts file (Windows)
# Add to C:\Windows\System32\drivers\etc\hosts:
# 127.0.0.1 vision
# exp-dash vision
# Option 3: Configure DNS
# Point 'vision' hostname to your server's IP address

View File

@@ -8,7 +8,7 @@
#
# HOSTNAME SETUP:
# To use 'vision' hostname instead of 'localhost':
# 1. Add to /etc/hosts: 127.0.0.1 vision
# 1. Add to /etc/hosts: exp-dash vision
# 2. Or configure DNS to point 'vision' to the server IP
# 3. Update camera_preview.html: API_BASE = 'http://localhost:8000'
###############################################################################
@@ -28,7 +28,7 @@
# Option 1: Using 'vision' hostname (recommended for production)
# - Requires hostname resolution setup
# - Add to /etc/hosts: 127.0.0.1 vision
# - Add to /etc/hosts: exp-dash vision
# - Or configure DNS: vision -> server IP address
# - Update camera_preview.html: API_BASE = 'http://localhost:8000'
# - Set @baseUrl = http://localhost:8000

View File

@@ -100,7 +100,7 @@ server 3.us.pool.ntp.org iburst
# Security settings
restrict default kod notrap nomodify nopeer noquery
restrict -6 default kod notrap nomodify nopeer noquery
restrict 127.0.0.1
restrict exp-dash
restrict -6 ::1
# Local clock as fallback

View File

@@ -201,6 +201,57 @@ CREATE TABLE public.pecan_diameter_measurements (
**Purpose**: Individual pecan diameter measurements (up to 10 per phase).
### 3. Conductor Availability Management
#### `public.conductor_availability`
```sql
CREATE TABLE public.conductor_availability (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE,
available_from TIMESTAMP WITH TIME ZONE NOT NULL,
available_to TIMESTAMP WITH TIME ZONE NOT NULL,
notes TEXT, -- Optional notes about the availability
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'cancelled')),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_by UUID NOT NULL REFERENCES public.user_profiles(id),
-- Ensure available_to is after available_from
CONSTRAINT valid_time_range CHECK (available_to > available_from),
-- Ensure availability is in the future (can be modified if needed for past records)
CONSTRAINT future_availability CHECK (available_from >= NOW() - INTERVAL '1 day')
);
```
**Purpose**: Stores conductor availability windows for experiment scheduling with overlap prevention.
#### `public.experiment_phase_assignments`
```sql
CREATE TABLE public.experiment_phase_assignments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
repetition_id UUID NOT NULL REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
conductor_id UUID NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE,
phase_name TEXT NOT NULL CHECK (phase_name IN ('pre-soaking', 'air-drying', 'cracking', 'shelling')),
scheduled_start_time TIMESTAMP WITH TIME ZONE NOT NULL,
scheduled_end_time TIMESTAMP WITH TIME ZONE NOT NULL,
status TEXT NOT NULL DEFAULT 'scheduled' CHECK (status IN ('scheduled', 'in-progress', 'completed', 'cancelled')),
notes TEXT, -- Optional notes about the assignment
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_by UUID NOT NULL REFERENCES public.user_profiles(id),
-- Ensure scheduled_end_time is after scheduled_start_time
CONSTRAINT valid_scheduled_time_range CHECK (scheduled_end_time > scheduled_start_time),
-- Ensure unique assignment per conductor per phase per repetition
CONSTRAINT unique_conductor_phase_assignment UNIQUE (repetition_id, conductor_id, phase_name)
);
```
**Purpose**: Assigns conductors to specific experiment repetition phases with scheduled times for future scheduling functionality.
## Key Functions
### Authentication & Authorization
@@ -218,6 +269,12 @@ CREATE TABLE public.pecan_diameter_measurements (
### Data Validation
- `validate_repetition_number()`: Ensures repetition numbers don't exceed experiment requirements
- `public.check_repetition_lock_before_withdrawal()`: Prevents withdrawal of locked repetitions
- `public.check_availability_overlap()`: Prevents overlapping availabilities for the same conductor
- `public.adjust_overlapping_availability()`: Automatically adjusts overlapping availabilities (alternative approach)
### Availability Management
- `public.get_available_conductors(start_time, end_time)`: Returns conductors available for a specific time range
- `public.is_conductor_available(conductor_id, start_time, end_time)`: Checks if a conductor is available for a specific time range
### Timestamp Management
- `public.handle_updated_at()`: Updates the updated_at timestamp
@@ -227,7 +284,7 @@ CREATE TABLE public.pecan_diameter_measurements (
### Access Control Summary
- **Admin**: Full access to all tables and operations
- **Conductor**: Can manage experiments, experiment phases, and repetitions, view all data
- **Conductor**: Can manage experiments, experiment phases, and repetitions, view all data, manage their own availability
- **Analyst**: Read-only access to all data
- **Data Recorder**: Can create and manage their own phase drafts and data
@@ -235,6 +292,8 @@ CREATE TABLE public.pecan_diameter_measurements (
- All authenticated users can view experiments, experiment phases, and repetitions
- Admin and conductor roles can manage experiment phases
- Users can only modify their own phase drafts (unless admin)
- Users can only manage their own availability (unless admin)
- Conductors can view their own experiment phase assignments
- Locked repetitions prevent draft modifications
- Submitted drafts cannot be withdrawn if repetition is locked
- Role-based access control for user management
@@ -260,6 +319,10 @@ The database includes comprehensive indexing for performance:
- Only one submitted draft per user per phase per repetition
- Moisture percentages must be between 0-100
- Weights and measurements must be non-negative
- Conductor availabilities cannot overlap for the same user
- Availability windows must have valid time ranges (end > start)
- Only one conductor assignment per phase per repetition
- Scheduled times must have valid ranges (end > start)
## Migration History
@@ -271,5 +334,6 @@ The schema has evolved through several migrations:
5. **Data Entry System**: Phase-specific draft and data management
6. **Constraint Fixes**: Refined business rules and constraints
7. **Experiment Phases**: Added experiment grouping for better organization
8. **Conductor Availability**: Added availability management and experiment phase assignments for scheduling
This schema supports a comprehensive pecan processing experiment management system with robust security, data integrity, and flexible role-based access control.

View File

@@ -0,0 +1,4 @@
# Local Supabase config for Vite dev server
VITE_SUPABASE_URL=http://127.0.0.1:54321
VITE_SUPABASE_ANON_KEY=[REDACTED]

View File

@@ -216,3 +216,5 @@ If you encounter issues:

View File

@@ -125,3 +125,5 @@

View File

@@ -31,3 +31,5 @@ export function CameraRoute({ cameraNumber }: CameraRouteProps) {

View File

@@ -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()}

View File

@@ -140,3 +140,5 @@ export function LiveCameraView({ cameraName }: LiveCameraViewProps) {

View File

@@ -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">

View File

@@ -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>

View 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>
)
}

View File

@@ -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
}
}

View File

@@ -1 +1 @@
v2.34.3
v2.40.7

View File

@@ -81,7 +81,7 @@ enabled = true
# Port to use for Supabase Studio.
port = 54323
# External URL of the API server that frontend connects to.
api_url = "http://127.0.0.1"
api_url = "http://exp-dash"
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
openai_api_key = "env(OPENAI_API_KEY)"
@@ -117,9 +117,9 @@ file_size_limit = "50MiB"
enabled = true
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
site_url = "http://127.0.0.1:3000"
site_url = "http://exp-dash:3000"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://127.0.0.1:3000"]
additional_redirect_urls = ["https://exp-dash:3000"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600
# If disabled, the refresh token will never expire.

View File

@@ -0,0 +1,341 @@
-- Add Conductor Availability and Experiment Phase Assignment Tables
-- This migration adds tables for conductor availability management and future experiment scheduling
-- =============================================
-- 1. CONDUCTOR AVAILABILITY TABLE
-- =============================================
-- Create conductor_availability table
CREATE TABLE IF NOT EXISTS public.conductor_availability (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE,
available_from TIMESTAMP WITH TIME ZONE NOT NULL,
available_to TIMESTAMP WITH TIME ZONE NOT NULL,
notes TEXT, -- Optional notes about the availability
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'cancelled')),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_by UUID NOT NULL REFERENCES public.user_profiles(id),
-- Ensure available_to is after available_from
CONSTRAINT valid_time_range CHECK (available_to > available_from),
-- Ensure availability is in the future (can be modified if needed for past records)
CONSTRAINT future_availability CHECK (available_from >= NOW() - INTERVAL '1 day')
);
-- =============================================
-- 2. EXPERIMENT PHASE ASSIGNMENTS TABLE (Future Scheduling)
-- =============================================
-- Create experiment_phase_assignments table for scheduling conductors to experiment phases
CREATE TABLE IF NOT EXISTS public.experiment_phase_assignments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
repetition_id UUID NOT NULL REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
conductor_id UUID NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE,
phase_name TEXT NOT NULL CHECK (phase_name IN ('pre-soaking', 'air-drying', 'cracking', 'shelling')),
scheduled_start_time TIMESTAMP WITH TIME ZONE NOT NULL,
scheduled_end_time TIMESTAMP WITH TIME ZONE NOT NULL,
status TEXT NOT NULL DEFAULT 'scheduled' CHECK (status IN ('scheduled', 'in-progress', 'completed', 'cancelled')),
notes TEXT, -- Optional notes about the assignment
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_by UUID NOT NULL REFERENCES public.user_profiles(id),
-- Ensure scheduled_end_time is after scheduled_start_time
CONSTRAINT valid_scheduled_time_range CHECK (scheduled_end_time > scheduled_start_time),
-- Ensure unique assignment per conductor per phase per repetition
CONSTRAINT unique_conductor_phase_assignment UNIQUE (repetition_id, conductor_id, phase_name)
);
-- =============================================
-- 3. INDEXES FOR PERFORMANCE
-- =============================================
-- Conductor availability indexes
CREATE INDEX IF NOT EXISTS idx_conductor_availability_user_id ON public.conductor_availability(user_id);
CREATE INDEX IF NOT EXISTS idx_conductor_availability_available_from ON public.conductor_availability(available_from);
CREATE INDEX IF NOT EXISTS idx_conductor_availability_available_to ON public.conductor_availability(available_to);
CREATE INDEX IF NOT EXISTS idx_conductor_availability_status ON public.conductor_availability(status);
CREATE INDEX IF NOT EXISTS idx_conductor_availability_created_by ON public.conductor_availability(created_by);
CREATE INDEX IF NOT EXISTS idx_conductor_availability_time_range ON public.conductor_availability(available_from, available_to);
-- Experiment phase assignments indexes
CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_experiment_id ON public.experiment_phase_assignments(experiment_id);
CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_repetition_id ON public.experiment_phase_assignments(repetition_id);
CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_conductor_id ON public.experiment_phase_assignments(conductor_id);
CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_phase_name ON public.experiment_phase_assignments(phase_name);
CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_status ON public.experiment_phase_assignments(status);
CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_scheduled_start ON public.experiment_phase_assignments(scheduled_start_time);
CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_created_by ON public.experiment_phase_assignments(created_by);
-- =============================================
-- 4. FUNCTIONS FOR OVERLAP PREVENTION
-- =============================================
-- Function to check for overlapping availabilities
CREATE OR REPLACE FUNCTION public.check_availability_overlap()
RETURNS TRIGGER AS $$
DECLARE
overlap_count INTEGER;
BEGIN
-- Check for overlapping availabilities for the same user
SELECT COUNT(*) INTO overlap_count
FROM public.conductor_availability
WHERE user_id = NEW.user_id
AND id != COALESCE(NEW.id, '00000000-0000-0000-0000-000000000000'::UUID)
AND status = 'active'
AND (
-- New availability starts during an existing one
(NEW.available_from >= available_from AND NEW.available_from < available_to) OR
-- New availability ends during an existing one
(NEW.available_to > available_from AND NEW.available_to <= available_to) OR
-- New availability completely contains an existing one
(NEW.available_from <= available_from AND NEW.available_to >= available_to) OR
-- Existing availability completely contains the new one
(available_from <= NEW.available_from AND available_to >= NEW.available_to)
);
IF overlap_count > 0 THEN
RAISE EXCEPTION 'Availability overlaps with existing availability for user %. Please adjust the time range or cancel the conflicting availability.', NEW.user_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Function to automatically adjust overlapping availabilities (alternative approach)
CREATE OR REPLACE FUNCTION public.adjust_overlapping_availability()
RETURNS TRIGGER AS $$
DECLARE
overlapping_record RECORD;
BEGIN
-- Find overlapping availabilities and adjust them
FOR overlapping_record IN
SELECT id, available_from, available_to
FROM public.conductor_availability
WHERE user_id = NEW.user_id
AND id != COALESCE(NEW.id, '00000000-0000-0000-0000-000000000000'::UUID)
AND status = 'active'
AND (
(NEW.available_from >= available_from AND NEW.available_from < available_to) OR
(NEW.available_to > available_from AND NEW.available_to <= available_to) OR
(NEW.available_from <= available_from AND NEW.available_to >= available_to) OR
(available_from <= NEW.available_from AND available_to >= NEW.available_to)
)
LOOP
-- Adjust the overlapping record to end where the new one starts
IF overlapping_record.available_from < NEW.available_from AND overlapping_record.available_to > NEW.available_from THEN
UPDATE public.conductor_availability
SET available_to = NEW.available_from,
updated_at = NOW()
WHERE id = overlapping_record.id;
END IF;
-- If the overlapping record starts after the new one, adjust it to start where the new one ends
IF overlapping_record.available_from < NEW.available_to AND overlapping_record.available_to > NEW.available_to THEN
UPDATE public.conductor_availability
SET available_from = NEW.available_to,
updated_at = NOW()
WHERE id = overlapping_record.id;
END IF;
-- If the overlapping record is completely contained within the new one, cancel it
IF overlapping_record.available_from >= NEW.available_from AND overlapping_record.available_to <= NEW.available_to THEN
UPDATE public.conductor_availability
SET status = 'cancelled',
updated_at = NOW()
WHERE id = overlapping_record.id;
END IF;
END LOOP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- =============================================
-- 5. TRIGGERS
-- =============================================
-- Create trigger for updated_at on conductor_availability
CREATE TRIGGER set_updated_at_conductor_availability
BEFORE UPDATE ON public.conductor_availability
FOR EACH ROW
EXECUTE FUNCTION public.handle_updated_at();
-- Create trigger for updated_at on experiment_phase_assignments
CREATE TRIGGER set_updated_at_experiment_phase_assignments
BEFORE UPDATE ON public.experiment_phase_assignments
FOR EACH ROW
EXECUTE FUNCTION public.handle_updated_at();
-- Create trigger to prevent overlapping availabilities (strict approach)
CREATE TRIGGER trigger_check_availability_overlap
BEFORE INSERT OR UPDATE ON public.conductor_availability
FOR EACH ROW
EXECUTE FUNCTION public.check_availability_overlap();
-- Alternative trigger to automatically adjust overlapping availabilities (uncomment if preferred)
-- CREATE TRIGGER trigger_adjust_overlapping_availability
-- BEFORE INSERT OR UPDATE ON public.conductor_availability
-- FOR EACH ROW
-- EXECUTE FUNCTION public.adjust_overlapping_availability();
-- =============================================
-- 6. HELPER FUNCTIONS
-- =============================================
-- Function to get available conductors for a specific time range
CREATE OR REPLACE FUNCTION public.get_available_conductors(
start_time TIMESTAMP WITH TIME ZONE,
end_time TIMESTAMP WITH TIME ZONE
)
RETURNS TABLE (
user_id UUID,
email TEXT,
available_from TIMESTAMP WITH TIME ZONE,
available_to TIMESTAMP WITH TIME ZONE
) AS $$
BEGIN
RETURN QUERY
SELECT
ca.user_id,
up.email,
ca.available_from,
ca.available_to
FROM public.conductor_availability ca
JOIN public.user_profiles up ON ca.user_id = up.id
JOIN public.user_roles ur ON up.id = ur.user_id
JOIN public.roles r ON ur.role_id = r.id
WHERE ca.status = 'active'
AND r.name = 'conductor'
AND ca.available_from <= start_time
AND ca.available_to >= end_time
ORDER BY up.email;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function to check if a conductor is available for a specific time range
CREATE OR REPLACE FUNCTION public.is_conductor_available(
conductor_user_id UUID,
start_time TIMESTAMP WITH TIME ZONE,
end_time TIMESTAMP WITH TIME ZONE
)
RETURNS BOOLEAN AS $$
DECLARE
availability_count INTEGER;
BEGIN
SELECT COUNT(*) INTO availability_count
FROM public.conductor_availability
WHERE user_id = conductor_user_id
AND status = 'active'
AND available_from <= start_time
AND available_to >= end_time;
RETURN availability_count > 0;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- =============================================
-- 7. ROW LEVEL SECURITY (RLS)
-- =============================================
-- Enable RLS on new tables
ALTER TABLE public.conductor_availability ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.experiment_phase_assignments ENABLE ROW LEVEL SECURITY;
-- Conductor availability policies
CREATE POLICY "conductor_availability_select_policy" ON public.conductor_availability
FOR SELECT
TO authenticated
USING (
-- Users can view their own availability, admins can view all
user_id = auth.uid() OR public.is_admin()
);
CREATE POLICY "conductor_availability_insert_policy" ON public.conductor_availability
FOR INSERT
TO authenticated
WITH CHECK (
-- Users can create their own availability, admins can create for anyone
(user_id = auth.uid() AND created_by = auth.uid()) OR public.is_admin()
);
CREATE POLICY "conductor_availability_update_policy" ON public.conductor_availability
FOR UPDATE
TO authenticated
USING (
-- Users can update their own availability, admins can update any
user_id = auth.uid() OR public.is_admin()
)
WITH CHECK (
-- Users can update their own availability, admins can update any
user_id = auth.uid() OR public.is_admin()
);
CREATE POLICY "conductor_availability_delete_policy" ON public.conductor_availability
FOR DELETE
TO authenticated
USING (
-- Users can delete their own availability, admins can delete any
user_id = auth.uid() OR public.is_admin()
);
-- Experiment phase assignments policies
CREATE POLICY "experiment_phase_assignments_select_policy" ON public.experiment_phase_assignments
FOR SELECT
TO authenticated
USING (
-- Conductors can view their own assignments, admins can view all
conductor_id = auth.uid() OR public.is_admin()
);
CREATE POLICY "experiment_phase_assignments_insert_policy" ON public.experiment_phase_assignments
FOR INSERT
TO authenticated
WITH CHECK (
-- Only admins and conductors can create assignments
public.can_manage_experiments()
);
CREATE POLICY "experiment_phase_assignments_update_policy" ON public.experiment_phase_assignments
FOR UPDATE
TO authenticated
USING (
-- Conductors can update their own assignments, admins can update any
conductor_id = auth.uid() OR public.is_admin()
)
WITH CHECK (
-- Conductors can update their own assignments, admins can update any
conductor_id = auth.uid() OR public.is_admin()
);
CREATE POLICY "experiment_phase_assignments_delete_policy" ON public.experiment_phase_assignments
FOR DELETE
TO authenticated
USING (
-- Only admins can delete assignments
public.is_admin()
);
-- =============================================
-- 8. COMMENTS FOR DOCUMENTATION
-- =============================================
COMMENT ON TABLE public.conductor_availability IS 'Stores conductor availability windows for experiment scheduling';
COMMENT ON TABLE public.experiment_phase_assignments IS 'Assigns conductors to specific experiment repetition phases with scheduled times';
COMMENT ON COLUMN public.conductor_availability.available_from IS 'Start time of availability window';
COMMENT ON COLUMN public.conductor_availability.available_to IS 'End time of availability window';
COMMENT ON COLUMN public.conductor_availability.notes IS 'Optional notes about the availability period';
COMMENT ON COLUMN public.conductor_availability.status IS 'Status of the availability (active or cancelled)';
COMMENT ON COLUMN public.experiment_phase_assignments.phase_name IS 'Experiment phase being assigned (pre-soaking, air-drying, cracking, shelling)';
COMMENT ON COLUMN public.experiment_phase_assignments.scheduled_start_time IS 'Planned start time for the phase';
COMMENT ON COLUMN public.experiment_phase_assignments.scheduled_end_time IS 'Planned end time for the phase';
COMMENT ON COLUMN public.experiment_phase_assignments.status IS 'Current status of the assignment';
COMMENT ON COLUMN public.experiment_phase_assignments.notes IS 'Optional notes about the assignment';

View File

@@ -0,0 +1,49 @@
-- Add password reset function for admin use
-- This migration adds a function to reset user passwords back to the default "password123"
-- Function to reset user password (admin only)
CREATE OR REPLACE FUNCTION public.reset_user_password(
target_user_id UUID
)
RETURNS JSON AS $$
DECLARE
user_email TEXT;
result JSON;
BEGIN
-- Only admins can reset passwords
IF NOT public.is_admin() THEN
RAISE EXCEPTION 'Only administrators can reset user passwords';
END IF;
-- Check if target user exists
SELECT email INTO user_email
FROM public.user_profiles
WHERE id = target_user_id;
IF user_email IS NULL THEN
RAISE EXCEPTION 'User not found';
END IF;
-- Update the password in auth.users table
UPDATE auth.users
SET
encrypted_password = crypt('password123', gen_salt('bf')),
updated_at = NOW()
WHERE id = target_user_id;
-- Return result
result := json_build_object(
'user_id', target_user_id,
'email', user_email,
'new_password', 'password123',
'reset_at', NOW()
);
RETURN result;
EXCEPTION
WHEN OTHERS THEN
RAISE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

View File

@@ -0,0 +1,12 @@
-- Add first_name and last_name fields to user_profiles table
-- This migration adds name fields to store user's first and last names
-- Add first_name and last_name columns to user_profiles table
ALTER TABLE public.user_profiles
ADD COLUMN first_name TEXT,
ADD COLUMN last_name TEXT;
-- Add comments for documentation
COMMENT ON COLUMN public.user_profiles.first_name IS 'User first name';
COMMENT ON COLUMN public.user_profiles.last_name IS 'User last name';

View File

@@ -0,0 +1,62 @@
-- Add change password function for users
-- This migration adds a function to allow users to change their own password
-- Function to change user password (user can only change their own password)
CREATE OR REPLACE FUNCTION public.change_user_password(
current_password TEXT,
new_password TEXT
)
RETURNS JSON AS $$
DECLARE
user_id UUID;
user_email TEXT;
result JSON;
BEGIN
-- Get current user ID
user_id := auth.uid();
IF user_id IS NULL THEN
RAISE EXCEPTION 'User not authenticated';
END IF;
-- Get user email
SELECT email INTO user_email
FROM public.user_profiles
WHERE id = user_id;
IF user_email IS NULL THEN
RAISE EXCEPTION 'User profile not found';
END IF;
-- Verify current password
IF NOT EXISTS (
SELECT 1
FROM auth.users
WHERE id = user_id
AND encrypted_password = crypt(current_password, encrypted_password)
) THEN
RAISE EXCEPTION 'Current password is incorrect';
END IF;
-- Update the password in auth.users table
UPDATE auth.users
SET
encrypted_password = crypt(new_password, gen_salt('bf')),
updated_at = NOW()
WHERE id = user_id;
-- Return result
result := json_build_object(
'user_id', user_id,
'email', user_email,
'password_changed_at', NOW()
);
RETURN result;
EXCEPTION
WHEN OTHERS THEN
RAISE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

View File

@@ -47,8 +47,8 @@ INSERT INTO auth.users (
);
-- Create user profile
INSERT INTO public.user_profiles (id, email, status)
SELECT id, email, 'active'
INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
SELECT id, email, 'Alireza', 'Vaezi', 'active'
FROM auth.users
WHERE email = 's.alireza.v@gmail.com'
;
@@ -66,7 +66,443 @@ AND r.name = 'admin'
;
-- =============================================
-- 3. CREATE EXPERIMENT PHASES
-- 3. CREATE ADDITIONAL USERS
-- =============================================
-- Create Claire Floyd (Conductor & Data Recorder)
INSERT INTO auth.users (
instance_id,
id,
aud,
role,
email,
encrypted_password,
email_confirmed_at,
created_at,
updated_at,
confirmation_token,
email_change,
email_change_token_new,
recovery_token
) VALUES (
'00000000-0000-0000-0000-000000000000',
uuid_generate_v4(),
'authenticated',
'authenticated',
'Ashlyn.Floyd@uga.edu',
crypt('password123', gen_salt('bf')),
NOW(),
NOW(),
NOW(),
'',
'',
'',
''
);
INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
SELECT id, email, 'Claire', 'Floyd', 'active'
FROM auth.users
WHERE email = 'Ashlyn.Floyd@uga.edu'
;
INSERT INTO public.user_roles (user_id, role_id, assigned_by)
SELECT
up.id,
r.id,
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')
FROM public.user_profiles up
CROSS JOIN public.roles r
WHERE up.email = 'Ashlyn.Floyd@uga.edu'
AND r.name IN ('conductor', 'data recorder')
;
-- Create Bruna Dos-Santos (Conductor & Data Recorder)
INSERT INTO auth.users (
instance_id,
id,
aud,
role,
email,
encrypted_password,
email_confirmed_at,
created_at,
updated_at,
confirmation_token,
email_change,
email_change_token_new,
recovery_token
) VALUES (
'00000000-0000-0000-0000-000000000000',
uuid_generate_v4(),
'authenticated',
'authenticated',
'bkvsantos@uga.edu',
crypt('password123', gen_salt('bf')),
NOW(),
NOW(),
NOW(),
'',
'',
'',
''
);
INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
SELECT id, email, 'Bruna', 'Dos-Santos', 'active'
FROM auth.users
WHERE email = 'bkvsantos@uga.edu'
;
INSERT INTO public.user_roles (user_id, role_id, assigned_by)
SELECT
up.id,
r.id,
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')
FROM public.user_profiles up
CROSS JOIN public.roles r
WHERE up.email = 'bkvsantos@uga.edu'
AND r.name IN ('conductor', 'data recorder')
;
-- Create Beni Rodriguez (Conductor & Data Recorder)
INSERT INTO auth.users (
instance_id,
id,
aud,
role,
email,
encrypted_password,
email_confirmed_at,
created_at,
updated_at,
confirmation_token,
email_change,
email_change_token_new,
recovery_token
) VALUES (
'00000000-0000-0000-0000-000000000000',
uuid_generate_v4(),
'authenticated',
'authenticated',
'Beni.Rodriguez@uga.edu',
crypt('password123', gen_salt('bf')),
NOW(),
NOW(),
NOW(),
'',
'',
'',
''
);
INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
SELECT id, email, 'Beni', 'Rodriguez', 'active'
FROM auth.users
WHERE email = 'Beni.Rodriguez@uga.edu'
;
INSERT INTO public.user_roles (user_id, role_id, assigned_by)
SELECT
up.id,
r.id,
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')
FROM public.user_profiles up
CROSS JOIN public.roles r
WHERE up.email = 'Beni.Rodriguez@uga.edu'
AND r.name IN ('conductor', 'data recorder')
;
-- Create Brendan Surio (Data Recorder)
INSERT INTO auth.users (
instance_id,
id,
aud,
role,
email,
encrypted_password,
email_confirmed_at,
created_at,
updated_at,
confirmation_token,
email_change,
email_change_token_new,
recovery_token
) VALUES (
'00000000-0000-0000-0000-000000000000',
uuid_generate_v4(),
'authenticated',
'authenticated',
'Brendan.Surio@uga.edu',
crypt('password123', gen_salt('bf')),
NOW(),
NOW(),
NOW(),
'',
'',
'',
''
);
INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
SELECT id, email, 'Brendan', 'Surio', 'active'
FROM auth.users
WHERE email = 'Brendan.Surio@uga.edu'
;
INSERT INTO public.user_roles (user_id, role_id, assigned_by)
SELECT
up.id,
r.id,
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')
FROM public.user_profiles up
CROSS JOIN public.roles r
WHERE up.email = 'Brendan.Surio@uga.edu'
AND r.name = 'data recorder'
;
-- Create William Mcconnell (Data Recorder)
INSERT INTO auth.users (
instance_id,
id,
aud,
role,
email,
encrypted_password,
email_confirmed_at,
created_at,
updated_at,
confirmation_token,
email_change,
email_change_token_new,
recovery_token
) VALUES (
'00000000-0000-0000-0000-000000000000',
uuid_generate_v4(),
'authenticated',
'authenticated',
'William.McConnell@uga.edu',
crypt('password123', gen_salt('bf')),
NOW(),
NOW(),
NOW(),
'',
'',
'',
''
);
INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
SELECT id, email, 'William', 'Mcconnell', 'active'
FROM auth.users
WHERE email = 'William.McConnell@uga.edu'
;
INSERT INTO public.user_roles (user_id, role_id, assigned_by)
SELECT
up.id,
r.id,
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')
FROM public.user_profiles up
CROSS JOIN public.roles r
WHERE up.email = 'William.McConnell@uga.edu'
AND r.name = 'data recorder'
;
-- Create Camille Deguzman (Data Recorder)
INSERT INTO auth.users (
instance_id,
id,
aud,
role,
email,
encrypted_password,
email_confirmed_at,
created_at,
updated_at,
confirmation_token,
email_change,
email_change_token_new,
recovery_token
) VALUES (
'00000000-0000-0000-0000-000000000000',
uuid_generate_v4(),
'authenticated',
'authenticated',
'cpd08598@uga.edu',
crypt('password123', gen_salt('bf')),
NOW(),
NOW(),
NOW(),
'',
'',
'',
''
);
INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
SELECT id, email, 'Camille', 'Deguzman', 'active'
FROM auth.users
WHERE email = 'cpd08598@uga.edu'
;
INSERT INTO public.user_roles (user_id, role_id, assigned_by)
SELECT
up.id,
r.id,
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')
FROM public.user_profiles up
CROSS JOIN public.roles r
WHERE up.email = 'cpd08598@uga.edu'
AND r.name = 'data recorder'
;
-- Create Justin Hetzler (Data Recorder)
INSERT INTO auth.users (
instance_id,
id,
aud,
role,
email,
encrypted_password,
email_confirmed_at,
created_at,
updated_at,
confirmation_token,
email_change,
email_change_token_new,
recovery_token
) VALUES (
'00000000-0000-0000-0000-000000000000',
uuid_generate_v4(),
'authenticated',
'authenticated',
'Justin.Hetzler@uga.edu',
crypt('password123', gen_salt('bf')),
NOW(),
NOW(),
NOW(),
'',
'',
'',
''
);
INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
SELECT id, email, 'Justin', 'Hetzler', 'active'
FROM auth.users
WHERE email = 'Justin.Hetzler@uga.edu'
;
INSERT INTO public.user_roles (user_id, role_id, assigned_by)
SELECT
up.id,
r.id,
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')
FROM public.user_profiles up
CROSS JOIN public.roles r
WHERE up.email = 'Justin.Hetzler@uga.edu'
AND r.name = 'data recorder'
;
-- Create Joshua Wilson (Data Recorder)
INSERT INTO auth.users (
instance_id,
id,
aud,
role,
email,
encrypted_password,
email_confirmed_at,
created_at,
updated_at,
confirmation_token,
email_change,
email_change_token_new,
recovery_token
) VALUES (
'00000000-0000-0000-0000-000000000000',
uuid_generate_v4(),
'authenticated',
'authenticated',
'jdw58940@uga.edu',
crypt('password123', gen_salt('bf')),
NOW(),
NOW(),
NOW(),
'',
'',
'',
''
);
INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
SELECT id, email, 'Joshua', 'Wilson', 'active'
FROM auth.users
WHERE email = 'jdw58940@uga.edu'
;
INSERT INTO public.user_roles (user_id, role_id, assigned_by)
SELECT
up.id,
r.id,
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')
FROM public.user_profiles up
CROSS JOIN public.roles r
WHERE up.email = 'jdw58940@uga.edu'
AND r.name = 'data recorder'
;
-- Create Sydney Orlofsky (Data Recorder)
INSERT INTO auth.users (
instance_id,
id,
aud,
role,
email,
encrypted_password,
email_confirmed_at,
created_at,
updated_at,
confirmation_token,
email_change,
email_change_token_new,
recovery_token
) VALUES (
'00000000-0000-0000-0000-000000000000',
uuid_generate_v4(),
'authenticated',
'authenticated',
'Sydney.Orlofsky@uga.edu',
crypt('password123', gen_salt('bf')),
NOW(),
NOW(),
NOW(),
'',
'',
'',
''
);
INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
SELECT id, email, 'Sydney', 'Orlofsky', 'active'
FROM auth.users
WHERE email = 'Sydney.Orlofsky@uga.edu'
;
INSERT INTO public.user_roles (user_id, role_id, assigned_by)
SELECT
up.id,
r.id,
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')
FROM public.user_profiles up
CROSS JOIN public.roles r
WHERE up.email = 'Sydney.Orlofsky@uga.edu'
AND r.name = 'data recorder'
;
-- =============================================
-- 4. CREATE EXPERIMENT PHASES
-- =============================================
-- Create "Phase 2 of JC Experiments" phase
@@ -80,7 +516,7 @@ WHERE up.email = 's.alireza.v@gmail.com'
;
-- =============================================
-- 4. INSERT EXPERIMENTS (First 10 as example)
-- 5. INSERT EXPERIMENTS (First 10 as example)
-- =============================================
INSERT INTO public.experiments (
@@ -130,7 +566,7 @@ INSERT INTO public.experiments (
;
-- =============================================
-- 5. CREATE SAMPLE EXPERIMENT REPETITIONS
-- 6. CREATE SAMPLE EXPERIMENT REPETITIONS
-- =============================================
-- Create repetitions for first 5 experiments as examples