Merge branch 'main' into db-fixes-and-patches

This commit is contained in:
Alireza Vaezi
2026-03-09 12:50:57 -04:00
committed by GitHub
30 changed files with 2277 additions and 33 deletions

View File

@@ -10,3 +10,10 @@ VITE_VISION_SYSTEM_REMOTE_URL=http://exp-dash:3002/assets/remoteEntry.js?v=$(dat
# API URLs
VITE_VISION_API_URL=http://exp-dash:8000
VITE_MEDIA_API_URL=http://exp-dash:8090
# Supabase Configuration
VITE_SUPABASE_URL=https://your-project-url.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key
# Microsoft Entra (Azure AD) OAuth Configuration
VITE_ENABLE_MICROSOFT_LOGIN=true

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { supabase } from './lib/supabase'
import { supabase, userManagement } from './lib/supabase'
import { Login } from './components/Login'
import { Dashboard } from './components/Dashboard'
import { CameraRoute } from './components/CameraRoute'
@@ -14,11 +14,18 @@ function App() {
checkAuthState()
// Listen for auth changes
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
const { data: { subscription } } = supabase.auth.onAuthStateChange((event: string, session: any) => {
console.log('Auth state changed:', event, !!session)
setIsAuthenticated(!!session)
setLoading(false)
// Sync OAuth user on successful sign in (creates user profile if needed)
if ((event === 'SIGNED_IN' || event === 'INITIAL_SESSION') && session) {
userManagement.syncOAuthUser().catch((err) => {
console.error('Failed to sync OAuth user:', err)
})
}
// Handle signout route
if (event === 'SIGNED_OUT') {
setCurrentRoute('/')

View File

@@ -1,5 +1,6 @@
import { useState } from 'react'
import { supabase } from '../lib/supabase'
import { MicrosoftIcon } from './MicrosoftIcon'
interface LoginProps {
onLoginSuccess: () => void
@@ -10,6 +11,7 @@ export function Login({ onLoginSuccess }: LoginProps) {
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const enableMicrosoftLogin = import.meta.env.VITE_ENABLE_MICROSOFT_LOGIN === 'true'
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
@@ -35,6 +37,32 @@ export function Login({ onLoginSuccess }: LoginProps) {
}
}
const handleMicrosoftLogin = async () => {
setLoading(true)
setError(null)
try {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'azure',
options: {
scopes: 'email openid profile',
redirectTo: `${window.location.origin}/`,
},
})
if (error) {
setError(error.message)
setLoading(false)
}
// If successful, user will be redirected to Microsoft login
// and then back to the app, so we don't stop loading here
} catch (err) {
setError('An unexpected error occurred during Microsoft login')
console.error('Microsoft login error:', err)
setLoading(false)
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<div className="max-w-md w-full space-y-8">
@@ -108,6 +136,33 @@ export function Login({ onLoginSuccess }: LoginProps) {
</button>
</div>
</form>
{enableMicrosoftLogin && (
<>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300 dark:border-gray-700" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-gray-50 dark:bg-gray-900 text-gray-500 dark:text-gray-400">
Or continue with
</span>
</div>
</div>
<div>
<button
type="button"
onClick={handleMicrosoftLogin}
disabled={loading}
className="w-full flex items-center justify-center gap-3 py-2 px-4 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
<MicrosoftIcon className="w-5 h-5" />
<span>Sign in with Microsoft</span>
</button>
</div>
</>
)}
</div>
</div>
)

View File

@@ -0,0 +1,11 @@
export function MicrosoftIcon({ className = "w-5 h-5" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 23 23" xmlns="http://www.w3.org/2000/svg">
<path fill="#f3f3f3" d="M0 0h23v23H0z" />
<path fill="#f35325" d="M1 1h10v10H1z" />
<path fill="#81bc06" d="M12 1h10v10H12z" />
<path fill="#05a6f0" d="M1 12h10v10H1z" />
<path fill="#ffba08" d="M12 12h10v10H12z" />
</svg>
)
}

View File

@@ -628,6 +628,60 @@ export const userManagement = {
if (error) throw error
return data
},
// Sync OAuth user - ensures user profile exists for OAuth-authenticated users
async syncOAuthUser(): Promise<void> {
try {
const { data: { user: authUser }, error: authError } = await supabase.auth.getUser()
if (authError || !authUser) {
console.warn('No authenticated user found for OAuth sync')
return
}
// Check if user profile already exists
const { data: existingProfile, error: checkError } = await supabase
.from('user_profiles')
.select('id')
.eq('id', authUser.id)
.single()
// If profile already exists, no need to create it
if (existingProfile && !checkError) {
console.log('User profile already exists for user:', authUser.id)
return
}
// If error is not "no rows returned", it's a real error
if (checkError && checkError.code !== 'PGRST116') {
console.error('Error checking for existing profile:', checkError)
return
}
// Create user profile for new OAuth user
const { error: insertError } = await supabase
.from('user_profiles')
.insert({
id: authUser.id,
email: authUser.email || '',
status: 'active'
})
if (insertError) {
// Ignore "duplicate key value" errors in case of race condition
if (insertError.code === '23505') {
console.log('User profile was already created (race condition handled)')
return
}
console.error('Error creating user profile for OAuth user:', insertError)
return
}
console.log('Successfully created user profile for OAuth user:', authUser.id)
} catch (error) {
console.error('Unexpected error in syncOAuthUser:', error)
}
}
}

View File

@@ -4,6 +4,7 @@ interface ImportMetaEnv {
readonly VITE_SUPABASE_URL: string;
readonly VITE_SUPABASE_ANON_KEY: string;
readonly VITE_VISION_API_URL?: string; // optional; defaults to "/api" via vite proxy
readonly VITE_ENABLE_MICROSOFT_LOGIN?: string; // optional; enable Microsoft Entra authentication
}
interface ImportMeta {

View File

@@ -0,0 +1,46 @@
-- OAuth User Synchronization
-- This migration adds functionality to automatically create user profiles when users sign up via OAuth
-- =============================================
-- 1. CREATE FUNCTION FOR OAUTH USER AUTO-PROFILE CREATION
-- =============================================
CREATE OR REPLACE FUNCTION public.handle_new_oauth_user()
RETURNS TRIGGER AS $$
BEGIN
-- Check if user profile already exists
IF NOT EXISTS (
SELECT 1 FROM public.user_profiles WHERE id = NEW.id
) THEN
-- Create user profile with default active status
INSERT INTO public.user_profiles (id, email, status)
VALUES (
NEW.id,
NEW.email,
'active'
)
ON CONFLICT (id) DO NOTHING;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- =============================================
-- 2. CREATE TRIGGER FOR NEW AUTH USERS
-- =============================================
-- Drop the trigger if it exists to avoid conflicts
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
-- Create trigger that fires after a new user is created in auth.users
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_oauth_user();
-- =============================================
-- 3. COMMENT FOR DOCUMENTATION
-- =============================================
COMMENT ON FUNCTION public.handle_new_oauth_user() IS
'Automatically creates a user profile in public.user_profiles when a new user is created via OAuth in auth.users. This ensures OAuth users are immediately accessible in the application without manual provisioning.';