refactor storage layer
This commit is contained in:
9
src-tauri/migrations/20230814083543_add_events_table.sql
Normal file
9
src-tauri/migrations/20230814083543_add_events_table.sql
Normal 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
|
||||||
|
);
|
||||||
@@ -3,13 +3,12 @@
|
|||||||
windows_subsystem = "windows"
|
windows_subsystem = "windows"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
use std::time::Duration;
|
mod opg;
|
||||||
|
|
||||||
// use rand::distributions::{Alphanumeric, DistString};
|
use opg::opengraph;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
use tauri_plugin_sql::{Migration, MigrationKind};
|
use tauri_plugin_sql::{Migration, MigrationKind};
|
||||||
use webpage::{Webpage, WebpageOptions};
|
|
||||||
use window_vibrancy::{apply_mica, apply_vibrancy, NSVisualEffectMaterial};
|
use window_vibrancy::{apply_mica, apply_vibrancy, NSVisualEffectMaterial};
|
||||||
|
|
||||||
#[derive(Clone, serde::Serialize)]
|
#[derive(Clone, serde::Serialize)]
|
||||||
@@ -18,71 +17,6 @@ struct Payload {
|
|||||||
cwd: String,
|
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]
|
#[tauri::command]
|
||||||
async fn close_splashscreen(window: tauri::Window) {
|
async fn close_splashscreen(window: tauri::Window) {
|
||||||
// Close splashscreen
|
// Close splashscreen
|
||||||
@@ -184,6 +118,12 @@ fn main() {
|
|||||||
sql: include_str!("../migrations/20230811074423_rename_blocks_to_widgets.sql"),
|
sql: include_str!("../migrations/20230811074423_rename_blocks_to_widgets.sql"),
|
||||||
kind: MigrationKind::Up,
|
kind: MigrationKind::Up,
|
||||||
},
|
},
|
||||||
|
Migration {
|
||||||
|
version: 20230814083543,
|
||||||
|
description: "add events",
|
||||||
|
sql: include_str!("../migrations/20230814083543_add_events_table.sql"),
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
|
|||||||
67
src-tauri/src/opg.rs
Normal file
67
src-tauri/src/opg.rs
Normal 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;
|
||||||
|
}
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
|
import { appConfigDir } from '@tauri-apps/api/path';
|
||||||
|
import { Stronghold } from '@tauri-apps/plugin-stronghold';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Resolver, useForm } from 'react-hook-form';
|
import { Resolver, useForm } from 'react-hook-form';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
|
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
|
||||||
|
|
||||||
import { useStronghold } from '@stores/stronghold';
|
import { useStronghold } from '@stores/stronghold';
|
||||||
|
|
||||||
import { useAccount } from '@utils/hooks/useAccount';
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
import { useSecureStorage } from '@utils/hooks/useSecureStorage';
|
|
||||||
|
|
||||||
type FormValues = {
|
type FormValues = {
|
||||||
password: string;
|
password: string;
|
||||||
@@ -35,7 +38,7 @@ export function UnlockScreen() {
|
|||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
const { account } = useAccount();
|
const { account } = useAccount();
|
||||||
const { load } = useSecureStorage();
|
const { db } = useStorage();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -47,9 +50,14 @@ export function UnlockScreen() {
|
|||||||
const onSubmit = async (data: { [x: string]: string }) => {
|
const onSubmit = async (data: { [x: string]: string }) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
if (data.password.length > 3) {
|
if (data.password.length > 3) {
|
||||||
// load private in secure storage
|
|
||||||
try {
|
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);
|
setPrivkey(privkey);
|
||||||
// redirect to home
|
// redirect to home
|
||||||
navigate('/', { replace: true });
|
navigate('/', { replace: true });
|
||||||
|
|||||||
142
src/libs/storage/instance.ts
Normal file
142
src/libs/storage/instance.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/libs/storage/provider.tsx
Normal file
50
src/libs/storage/provider.tsx
Normal 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 };
|
||||||
@@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client';
|
|||||||
|
|
||||||
import { NDKProvider } from '@libs/ndk/provider';
|
import { NDKProvider } from '@libs/ndk/provider';
|
||||||
import { getSetting } from '@libs/storage';
|
import { getSetting } from '@libs/storage';
|
||||||
|
import { StorageProvider } from '@libs/storage/provider';
|
||||||
|
|
||||||
import App from './app';
|
import App from './app';
|
||||||
|
|
||||||
@@ -21,8 +22,10 @@ const root = createRoot(container);
|
|||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<NDKProvider>
|
<StorageProvider>
|
||||||
<App />
|
<NDKProvider>
|
||||||
</NDKProvider>
|
<App />
|
||||||
|
</NDKProvider>
|
||||||
|
</StorageProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user