diff --git a/apps/api/turbo.json b/apps/api/turbo.json index 8e5097806..13c506cbe 100644 --- a/apps/api/turbo.json +++ b/apps/api/turbo.json @@ -8,18 +8,18 @@ "inputs": ["server/**/*", "libs/**/*", "Cargo.toml", "Cargo.lock"] }, "start": { - "dependsOn": ["@buster/database#start", "build"], + "dependsOn": ["@buster-app/supabase#start", "build"], "cache": true, "persistent": true }, "dev": { - "dependsOn": ["@buster/database#start"], + "dependsOn": ["@buster-app/supabase#start"], "cache": false, "persistent": true, "outputs": [] }, "dev:fast": { - "dependsOn": ["@buster/database#start"], + "dependsOn": ["@buster-app/supabase#start"], "cache": false, "persistent": true, "outputs": [] diff --git a/apps/electric-server/turbo.json b/apps/electric-server/turbo.json index 0405baaa1..d49bdf216 100644 --- a/apps/electric-server/turbo.json +++ b/apps/electric-server/turbo.json @@ -3,19 +3,19 @@ "extends": ["//"], "tasks": { "start": { - "dependsOn": ["@buster/database#start"], + "dependsOn": ["@buster-app/supabase#start"], "cache": true, "persistent": false, "outputs": [] }, "dev": { - "dependsOn": ["@buster/database#start"], + "dependsOn": ["@buster-app/supabase#start"], "cache": false, "persistent": true, "outputs": [] }, "dev:fast": { - "dependsOn": ["@buster/database#start"], + "dependsOn": ["@buster-app/supabase#start"], "cache": false, "persistent": true, "outputs": [] diff --git a/apps/server/turbo.json b/apps/server/turbo.json index d6f6ffeb7..0d20fa7f3 100644 --- a/apps/server/turbo.json +++ b/apps/server/turbo.json @@ -7,14 +7,14 @@ "outputs": ["dist/**"] }, "start": { - "dependsOn": ["@buster/database#start", "build"], + "dependsOn": ["@buster-app/supabase#start", "build"], "cache": true, "persistent": true }, "dev": { "cache": false, "persistent": true, - "dependsOn": ["@buster/database#start", "^build"], + "dependsOn": ["@buster-app/supabase#start", "^build"], "with": [ "@buster/ai#dev", "@buster/server-shared#dev", @@ -28,7 +28,7 @@ "dev:fast": { "cache": false, "persistent": true, - "dependsOn": ["@buster/database#start", "^build"], + "dependsOn": ["@buster-app/supabase#start", "^build"], "with": [ "@buster/ai#dev:fast", "@buster/server-shared#dev:fast", diff --git a/apps/supabase/.branches/_current_branch b/apps/supabase/.branches/_current_branch new file mode 100644 index 000000000..88d050b19 --- /dev/null +++ b/apps/supabase/.branches/_current_branch @@ -0,0 +1 @@ +main \ No newline at end of file diff --git a/apps/supabase/.gitignore b/apps/supabase/.gitignore index a1e9dc61e..dac49e4e5 100644 --- a/apps/supabase/.gitignore +++ b/apps/supabase/.gitignore @@ -3,3 +3,4 @@ volumes/storage .env test.http docker-compose.override.yml +.temp diff --git a/packages/database/supabase/config.toml b/apps/supabase/config.toml similarity index 100% rename from packages/database/supabase/config.toml rename to apps/supabase/config.toml diff --git a/apps/supabase/package.json b/apps/supabase/package.json index cd92635ac..7eed7e343 100644 --- a/apps/supabase/package.json +++ b/apps/supabase/package.json @@ -2,12 +2,12 @@ "name": "@buster-app/supabase", "version": "0.0.1", "private": false, - "dependencies": { - "@buster/database": "workspace:*" - }, + "dependencies": {}, "scripts": { "lint": "biome check . --write", "lint:fix": "biome check . --write", - "start": "echo 'Please run this command through Turbo from the root: turbo start'" + "start": "supabase start", + "stop": "supabase stop", + "reset": "supabase stop && supabase start && supabase db reset" } } diff --git a/apps/supabase/turbo.json b/apps/supabase/turbo.json index 39c113429..9bd73d87f 100644 --- a/apps/supabase/turbo.json +++ b/apps/supabase/turbo.json @@ -3,7 +3,19 @@ "extends": ["//"], "tasks": { "start": { - "dependsOn": ["@buster/database#start"], + "dependsOn": [], + "cache": false, + "persistent": false, + "outputs": [] + }, + "stop": { + "dependsOn": [], + "cache": false, + "persistent": false, + "outputs": [] + }, + "reset": { + "dependsOn": ["stop"], "cache": false, "persistent": false, "outputs": [] diff --git a/apps/trigger/turbo.json b/apps/trigger/turbo.json index ef70b40d6..06cd75cba 100644 --- a/apps/trigger/turbo.json +++ b/apps/trigger/turbo.json @@ -15,8 +15,8 @@ "dev": { "cache": false, "persistent": true, - "dependsOn": ["^build"], - "with": ["@buster-app/supabase#start"] + "dependsOn": ["^build","@buster-app/supabase#start"], + "with": ["@buster/database#dev"] }, "dev:fast": { "cache": false, diff --git a/apps/web/src/components/features/auth/ResetEmailForm.tsx b/apps/web/src/components/features/auth/ResetEmailForm.tsx index 5a07306d9..eda314dff 100644 --- a/apps/web/src/components/features/auth/ResetEmailForm.tsx +++ b/apps/web/src/components/features/auth/ResetEmailForm.tsx @@ -1,3 +1,4 @@ +import { useMutation } from '@tanstack/react-query'; import { Link } from '@tanstack/react-router'; import type React from 'react'; import { useState } from 'react'; @@ -5,32 +6,26 @@ import { Button } from '@/components/ui/buttons'; import { SuccessCard } from '@/components/ui/card/SuccessCard'; import { Input } from '@/components/ui/inputs'; import { Text, Title } from '@/components/ui/typography'; -import { useBusterNotifications } from '@/context/BusterNotifications'; import { resetPasswordEmailSend } from '@/integrations/supabase/resetPassword'; import { cn } from '@/lib/classMerge'; import { isValidEmail } from '@/lib/email'; -import { timeout } from '@/lib/timeout'; export const ResetEmailForm: React.FC<{ queryEmail: string; }> = ({ queryEmail }) => { - const [loading, setLoading] = useState(false); const [email, setEmail] = useState(queryEmail); const [emailSent, setEmailSent] = useState(false); - const { openErrorNotification } = useBusterNotifications(); + const { mutateAsync: resetPasswordEmailSendMutation, isPending: loading } = useMutation({ + mutationFn: resetPasswordEmailSend, + }); const disabled = !email || !isValidEmail(email); const handleResetPassword = async () => { if (disabled) return; - setLoading(true); - const [res] = await Promise.all([resetPasswordEmailSend({ data: { email } }), timeout(450)]); - if (res?.error) { - openErrorNotification(res.error); - } else { - setEmailSent(true); - } - setLoading(false); + const [res] = await Promise.all([resetPasswordEmailSendMutation({ data: { email } })]); + + setEmailSent(true); }; if (emailSent) { diff --git a/apps/web/src/components/features/auth/ResetPasswordForm.tsx b/apps/web/src/components/features/auth/ResetPasswordForm.tsx index 337d56026..461205c27 100644 --- a/apps/web/src/components/features/auth/ResetPasswordForm.tsx +++ b/apps/web/src/components/features/auth/ResetPasswordForm.tsx @@ -3,6 +3,7 @@ import type { User } from '@supabase/supabase-js'; import { useRouter } from '@tanstack/react-router'; import type React from 'react'; import { useCallback, useState } from 'react'; +import { useGetMyUserInfo } from '@/api/buster_rest/users'; import { Button } from '@/components/ui/buttons'; import { SuccessCard } from '@/components/ui/card/SuccessCard'; import { Input } from '@/components/ui/inputs'; @@ -13,8 +14,8 @@ import { PolicyCheck } from './PolicyCheck'; export const ResetPasswordForm: React.FC<{ supabaseUser: Pick; - busterUser: UserResponse; -}> = ({ supabaseUser, busterUser }) => { +}> = ({ supabaseUser }) => { + const { data: busterUser } = useGetMyUserInfo(); const router = useRouter(); const [loading, setLoading] = useState(false); const [resetSuccess, setResetSuccess] = useState(false); diff --git a/apps/web/src/integrations/supabase/resetPassword.ts b/apps/web/src/integrations/supabase/resetPassword.ts index 8e343e65d..ec7267b11 100644 --- a/apps/web/src/integrations/supabase/resetPassword.ts +++ b/apps/web/src/integrations/supabase/resetPassword.ts @@ -2,6 +2,7 @@ import { createServerFn } from '@tanstack/react-start'; import { z } from 'zod'; import { env } from '@/env'; import { ServerRoute as AuthCallbackRoute } from '../../routes/auth.callback'; +import { Route as AuthResetPasswordRoute } from '../../routes/auth.reset-password'; import { getSupabaseServerClient } from './server'; export const resetPasswordEmailSend = createServerFn({ method: 'POST' }) @@ -10,14 +11,17 @@ export const resetPasswordEmailSend = createServerFn({ method: 'POST' }) const supabase = await getSupabaseServerClient(); const url = env.VITE_PUBLIC_URL; - const authURLFull = `${url}${AuthCallbackRoute.to}`; + const authURLFull = `${url}${AuthResetPasswordRoute.to}`; + + console.log('email', email); + console.log('authURLFull', authURLFull); const { error } = await supabase.auth.resetPasswordForEmail(email, { redirectTo: authURLFull, }); if (error) { - return { error: error.message }; + throw new Error(error.message); } return; @@ -28,6 +32,12 @@ export const resetPassword = createServerFn({ method: 'POST' }) .handler(async ({ data: { password } }) => { const supabase = await getSupabaseServerClient(); + const { data: user } = await supabase.auth.getUser(); + + if (!user?.user) { + throw new Error('User not found'); + } + const { error } = await supabase.auth.updateUser({ password }); if (error) { diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 8945c90a1..1f30b7bb9 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -127,6 +127,7 @@ import { Route as AppAppAssetChatsChatIdReportsReportIdMetricsMetricIdContentCha import { Route as AppAppAssetChatsChatIdDashboardsDashboardIdMetricsMetricIdContentSqlRouteImport } from './routes/app/_app/_asset/chats.$chatId/dashboards.$dashboardId/metrics.$metricId/_content/sql' import { Route as AppAppAssetChatsChatIdDashboardsDashboardIdMetricsMetricIdContentResultsRouteImport } from './routes/app/_app/_asset/chats.$chatId/dashboards.$dashboardId/metrics.$metricId/_content/results' import { Route as AppAppAssetChatsChatIdDashboardsDashboardIdMetricsMetricIdContentChartRouteImport } from './routes/app/_app/_asset/chats.$chatId/dashboards.$dashboardId/metrics.$metricId/_content/chart' +import { ServerRoute as AuthConfirmServerRouteImport } from './routes/auth.confirm' import { ServerRoute as AuthCallbackServerRouteImport } from './routes/auth.callback' const AppAppAssetReportsReportIdRouteImport = createFileRoute( @@ -939,6 +940,11 @@ const AppAppAssetChatsChatIdDashboardsDashboardIdMetricsMetricIdContentChartRout AppAppAssetChatsChatIdDashboardsDashboardIdMetricsMetricIdContentRoute, } as any, ) +const AuthConfirmServerRoute = AuthConfirmServerRouteImport.update({ + id: '/auth/confirm', + path: '/auth/confirm', + getParentRoute: () => rootServerRouteImport, +} as any) const AuthCallbackServerRoute = AuthCallbackServerRouteImport.update({ id: '/auth/callback', path: '/auth/callback', @@ -1621,24 +1627,28 @@ export interface RootRouteChildren { } export interface FileServerRoutesByFullPath { '/auth/callback': typeof AuthCallbackServerRoute + '/auth/confirm': typeof AuthConfirmServerRoute } export interface FileServerRoutesByTo { '/auth/callback': typeof AuthCallbackServerRoute + '/auth/confirm': typeof AuthConfirmServerRoute } export interface FileServerRoutesById { __root__: typeof rootServerRouteImport '/auth/callback': typeof AuthCallbackServerRoute + '/auth/confirm': typeof AuthConfirmServerRoute } export interface FileServerRouteTypes { fileServerRoutesByFullPath: FileServerRoutesByFullPath - fullPaths: '/auth/callback' + fullPaths: '/auth/callback' | '/auth/confirm' fileServerRoutesByTo: FileServerRoutesByTo - to: '/auth/callback' - id: '__root__' | '/auth/callback' + to: '/auth/callback' | '/auth/confirm' + id: '__root__' | '/auth/callback' | '/auth/confirm' fileServerRoutesById: FileServerRoutesById } export interface RootServerRouteChildren { AuthCallbackServerRoute: typeof AuthCallbackServerRoute + AuthConfirmServerRoute: typeof AuthConfirmServerRoute } declare module '@tanstack/react-router' { @@ -2522,6 +2532,13 @@ declare module '@tanstack/react-router' { } declare module '@tanstack/react-start/server' { interface ServerFileRoutesByPath { + '/auth/confirm': { + id: '/auth/confirm' + path: '/auth/confirm' + fullPath: '/auth/confirm' + preLoaderRoute: typeof AuthConfirmServerRouteImport + parentRoute: typeof rootServerRouteImport + } '/auth/callback': { id: '/auth/callback' path: '/auth/callback' @@ -3309,6 +3326,7 @@ export const routeTree = rootRouteImport ._addFileTypes() const rootServerRouteChildren: RootServerRouteChildren = { AuthCallbackServerRoute: AuthCallbackServerRoute, + AuthConfirmServerRoute: AuthConfirmServerRoute, } export const serverRouteTree = rootServerRouteImport ._addFileChildren(rootServerRouteChildren) diff --git a/apps/web/src/routes/auth.confirm.tsx b/apps/web/src/routes/auth.confirm.tsx new file mode 100644 index 000000000..961882250 --- /dev/null +++ b/apps/web/src/routes/auth.confirm.tsx @@ -0,0 +1,54 @@ +import type { EmailOtpType } from '@supabase/supabase-js'; +import { redirect } from '@tanstack/react-router'; +import { createServerFileRoute } from '@tanstack/react-start/server'; +import { z } from 'zod'; +import { getSupabaseServerClient } from '@/integrations/supabase/server'; +import { Route as AuthResetPasswordRoute } from './auth.reset-password'; + +const searchParamsSchema = z.object({ + code: z.string().optional(), + token_hash: z.string().optional(), + next: z.string().optional(), + type: z.string().optional(), +}); + +export const ServerRoute = createServerFileRoute('/auth/confirm').methods({ + GET: async ({ request }) => { + const url = new URL(request.url); + const { data: searchParams } = searchParamsSchema.safeParse({ + code: url.searchParams.get('code') || undefined, + token_hash: url.searchParams.get('token_hash') || undefined, + type: url.searchParams.get('type') || undefined, + next: url.searchParams.get('next') || undefined, + }); + + if (!searchParams) { + return new Response('Invalid search params', { status: 400 }); + } + + const supabase = await getSupabaseServerClient(); + + const { token_hash, type } = searchParams; + + if (!token_hash || !type) { + return new Response('Invalid search params', { status: 400 }); + } + + const { data, error } = await supabase.auth.verifyOtp({ + token_hash: token_hash, + type: type as EmailOtpType, + }); + + if (!error) { + throw redirect({ + to: AuthResetPasswordRoute.to, + search: { + email: data.user?.email || '', + }, + }); + } + + console.error('Error verifying OTP', error); + return new Response('Error verifying OTP', { status: 400 }); + }, +}); diff --git a/apps/web/src/routes/auth.reset-password.tsx b/apps/web/src/routes/auth.reset-password.tsx index e730753ae..85d21ea78 100644 --- a/apps/web/src/routes/auth.reset-password.tsx +++ b/apps/web/src/routes/auth.reset-password.tsx @@ -1,17 +1,10 @@ import { createFileRoute } from '@tanstack/react-router'; import { z } from 'zod'; -import { prefetchGetMyUserInfo } from '@/api/buster_rest/users'; import { ResetEmailForm } from '@/components/features/auth/ResetEmailForm'; import { ResetPasswordForm } from '@/components/features/auth/ResetPasswordForm'; import { useGetSupabaseUser } from '@/context/Supabase'; export const Route = createFileRoute('/auth/reset-password')({ - loader: async ({ context }) => { - const user = await prefetchGetMyUserInfo(context.queryClient); - return { - user, - }; - }, head: () => ({ meta: [ { title: 'Reset Password' }, @@ -22,26 +15,17 @@ export const Route = createFileRoute('/auth/reset-password')({ }), component: RouteComponent, validateSearch: z.object({ - email: z.string(), + email: z.string().optional(), }), }); function RouteComponent() { - const { user } = Route.useLoaderData(); const { email } = Route.useSearch(); const supabaseUser = useGetSupabaseUser(); - if (email) { - return ; + if (email || supabaseUser?.is_anonymous || !supabaseUser) { + return ; } - if (!supabaseUser || !user) { - return ( -
- We were unable to find your account -
- ); - } - - return ; + return ; } diff --git a/packages/database/package.json b/packages/database/package.json index 48a13cbe4..6932f62a5 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -31,15 +31,12 @@ "build": "tsc", "build:dry-run": "tsc", "db:init": "echo 'Initializing database...'", - "start": "supabase start", - "db:reset": "supabase stop && supabase start && supabase db reset", "db:seed": "tsx scripts/seed.ts", "db:dump": "tsx scripts/dump.ts", "db:migrate": "drizzle-kit migrate", "db:generate": "drizzle-kit generate", "db:push": "drizzle-kit push", "db:studio": "drizzle-kit studio", - "db:stop": "supabase stop", "dev": "tsc --watch", "lint": "biome check --write", "test": "vitest run", @@ -53,6 +50,7 @@ "@buster/env-utils": "workspace:*", "@buster/typescript-config": "workspace:*", "@buster/vitest-config": "workspace:*", + "@buster-app/supabase": "workspace:*", "ai": "catalog:", "drizzle-kit": "^0.31.4", "drizzle-orm": "catalog:", diff --git a/packages/database/scripts/start-supabase.ts b/packages/database/scripts/start-supabase.ts deleted file mode 100644 index d9fb91745..000000000 --- a/packages/database/scripts/start-supabase.ts +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env tsx - -import { exec } from 'node:child_process'; -import { promisify } from 'node:util'; - -const execAsync = promisify(exec); - -async function runCommand(command: string): Promise { - console.log(`🔄 Running: ${command}`); - - try { - const { stdout, stderr } = await execAsync(command); - - if (stdout) { - console.log(stdout); - } - - if (stderr) { - console.error(stderr); - } - - console.log(`✅ Successfully completed: ${command}\n`); - } catch (error) { - console.error(`❌ Error running command: ${command}`); - console.error(error); - throw error; - } -} - -async function startSupabase(): Promise { - console.log('🚀 Starting Supabase setup...\n'); - - try { - // Start Supabase - await runCommand('npm run db:start-supabase'); - - // Reset the database - await runCommand('npm run db:reset'); - - console.log('🎉 Supabase setup completed successfully!'); - } catch (error) { - console.error('💥 Supabase setup failed:', error); - process.exit(1); - } -} - -// Run the script -startSupabase(); diff --git a/packages/database/supabase/.gitignore b/packages/database/supabase/.gitignore deleted file mode 100644 index a3ad88055..000000000 --- a/packages/database/supabase/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Supabase -.branches -.temp -.env diff --git a/packages/database/supabase/seed.sql b/packages/database/supabase/seed.sql deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/database/turbo.json b/packages/database/turbo.json index 486f77978..e686b01d1 100644 --- a/packages/database/turbo.json +++ b/packages/database/turbo.json @@ -9,25 +9,17 @@ "db:init": { "cache": false, "persistent": false, - "dependsOn": ["db:seed"] - }, - "start": { - "cache": false, - "persistent": false, - "outputs": [] - }, - "db:reset": { - "cache": false, - "persistent": false + "dependsOn": ["db:seed", "@buster-app/supabase#start"] }, "db:migrate": { "cache": false, - "persistent": false + "persistent": false, + "dependsOn": ["@buster-app/supabase#start"] }, "db:seed": { "cache": false, "persistent": false, - "dependsOn": ["db:migrate"] + "dependsOn": ["db:migrate", "@buster-app/supabase#start"] }, "db:dump": { "cache": false, @@ -45,14 +37,9 @@ "cache": false, "persistent": true }, - "db:stop": { - "cache": false, - "persistent": false - }, "dev": { "cache": false, - "persistent": true, - "dependsOn": ["start"] + "persistent": true } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7c39e462..a9b36c0ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -329,11 +329,7 @@ importers: specifier: ^4.17.12 version: 4.17.12 - apps/supabase: - dependencies: - '@buster/database': - specifier: workspace:* - version: link:../../packages/database + apps/supabase: {} apps/trigger: dependencies: @@ -1159,6 +1155,9 @@ importers: packages/database: dependencies: + '@buster-app/supabase': + specifier: workspace:* + version: link:../../apps/supabase '@buster/env-utils': specifier: workspace:* version: link:../env-utils