Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/lib/stores/debate-context-store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { create } from 'zustand';
import { supabase } from '@/lib/supabase/client';
import { currentUserId } from '@/lib/supabase/current-user';
import type {
DebateContextFeature,
DebateContextOutputKind,
Expand Down Expand Up @@ -118,8 +119,7 @@ export const useDebateContextStore = create<DebateContextState>()((set, get) =>
set((state) => ({ pins: { ...state.pins, [debateId]: withPin(state.pins[debateId] ?? [], ref, !pinned) } }));
const save = async (): Promise<{ error: { message?: string; code?: string } | null }> => {
if (generation !== pinGeneration) return { error: null };
const { data } = await supabase.auth.getUser();
const userId = data.user?.id;
const userId = await currentUserId();
if (generation !== pinGeneration) return { error: null };
if (!userId) return { error: { message: 'Sign in to pin results' } };
return pinned
Expand Down
12 changes: 6 additions & 6 deletions src/lib/stores/debate-store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import { supabase } from '@/lib/supabase/client';
import { currentUserId } from '@/lib/supabase/current-user';

/** A favor, En contra, both sides, or not side-specific. */
export type DebateSide = 'pro' | 'con' | 'both' | 'neutral';
Expand Down Expand Up @@ -95,8 +96,7 @@ function sortDebates(debates: Debate[]): Debate[] {
}

async function requireUserId(): Promise<string> {
const { data } = await supabase.auth.getUser();
const userId = data.user?.id;
const userId = await currentUserId();
if (!userId) throw new Error('Sign in to manage debates');
return userId;
}
Expand Down Expand Up @@ -126,10 +126,10 @@ export const useDebateStore = create<DebateState>()(

hydrate: async () => {
const epoch = hydrationEpoch;
const { data: { user } } = await supabase.auth.getUser();
if (epoch !== hydrationEpoch || !user) return;
if (get().ownerUserId !== user.id) {
set({ ownerUserId: user.id, debates: [], currentDebateId: null, oneOff: false, hydrated: false });
const userId = await currentUserId();
if (epoch !== hydrationEpoch || !userId) return;
if (get().ownerUserId !== userId) {
set({ ownerUserId: userId, debates: [], currentDebateId: null, oneOff: false, hydrated: false });
}
const writesBefore = debateWrites;
const [debatesResult, currentResult] = await Promise.all([
Expand Down
110 changes: 42 additions & 68 deletions src/lib/stores/session-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { BrainstormOutput, BrainstormInput } from '../ai/schemas/brainstorm
import type { FactoidsOutput, FactoidsInput } from '../ai/schemas/factoids';
import type { AskOutput } from '../ai/schemas/ask';
import { supabase } from '@/lib/supabase/client';
import { currentUserId } from '@/lib/supabase/current-user';
import i18n from '@/i18n';
import { activeDebateId, useDebateStore, type DebateSide } from './debate-store';

Expand Down Expand Up @@ -350,8 +351,11 @@ function mergeSourceFolders(currentFolders: string[], sources: Source[] = []) {
}

async function getUserId() {
const { data } = await supabase.auth.getUser();
return data.user?.id || null;
return await currentUserId();
}

function signInRequired(action: string): Error {
return new Error(`Sign in to ${action}`);
}

type EqQuery = {
Expand Down Expand Up @@ -574,13 +578,7 @@ export const useSessionStore = create<SessionState>()(
const place = (list: Source[], item: Source) =>
restoredId ? [...list, item].sort((x, y) => y.createdAt - x.createdAt) : [item, ...list];
const userId = await getUserId();
if (!userId) {
set((state) => ({
sourceFolders: mergeSourceFolders(state.sourceFolders, [localItem]),
sourceLibrary: place(state.sourceLibrary, localItem),
}));
return;
}
if (!userId) throw signInRequired('save this source');

const folderResult = await supabase
.from('source_folders')
Expand Down Expand Up @@ -625,14 +623,17 @@ export const useSessionStore = create<SessionState>()(
},
createSourceFolder: async (folder) => {
const normalized = normalizeFolder(folder);
const userId = await getUserId();
if (!userId) throw signInRequired('save this folder');
throwOnWriteError(
await supabase
.from('source_folders')
.upsert({ user_id: userId, name: normalized }, { onConflict: 'user_id,name' }),
'save source folder',
);
set((state) => ({
sourceFolders: mergeSourceFolders([...state.sourceFolders, normalized], state.sourceLibrary),
}));
const userId = await getUserId();
if (!userId) return;
await supabase
.from('source_folders')
.upsert({ user_id: userId, name: normalized }, { onConflict: 'user_id,name' });
},
updateSource: async (id, patch) => {
const current = get().sourceLibrary.find((s) => s.id === id);
Expand Down Expand Up @@ -660,7 +661,7 @@ export const useSessionStore = create<SessionState>()(

try {
const userId = await getUserId();
if (!userId) return;
if (!userId) throw signInRequired('save this source');
if (nextSource.folder) {
const folderResult = await supabase
.from('source_folders')
Expand Down Expand Up @@ -725,7 +726,7 @@ export const useSessionStore = create<SessionState>()(
set((state) => ({ sourceLibrary: state.sourceLibrary.map((source) => updates.get(source.id) ?? source) }));
try {
const userId = await getUserId();
if (!userId) return;
if (!userId) throw signInRequired('save this source');
await Promise.all(Array.from(updates.values()).map(async (source) => {
const result = await applyUserScopedIdFilter(
supabase.from('sources').update({ metadata: source.metadata ?? null }),
Expand Down Expand Up @@ -761,7 +762,7 @@ export const useSessionStore = create<SessionState>()(
};

const userId = await getUserId();
if (!userId) return applyRename();
if (!userId) throw signInRequired('rename this folder');
const folderResult = await supabase
.from('source_folders')
.upsert({ user_id: userId, name: to }, { onConflict: 'user_id,name' });
Expand Down Expand Up @@ -802,7 +803,7 @@ export const useSessionStore = create<SessionState>()(
};

const userId = await getUserId();
if (!userId) return applyDelete();
if (!userId) throw signInRequired('delete this folder');
if (affected.length > 0) {
throwOnWriteError(
await supabase.from('sources').update({ folder: 'General' }).eq('user_id', userId).eq('debate_id', debateId).eq('folder', folder),
Expand All @@ -822,13 +823,12 @@ export const useSessionStore = create<SessionState>()(
const debateId = useDebateStore.getState().currentDebateId;
if (!debateId) throw new DebateRequiredError();
const userId = await getUserId();
if (userId) {
throwOnWriteError(
await supabase.from('sources').update({ is_active: isActive })
.eq('user_id', userId).eq('debate_id', debateId).eq('folder', normalized),
'update source folder activity',
);
}
if (!userId) throw signInRequired('update this folder');
throwOnWriteError(
await supabase.from('sources').update({ is_active: isActive })
.eq('user_id', userId).eq('debate_id', debateId).eq('folder', normalized),
'update source folder activity',
);
set((state) => ({
sourceLibrary: state.sourceLibrary.map((source) =>
source.debateId === debateId && normalizeFolder(source.folder) === normalized ? { ...source, isActive } : source
Expand All @@ -837,23 +837,21 @@ export const useSessionStore = create<SessionState>()(
},
removeSource: async (id) => {
const userId = await getUserId();
if (userId) {
const result = await applyUserScopedIdFilter(supabase.from('sources').delete(), userId, id);
throwOnWriteError(result, 'delete source');
}
if (!userId) throw signInRequired('delete this source');
const result = await applyUserScopedIdFilter(supabase.from('sources').delete(), userId, id);
throwOnWriteError(result, 'delete source');
set((state) => ({ sourceLibrary: state.sourceLibrary.filter((source) => source.id !== id) }));
},
toggleSource: async (id) => {
const current = get().sourceLibrary.find((source) => source.id === id);
if (!current) return;
const nextValue = !current.isActive;
const userId = await getUserId();
if (userId) {
throwOnWriteError(
await applyUserScopedIdFilter(supabase.from('sources').update({ is_active: nextValue }), userId, id),
'toggle source activity',
);
}
if (!userId) throw signInRequired('update this source');
throwOnWriteError(
await applyUserScopedIdFilter(supabase.from('sources').update({ is_active: nextValue }), userId, id),
'toggle source activity',
);
set((state) => ({
sourceLibrary: state.sourceLibrary.map((source) => source.id === id ? { ...source, isActive: nextValue } : source),
}));
Expand Down Expand Up @@ -887,12 +885,8 @@ export const useSessionStore = create<SessionState>()(
saveCurrentCase: async (title) => {
const current = get().generateResult;
if (!current) return;
const localItem = { id: generateId(), data: current, createdAt: Date.now(), title };
const userId = await getUserId();
if (!userId) {
set((state) => ({ savedCases: [localItem, ...state.savedCases] }));
return;
}
if (!userId) throw signInRequired('save this case');
const { data, error } = await supabase
.from('cases')
.insert({ user_id: userId, title, input: {}, output: current })
Expand Down Expand Up @@ -948,12 +942,8 @@ export const useSessionStore = create<SessionState>()(
saveCurrentAttack: async (title) => {
const current = get().attackResult;
if (!current) return;
const localItem = { id: generateId(), data: current, createdAt: Date.now(), title };
const userId = await getUserId();
if (!userId) {
set((state) => ({ savedAttacks: [localItem, ...state.savedAttacks] }));
return;
}
if (!userId) throw signInRequired('save this attack');
const { data, error } = await supabase
.from('attacks')
.insert({ user_id: userId, title, input: {}, output: current })
Expand All @@ -980,12 +970,8 @@ export const useSessionStore = create<SessionState>()(
saveCurrentBrainstorm: async (title) => {
const current = get().brainstormResult;
if (!current) return;
const localItem = { id: generateId(), data: current, createdAt: Date.now(), title };
const userId = await getUserId();
if (!userId) {
set((state) => ({ savedBrainstorms: [localItem, ...state.savedBrainstorms] }));
return;
}
if (!userId) throw signInRequired('save this brainstorm');
const { data, error } = await supabase
.from('brainstorms')
.insert({ user_id: userId, title, input: {}, output: current })
Expand All @@ -1012,12 +998,8 @@ export const useSessionStore = create<SessionState>()(
saveCurrentFactoids: async (title) => {
const current = get().factoidsResult;
if (!current) return;
const localItem = { id: generateId(), data: current, createdAt: Date.now(), title };
const userId = await getUserId();
if (!userId) {
set((state) => ({ savedFactoids: [localItem, ...state.savedFactoids] }));
return;
}
if (!userId) throw signInRequired('save these factoids');
const { data, error } = await supabase
.from('factoids')
.insert({ user_id: userId, title, input: {}, output: current })
Expand Down Expand Up @@ -1050,12 +1032,8 @@ export const useSessionStore = create<SessionState>()(
saveCurrentChat: async (title) => {
const current = get().askMessages;
if (current.length === 0) return;
const localItem = { id: generateId(), data: current, createdAt: Date.now(), title };
const userId = await getUserId();
if (!userId) {
set((state) => ({ savedChats: [localItem, ...state.savedChats] }));
return;
}
if (!userId) throw signInRequired('save this chat');
const { data: chat, error } = await supabase
.from('chats')
.insert({ user_id: userId, title })
Expand Down Expand Up @@ -1084,12 +1062,8 @@ export const useSessionStore = create<SessionState>()(
},

saveFlowchart: async (title, data, debateId = activeDebateId()) => {
const localItem = { id: generateId(), data, createdAt: Date.now(), title, debateId, side: 'neutral' as const };
const userId = await getUserId();
if (!userId) {
set((state) => ({ savedFlowcharts: [localItem, ...state.savedFlowcharts] }));
return;
}
if (!userId) throw signInRequired('save this flowchart');
const { data: saved, error } = await supabase
.from('flowcharts')
.insert({
Expand Down Expand Up @@ -1118,7 +1092,7 @@ export const useSessionStore = create<SessionState>()(

recordResult: async (kind, { title, input, output, debateId, side }) => {
const userId = await getUserId();
if (!userId) throw new Error('[SessionStore] Sign in to save results to a debate');
if (!userId) throw signInRequired('save results to a debate');
const { data, error } = await supabase
.from(RESULT_TABLES[kind])
.insert({ user_id: userId, debate_id: debateId, side, title: title.slice(0, 300) || 'Untitled', input: input ?? {}, output })
Expand All @@ -1135,7 +1109,7 @@ export const useSessionStore = create<SessionState>()(

updateRecordedResult: async (kind, id, output) => {
const userId = await getUserId();
if (!userId) return;
if (!userId) throw signInRequired('update this result');
throwOnWriteError(
await applyUserScopedIdFilter(supabase.from(RESULT_TABLES[kind]).update({ output }), userId, id),
`update ${kind}`,
Expand Down Expand Up @@ -1389,7 +1363,7 @@ async function syncAskChatNow(debateId: string | null): Promise<void> {
if (askMessages.length === 0 || askPersistedCount >= askMessages.length) return;
if (!askChatId && !debateId) return;
const userId = await getUserId();
if (!userId) return;
if (!userId) throw signInRequired('save this chat');
// Clearing or replacing the conversation while a write is in flight must not
// hand this chat or its count to the next conversation.
const sameConversation = () => {
Expand Down
4 changes: 2 additions & 2 deletions src/lib/stores/settings-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '../../../supabase/functions/_shared/model-catalog';
import type { ProviderConfig } from '../ai/providers/types';
import { supabase } from '@/lib/supabase/client';
import { currentUserId } from '@/lib/supabase/current-user';

export type FeatureTab = 'generate' | 'attack' | 'brainstorm' | 'factoids' | 'ask' | 'sparring';
type CoreFeatureTab = Exclude<FeatureTab, 'sparring'>;
Expand Down Expand Up @@ -164,8 +165,7 @@ async function canAccessProModels(settings: Record<string, unknown>): Promise<bo
}

async function getUserId() {
const { data } = await supabase.auth.getUser();
return data.user?.id || null;
return await currentUserId();
}

// The auth trigger provisions every settings row, and clients may only UPDATE
Expand Down
9 changes: 5 additions & 4 deletions src/lib/stores/sparring-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import { z } from 'zod';
import { supabase } from '@/lib/supabase/client';
import { currentUserId } from '@/lib/supabase/current-user';
import {
SparringScorecardSchema,
SparringSpeakerSchema,
Expand Down Expand Up @@ -478,9 +479,9 @@ export const useSparringStore = create<SparringStoreState>()(
hydrateSavedSessions: async () => {
if (get().hydrated) return;
const epoch = hydrationEpoch;
const { data: { user } } = await supabase.auth.getUser();
const userId = await currentUserId();
if (epoch !== hydrationEpoch) return;
if (!user) {
if (!userId) {
set({ hydrated: true });
return;
}
Expand All @@ -505,8 +506,8 @@ export const useSparringStore = create<SparringStoreState>()(
deleteSavedSession: async (id) => {
const previous = get().savedSessions;
set({ savedSessions: previous.filter((session) => session.id !== id) });
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
const userId = await currentUserId();
if (!userId) return;
const { error } = await supabase.from('sparring_sessions').delete().eq('id', id);
if (error) {
set({ savedSessions: previous });
Expand Down
40 changes: 40 additions & 0 deletions src/lib/supabase/current-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { getActiveUserId, supabase } from './client';

/**
* Who is signed in, without treating an Auth-server failure as signed out.
*
* `auth.getUser()` asks the server. When that call fails, the old stores saved
* only in this browser and told the student the save worked. The id AuthProvider
* already has, then the local session, still name the user so the write can go
* to the database (or fail in the open).
*/
function rememberedUserId(): string | null {
// Some unit tests replace the client module and omit this export. Reading a
// missing mock export throws; that is the same as "nothing remembered".
try {
if (typeof getActiveUserId !== 'function') return null;
return getActiveUserId();
} catch {
return null;
}
}

export async function currentUserId(): Promise<string | null> {
const active = rememberedUserId();
if (active) return active;

const auth = supabase.auth;
if (typeof auth?.getUser === 'function') {
try {
const { data, error } = await auth.getUser();
if (!error && data.user?.id) return data.user.id;
} catch {
// The Auth server did not answer. Fall through to the local session.
}
}

if (typeof auth?.getSession !== 'function') return null;
const { data, error } = await auth.getSession();
if (error) return null;
return data.session?.user?.id ?? null;
}
Loading
Loading