refactor storage layer

This commit is contained in:
Ren Amamiya 2023-08-14 18:15:58 +07:00
parent 823b203b73
commit adca37223c
7 changed files with 294 additions and 75 deletions

View File

@ -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
);

View File

@ -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(),

67
src-tauri/src/opg.rs Normal file
View File

@ -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;
}

View File

@ -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<boolean>(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 });

View File

@ -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<Widget> = 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();
}
}

View File

@ -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<StorageContext>({
db: undefined,
});
const StorageProvider = ({ children }: PropsWithChildren<object>) => {
const [db, setDB] = useState<LumeStorage>(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 (
<StorageContext.Provider
value={{
db,
}}
>
{children}
</StorageContext.Provider>
);
};
const useStorage = () => {
const context = useContext(StorageContext);
if (context === undefined) {
throw new Error('Storage not found');
}
return context;
};
export { StorageProvider, useStorage };

View File

@ -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(
<QueryClientProvider client={queryClient}>
<NDKProvider>
<App />
</NDKProvider>
<StorageProvider>
<NDKProvider>
<App />
</NDKProvider>
</StorageProvider>
</QueryClientProvider>
);