From 02d0ca14a3749d31310a39400bdfb81c710597ed Mon Sep 17 00:00:00 2001 From: Alphha Date: Sat, 14 Mar 2026 00:03:12 +0530 Subject: [PATCH 01/36] feat: migrate from SQLite/JWT to Supabase (Auth, DB, Storage) Complete migration of TaskPulse AI backend and frontend to Supabase: Backend: - Replace JWT+bcrypt auth with Supabase Auth (email/password + Google OAuth) - Replace SQLite with PostgreSQL via Supabase connection pooler (Supavisor) - Add SSL context for pooler self-signed certs in database.py - Add Supabase client (supabase_client.py) and storage service - Remove Session model and related schemas (Supabase manages sessions) - Update all models to use native PostgreSQL types (JSONB, UUID, etc.) - Remove redundant SQLite column defaults and adapt enum handling - Add verify_supabase_token() for JWT verification in dependencies.py - Update WebSocket auth in chat.py to use Supabase tokens - Add migration script and full PostgreSQL DDL schema (39 tables) - Update requirements.txt with supabase, asyncpg, python-jose deps Frontend: - Add Supabase JS client (supabase.ts) and AuthCallback page - Update authStore to use Supabase Auth for login/signup/logout - Update LoginPage and SignupPage for Supabase email+Google OAuth - Remove google.d.ts (no longer needed with Supabase OAuth) - Update api-client to use Supabase session tokens Co-Authored-By: Claude Opus 4.6 --- Frontend/.env.example | 3 + Frontend/package.json | 1 + Frontend/src/App.tsx | 4 + Frontend/src/google.d.ts | 13 - Frontend/src/lib/api-client.ts | 23 +- Frontend/src/lib/supabase.ts | 12 + Frontend/src/pages/AuthCallback.tsx | 32 + Frontend/src/pages/LoginPage.tsx | 61 +- Frontend/src/pages/SignupPage.tsx | 59 +- Frontend/src/services/auth.service.ts | 42 - Frontend/src/store/authStore.ts | 102 +- Frontend/src/types/api.ts | 13 +- backend/.env.example | 19 +- backend/app/api/v1/ai_unblock.py | 37 +- backend/app/api/v1/auth.py | 357 +--- backend/app/api/v1/chat.py | 6 +- backend/app/api/v1/dependencies.py | 39 +- backend/app/config.py | 18 +- backend/app/core/middleware.py | 1 - backend/app/core/security.py | 386 ++-- backend/app/database.py | 64 +- backend/app/models/__init__.py | 4 +- backend/app/models/agent.py | 75 +- backend/app/models/audit.py | 67 +- backend/app/models/automation.py | 131 +- backend/app/models/checkin.py | 35 +- backend/app/models/knowledge_base.py | 113 +- backend/app/models/notification.py | 76 +- backend/app/models/organization.py | 16 +- backend/app/models/prediction.py | 43 +- backend/app/models/skill.py | 152 +- backend/app/models/task.py | 94 +- backend/app/models/user.py | 81 +- backend/app/models/workforce.py | 40 +- backend/app/schemas/__init__.py | 2 +- backend/app/schemas/user.py | 18 - backend/app/services/auth_service.py | 474 ++--- backend/app/services/storage_service.py | 110 ++ backend/app/services/unblock_service.py | 8 +- backend/app/supabase_client.py | 35 + backend/requirements.txt | 13 +- backend/schema.sql | 1608 +++++++++++++++++ backend/scripts/migrate_to_supabase.py | 707 ++++++++ backend/supabase/.gitignore | 8 + backend/supabase/config.toml | 384 ++++ .../20260313000000_initial_schema.sql | 1607 ++++++++++++++++ 46 files changed, 5390 insertions(+), 1803 deletions(-) create mode 100644 Frontend/.env.example delete mode 100644 Frontend/src/google.d.ts create mode 100644 Frontend/src/lib/supabase.ts create mode 100644 Frontend/src/pages/AuthCallback.tsx create mode 100644 backend/app/services/storage_service.py create mode 100644 backend/app/supabase_client.py create mode 100644 backend/schema.sql create mode 100644 backend/scripts/migrate_to_supabase.py create mode 100644 backend/supabase/.gitignore create mode 100644 backend/supabase/config.toml create mode 100644 backend/supabase/migrations/20260313000000_initial_schema.sql diff --git a/Frontend/.env.example b/Frontend/.env.example new file mode 100644 index 0000000..e9f1684 --- /dev/null +++ b/Frontend/.env.example @@ -0,0 +1,3 @@ +# Supabase Configuration +VITE_SUPABASE_URL=https://your-project.supabase.co +VITE_SUPABASE_ANON_KEY=your-anon-key diff --git a/Frontend/package.json b/Frontend/package.json index 2c18701..6aab7eb 100644 --- a/Frontend/package.json +++ b/Frontend/package.json @@ -15,6 +15,7 @@ "dependencies": { "@gsap/react": "^2.1.2", "@hookform/resolvers": "^5.2.2", + "@supabase/supabase-js": "^2.49.0", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-aspect-ratio": "^1.1.8", diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index 08b1796..6baf945 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -24,6 +24,7 @@ import SkillsPage from './pages/SkillsPage'; import PredictionsPage from './pages/PredictionsPage'; import WorkforcePage from './pages/WorkforcePage'; import IntegrationsPage from './pages/IntegrationsPage'; +import AuthCallback from './pages/AuthCallback'; // Components import { Toaster } from '@/components/ui/sonner'; @@ -73,6 +74,8 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) { function App() { useEffect(() => { initTheme(); + const unsubscribe = useAuthStore.getState().initAuth(); + return () => { unsubscribe(); }; }, []); return ( @@ -85,6 +88,7 @@ function App() { } /> } /> } /> + } /> {/* Protected Routes */} void }): void; - renderButton(element: HTMLElement, options: Record): void; - prompt(): void; -} - -interface Window { - google?: { - accounts: { - id: GoogleAccountsId; - }; - }; -} diff --git a/Frontend/src/lib/api-client.ts b/Frontend/src/lib/api-client.ts index c831600..765f563 100644 --- a/Frontend/src/lib/api-client.ts +++ b/Frontend/src/lib/api-client.ts @@ -15,7 +15,7 @@ */ import axios from 'axios'; -import type { ApiTokenResponse } from '@/types/api'; +import { supabase } from '@/lib/supabase'; const AUTH_STORAGE_KEY = 'taskpulse-auth'; @@ -146,19 +146,16 @@ apiClient.interceptors.response.use( originalRequest._retry = true; isRefreshing = true; - const { refreshToken } = getTokens(); - if (!refreshToken) { - clearAuth(); - return Promise.reject(error); - } - try { - const { data } = await axios.post('/api/v1/auth/refresh', { - refresh_token: refreshToken, - }); - setTokens(data.access_token, data.refresh_token); - processQueue(null, data.access_token); - originalRequest.headers.Authorization = `Bearer ${data.access_token}`; + const { data: refreshData, error: refreshErr } = await supabase.auth.refreshSession(); + if (refreshErr || !refreshData.session) { + throw refreshErr || new Error('No session after refresh'); + } + const newAccessToken = refreshData.session.access_token; + const newRefreshToken = refreshData.session.refresh_token; + setTokens(newAccessToken, newRefreshToken); + processQueue(null, newAccessToken); + originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; return apiClient(originalRequest); } catch (refreshError) { processQueue(refreshError, null); diff --git a/Frontend/src/lib/supabase.ts b/Frontend/src/lib/supabase.ts new file mode 100644 index 0000000..9459512 --- /dev/null +++ b/Frontend/src/lib/supabase.ts @@ -0,0 +1,12 @@ +import { createClient } from '@supabase/supabase-js'; + +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; +const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; + +if (!supabaseUrl || !supabaseAnonKey) { + throw new Error( + 'Missing Supabase environment variables. Set VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY in your .env file.' + ); +} + +export const supabase = createClient(supabaseUrl, supabaseAnonKey); diff --git a/Frontend/src/pages/AuthCallback.tsx b/Frontend/src/pages/AuthCallback.tsx new file mode 100644 index 0000000..277e950 --- /dev/null +++ b/Frontend/src/pages/AuthCallback.tsx @@ -0,0 +1,32 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Loader2 } from 'lucide-react'; + +/** + * OAuth callback page. + * + * After Supabase redirects back from Google OAuth, the Supabase client + * automatically exchanges the code/hash for a session. The + * onAuthStateChange listener in authStore handles the rest (setting + * user + tokens in Zustand). We just wait briefly and redirect. + */ +export default function AuthCallback() { + const navigate = useNavigate(); + + useEffect(() => { + // Give onAuthStateChange a moment to fire, then redirect + const timer = setTimeout(() => { + navigate('/dashboard', { replace: true }); + }, 1500); + return () => clearTimeout(timer); + }, [navigate]); + + return ( +
+
+ +

Completing sign in...

+
+
+ ); +} diff --git a/Frontend/src/pages/LoginPage.tsx b/Frontend/src/pages/LoginPage.tsx index fbe94e3..14637ae 100644 --- a/Frontend/src/pages/LoginPage.tsx +++ b/Frontend/src/pages/LoginPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState } from 'react'; import { motion } from 'framer-motion'; import { Link, useNavigate } from 'react-router-dom'; import { useMutation } from '@tanstack/react-query'; @@ -29,8 +29,6 @@ import { authService } from '@/services/auth.service'; import { getApiErrorMessage } from '@/lib/api-client'; import { toast } from 'sonner'; -const GOOGLE_CLIENT_ID = '1058266717863-cajvn6yp11006rd62pl641c1009m50vc.apps.googleusercontent.com'; - function ForgotPasswordDialog() { const [email, setEmail] = useState(''); const [open, setOpen] = useState(false); @@ -94,49 +92,14 @@ function ForgotPasswordDialog() { export default function LoginPage() { const navigate = useNavigate(); - const { login, googleLogin, isLoading } = useAuthStore(); + const { login, oauthLogin, isLoading } = useAuthStore(); const [showPassword, setShowPassword] = useState(false); - const googleBtnRef = useRef(null); const [formData, setFormData] = useState({ email: '', password: '', rememberMe: false, }); - // Load Google Identity Services script and render button - useEffect(() => { - const script = document.createElement('script'); - script.src = 'https://accounts.google.com/gsi/client'; - script.async = true; - script.defer = true; - script.onload = () => { - if (window.google && googleBtnRef.current) { - window.google.accounts.id.initialize({ - client_id: GOOGLE_CLIENT_ID, - callback: handleGoogleResponse, - }); - window.google.accounts.id.renderButton(googleBtnRef.current, { - theme: 'outline', - size: 'large', - width: googleBtnRef.current.offsetWidth, - text: 'signin_with', - }); - } - }; - document.head.appendChild(script); - return () => { script.remove(); }; - }, []); // eslint-disable-line react-hooks/exhaustive-deps - - const handleGoogleResponse = async (response: { credential: string }) => { - try { - await googleLogin(response.credential); - toast.success('Welcome!'); - navigate('/dashboard'); - } catch (error) { - toast.error(getApiErrorMessage(error)); - } - }; - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -180,7 +143,25 @@ export default function LoginPage() { {/* Social Login */}
-
+