fix(auth): route supabase auth via caddy and harden session setup
This commit is contained in:
parent
bdd5ff768e
commit
3cc12c8b48
|
|
@ -1,5 +1,7 @@
|
||||||
chat.serverbob.org {
|
chat.serverbob.org {
|
||||||
encode gzip
|
encode gzip
|
||||||
|
@supabaseAuth path /auth/v1/*
|
||||||
|
reverse_proxy @supabaseAuth pulse-kong:8000
|
||||||
reverse_proxy pulse:4991
|
reverse_proxy pulse:4991
|
||||||
tls zax@serverbob.org
|
tls zax@serverbob.org
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { logDebug } from '@/helpers/browser-logger';
|
||||||
import { getHostFromServer } from '@/helpers/get-file-url';
|
import { getHostFromServer } from '@/helpers/get-file-url';
|
||||||
import { applyServerPreferences } from '@/lib/preferences-apply';
|
import { applyServerPreferences } from '@/lib/preferences-apply';
|
||||||
import { seedPreferencesFromLocalStorage } from '@/lib/preferences-seed';
|
import { seedPreferencesFromLocalStorage } from '@/lib/preferences-seed';
|
||||||
|
import { getAccessToken } from '@/lib/supabase';
|
||||||
import { cleanup, connectToTRPC, getHomeTRPCClient } from '@/lib/trpc';
|
import { cleanup, connectToTRPC, getHomeTRPCClient } from '@/lib/trpc';
|
||||||
import { type TPublicServerSettings, type TServerInfo } from '@pulse/shared';
|
import { type TPublicServerSettings, type TServerInfo } from '@pulse/shared';
|
||||||
import { store } from '../store';
|
import { store } from '../store';
|
||||||
|
|
@ -71,6 +72,11 @@ export const connect = async () => {
|
||||||
throw new Error('Failed to fetch server info');
|
throw new Error('Failed to fetch server info');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const accessToken = await getAccessToken();
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new Error('Missing authentication token');
|
||||||
|
}
|
||||||
|
|
||||||
const host = getHostFromServer();
|
const host = getHostFromServer();
|
||||||
const trpc = await connectToTRPC(host);
|
const trpc = await connectToTRPC(host);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import {
|
||||||
setLocalStorageItem
|
setLocalStorageItem
|
||||||
} from '@/helpers/storage';
|
} from '@/helpers/storage';
|
||||||
import { useForm } from '@/hooks/use-form';
|
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 type { Provider } from '@supabase/supabase-js';
|
||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
@ -62,6 +62,38 @@ const Connect = memo(() => {
|
||||||
return invite || undefined;
|
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(
|
const onRememberCredentialsChange = useCallback(
|
||||||
(checked: boolean) => {
|
(checked: boolean) => {
|
||||||
loginForm.onChange('rememberCredentials', checked);
|
loginForm.onChange('rememberCredentials', checked);
|
||||||
|
|
@ -81,6 +113,10 @@ const Connect = memo(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (!ensureSupabaseReady()) {
|
||||||
|
throw new Error('Authentication client is not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
const url = getUrlFromServer();
|
const url = getUrlFromServer();
|
||||||
const response = await fetch(`${url}/login`, {
|
const response = await fetch(`${url}/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -102,10 +138,7 @@ const Connect = memo(() => {
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
await supabase.auth.setSession({
|
await establishSession(data.accessToken, data.refreshToken);
|
||||||
access_token: data.accessToken,
|
|
||||||
refresh_token: data.refreshToken
|
|
||||||
});
|
|
||||||
|
|
||||||
if (loginForm.values.rememberCredentials) {
|
if (loginForm.values.rememberCredentials) {
|
||||||
setLocalStorageItem(LocalStorageKey.EMAIL, loginForm.values.email);
|
setLocalStorageItem(LocalStorageKey.EMAIL, loginForm.values.email);
|
||||||
|
|
@ -137,12 +170,16 @@ const Connect = memo(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [loginForm.values, loginForm.setErrors, inviteCode]);
|
}, [loginForm.values, loginForm.setErrors, inviteCode, ensureSupabaseReady, establishSession]);
|
||||||
|
|
||||||
const onRegisterClick = useCallback(async () => {
|
const onRegisterClick = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (!ensureSupabaseReady()) {
|
||||||
|
throw new Error('Authentication client is not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
const url = getUrlFromServer();
|
const url = getUrlFromServer();
|
||||||
const response = await fetch(`${url}/register`, {
|
const response = await fetch(`${url}/register`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -166,10 +203,7 @@ const Connect = memo(() => {
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
await supabase.auth.setSession({
|
await establishSession(data.accessToken, data.refreshToken);
|
||||||
access_token: data.accessToken,
|
|
||||||
refresh_token: data.refreshToken
|
|
||||||
});
|
|
||||||
|
|
||||||
await connect();
|
await connect();
|
||||||
|
|
||||||
|
|
@ -197,10 +231,15 @@ const Connect = memo(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [registerForm.values, registerForm.setErrors, inviteCode]);
|
}, [registerForm.values, registerForm.setErrors, inviteCode, ensureSupabaseReady, establishSession]);
|
||||||
|
|
||||||
const onOAuthClick = useCallback(
|
const onOAuthClick = useCallback(
|
||||||
async (provider: string) => {
|
async (provider: string) => {
|
||||||
|
if (!ensureSupabaseReady()) {
|
||||||
|
toast.error('Authentication client is not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const redirectTo = new URL(window.location.origin);
|
const redirectTo = new URL(window.location.origin);
|
||||||
|
|
||||||
if (inviteCode) {
|
if (inviteCode) {
|
||||||
|
|
@ -214,7 +253,7 @@ const Connect = memo(() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[inviteCode]
|
[inviteCode, ensureSupabaseReady]
|
||||||
);
|
);
|
||||||
|
|
||||||
const logoSrc = useMemo(() => {
|
const logoSrc = useMemo(() => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue