From 44c8c3f6dd7008b2a4338f085cf00f33338ae1fa Mon Sep 17 00:00:00 2001 From: salirezav Date: Mon, 22 Sep 2025 11:20:15 -0400 Subject: [PATCH] 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. --- .env.example | 2 +- .gitignore | 1 + .../ai_agent/guides/AI_INTEGRATION_GUIDE.md | 4 +- .../ai_agent/references/api-endpoints.http | 4 +- camera-management-api/setup_timezone.sh | 2 +- database_schema.md | 66 ++- management-dashboard-web-app/.env.backup | 4 + .../CAMERA_ROUTE_IMPLEMENTATION.md | 2 + .../public/camera-test.html | 2 + .../src/components/CameraRoute.tsx | 2 + .../src/components/DashboardLayout.tsx | 6 +- .../src/components/LiveCameraView.tsx | 2 + .../src/components/TopNavbar.tsx | 28 +- .../src/components/UserManagement.tsx | 47 +- .../src/components/UserProfile.tsx | 312 ++++++++++++ .../src/lib/supabase.ts | 27 ++ .../supabase/.temp/cli-latest | 2 +- .../supabase/config.toml | 6 +- ...50102000001_add_conductor_availability.sql | 341 +++++++++++++ ...0103000001_add_password_reset_function.sql | 49 ++ .../20250103000002_add_user_names.sql | 12 + ...103000003_add_change_password_function.sql | 62 +++ .../supabase/seed.sql | 446 +++++++++++++++++- 23 files changed, 1398 insertions(+), 31 deletions(-) create mode 100755 management-dashboard-web-app/.env.backup create mode 100644 management-dashboard-web-app/src/components/UserProfile.tsx create mode 100644 management-dashboard-web-app/supabase/migrations/20250102000001_add_conductor_availability.sql create mode 100644 management-dashboard-web-app/supabase/migrations/20250103000001_add_password_reset_function.sql create mode 100644 management-dashboard-web-app/supabase/migrations/20250103000002_add_user_names.sql create mode 100644 management-dashboard-web-app/supabase/migrations/20250103000003_add_change_password_function.sql diff --git a/.env.example b/.env.example index d3e1525..e2ce5f2 100644 --- a/.env.example +++ b/.env.example @@ -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) diff --git a/.gitignore b/.gitignore index 3c3dbed..0937301 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/camera-management-api/ai_agent/guides/AI_INTEGRATION_GUIDE.md b/camera-management-api/ai_agent/guides/AI_INTEGRATION_GUIDE.md index a10f096..64e0172 100644 --- a/camera-management-api/ai_agent/guides/AI_INTEGRATION_GUIDE.md +++ b/camera-management-api/ai_agent/guides/AI_INTEGRATION_GUIDE.md @@ -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 diff --git a/camera-management-api/ai_agent/references/api-endpoints.http b/camera-management-api/ai_agent/references/api-endpoints.http index 0d3336d..0e23aa6 100644 --- a/camera-management-api/ai_agent/references/api-endpoints.http +++ b/camera-management-api/ai_agent/references/api-endpoints.http @@ -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 diff --git a/camera-management-api/setup_timezone.sh b/camera-management-api/setup_timezone.sh index 83a5e49..3fbfcc8 100755 --- a/camera-management-api/setup_timezone.sh +++ b/camera-management-api/setup_timezone.sh @@ -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 diff --git a/database_schema.md b/database_schema.md index e36da75..0fffca0 100644 --- a/database_schema.md +++ b/database_schema.md @@ -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. diff --git a/management-dashboard-web-app/.env.backup b/management-dashboard-web-app/.env.backup new file mode 100755 index 0000000..4ee0560 --- /dev/null +++ b/management-dashboard-web-app/.env.backup @@ -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] + diff --git a/management-dashboard-web-app/CAMERA_ROUTE_IMPLEMENTATION.md b/management-dashboard-web-app/CAMERA_ROUTE_IMPLEMENTATION.md index 66ee35d..a515d80 100755 --- a/management-dashboard-web-app/CAMERA_ROUTE_IMPLEMENTATION.md +++ b/management-dashboard-web-app/CAMERA_ROUTE_IMPLEMENTATION.md @@ -216,3 +216,5 @@ If you encounter issues: + + diff --git a/management-dashboard-web-app/public/camera-test.html b/management-dashboard-web-app/public/camera-test.html index 9404112..93ef557 100755 --- a/management-dashboard-web-app/public/camera-test.html +++ b/management-dashboard-web-app/public/camera-test.html @@ -125,3 +125,5 @@ + + diff --git a/management-dashboard-web-app/src/components/CameraRoute.tsx b/management-dashboard-web-app/src/components/CameraRoute.tsx index 9172e8b..aa0d688 100755 --- a/management-dashboard-web-app/src/components/CameraRoute.tsx +++ b/management-dashboard-web-app/src/components/CameraRoute.tsx @@ -31,3 +31,5 @@ export function CameraRoute({ cameraNumber }: CameraRouteProps) { + + diff --git a/management-dashboard-web-app/src/components/DashboardLayout.tsx b/management-dashboard-web-app/src/components/DashboardLayout.tsx index 9e1723d..778d05a 100755 --- a/management-dashboard-web-app/src/components/DashboardLayout.tsx +++ b/management-dashboard-web-app/src/components/DashboardLayout.tsx @@ -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 case 'video-library': return + case 'profile': + return default: return } @@ -274,6 +277,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps currentView={currentView} onToggleSidebar={handleToggleSidebar} isSidebarOpen={isMobileOpen} + onNavigateToProfile={() => handleViewChange('profile')} />
{renderCurrentView()} diff --git a/management-dashboard-web-app/src/components/LiveCameraView.tsx b/management-dashboard-web-app/src/components/LiveCameraView.tsx index 990a8f4..72c4867 100755 --- a/management-dashboard-web-app/src/components/LiveCameraView.tsx +++ b/management-dashboard-web-app/src/components/LiveCameraView.tsx @@ -140,3 +140,5 @@ export function LiveCameraView({ cameraName }: LiveCameraViewProps) { + + diff --git a/management-dashboard-web-app/src/components/TopNavbar.tsx b/management-dashboard-web-app/src/components/TopNavbar.tsx index b5cb594..e5f10c6 100755 --- a/management-dashboard-web-app/src/components/TopNavbar.tsx +++ b/management-dashboard-web-app/src/components/TopNavbar.tsx @@ -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({ >
- {user.email.charAt(0).toUpperCase()} + {(user.first_name || user.email).charAt(0).toUpperCase()}
- {user.email.split('@')[0]} + + {user.first_name && user.last_name + ? `${user.first_name} ${user.last_name}` + : user.email.split('@')[0] + } +
- {user.email.split('@')[0]} + {user.first_name && user.last_name + ? `${user.first_name} ${user.last_name}` + : user.email.split('@')[0] + } {user.email} @@ -183,7 +193,13 @@ export function TopNavbar({
  • -
    +
    +
  • diff --git a/management-dashboard-web-app/src/components/UserManagement.tsx b/management-dashboard-web-app/src/components/UserManagement.tsx index a315a24..93083fa 100755 --- a/management-dashboard-web-app/src/components/UserManagement.tsx +++ b/management-dashboard-web-app/src/components/UserManagement.tsx @@ -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({
    ) : ( - +
    + + +
    )} diff --git a/management-dashboard-web-app/src/components/UserProfile.tsx b/management-dashboard-web-app/src/components/UserProfile.tsx new file mode 100644 index 0000000..7d4115a --- /dev/null +++ b/management-dashboard-web-app/src/components/UserProfile.tsx @@ -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(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 ( +
    +
    +
    + +
    +
    +

    Change Password

    + +
    + +
    +
    + + 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} + /> +
    + +
    + + 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} + /> +
    + +
    + + 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} + /> +
    +
    + + {error && ( +
    +

    {error}

    +
    + )} + +
    + + +
    +
    +
    +
    + ) +} + +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 ( +
    +
    + {/* Header */} +
    +
    +
    +

    User Profile

    +

    Manage your account information and settings

    +
    +
    +
    + {(user.first_name || user.email).charAt(0).toUpperCase()} +
    +
    +
    +
    + + {/* Profile Information */} +
    +
    + {/* Basic Information */} +
    +

    Basic Information

    + +
    + +
    + {user.first_name && user.last_name + ? `${user.first_name} ${user.last_name}` + : 'Name not set' + } +
    +
    + +
    + +
    + {user.email} +
    +
    + +
    + +
    + + {user.status.charAt(0).toUpperCase() + user.status.slice(1)} + +
    +
    + +
    + +
    + {formatDate(user.created_at)} +
    +
    +
    + + {/* Roles and Permissions */} +
    +

    Roles & Permissions

    + +
    + +
    + {user.roles.map((role) => ( + + {role.charAt(0).toUpperCase() + role.slice(1)} + + ))} +
    +
    + +
    + +
    + {formatDate(user.updated_at)} +
    +
    +
    +
    + + {/* Actions */} +
    +

    Account Actions

    + +
    + +
    +
    +
    +
    + + {/* Change Password Modal */} + setShowChangePasswordModal(false)} + onSuccess={() => { + // Could add success notification here + }} + /> +
    + ) +} diff --git a/management-dashboard-web-app/src/lib/supabase.ts b/management-dashboard-web-app/src/lib/supabase.ts index a7555de..e8071fe 100755 --- a/management-dashboard-web-app/src/lib/supabase.ts +++ b/management-dashboard-web-app/src/lib/supabase.ts @@ -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 } } diff --git a/management-dashboard-web-app/supabase/.temp/cli-latest b/management-dashboard-web-app/supabase/.temp/cli-latest index 322987f..75788de 100755 --- a/management-dashboard-web-app/supabase/.temp/cli-latest +++ b/management-dashboard-web-app/supabase/.temp/cli-latest @@ -1 +1 @@ -v2.34.3 \ No newline at end of file +v2.40.7 \ No newline at end of file diff --git a/management-dashboard-web-app/supabase/config.toml b/management-dashboard-web-app/supabase/config.toml index 2848976..812a04a 100755 --- a/management-dashboard-web-app/supabase/config.toml +++ b/management-dashboard-web-app/supabase/config.toml @@ -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. diff --git a/management-dashboard-web-app/supabase/migrations/20250102000001_add_conductor_availability.sql b/management-dashboard-web-app/supabase/migrations/20250102000001_add_conductor_availability.sql new file mode 100644 index 0000000..96c6f0d --- /dev/null +++ b/management-dashboard-web-app/supabase/migrations/20250102000001_add_conductor_availability.sql @@ -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'; + + diff --git a/management-dashboard-web-app/supabase/migrations/20250103000001_add_password_reset_function.sql b/management-dashboard-web-app/supabase/migrations/20250103000001_add_password_reset_function.sql new file mode 100644 index 0000000..c7bb98b --- /dev/null +++ b/management-dashboard-web-app/supabase/migrations/20250103000001_add_password_reset_function.sql @@ -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; + diff --git a/management-dashboard-web-app/supabase/migrations/20250103000002_add_user_names.sql b/management-dashboard-web-app/supabase/migrations/20250103000002_add_user_names.sql new file mode 100644 index 0000000..46d2b71 --- /dev/null +++ b/management-dashboard-web-app/supabase/migrations/20250103000002_add_user_names.sql @@ -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'; + diff --git a/management-dashboard-web-app/supabase/migrations/20250103000003_add_change_password_function.sql b/management-dashboard-web-app/supabase/migrations/20250103000003_add_change_password_function.sql new file mode 100644 index 0000000..5942e5c --- /dev/null +++ b/management-dashboard-web-app/supabase/migrations/20250103000003_add_change_password_function.sql @@ -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; + diff --git a/management-dashboard-web-app/supabase/seed.sql b/management-dashboard-web-app/supabase/seed.sql index 79b6a2f..8a2134c 100755 --- a/management-dashboard-web-app/supabase/seed.sql +++ b/management-dashboard-web-app/supabase/seed.sql @@ -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