fix(auth): route supabase auth via caddy and harden session setup

This commit is contained in:
ServerBob 2026-03-05 08:01:27 +00:00
parent bdd5ff768e
commit 3cc12c8b48
3 changed files with 59 additions and 12 deletions

View File

@ -1,5 +1,7 @@
chat.serverbob.org {
encode gzip
@supabaseAuth path /auth/v1/*
reverse_proxy @supabaseAuth pulse-kong:8000
reverse_proxy pulse:4991
tls zax@serverbob.org
}

View File

@ -12,6 +12,7 @@ import { logDebug } from '@/helpers/browser-logger';
import { getHostFromServer } from '@/helpers/get-file-url';
import { applyServerPreferences } from '@/lib/preferences-apply';
import { seedPreferencesFromLocalStorage } from '@/lib/preferences-seed';
import { getAccessToken } from '@/lib/supabase';
import { cleanup, connectToTRPC, getHomeTRPCClient } from '@/lib/trpc';
import { type TPublicServerSettings, type TServerInfo } from '@pulse/shared';
import { store } from '../store';
@ -71,6 +72,11 @@ export const connect = async () => {
throw new Error('Failed to fetch server info');
}
const accessToken = await getAccessToken();
if (!accessToken) {
throw new Error('Missing authentication token');
}
const host = getHostFromServer();
const trpc = await connectToTRPC(host);

View File

@ -17,7 +17,7 @@ import {
setLocalStorageItem
} from '@/helpers/storage';
import { useForm } from '@/hooks/use-form';
import { supabase } from '@/lib/supabase';
import { getAccessToken, initSupabase, supabase } from '@/lib/supabase';
import type { Provider } from '@supabase/supabase-js';
import { memo, useCallback, useMemo, useState } from 'react';
import { toast } from 'sonner';
@ -62,6 +62,38 @@ const Connect = memo(() => {
return invite || undefined;
}, []);
const ensureSupabaseReady = useCallback(() => {
if (supabase) return true;
if (info?.supabaseUrl && info?.supabaseAnonKey) {
initSupabase(info.supabaseUrl, info.supabaseAnonKey);
return !!supabase;
}
return false;
}, [info?.supabaseUrl, info?.supabaseAnonKey]);
const establishSession = useCallback(
async (accessToken: string, refreshToken: string) => {
const { error } = await supabase.auth.setSession({
access_token: accessToken,
refresh_token: refreshToken
});
if (error) {
throw new Error(`Failed to store session: ${error.message}`);
}
// Ensure token is visible to the ws connectionParams path before connect().
for (let i = 0; i < 5; i += 1) {
const token = await getAccessToken();
if (token) return;
await new Promise((resolve) => setTimeout(resolve, 50));
}
throw new Error('Failed to read authentication token after login');
},
[]
);
const onRememberCredentialsChange = useCallback(
(checked: boolean) => {
loginForm.onChange('rememberCredentials', checked);
@ -81,6 +113,10 @@ const Connect = memo(() => {
setLoading(true);
try {
if (!ensureSupabaseReady()) {
throw new Error('Authentication client is not initialized');
}
const url = getUrlFromServer();
const response = await fetch(`${url}/login`, {
method: 'POST',
@ -102,10 +138,7 @@ const Connect = memo(() => {
refreshToken: string;
};
await supabase.auth.setSession({
access_token: data.accessToken,
refresh_token: data.refreshToken
});
await establishSession(data.accessToken, data.refreshToken);
if (loginForm.values.rememberCredentials) {
setLocalStorageItem(LocalStorageKey.EMAIL, loginForm.values.email);
@ -137,12 +170,16 @@ const Connect = memo(() => {
setLoading(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loginForm.values, loginForm.setErrors, inviteCode]);
}, [loginForm.values, loginForm.setErrors, inviteCode, ensureSupabaseReady, establishSession]);
const onRegisterClick = useCallback(async () => {
setLoading(true);
try {
if (!ensureSupabaseReady()) {
throw new Error('Authentication client is not initialized');
}
const url = getUrlFromServer();
const response = await fetch(`${url}/register`, {
method: 'POST',
@ -166,10 +203,7 @@ const Connect = memo(() => {
refreshToken: string;
};
await supabase.auth.setSession({
access_token: data.accessToken,
refresh_token: data.refreshToken
});
await establishSession(data.accessToken, data.refreshToken);
await connect();
@ -197,10 +231,15 @@ const Connect = memo(() => {
setLoading(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [registerForm.values, registerForm.setErrors, inviteCode]);
}, [registerForm.values, registerForm.setErrors, inviteCode, ensureSupabaseReady, establishSession]);
const onOAuthClick = useCallback(
async (provider: string) => {
if (!ensureSupabaseReady()) {
toast.error('Authentication client is not initialized');
return;
}
const redirectTo = new URL(window.location.origin);
if (inviteCode) {
@ -214,7 +253,7 @@ const Connect = memo(() => {
}
});
},
[inviteCode]
[inviteCode, ensureSupabaseReady]
);
const logoSrc = useMemo(() => {