From adca37223cb32f90b09ceff650ff804594800402 Mon Sep 17 00:00:00 2001 From: Ren Amamiya <123083837+reyamir@users.noreply.github.com> Date: Mon, 14 Aug 2023 18:15:58 +0700 Subject: [PATCH] refactor storage layer --- .../20230814083543_add_events_table.sql | 9 ++ src-tauri/src/main.rs | 76 +--------- src-tauri/src/opg.rs | 67 +++++++++ src/app/auth/unlock.tsx | 16 +- src/libs/storage/instance.ts | 142 ++++++++++++++++++ src/libs/storage/provider.tsx | 50 ++++++ src/main.tsx | 9 +- 7 files changed, 294 insertions(+), 75 deletions(-) create mode 100644 src-tauri/migrations/20230814083543_add_events_table.sql create mode 100644 src-tauri/src/opg.rs create mode 100644 src/libs/storage/instance.ts create mode 100644 src/libs/storage/provider.tsx diff --git a/src-tauri/migrations/20230814083543_add_events_table.sql b/src-tauri/migrations/20230814083543_add_events_table.sql new file mode 100644 index 00000000..31049a6c --- /dev/null +++ b/src-tauri/migrations/20230814083543_add_events_table.sql @@ -0,0 +1,9 @@ +-- Add migration script here +CREATE TABLE + events ( + id INTEGER NOT NULL PRIMARY KEY, + cache_key TEXT NOT NULL UNIQUE, + event_id TEXT NOT NULL UNIQUE, + event_kind INTEGER NOT NULL DEFAULT 1, + event TEXT NOT NULL + ); \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ed45ae11..5bf16f95 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,13 +3,12 @@ windows_subsystem = "windows" )] -use std::time::Duration; +mod opg; -// use rand::distributions::{Alphanumeric, DistString}; +use opg::opengraph; use tauri::Manager; use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_sql::{Migration, MigrationKind}; -use webpage::{Webpage, WebpageOptions}; use window_vibrancy::{apply_mica, apply_vibrancy, NSVisualEffectMaterial}; #[derive(Clone, serde::Serialize)] @@ -18,71 +17,6 @@ struct Payload { cwd: String, } -#[derive(serde::Serialize)] -struct OpenGraphResponse { - title: String, - description: String, - url: String, - image: String, -} - -async fn fetch_opengraph(url: String) -> OpenGraphResponse { - let options = WebpageOptions { - allow_insecure: false, - max_redirections: 3, - timeout: Duration::from_secs(15), - useragent: "lume - desktop app".to_string(), - ..Default::default() - }; - - let result = match Webpage::from_url(&url, options) { - Ok(webpage) => webpage, - Err(_) => { - return OpenGraphResponse { - title: "".to_string(), - description: "".to_string(), - url: "".to_string(), - image: "".to_string(), - } - } - }; - - let html = result.html; - - return OpenGraphResponse { - title: html - .opengraph - .properties - .get("title") - .cloned() - .unwrap_or_default(), - description: html - .opengraph - .properties - .get("description") - .cloned() - .unwrap_or_default(), - url: html - .opengraph - .properties - .get("url") - .cloned() - .unwrap_or_default(), - image: html - .opengraph - .images - .get(0) - .and_then(|i| Some(i.url.clone())) - .unwrap_or_default(), - }; -} - -#[tauri::command] -async fn opengraph(url: String) -> OpenGraphResponse { - let result = fetch_opengraph(url).await; - return result; -} - #[tauri::command] async fn close_splashscreen(window: tauri::Window) { // Close splashscreen @@ -184,6 +118,12 @@ fn main() { sql: include_str!("../migrations/20230811074423_rename_blocks_to_widgets.sql"), kind: MigrationKind::Up, }, + Migration { + version: 20230814083543, + description: "add events", + sql: include_str!("../migrations/20230814083543_add_events_table.sql"), + kind: MigrationKind::Up, + }, ], ) .build(), diff --git a/src-tauri/src/opg.rs b/src-tauri/src/opg.rs new file mode 100644 index 00000000..a99e2c30 --- /dev/null +++ b/src-tauri/src/opg.rs @@ -0,0 +1,67 @@ +use std::time::Duration; +use webpage::{Webpage, WebpageOptions}; + +#[derive(serde::Serialize)] +pub struct OpenGraphResponse { + title: String, + description: String, + url: String, + image: String, +} + +async fn fetch_opengraph(url: String) -> OpenGraphResponse { + let options = WebpageOptions { + allow_insecure: false, + max_redirections: 3, + timeout: Duration::from_secs(15), + useragent: "lume - desktop app".to_string(), + ..Default::default() + }; + + let result = match Webpage::from_url(&url, options) { + Ok(webpage) => webpage, + Err(_) => { + return OpenGraphResponse { + title: "".to_string(), + description: "".to_string(), + url: "".to_string(), + image: "".to_string(), + } + } + }; + + let html = result.html; + + return OpenGraphResponse { + title: html + .opengraph + .properties + .get("title") + .cloned() + .unwrap_or_default(), + description: html + .opengraph + .properties + .get("description") + .cloned() + .unwrap_or_default(), + url: html + .opengraph + .properties + .get("url") + .cloned() + .unwrap_or_default(), + image: html + .opengraph + .images + .get(0) + .and_then(|i| Some(i.url.clone())) + .unwrap_or_default(), + }; +} + +#[tauri::command] +pub async fn opengraph(url: String) -> OpenGraphResponse { + let result = fetch_opengraph(url).await; + return result; +} diff --git a/src/app/auth/unlock.tsx b/src/app/auth/unlock.tsx index 2306256a..471744e9 100644 --- a/src/app/auth/unlock.tsx +++ b/src/app/auth/unlock.tsx @@ -1,13 +1,16 @@ +import { appConfigDir } from '@tauri-apps/api/path'; +import { Stronghold } from '@tauri-apps/plugin-stronghold'; import { useState } from 'react'; import { Resolver, useForm } from 'react-hook-form'; import { Link, useNavigate } from 'react-router-dom'; +import { useStorage } from '@libs/storage/provider'; + import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons'; import { useStronghold } from '@stores/stronghold'; import { useAccount } from '@utils/hooks/useAccount'; -import { useSecureStorage } from '@utils/hooks/useSecureStorage'; type FormValues = { password: string; @@ -35,7 +38,7 @@ export function UnlockScreen() { const [loading, setLoading] = useState(false); const { account } = useAccount(); - const { load } = useSecureStorage(); + const { db } = useStorage(); const { register, @@ -47,9 +50,14 @@ export function UnlockScreen() { const onSubmit = async (data: { [x: string]: string }) => { setLoading(true); if (data.password.length > 3) { - // load private in secure storage try { - const privkey = await load(account.pubkey, data.password); + const dir = await appConfigDir(); + const stronghold = await Stronghold.load(`${dir}/lume.stronghold`, data.password); + + db.secureDB = stronghold; + + const privkey = await db.secureLoad(account.pubkey); + setPrivkey(privkey); // redirect to home navigate('/', { replace: true }); diff --git a/src/libs/storage/instance.ts b/src/libs/storage/instance.ts new file mode 100644 index 00000000..15100eeb --- /dev/null +++ b/src/libs/storage/instance.ts @@ -0,0 +1,142 @@ +import Database from '@tauri-apps/plugin-sql'; +import { Stronghold } from '@tauri-apps/plugin-stronghold'; + +import { Account, Widget } from '@utils/types'; + +export class LumeStorage { + public db: Database; + public secureDB: Stronghold; + + constructor(sqlite: Database, stronghold?: Stronghold) { + this.db = sqlite; + this.secureDB = stronghold ?? undefined; + } + + private async getSecureClient() { + try { + return await this.secureDB.loadClient('lume'); + } catch { + return await this.secureDB.createClient('lume'); + } + } + + public async secureSave(key: string, value: string) { + if (!this.secureDB) throw new Error("Stronghold isn't initialize"); + + const client = await this.getSecureClient(); + const store = client.getStore(); + await store.insert(key, Array.from(new TextEncoder().encode(value))); + return await this.secureDB.save(); + } + + public async secureLoad(key: string) { + if (!this.secureDB) throw new Error("Stronghold isn't initialize"); + + const client = await this.getSecureClient(); + const store = client.getStore(); + const value = await store.get(key); + const decoded = new TextDecoder().decode(new Uint8Array(value)); + return decoded; + } + + public async getActiveAccount() { + const account: Account = await this.db.select( + 'SELECT * FROM accounts WHERE is_active = 1;' + )?.[0]; + if (account) { + if (typeof account.follows === 'string') + account.follows = JSON.parse(account.follows); + + if (typeof account.network === 'string') + account.network = JSON.parse(account.network); + + return account; + } else { + throw new Error('Account not found'); + } + } + + public async createAccount(npub: string, pubkey: string) { + const res = await this.db.execute( + 'INSERT OR IGNORE INTO accounts (npub, pubkey, privkey, is_active) VALUES ($1, $2, $3, $4);', + [npub, pubkey, 'privkey is stored in secure storage', 1] + ); + if (res) { + const account = await this.getActiveAccount(); + return account; + } else { + console.error('create account failed'); + } + } + + public async updateAccount(column: string, value: string | string[]) { + const account = await this.getActiveAccount(); + return await this.db.execute(`UPDATE accounts SET ${column} = $1 WHERE id = $2;`, [ + value, + account.id, + ]); + } + + public async getWidgets() { + const account = await this.getActiveAccount(); + const result: Array = await this.db.select( + `SELECT * FROM widgets WHERE account_id = "${account.id}" ORDER BY created_at DESC;` + ); + return result; + } + + public async createWidget(kind: number, title: string, content: string | string[]) { + const account = await this.getActiveAccount(); + const insert = await this.db.execute( + 'INSERT OR IGNORE INTO widgets (account_id, kind, title, content) VALUES ($1, $2, $3, $4);', + [account.id, kind, title, content] + ); + if (insert) { + const widget: Widget = await this.db.select( + 'SELECT * FROM widgets ORDER BY id DESC LIMIT 1;' + )?.[0]; + if (!widget) console.error('get created widget failed'); + return widget; + } else { + console.error('create widget failed'); + } + } + + public async removeWidget(id: string) { + return await this.db.execute('DELETE FROM widgets WHERE id = $1;', [id]); + } + + public async createEvent( + cacheKey: string, + event_id: string, + event_kind: number, + event: string + ) { + return await this.db.execute( + 'INSERT OR IGNORE INTO events (cache_key, event_id, event_kind, event) VALUES ($1, $2, $3, $4);', + [cacheKey, event_id, event_kind, event] + ); + } + + public async getEventByKey(cacheKey: string) { + const event = await this.db.select( + 'SELECT * FROM events WHERE cache_key = $1 ORDER BY id DESC LIMIT 1;', + [cacheKey] + )?.[0]; + if (!event) console.error('failed to get event by cache_key: ', cacheKey); + return event; + } + + public async getEventByID(id: string) { + const event = await this.db.select( + 'SELECT * FROM events WHERE event_id = $1 ORDER BY id DESC LIMIT 1;', + [id] + )?.[0]; + if (!event) console.error('failed to get event by id: ', id); + return event; + } + + public async close() { + return this.db.close(); + } +} diff --git a/src/libs/storage/provider.tsx b/src/libs/storage/provider.tsx new file mode 100644 index 00000000..4adc2571 --- /dev/null +++ b/src/libs/storage/provider.tsx @@ -0,0 +1,50 @@ +import Database from '@tauri-apps/plugin-sql'; +import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react'; + +import { LumeStorage } from '@libs/storage/instance'; + +interface StorageContext { + db: LumeStorage; +} + +const StorageContext = createContext({ + db: undefined, +}); + +const StorageProvider = ({ children }: PropsWithChildren) => { + const [db, setDB] = useState(undefined); + + async function initLumeStorage() { + const sqlite = await Database.load('sqlite:lume.db'); + const lumeStorage = new LumeStorage(sqlite); + setDB(lumeStorage); + } + + useEffect(() => { + if (!db) initLumeStorage(); + + return () => { + db.close(); + }; + }, []); + + return ( + + {children} + + ); +}; + +const useStorage = () => { + const context = useContext(StorageContext); + if (context === undefined) { + throw new Error('Storage not found'); + } + return context; +}; + +export { StorageProvider, useStorage }; diff --git a/src/main.tsx b/src/main.tsx index 6c5beca5..71a81ce1 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client'; import { NDKProvider } from '@libs/ndk/provider'; import { getSetting } from '@libs/storage'; +import { StorageProvider } from '@libs/storage/provider'; import App from './app'; @@ -21,8 +22,10 @@ const root = createRoot(container); root.render( - - - + + + + + );