mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Implement E2EE and message content moderation
This commit is contained in:
1 parent
e1ff87dc03
commit
198cd80dfd
74 files changed
+12939
-2178
No files matched your search
@@ -1,7 +1,23 @@
|
||||
import { create } from "zustand";
|
||||
import { supabase } from "../lib/supabase";
|
||||
import { Events } from "../lib/analytics";
|
||||
import { clearKeys } from "../lib/encryption";
|
||||
import type { Profile, Couple } from "@/types";
|
||||
import type { RSAPublicKeyJWK, RSAPrivateKeyJWK } from "../lib/encryption/types";
|
||||
|
||||
/**
|
||||
* Encryption key state - stored globally to ensure all components
|
||||
* share the same key state and avoid race conditions during encryption.
|
||||
*/
|
||||
interface EncryptionKeyState {
|
||||
privateKeyJwk: RSAPrivateKeyJWK | null;
|
||||
publicKeyJwk: RSAPublicKeyJWK | null;
|
||||
hasKeys: boolean;
|
||||
isLoadingKeys: boolean;
|
||||
keysError: Error | null;
|
||||
/** Incremented when keys change - consumers can use this to trigger re-renders */
|
||||
keysVersion: number;
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
user: Profile | null;
|
||||
@@ -10,11 +26,19 @@ interface AuthState {
|
||||
isLoading: boolean;
|
||||
isAuthenticated: boolean;
|
||||
|
||||
// Encryption key state (global to avoid hook instance sync issues)
|
||||
encryptionKeys: EncryptionKeyState;
|
||||
|
||||
// Actions
|
||||
fetchUser: () => Promise<void>;
|
||||
fetchCouple: () => Promise<void>;
|
||||
refreshPartner: () => Promise<Profile | null>;
|
||||
signOut: () => Promise<void>;
|
||||
setUser: (user: Profile | null) => void;
|
||||
|
||||
// Encryption key actions
|
||||
setEncryptionKeys: (keys: Partial<EncryptionKeyState>) => void;
|
||||
clearEncryptionKeys: () => void;
|
||||
}
|
||||
|
||||
// Import other stores lazily to avoid circular dependency issues
|
||||
@@ -26,12 +50,22 @@ const getOtherStores = () => {
|
||||
return { useMatchStore, usePacksStore, useMessageStore, useSubscriptionStore };
|
||||
};
|
||||
|
||||
const initialEncryptionKeyState: EncryptionKeyState = {
|
||||
privateKeyJwk: null,
|
||||
publicKeyJwk: null,
|
||||
hasKeys: false,
|
||||
isLoadingKeys: true,
|
||||
keysError: null,
|
||||
keysVersion: 0,
|
||||
};
|
||||
|
||||
export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
user: null,
|
||||
couple: null,
|
||||
partner: null,
|
||||
isLoading: true,
|
||||
isAuthenticated: false,
|
||||
encryptionKeys: initialEncryptionKeyState,
|
||||
|
||||
fetchUser: async () => {
|
||||
try {
|
||||
@@ -49,6 +83,14 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
|
||||
if (authError || !authUser) {
|
||||
console.log("[Auth] Session invalid, signing out:", authError?.message);
|
||||
|
||||
// Clear E2EE keys
|
||||
try {
|
||||
await clearKeys();
|
||||
} catch (error) {
|
||||
console.error("Failed to clear encryption keys:", error);
|
||||
}
|
||||
|
||||
// Session is invalid - clear everything
|
||||
set({ user: null, couple: null, partner: null, isAuthenticated: false, isLoading: false });
|
||||
// Clear other stores
|
||||
@@ -108,8 +150,37 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
set({ couple, partner });
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh partner's profile data (useful to get latest public_key_jwk for E2EE)
|
||||
* Returns the updated partner profile or null if not found
|
||||
*/
|
||||
refreshPartner: async () => {
|
||||
const user = get().user;
|
||||
if (!user?.couple_id) return null;
|
||||
|
||||
const { data: partner } = await supabase
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("couple_id", user.couple_id)
|
||||
.neq("id", user.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (partner) {
|
||||
set({ partner });
|
||||
}
|
||||
return partner;
|
||||
},
|
||||
|
||||
signOut: async () => {
|
||||
Events.signOut();
|
||||
|
||||
// Clear E2EE keys from storage
|
||||
try {
|
||||
await clearKeys();
|
||||
} catch (error) {
|
||||
console.error("Failed to clear encryption keys:", error);
|
||||
}
|
||||
|
||||
// Clear local state FIRST to ensure UI updates even if Supabase call fails
|
||||
set({
|
||||
user: null,
|
||||
@@ -117,6 +188,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
partner: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
encryptionKeys: initialEncryptionKeyState,
|
||||
});
|
||||
// Clear other stores
|
||||
const { useMatchStore, usePacksStore, useMessageStore, useSubscriptionStore } = getOtherStores();
|
||||
@@ -150,4 +222,28 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
useSubscriptionStore.getState().clearSubscription();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update encryption key state. This is the ONLY way to update keys
|
||||
* to ensure all consumers see the same state.
|
||||
*/
|
||||
setEncryptionKeys: (keys) => {
|
||||
set((state) => ({
|
||||
encryptionKeys: {
|
||||
...state.encryptionKeys,
|
||||
...keys,
|
||||
// Increment version when keys actually change
|
||||
keysVersion: (keys.publicKeyJwk !== undefined || keys.privateKeyJwk !== undefined)
|
||||
? state.encryptionKeys.keysVersion + 1
|
||||
: state.encryptionKeys.keysVersion,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear encryption keys (called on sign out or when leaving couple)
|
||||
*/
|
||||
clearEncryptionKeys: () => {
|
||||
set({ encryptionKeys: initialEncryptionKeyState });
|
||||
},
|
||||
}));
|
||||
Reference in new issue
Block a user