Implement E2EE and message content moderation

This commit is contained in:
Jamie Brunton committed 2026-01-02 20:49:11 +00:00
1 parent e1ff87dc03
commit 198cd80dfd
74 files changed
+12939 -2178

No files matched your search

+96
View File
@@ -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 });
},
}));