Skip to content

Commit 30c1b7d

Browse files
authored
macOS: add Delete Account & Data flow + backend endpoint (#5461)
## Summary - add **Delete Account & Data** action in macOS Settings > Profile/Account - call new desktop backend endpoint `DELETE /v1/users/delete-account` - implement server-side account purge for user-scoped data + delete Firebase Auth user (idempotent on `USER_NOT_FOUND`) - sign out and return user to clean onboarding state after successful deletion - make dev app title explicit (`Omi Dev`) and harden dev scripts for local backend/prod Firebase testing - add AGENTS safety rule to never kill production `/Applications/omi.app` during dev runs ## Verification - `cargo build --release` in `desktop/Backend-Rust` - `swift build` in `desktop/Desktop` ## Notes - PR intentionally contains only the account-deletion + dev-launch safety fixes from this workstream, excluding unrelated local edits.
2 parents fe5f67d + 9838c93 commit 30c1b7d

File tree

8 files changed

+592
-30
lines changed

8 files changed

+592
-30
lines changed

AGENTS.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ These rules apply to Codex when working in this repository.
66

77
- Install pre-commit hook: `ln -s -f ../../scripts/pre-commit .git/hooks/pre-commit`
88

9+
## Safety Rules
10+
11+
- Never kill, stop, or restart the production macOS app (`/Applications/omi.app`, bundle id `com.omi.computer-macos`) during local development or testing.
12+
- Development scripts/commands must target only dev app processes (for example `Omi Dev.app` / `com.omi.desktop-dev`), never production.
13+
914
## Coding Guidelines
1015

1116
### Backend
@@ -69,4 +74,4 @@ Always format code after making changes. The pre-commit hook handles this automa
6974

7075
- Always run tests before committing:
7176
- Backend changes: run `backend/test.sh`
72-
- App changes: run `app/test.sh`
77+
- App changes: run `app/test.sh`

desktop/Backend-Rust/src/routes/users.rs

Lines changed: 301 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
use axum::{
55
extract::{Query, State},
66
http::StatusCode,
7-
routing::get,
7+
routing::{delete, get},
88
Json, Router,
99
};
1010
use serde::Deserialize;
11+
use std::collections::HashSet;
1112

1213
use crate::auth::AuthUser;
1314
use crate::models::{
@@ -17,6 +18,7 @@ use crate::models::{
1718
UpdateTranscriptionPreferencesRequest, UpdateUserProfileRequest, UserLanguage, UserProfile,
1819
UserSettingsStatusResponse, AssistantSettingsData,
1920
};
21+
use crate::services::firestore::{LLM_USAGE_SUBCOLLECTION, SCREEN_ACTIVITY_SUBCOLLECTION};
2022
use crate::AppState;
2123

2224
// ============================================================================
@@ -553,6 +555,302 @@ async fn update_assistant_settings(
553555
}
554556
}
555557

558+
// ============================================================================
559+
// Account Deletion
560+
// ============================================================================
561+
562+
/// DELETE /v1/users/delete-account
563+
/// Deletes all server-side data for the authenticated user and then deletes Firebase Auth account.
564+
async fn delete_account(
565+
State(state): State<AppState>,
566+
user: AuthUser,
567+
) -> Result<Json<UserSettingsStatusResponse>, StatusCode> {
568+
tracing::info!("Deleting account and all data for user {}", user.uid);
569+
570+
// Conversations
571+
let conversations = state
572+
.firestore
573+
.get_conversations(&user.uid, 5000, 0, true, &[], None, None, None, None)
574+
.await
575+
.map_err(|e| {
576+
tracing::error!("Failed to list conversations during delete-account: {}", e);
577+
StatusCode::INTERNAL_SERVER_ERROR
578+
})?;
579+
for conversation in conversations {
580+
state
581+
.firestore
582+
.delete_conversation(&user.uid, &conversation.id)
583+
.await
584+
.map_err(|e| {
585+
tracing::error!("Failed to delete conversation {}: {}", conversation.id, e);
586+
StatusCode::INTERNAL_SERVER_ERROR
587+
})?;
588+
}
589+
590+
// Action items
591+
let action_items = state
592+
.firestore
593+
.get_action_items(
594+
&user.uid, 5000, 0, None, None, None, None, None, None, None, Some(true),
595+
)
596+
.await
597+
.map_err(|e| {
598+
tracing::error!("Failed to list action items during delete-account: {}", e);
599+
StatusCode::INTERNAL_SERVER_ERROR
600+
})?;
601+
for item in action_items {
602+
state
603+
.firestore
604+
.delete_action_item(&user.uid, &item.id)
605+
.await
606+
.map_err(|e| {
607+
tracing::error!("Failed to delete action item {}: {}", item.id, e);
608+
StatusCode::INTERNAL_SERVER_ERROR
609+
})?;
610+
}
611+
612+
// Staged tasks
613+
let staged_tasks = state
614+
.firestore
615+
.get_staged_tasks(&user.uid, 5000, 0)
616+
.await
617+
.map_err(|e| {
618+
tracing::error!("Failed to list staged tasks during delete-account: {}", e);
619+
StatusCode::INTERNAL_SERVER_ERROR
620+
})?;
621+
for task in staged_tasks {
622+
state
623+
.firestore
624+
.delete_staged_task(&user.uid, &task.id)
625+
.await
626+
.map_err(|e| {
627+
tracing::error!("Failed to delete staged task {}: {}", task.id, e);
628+
StatusCode::INTERNAL_SERVER_ERROR
629+
})?;
630+
}
631+
632+
// Memories
633+
state
634+
.firestore
635+
.delete_all_memories(&user.uid)
636+
.await
637+
.map_err(|e| {
638+
tracing::error!("Failed to delete all memories during delete-account: {}", e);
639+
StatusCode::INTERNAL_SERVER_ERROR
640+
})?;
641+
642+
// Focus sessions
643+
let focus_sessions = state
644+
.firestore
645+
.get_focus_sessions(&user.uid, 5000, 0, None)
646+
.await
647+
.map_err(|e| {
648+
tracing::error!("Failed to list focus sessions during delete-account: {}", e);
649+
StatusCode::INTERNAL_SERVER_ERROR
650+
})?;
651+
for session in focus_sessions {
652+
state
653+
.firestore
654+
.delete_focus_session(&user.uid, &session.id)
655+
.await
656+
.map_err(|e| {
657+
tracing::error!("Failed to delete focus session {}: {}", session.id, e);
658+
StatusCode::INTERNAL_SERVER_ERROR
659+
})?;
660+
}
661+
662+
// Advice
663+
let advice_items = state
664+
.firestore
665+
.get_advice(&user.uid, 5000, 0, None, true)
666+
.await
667+
.map_err(|e| {
668+
tracing::error!("Failed to list advice during delete-account: {}", e);
669+
StatusCode::INTERNAL_SERVER_ERROR
670+
})?;
671+
for advice in advice_items {
672+
state
673+
.firestore
674+
.delete_advice(&user.uid, &advice.id)
675+
.await
676+
.map_err(|e| {
677+
tracing::error!("Failed to delete advice {}: {}", advice.id, e);
678+
StatusCode::INTERNAL_SERVER_ERROR
679+
})?;
680+
}
681+
682+
// Messages
683+
state
684+
.firestore
685+
.delete_messages(&user.uid, None)
686+
.await
687+
.map_err(|e| {
688+
tracing::error!("Failed to delete messages during delete-account: {}", e);
689+
StatusCode::INTERNAL_SERVER_ERROR
690+
})?;
691+
692+
// Chat sessions
693+
let chat_sessions = state
694+
.firestore
695+
.get_chat_sessions(&user.uid, None, 5000, 0, None)
696+
.await
697+
.map_err(|e| {
698+
tracing::error!("Failed to list chat sessions during delete-account: {}", e);
699+
StatusCode::INTERNAL_SERVER_ERROR
700+
})?;
701+
for session in chat_sessions {
702+
state
703+
.firestore
704+
.delete_chat_session(&user.uid, &session.id)
705+
.await
706+
.map_err(|e| {
707+
tracing::error!("Failed to delete chat session {}: {}", session.id, e);
708+
StatusCode::INTERNAL_SERVER_ERROR
709+
})?;
710+
}
711+
712+
// Folders
713+
let folders = state.firestore.get_folders(&user.uid).await.map_err(|e| {
714+
tracing::error!("Failed to list folders during delete-account: {}", e);
715+
StatusCode::INTERNAL_SERVER_ERROR
716+
})?;
717+
for folder in folders {
718+
state
719+
.firestore
720+
.delete_folder(&user.uid, &folder.id, None)
721+
.await
722+
.map_err(|e| {
723+
tracing::error!("Failed to delete folder {}: {}", folder.id, e);
724+
StatusCode::INTERNAL_SERVER_ERROR
725+
})?;
726+
}
727+
728+
// Goals (active + completed)
729+
let mut goal_ids = HashSet::new();
730+
let active_goals = state.firestore.get_user_goals(&user.uid, 5000).await.map_err(|e| {
731+
tracing::error!("Failed to list active goals during delete-account: {}", e);
732+
StatusCode::INTERNAL_SERVER_ERROR
733+
})?;
734+
let completed_goals = state
735+
.firestore
736+
.get_completed_goals(&user.uid, 5000)
737+
.await
738+
.map_err(|e| {
739+
tracing::error!("Failed to list completed goals during delete-account: {}", e);
740+
StatusCode::INTERNAL_SERVER_ERROR
741+
})?;
742+
for goal in active_goals {
743+
goal_ids.insert(goal.id);
744+
}
745+
for goal in completed_goals {
746+
goal_ids.insert(goal.id);
747+
}
748+
for goal_id in goal_ids {
749+
state
750+
.firestore
751+
.delete_goal(&user.uid, &goal_id)
752+
.await
753+
.map_err(|e| {
754+
tracing::error!("Failed to delete goal {}: {}", goal_id, e);
755+
StatusCode::INTERNAL_SERVER_ERROR
756+
})?;
757+
}
758+
759+
// People
760+
let people = state.firestore.get_people(&user.uid).await.map_err(|e| {
761+
tracing::error!("Failed to list people during delete-account: {}", e);
762+
StatusCode::INTERNAL_SERVER_ERROR
763+
})?;
764+
for person in people {
765+
state
766+
.firestore
767+
.delete_person(&user.uid, &person.id)
768+
.await
769+
.map_err(|e| {
770+
tracing::error!("Failed to delete person {}: {}", person.id, e);
771+
StatusCode::INTERNAL_SERVER_ERROR
772+
})?;
773+
}
774+
775+
// Persona
776+
if let Some(persona) = state
777+
.firestore
778+
.get_user_persona(&user.uid)
779+
.await
780+
.map_err(|e| {
781+
tracing::error!("Failed to get user persona during delete-account: {}", e);
782+
StatusCode::INTERNAL_SERVER_ERROR
783+
})?
784+
{
785+
state
786+
.firestore
787+
.delete_persona(&persona.id)
788+
.await
789+
.map_err(|e| {
790+
tracing::error!("Failed to delete persona {}: {}", persona.id, e);
791+
StatusCode::INTERNAL_SERVER_ERROR
792+
})?;
793+
}
794+
795+
// Knowledge graph
796+
state
797+
.firestore
798+
.delete_kg_data(&user.uid)
799+
.await
800+
.map_err(|e| {
801+
tracing::error!("Failed to delete knowledge graph during delete-account: {}", e);
802+
StatusCode::INTERNAL_SERVER_ERROR
803+
})?;
804+
805+
// Subcollections not otherwise covered
806+
state
807+
.firestore
808+
.delete_all_documents_in_subcollection(&user.uid, SCREEN_ACTIVITY_SUBCOLLECTION)
809+
.await
810+
.map_err(|e| {
811+
tracing::error!("Failed to delete screen activity during delete-account: {}", e);
812+
StatusCode::INTERNAL_SERVER_ERROR
813+
})?;
814+
state
815+
.firestore
816+
.delete_all_documents_in_subcollection(&user.uid, LLM_USAGE_SUBCOLLECTION)
817+
.await
818+
.map_err(|e| {
819+
tracing::error!("Failed to delete llm usage during delete-account: {}", e);
820+
StatusCode::INTERNAL_SERVER_ERROR
821+
})?;
822+
823+
// Delete root users/{uid} document after subcollections are purged
824+
state
825+
.firestore
826+
.delete_user_root_document(&user.uid)
827+
.await
828+
.map_err(|e| {
829+
tracing::error!("Failed to delete root user document during delete-account: {}", e);
830+
StatusCode::INTERNAL_SERVER_ERROR
831+
})?;
832+
833+
// Delete Firebase Auth account via admin API (service account OAuth).
834+
let project_id = state
835+
.config
836+
.firebase_project_id
837+
.clone()
838+
.unwrap_or_else(|| "based-hardware".to_string());
839+
state
840+
.firestore
841+
.delete_firebase_auth_user(&project_id, &user.uid)
842+
.await
843+
.map_err(|e| {
844+
tracing::error!("Failed to delete Firebase Auth account for {}: {}", user.uid, e);
845+
StatusCode::INTERNAL_SERVER_ERROR
846+
})?;
847+
848+
tracing::info!("Account deletion completed for user {}", user.uid);
849+
Ok(Json(UserSettingsStatusResponse {
850+
status: "ok".to_string(),
851+
}))
852+
}
853+
556854
// ============================================================================
557855
// Router
558856
// ============================================================================
@@ -601,4 +899,6 @@ pub fn users_routes() -> Router<AppState> {
601899
"/v1/users/assistant-settings",
602900
get(get_assistant_settings).patch(update_assistant_settings),
603901
)
902+
// Account deletion
903+
.route("/v1/users/delete-account", delete(delete_account))
604904
}

0 commit comments

Comments
 (0)