global fix and add tauri app
This commit is contained in:
30
tauri_app/src-tauri/Cargo.toml
Normal file
30
tauri_app/src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "lendry_id_super_app"
|
||||
version = "0.1.0"
|
||||
description = "Lendry ID Super App"
|
||||
authors = ["Lendry"]
|
||||
edition = "2021"
|
||||
rust-version = "1.77"
|
||||
|
||||
[lib]
|
||||
name = "lendry_id_super_app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-stronghold = "2"
|
||||
url = "2"
|
||||
dirs-next = "2"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = "s"
|
||||
strip = true
|
||||
3
tauri_app/src-tauri/build.rs
Normal file
3
tauri_app/src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build();
|
||||
}
|
||||
11
tauri_app/src-tauri/capabilities/default.json
Normal file
11
tauri_app/src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Основные разрешения Lendry ID Super App",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default",
|
||||
"stronghold:default"
|
||||
]
|
||||
}
|
||||
30
tauri_app/src-tauri/mobile-permissions.md
Normal file
30
tauri_app/src-tauri/mobile-permissions.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Mobile Permissions
|
||||
|
||||
После генерации native-проектов командами:
|
||||
|
||||
```bash
|
||||
npm run android:init
|
||||
npm run ios:init
|
||||
```
|
||||
|
||||
проверьте, что Tauri CLI добавил разрешения. Если нет, внесите их вручную.
|
||||
|
||||
## Android
|
||||
|
||||
`src-tauri/gen/android/app/src/main/AndroidManifest.xml`:
|
||||
|
||||
```xml
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
```
|
||||
|
||||
## iOS
|
||||
|
||||
`src-tauri/gen/apple/<target>/Info.plist`:
|
||||
|
||||
```xml
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Камера нужна для сканирования QR-кода входа Lendry ID.</string>
|
||||
```
|
||||
|
||||
QR-сканер использует HTML5 WebRTC внутри WebView. TOTP-секреты сохраняются через `tauri-plugin-stronghold`, а не в LocalStorage.
|
||||
148
tauri_app/src-tauri/src/lib.rs
Normal file
148
tauri_app/src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::Manager;
|
||||
use url::Url;
|
||||
|
||||
const MAX_LABEL_LEN: usize = 80;
|
||||
const MAX_ISSUER_LEN: usize = 80;
|
||||
const MIN_TOTP_SECRET_LEN: usize = 16;
|
||||
const MAX_TOTP_SECRET_LEN: usize = 256;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ValidationResult {
|
||||
ok: bool,
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TotpProfileInput {
|
||||
issuer: String,
|
||||
label: String,
|
||||
secret: String,
|
||||
digits: Option<u8>,
|
||||
period: Option<u16>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn validate_qr_session_payload(payload: String) -> Result<ValidationResult, String> {
|
||||
let trimmed = payload.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err("QR-код пустой".into());
|
||||
}
|
||||
|
||||
let session_id = match Url::parse(trimmed) {
|
||||
Ok(url) => url
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "sessionId" || key == "session_id")
|
||||
.map(|(_, value)| value.to_string())
|
||||
.or_else(|| {
|
||||
url.path_segments()
|
||||
.and_then(|segments| segments.last().map(ToString::to_string))
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
Err(_) => trimmed.to_string(),
|
||||
};
|
||||
|
||||
validate_uuid_like(&session_id)?;
|
||||
Ok(ValidationResult {
|
||||
ok: true,
|
||||
value: session_id,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn validate_api_base_url(url: String) -> Result<ValidationResult, String> {
|
||||
let parsed = Url::parse(url.trim()).map_err(|_| "Некорректный URL API".to_string())?;
|
||||
match parsed.scheme() {
|
||||
"http" | "https" => Ok(ValidationResult {
|
||||
ok: true,
|
||||
value: parsed.as_str().trim_end_matches('/').to_string(),
|
||||
}),
|
||||
_ => Err("URL API должен начинаться с http:// или https://".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn validate_totp_profile_input(input: TotpProfileInput) -> Result<ValidationResult, String> {
|
||||
let issuer = input.issuer.trim();
|
||||
let label = input.label.trim();
|
||||
let secret = normalize_base32_secret(&input.secret);
|
||||
let digits = input.digits.unwrap_or(6);
|
||||
let period = input.period.unwrap_or(30);
|
||||
|
||||
if issuer.is_empty() || issuer.len() > MAX_ISSUER_LEN {
|
||||
return Err("Название сервиса должно содержать от 1 до 80 символов".into());
|
||||
}
|
||||
if label.is_empty() || label.len() > MAX_LABEL_LEN {
|
||||
return Err("Название аккаунта должно содержать от 1 до 80 символов".into());
|
||||
}
|
||||
if secret.len() < MIN_TOTP_SECRET_LEN || secret.len() > MAX_TOTP_SECRET_LEN {
|
||||
return Err("TOTP secret должен содержать от 16 до 256 символов base32".into());
|
||||
}
|
||||
if !secret.chars().all(|ch| matches!(ch, 'A'..='Z' | '2'..='7' | '=')) {
|
||||
return Err("TOTP secret должен быть в формате base32".into());
|
||||
}
|
||||
if digits != 6 && digits != 8 {
|
||||
return Err("TOTP поддерживает только 6 или 8 цифр".into());
|
||||
}
|
||||
if !(15..=120).contains(&period) {
|
||||
return Err("Период TOTP должен быть от 15 до 120 секунд".into());
|
||||
}
|
||||
|
||||
Ok(ValidationResult {
|
||||
ok: true,
|
||||
value: secret,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_uuid_like(value: &str) -> Result<(), String> {
|
||||
let normalized = value.trim();
|
||||
if normalized.len() != 36 {
|
||||
return Err("QR-код не содержит корректный UUID сессии".into());
|
||||
}
|
||||
for (index, ch) in normalized.chars().enumerate() {
|
||||
let dash = matches!(index, 8 | 13 | 18 | 23);
|
||||
if dash && ch != '-' {
|
||||
return Err("QR-код не содержит корректный UUID сессии".into());
|
||||
}
|
||||
if !dash && !ch.is_ascii_hexdigit() {
|
||||
return Err("QR-код не содержит корректный UUID сессии".into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_base32_secret(secret: &str) -> String {
|
||||
secret
|
||||
.trim()
|
||||
.chars()
|
||||
.filter(|ch| !ch.is_whitespace() && *ch != '-')
|
||||
.map(|ch| ch.to_ascii_uppercase())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_stronghold::Builder::new(|password| {
|
||||
use tauri_plugin_stronghold::stronghold::Client;
|
||||
|
||||
let salt_path = dirs_next::data_dir()
|
||||
.unwrap_or_else(std::env::temp_dir)
|
||||
.join("lendry-id-stronghold.salt");
|
||||
tauri_plugin_stronghold::stronghold::derive_key_from_password(password.as_ref(), &salt_path)
|
||||
})
|
||||
.build())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
validate_qr_session_payload,
|
||||
validate_api_base_url,
|
||||
validate_totp_profile_input
|
||||
])
|
||||
.setup(|app| {
|
||||
let _ = app.path().app_data_dir().map(std::fs::create_dir_all);
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("ошибка запуска Lendry ID Super App");
|
||||
}
|
||||
3
tauri_app/src-tauri/src/main.rs
Normal file
3
tauri_app/src-tauri/src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
lendry_id_super_app_lib::run();
|
||||
}
|
||||
45
tauri_app/src-tauri/tauri.conf.json
Normal file
45
tauri_app/src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Lendry ID",
|
||||
"version": "0.1.0",
|
||||
"identifier": "ru.lendry.id.superapp",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": false,
|
||||
"windows": [
|
||||
{
|
||||
"title": "Lendry ID",
|
||||
"width": 1180,
|
||||
"height": 820,
|
||||
"minWidth": 390,
|
||||
"minHeight": 720,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; connect-src 'self' http://localhost:* https://* ws://localhost:* wss://*; img-src 'self' data: blob: https://*; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; script-src 'self';"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["dmg", "app", "msi", "deb", "rpm"],
|
||||
"category": "Productivity",
|
||||
"shortDescription": "Мессенджер, SSO-аутентификатор и TOTP в одном приложении",
|
||||
"longDescription": "Lendry ID Super App объединяет семейный мессенджер, QR-подтверждение входа и безопасное хранилище TOTP-кодов.",
|
||||
"ios": {
|
||||
"developmentTeam": ""
|
||||
},
|
||||
"android": {
|
||||
"minSdkVersion": 24
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"opener": {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user