diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4c18ba0..04bb747 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -7,6 +7,7 @@ name = "accounting" version = "0.0.1" dependencies = [ "anyhow", + "bcrypt", "cocoa", "log", "objc", @@ -399,6 +400,19 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bcrypt" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7" +dependencies = [ + "base64 0.22.1", + "blowfish", + "getrandom 0.2.15", + "subtle", + "zeroize", +] + [[package]] name = "bigdecimal" version = "0.3.1" @@ -509,6 +523,16 @@ dependencies = [ "piper", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "borsh" version = "1.3.0" @@ -782,6 +806,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -2658,6 +2692,15 @@ dependencies = [ "cfb", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -8324,6 +8367,12 @@ dependencies = [ "syn 2.0.71", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zvariant" version = "4.0.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 34c8266..9de34fc 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -36,6 +36,7 @@ log = "0.4" thiserror = "1.0.61" platform-dirs = "0.3.0" anyhow = "1.0.86" +bcrypt = "0.15.1" # specta = "=2.0.0-rc.12" # tauri-specta = { version = "=2.0.0-rc.11", features = ["javascript", "typescript"] } diff --git a/src-tauri/prisma/schema.prisma b/src-tauri/prisma/schema.prisma index 4801fac..9a0b984 100644 --- a/src-tauri/prisma/schema.prisma +++ b/src-tauri/prisma/schema.prisma @@ -17,6 +17,7 @@ model Company { currencies Currency[] clients Client[] settings Settings? + password String? cin String @unique // IČO vatId String? @unique // DIČ diff --git a/src-tauri/src/commands/company.rs b/src-tauri/src/commands/company.rs index 85406e7..9e75ec5 100644 --- a/src-tauri/src/commands/company.rs +++ b/src-tauri/src/commands/company.rs @@ -2,24 +2,41 @@ use crate::company; use crate::currency; use crate::template; use crate::DbState; +use bcrypt::{hash, DEFAULT_COST}; use prisma_client_rust::not; use prisma_client_rust::QueryError; use serde::Deserialize; +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompanyWithProtection { + id: i32, + name: String, + email: Option, + is_protected: bool, +} + #[tauri::command] pub async fn get_companies( client: DbState<'_>, exclude: Option, -) -> Result, QueryError> { - println!("Getting companies, exluding {:?}", exclude); - let data = client +) -> Result, QueryError> { + println!("Getting companies, excluding {:?}", exclude); + let companies = client .company() .find_many(vec![not![company::id::equals(exclude.unwrap_or(999))]]) .exec() - .await; + .await?; - println!("{:?}", data); - data + Ok(companies + .into_iter() + .map(|c| CompanyWithProtection { + id: c.id, + name: c.name, + email: c.email, + is_protected: c.password.is_some(), + }) + .collect()) } #[tauri::command] @@ -45,9 +62,9 @@ pub struct ManageCompanyData { zip: String, phone: Option, email: Option, - bank_account: Option, bank_iban: Option, + password: Option, } #[tauri::command] @@ -56,6 +73,14 @@ pub async fn create_company( data: ManageCompanyData, ) -> Result { debug!("Creating company {:?}", data); + + let password_hash = match data.password { + Some(password) => Some( + hash(password, DEFAULT_COST).map_err(|e| QueryError::PasswordHashing(e.to_string()))?, + ), + None => None, + }; + let company = client .company() .create( @@ -70,6 +95,7 @@ pub async fn create_company( company::phone::set(data.phone), company::bank_account::set(data.bank_account), company::bank_iban::set(data.bank_iban), + company::password::set(password_hash), ], ) .exec() @@ -168,3 +194,27 @@ pub async fn delete_company(client: DbState<'_>, id: i32) -> Result<(), String> Err(e) => Err(e.to_string()), } } + +#[tauri::command] +pub async fn validate_company_password( + client: DbState<'_>, + id: i32, + password: String, +) -> Result { + let company = client + .company() + .find_first(vec![company::id::equals(id)]) + .exec() + .await?; + + match company { + Some(c) => { + if let Some(hash) = c.password { + Ok(bcrypt::verify(password, &hash).unwrap_or(false)) + } else { + Ok(false) + } + } + None => Ok(false), + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 9878105..80deed4 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -95,6 +95,7 @@ async fn main() { commands::db::check_db, commands::db::migrate_and_populate, commands::company::get_company, + commands::company::validate_company_password, commands::company::create_company, commands::company::get_companies, commands::company::delete_company, diff --git a/src/Loader.tsx b/src/Loader.tsx index b1224e6..2e77467 100644 --- a/src/Loader.tsx +++ b/src/Loader.tsx @@ -8,7 +8,7 @@ const Loader: Component = () => { const startup = async () => { const data = await checkDb(); - navigate(data === 200 ? "/dashboard" : "/setup"); + navigate(data === 200 ? "/login" : "/setup"); }; startup(); diff --git a/src/bindings.ts b/src/bindings.ts index f3f1df3..18ec133 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -155,6 +155,9 @@ export async function migrateAndPopulate() { export async function getCompanies(exclude?: number) { return await invoke("get_companies", { exclude }); } +export async function validateCompanyPassword(id: number, password: string) { + return await invoke("validate_company_password", { id, password }); +} export async function getTemplates(indicies: Indicies, templateType?: "INVOICE" | "PROFORMA" | "RECEIVE") { return await invoke("get_templates", { companyId: state.companyId, indicies, templateType }); @@ -301,6 +304,8 @@ export type Company = { bankAccount: string | null; bankIban: string | null; + + isProtected: boolean; }; export type Template = { diff --git a/src/index.tsx b/src/index.tsx index 698af4e..4e6bc85 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -20,6 +20,7 @@ const ClientDetail = lazy(() => import("./screens/Dashboard/pages/Other/Clients/ const ManageClient = lazy(() => import("./screens/Dashboard/pages/Other/Clients/ManageClient")); const Settings = lazy(() => import("./screens/Dashboard/pages/Settings")); const Dashboard = lazy(() => import("./screens/Dashboard")); +const Login = lazy(() => import("./screens/Login")); const Setup = lazy(() => import("./screens/Setup")); const App = lazy(() => import("./App")); const Loader = lazy(() => import("./Loader")); @@ -40,6 +41,7 @@ render( + @@ -78,7 +80,7 @@ render( - } /> + } /> ), diff --git a/src/screens/Dashboard/index.tsx b/src/screens/Dashboard/index.tsx index 6b56381..b140131 100644 --- a/src/screens/Dashboard/index.tsx +++ b/src/screens/Dashboard/index.tsx @@ -16,13 +16,13 @@ const Dashboard: ParentComponent = (props) => { const fetchCompany = async (companyId: number) => { if (companyId === 0) { - navigate("/setup"); + navigate("/login"); return; } const companyData = await getCompany(companyId); if (!companyData) { - navigate("/setup"); + navigate("/login"); toast.error("Company not found"); return; } diff --git a/src/screens/Dashboard/pages/Other/Currencies/ManageCurrency.tsx b/src/screens/Dashboard/pages/Other/Currencies/ManageCurrency.tsx index d844f89..043668f 100644 --- a/src/screens/Dashboard/pages/Other/Currencies/ManageCurrency.tsx +++ b/src/screens/Dashboard/pages/Other/Currencies/ManageCurrency.tsx @@ -112,8 +112,7 @@ const ManageCurrency: Component = () => { {(field) => ( - { + const [companies, setCompanies] = createSignal([]); + const [selectedCompany, setSelectedCompany] = createSignal(null); + const [password, setPassword] = createSignal(""); + const [isValidating, setIsValidating] = createSignal(false); + const [passwordError, setPasswordError] = createSignal(null); + const navigate = useNavigate(); + const stateService = useSelector((state) => state.stateService); + + onMount(async () => { + const data = await getCompanies(); + console.log(data); + setCompanies(data); + }); + + const setCompany = async (company: Company) => { + if (company.isProtected) { + setIsValidating(true); + const isValid = await validateCompanyPassword(company.id, password()); + setIsValidating(false); + if (!isValid) { + setPasswordError("Invalid password"); + toast.error("Invalid password"); + return; + } + } + stateService.updateState({ companyId: company.id }); + toast.success("Switched company"); + navigate("/"); + }; + + const handleCompanyClick = (companyId: number) => { + setSelectedCompany((prev) => (prev === companyId ? null : companyId)); + setPassword(""); + setPasswordError(null); + }; + + return ( +
+
+

Manage Accounts

+

Switch accounts or sign in and sign out.

+
+ + {(company) => ( + +
handleCompanyClick(company.id)}> +
+
+
+
+
+

{company.name}

+

{company.email}

+
+
+
+ {company.isProtected && } + {selectedCompany() === company.id && } +
+
+ + {selectedCompany() === company.id && ( + + + setPassword(value)} + errors={passwordError() ? [passwordError()] : undefined} + class="w-full" + /> + + + + )} + + + )} + +
+ +
+
+ ); +}; + +export default Login; diff --git a/src/shared/components/Button.tsx b/src/shared/components/Button.tsx index aea14b7..e639b33 100644 --- a/src/shared/components/Button.tsx +++ b/src/shared/components/Button.tsx @@ -2,23 +2,30 @@ import type { ParentComponent } from "solid-js"; type ButtonType = "danger" | "warning" | "success" | "default"; -export const Button: ParentComponent<{ onClick?: (e: MouseEvent) => void; class?: string; type?: ButtonType }> = ({ - type = "default", - ...props -}) => { +export const Button: ParentComponent<{ + onClick?: (e: MouseEvent) => void; + class?: string; + type?: ButtonType; + disabled?: boolean; +}> = ({ type = "default", disabled = false, ...props }) => { return ( diff --git a/src/shared/components/Form/Input.tsx b/src/shared/components/Form/Input.tsx index 3f6a86f..6230b11 100644 --- a/src/shared/components/Form/Input.tsx +++ b/src/shared/components/Form/Input.tsx @@ -2,7 +2,7 @@ import type { ValidationError } from "@tanstack/solid-form"; import { For, Show, type Component } from "solid-js"; type TextInputProps = { - type: "text" | "date" | "email" | "tel"; + type: "text" | "date" | "email" | "tel" | "password"; onChange: (value: string) => void; label: string; placeholder?: string; diff --git a/src/store/services/stateService.ts b/src/store/services/stateService.ts index 63535dc..b8960d2 100644 --- a/src/store/services/stateService.ts +++ b/src/store/services/stateService.ts @@ -16,10 +16,10 @@ export const StateService = () => { const stateString = localStorage.getItem("state"); if (!stateString) { - setState({ companyId: 0, platform: await platform() }); + setState({ companyId: 0, platform: platform() }); } else { const parsedState = JSON.parse(stateString) as unknown as StateService; - setState({ ...parsedState, platform: await platform() }); + setState({ ...parsedState, platform: platform() }); } });