aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri/src/core/account_storage.rs
blob: df202cd30e57e8a1632b033bb641fff10c1c0fbb (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
use crate::core::auth::{Account, MicrosoftAccount, OfflineAccount};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use ts_rs::TS;

/// Stored account data for persistence
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[ts(export, export_to = "account.ts")]
pub struct AccountStore {
    pub accounts: Vec<StoredAccount>,
    pub active_account_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(tag = "type")]
#[ts(export, export_to = "account.ts")]
pub enum StoredAccount {
    Offline(OfflineAccount),
    Microsoft(StoredMicrosoftAccount),
}

/// Microsoft account with refresh token for persistence
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "account.ts")]
pub struct StoredMicrosoftAccount {
    pub username: String,
    pub uuid: String,
    pub access_token: String,
    pub refresh_token: Option<String>,
    pub ms_refresh_token: Option<String>, // Microsoft OAuth refresh token
    pub expires_at: i64,
}

impl StoredAccount {
    pub fn id(&self) -> String {
        match self {
            StoredAccount::Offline(a) => a.uuid.clone(),
            StoredAccount::Microsoft(a) => a.uuid.clone(),
        }
    }

    pub fn to_account(&self) -> Account {
        match self {
            StoredAccount::Offline(a) => Account::Offline(a.clone()),
            StoredAccount::Microsoft(a) => Account::Microsoft(MicrosoftAccount {
                username: a.username.clone(),
                uuid: a.uuid.clone(),
                access_token: a.access_token.clone(),
                refresh_token: a.refresh_token.clone(),
                expires_at: a.expires_at,
            }),
        }
    }

    pub fn from_account(account: &Account, ms_refresh_token: Option<String>) -> Self {
        match account {
            Account::Offline(a) => StoredAccount::Offline(a.clone()),
            Account::Microsoft(a) => StoredAccount::Microsoft(StoredMicrosoftAccount {
                username: a.username.clone(),
                uuid: a.uuid.clone(),
                access_token: a.access_token.clone(),
                refresh_token: a.refresh_token.clone(),
                ms_refresh_token,
                expires_at: a.expires_at,
            }),
        }
    }
}

#[derive(Debug, Clone, TS)]
#[ts(export, export_to = "account.ts")]
pub struct AccountStorage {
    file_path: PathBuf,
}

impl AccountStorage {
    pub fn new(app_data_dir: PathBuf) -> Self {
        Self {
            file_path: app_data_dir.join("accounts.json"),
        }
    }

    pub fn load(&self) -> AccountStore {
        if self.file_path.exists() {
            let content = fs::read_to_string(&self.file_path).unwrap_or_default();
            serde_json::from_str(&content).unwrap_or_default()
        } else {
            AccountStore::default()
        }
    }

    pub fn save(&self, store: &AccountStore) -> Result<(), String> {
        let content = serde_json::to_string_pretty(store).map_err(|e| e.to_string())?;
        if let Some(parent) = self.file_path.parent() {
            fs::create_dir_all(parent).map_err(|e| e.to_string())?;
        }
        fs::write(&self.file_path, content).map_err(|e| e.to_string())?;
        Ok(())
    }

    pub fn add_or_update_account(
        &self,
        account: &Account,
        ms_refresh_token: Option<String>,
    ) -> Result<(), String> {
        let mut store = self.load();
        let stored = StoredAccount::from_account(account, ms_refresh_token);
        let id = stored.id();

        // Remove existing account with same ID
        store.accounts.retain(|a| a.id() != id);
        store.accounts.push(stored);
        store.active_account_id = Some(id);

        self.save(&store)
    }

    pub fn remove_account(&self, uuid: &str) -> Result<(), String> {
        let mut store = self.load();
        store.accounts.retain(|a| a.id() != uuid);
        if store.active_account_id.as_deref() == Some(uuid) {
            store.active_account_id = store.accounts.first().map(|a| a.id());
        }
        self.save(&store)
    }

    pub fn get_active_account(&self) -> Option<(StoredAccount, Option<String>)> {
        let store = self.load();
        if let Some(active_id) = &store.active_account_id {
            store
                .accounts
                .iter()
                .find(|a| &a.id() == active_id)
                .map(|a| {
                    let ms_token = match a {
                        StoredAccount::Microsoft(m) => m.ms_refresh_token.clone(),
                        _ => None,
                    };
                    (a.clone(), ms_token)
                })
        } else {
            None
        }
    }

    #[allow(dead_code)]
    pub fn set_active_account(&self, uuid: &str) -> Result<(), String> {
        let mut store = self.load();
        if store.accounts.iter().any(|a| a.id() == uuid) {
            store.active_account_id = Some(uuid.to_string());
            self.save(&store)
        } else {
            Err("Account not found".to_string())
        }
    }

    #[allow(dead_code)]
    pub fn get_all_accounts(&self) -> Vec<StoredAccount> {
        self.load().accounts
    }
}