mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-01 18:19:43 +00:00
feat(backend): add standalone data plane
Replace the Supabase application data plane with the Node/PostgreSQL API, migrate mobile, admin, web, and MCP callers, add controlled cutover tooling, and define hardened Dokploy infrastructure.
This commit is contained in:
1 parent
01bb8225ae
commit
006761d3b0
294 files changed
+16259
-6138
No files matched your search
@@ -5,16 +5,16 @@ import { useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import Constants from 'expo-constants';
|
||||
import Animated, { FadeInDown } from 'react-native-reanimated';
|
||||
import { Image } from 'expo-image';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
|
||||
import { GradientBackground } from '../../components/ui';
|
||||
import { Paywall } from '../../components/paywall';
|
||||
import { CompactHeader } from '../../components/discovery';
|
||||
import { MediaImage } from '../../components/MediaImage';
|
||||
import { colors, featureColors, spacing, typography, radius } from '../../theme';
|
||||
import { useAuthStore, useSubscriptionStore } from '../../store';
|
||||
import { resetMatchesTutorial } from '../../lib/matchesTutorialSeen';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { profileSettingsApi } from '../../lib/profileSettingsApi';
|
||||
|
||||
// Components
|
||||
import { SettingsSection, MenuItem, SubscriptionCard } from './components';
|
||||
@@ -105,8 +105,8 @@ export function ProfileScreen() {
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
{user?.avatar_url ? (
|
||||
<Image
|
||||
source={{ uri: user.avatar_url }}
|
||||
<MediaImage
|
||||
reference={user.avatar_url}
|
||||
style={styles.avatar}
|
||||
cachePolicy="disk"
|
||||
transition={200}
|
||||
@@ -235,11 +235,7 @@ export function ProfileScreen() {
|
||||
onPress={async () => {
|
||||
if (!user?.id) return;
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('profiles')
|
||||
.update({ onboarding_completed: false })
|
||||
.eq('id', user.id);
|
||||
if (error) throw error;
|
||||
await profileSettingsApi.updateProfile({ onboarding_completed: false });
|
||||
await fetchUser();
|
||||
Alert.alert("Success", "Onboarding has been reset.");
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { SettingsSection } from './SettingsSection';
|
||||
import { colors, featureColors, spacing, typography, radius } from '../../../theme';
|
||||
import { Profile, Couple } from '../../../types';
|
||||
import { MediaImage } from '../../../components/MediaImage';
|
||||
|
||||
const ACCENT_GRADIENT = featureColors.profile.gradient as [string, string];
|
||||
|
||||
@@ -59,8 +59,8 @@ export const CoupleStatus: React.FC<CoupleStatusProps> = ({
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: partner.avatar_url }}
|
||||
<MediaImage
|
||||
reference={partner.avatar_url}
|
||||
style={styles.partnerAvatar}
|
||||
cachePolicy="disk"
|
||||
transition={200}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, TextInput, ActivityIndicator } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import Animated, { FadeInDown } from 'react-native-reanimated';
|
||||
import { GlassCard } from '../../../components/ui';
|
||||
import { colors, featureColors, spacing, typography } from '../../../theme';
|
||||
import { Profile } from '../../../types';
|
||||
import { MediaImage } from '../../../components/MediaImage';
|
||||
|
||||
const ACCENT_GRADIENT = featureColors.profile.gradient as [string, string];
|
||||
|
||||
@@ -55,8 +55,8 @@ export const ProfileHeader: React.FC<ProfileHeaderProps> = ({
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
{user?.avatar_url ? (
|
||||
<Image
|
||||
source={{ uri: user.avatar_url }}
|
||||
<MediaImage
|
||||
reference={user.avatar_url}
|
||||
style={styles.avatarImage}
|
||||
cachePolicy="disk"
|
||||
transition={200}
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useState } from 'react';
|
||||
import { Alert } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useAuthStore, useMatchStore, useMessageStore, usePacksStore } from '../../../store';
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { accountOperationsApi } from '../../../lib/accountOperationsApi';
|
||||
import { coupleApi } from '../../../lib/coupleApi';
|
||||
import { Events } from '../../../lib/analytics';
|
||||
|
||||
export function useCoupleManagement() {
|
||||
@@ -12,7 +13,7 @@ export function useCoupleManagement() {
|
||||
const { clearMessages } = useMessageStore();
|
||||
const { clearPacks } = usePacksStore();
|
||||
|
||||
const [isResettingProgress, setIsResettingProgress] = useState(false);
|
||||
const [isResettingProgress] = useState(false);
|
||||
const [isDeletingAccount, setIsDeletingAccount] = useState(false);
|
||||
|
||||
const handleUnpair = async () => {
|
||||
@@ -26,13 +27,11 @@ export function useCoupleManagement() {
|
||||
style: "destructive",
|
||||
onPress: async () => {
|
||||
try {
|
||||
await supabase.functions.invoke("manage-couple", {
|
||||
method: "DELETE",
|
||||
});
|
||||
await coupleApi.cancel();
|
||||
await fetchCouple();
|
||||
// Refresh user to update UI state
|
||||
await fetchUser();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
Alert.alert("Error", "Failed to unpair");
|
||||
}
|
||||
},
|
||||
@@ -43,11 +42,7 @@ export function useCoupleManagement() {
|
||||
|
||||
const handleDeleteRelationship = async () => {
|
||||
try {
|
||||
const { error } = await supabase.functions.invoke("delete-relationship", {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
await accountOperationsApi.deleteRelationship();
|
||||
|
||||
// Clear local stores
|
||||
clearMatches();
|
||||
@@ -68,11 +63,7 @@ export function useCoupleManagement() {
|
||||
|
||||
const handleResetProgress = async () => {
|
||||
try {
|
||||
const { error } = await supabase.functions.invoke("reset-couple-progress", {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
await accountOperationsApi.resetProgress();
|
||||
|
||||
// Clear local stores
|
||||
clearMatches();
|
||||
@@ -103,11 +94,7 @@ export function useCoupleManagement() {
|
||||
const handleDeleteAccount = async () => {
|
||||
setIsDeletingAccount(true);
|
||||
try {
|
||||
const { error } = await supabase.functions.invoke("delete-account", {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
await accountOperationsApi.deleteAccount();
|
||||
|
||||
// Clear all local data
|
||||
clearMatches();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Alert, Linking, Platform } from 'react-native';
|
||||
import { useAuthStore, useSubscriptionStore, usePacksStore } from '../../../store';
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { profileSettingsApi } from '../../../lib/profileSettingsApi';
|
||||
import { Events } from '../../../lib/analytics';
|
||||
import { useAvatarPicker } from '../../../hooks/useAvatarPicker';
|
||||
import {
|
||||
@@ -33,20 +33,10 @@ export function useProfileSettings() {
|
||||
const [isUpdatingName, setIsUpdatingName] = useState(false);
|
||||
|
||||
// Avatar - using shared hook with immediate upload on select
|
||||
const handleAvatarUploadComplete = useCallback(async (publicUrl: string) => {
|
||||
const handleAvatarUploadComplete = useCallback(async (_mediaReference: string) => {
|
||||
if (!user?.id) return;
|
||||
|
||||
try {
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
avatar_url: publicUrl,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq('id', user.id);
|
||||
|
||||
if (updateError) throw updateError;
|
||||
|
||||
await fetchUser();
|
||||
Events.avatarUploaded();
|
||||
} catch (error) {
|
||||
@@ -131,15 +121,7 @@ export function useProfileSettings() {
|
||||
setIsUpdatingName(true);
|
||||
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
name: trimmedName,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq('id', user.id);
|
||||
|
||||
if (error) throw error;
|
||||
await profileSettingsApi.updateProfile({ name: trimmedName });
|
||||
|
||||
await fetchUser();
|
||||
setIsEditingName(false);
|
||||
@@ -241,17 +223,11 @@ export function useProfileSettings() {
|
||||
const showExplicitContent = !value;
|
||||
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
hide_nsfw: value,
|
||||
max_intensity: maxIntensity,
|
||||
show_explicit_content: showExplicitContent,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq('id', user.id);
|
||||
|
||||
if (error) throw error;
|
||||
await profileSettingsApi.updateProfile({
|
||||
hide_nsfw: value,
|
||||
max_intensity: maxIntensity,
|
||||
show_explicit_content: showExplicitContent,
|
||||
});
|
||||
|
||||
// Refresh user data and packs to reflect the change
|
||||
await fetchUser();
|
||||
|
||||
@@ -11,15 +11,16 @@ import {
|
||||
Alert,
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { MediaImage } from '../../../components/MediaImage';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import Animated, { FadeInDown } from 'react-native-reanimated';
|
||||
|
||||
import { GradientBackground } from '../../../components/ui';
|
||||
import { colors, featureColors, spacing, typography, radius } from '../../../theme';
|
||||
import { useAuthStore } from '../../../store';
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { authClient } from '../../../lib/authClient';
|
||||
import { profileSettingsApi } from '../../../lib/profileSettingsApi';
|
||||
import { Events } from '../../../lib/analytics';
|
||||
import { useProfileSettings } from '../hooks';
|
||||
import { ScreenHeader, SettingsSection } from '../components';
|
||||
@@ -31,7 +32,7 @@ function useAuthEmail() {
|
||||
|
||||
useEffect(() => {
|
||||
const getEmail = async () => {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
const { data: { user } } = await authClient.auth.getUser();
|
||||
setEmail(user?.email ?? null);
|
||||
};
|
||||
getEmail();
|
||||
@@ -83,9 +84,7 @@ export function ProfileSettingsScreen() {
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
const updates: Record<string, any> = {
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
const updates: { name?: string; gender?: Gender | null } = {};
|
||||
|
||||
const changedFields: string[] = [];
|
||||
|
||||
@@ -104,12 +103,7 @@ export function ProfileSettingsScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from('profiles')
|
||||
.update(updates)
|
||||
.eq('id', user.id);
|
||||
|
||||
if (error) throw error;
|
||||
await profileSettingsApi.updateProfile(updates);
|
||||
|
||||
await fetchUser();
|
||||
Events.profileUpdated(changedFields);
|
||||
@@ -166,8 +160,8 @@ export function ProfileSettingsScreen() {
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
{user?.avatar_url ? (
|
||||
<Image
|
||||
source={{ uri: user.avatar_url }}
|
||||
<MediaImage
|
||||
reference={user.avatar_url}
|
||||
style={styles.avatarImage}
|
||||
cachePolicy="disk"
|
||||
transition={200}
|
||||
|
||||
@@ -40,7 +40,7 @@ if (Platform.OS === "android") {
|
||||
|
||||
import { GradientBackground, GlassButton, GlassCard, GlassInput } from "../../../components/ui";
|
||||
import { colors, spacing, radius, typography } from "../../../theme";
|
||||
import { supabase } from "../../../lib/supabase";
|
||||
import { authClient } from "../../../lib/authClient";
|
||||
import { getAuthError } from "../../../lib/errors";
|
||||
import { useAuthStore } from "../../../store";
|
||||
import { ScreenHeader } from "../components";
|
||||
@@ -84,18 +84,6 @@ export function SaveAccountScreen() {
|
||||
Alert.alert("Success", message);
|
||||
}, []);
|
||||
|
||||
const syncProfileEmail = useCallback(async (userId: string, nextEmail: string | null | undefined) => {
|
||||
if (!nextEmail) return;
|
||||
try {
|
||||
await supabase
|
||||
.from("profiles")
|
||||
.update({ email: nextEmail })
|
||||
.eq("id", userId);
|
||||
} catch {
|
||||
// Non-blocking (some envs may restrict this by RLS)
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSaveWithEmail = useCallback(async () => {
|
||||
if (!email.trim()) {
|
||||
showError("Please enter your email");
|
||||
@@ -107,7 +95,7 @@ export function SaveAccountScreen() {
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
const { error } = await supabase.auth.updateUser(
|
||||
const { error } = await authClient.auth.updateUser(
|
||||
{ email: email.trim() },
|
||||
{ emailRedirectTo } as any
|
||||
);
|
||||
@@ -134,7 +122,7 @@ export function SaveAccountScreen() {
|
||||
|
||||
const handleCheckVerification = useCallback(async () => {
|
||||
setIsChecking(true);
|
||||
const { data: { user }, error } = await supabase.auth.getUser();
|
||||
const { data: { user }, error } = await authClient.auth.getUser();
|
||||
setIsChecking(false);
|
||||
|
||||
if (error || !user) {
|
||||
@@ -151,7 +139,7 @@ export function SaveAccountScreen() {
|
||||
// If user chose password mode, set password after email is verified
|
||||
if (pendingPassword) {
|
||||
setIsSubmitting(true);
|
||||
const { error: passwordError } = await supabase.auth.updateUser({ password: pendingPassword });
|
||||
const { error: passwordError } = await authClient.auth.updateUser({ password: pendingPassword });
|
||||
setIsSubmitting(false);
|
||||
|
||||
if (passwordError) {
|
||||
@@ -160,12 +148,11 @@ export function SaveAccountScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
await syncProfileEmail(user.id, user.email);
|
||||
await fetchUser();
|
||||
|
||||
showSuccess("Account saved. You can now recover your account on any device.");
|
||||
router.navigate("/(app)/profile" as any);
|
||||
}, [pendingPassword, fetchUser, showError, showSuccess, router, syncProfileEmail]);
|
||||
}, [pendingPassword, fetchUser, showError, showSuccess, router]);
|
||||
|
||||
const handleLinkApple = useCallback(async () => {
|
||||
if (!AppleAuthentication) return;
|
||||
@@ -184,7 +171,7 @@ export function SaveAccountScreen() {
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
const { error } = await (supabase.auth.linkIdentity as any)({
|
||||
const { error } = await (authClient.auth.linkIdentity as any)({
|
||||
provider: "apple",
|
||||
token: credential.identityToken,
|
||||
});
|
||||
@@ -205,7 +192,7 @@ export function SaveAccountScreen() {
|
||||
const fullName = nameParts.join(" ");
|
||||
|
||||
if (fullName) {
|
||||
await supabase.auth.updateUser({
|
||||
await authClient.auth.updateUser({
|
||||
data: {
|
||||
full_name: fullName,
|
||||
given_name: credential.fullName.givenName,
|
||||
@@ -216,10 +203,6 @@ export function SaveAccountScreen() {
|
||||
}
|
||||
|
||||
// Refresh user/profile
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (user) {
|
||||
await syncProfileEmail(user.id, user.email);
|
||||
}
|
||||
await fetchUser();
|
||||
|
||||
showSuccess("Account saved with Apple.");
|
||||
@@ -228,7 +211,7 @@ export function SaveAccountScreen() {
|
||||
if (e?.code === "ERR_REQUEST_CANCELED") return;
|
||||
showError("Apple sign in failed. Please try again.");
|
||||
}
|
||||
}, [fetchUser, router, showError, showSuccess, syncProfileEmail]);
|
||||
}, [fetchUser, router, showError, showSuccess]);
|
||||
|
||||
const handleLinkGoogle = useCallback(async () => {
|
||||
if (!GoogleSignin) return;
|
||||
@@ -242,7 +225,7 @@ export function SaveAccountScreen() {
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
const { error } = await (supabase.auth.linkIdentity as any)({
|
||||
const { error } = await (authClient.auth.linkIdentity as any)({
|
||||
provider: "google",
|
||||
token: response.data.idToken,
|
||||
});
|
||||
@@ -253,10 +236,6 @@ export function SaveAccountScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (user) {
|
||||
await syncProfileEmail(user.id, user.email);
|
||||
}
|
||||
await fetchUser();
|
||||
|
||||
showSuccess("Account saved with Google.");
|
||||
@@ -264,7 +243,7 @@ export function SaveAccountScreen() {
|
||||
} catch {
|
||||
showError("Google sign in failed. Please try again.");
|
||||
}
|
||||
}, [fetchUser, router, showError, showSuccess, syncProfileEmail]);
|
||||
}, [fetchUser, router, showError, showSuccess]);
|
||||
|
||||
const showSocial = Platform.select({ ios: true, android: true, default: false });
|
||||
|
||||
|
||||
Reference in new issue
Block a user