260 lines
8.1 KiB
Rust
260 lines
8.1 KiB
Rust
use std::time::Duration;
|
|
|
|
use anyhow::{Error, anyhow};
|
|
use gpui::prelude::FluentBuilder;
|
|
use gpui::{
|
|
AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
|
Subscription, Task, Window, div,
|
|
};
|
|
use nostr_connect::prelude::*;
|
|
use state::{CoopAuthUrlHandler, NostrRegistry, USER_KEYRING};
|
|
use theme::ActiveTheme;
|
|
use ui::button::{Button, ButtonVariants};
|
|
use ui::input::{Input, InputEvent, InputState};
|
|
use ui::{Disableable, StyledExt, WindowExtension, v_flex};
|
|
|
|
#[derive(Debug)]
|
|
pub struct ImportIdentity {
|
|
/// Secret key input
|
|
key_input: Entity<InputState>,
|
|
|
|
/// Password input (if required)
|
|
pass_input: Entity<InputState>,
|
|
|
|
/// Error message
|
|
error: Entity<Option<SharedString>>,
|
|
|
|
/// Whether the user is currently loading
|
|
loading: bool,
|
|
|
|
/// Async tasks
|
|
tasks: Vec<Task<Result<(), Error>>>,
|
|
|
|
/// Input subscription
|
|
_subscription: Option<Subscription>,
|
|
}
|
|
|
|
impl ImportIdentity {
|
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
let key_input = cx.new(|cx| InputState::new(window, cx));
|
|
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
|
let error = cx.new(|_| None);
|
|
|
|
let input_subscription =
|
|
cx.subscribe_in(&key_input, window, |this, _input, event, window, cx| {
|
|
if let InputEvent::PressEnter { .. } = event {
|
|
this.login(window, cx);
|
|
};
|
|
});
|
|
|
|
Self {
|
|
key_input,
|
|
pass_input,
|
|
error,
|
|
loading: false,
|
|
tasks: vec![],
|
|
_subscription: Some(input_subscription),
|
|
}
|
|
}
|
|
|
|
fn login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let value = self.key_input.read(cx).value();
|
|
let password = self.pass_input.read(cx).value();
|
|
|
|
// Set loading state
|
|
self.set_loading(true, cx);
|
|
|
|
if value.starts_with("ncryptsec1") {
|
|
self.ncryptsec(value, password, window, cx);
|
|
return;
|
|
}
|
|
|
|
if value.starts_with("bunker://") {
|
|
match NostrConnectUri::parse(value) {
|
|
Ok(uri) => {
|
|
self.bunker(uri, window, cx);
|
|
}
|
|
Err(e) => {
|
|
self.set_error(e.to_string(), cx);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if let Ok(secret) = SecretKey::parse(&value) {
|
|
let keys = Keys::new(secret);
|
|
let nostr = NostrRegistry::global(cx);
|
|
|
|
// Update the signer
|
|
nostr.update(cx, |this, cx| {
|
|
this.set_signer(keys, cx);
|
|
});
|
|
} else {
|
|
self.set_error("Invalid key", cx);
|
|
}
|
|
}
|
|
|
|
fn ncryptsec<S>(&mut self, content: S, pwd: S, window: &mut Window, cx: &mut Context<Self>)
|
|
where
|
|
S: Into<String>,
|
|
{
|
|
let nostr = NostrRegistry::global(cx);
|
|
let content: String = content.into();
|
|
let password: String = pwd.into();
|
|
|
|
if password.is_empty() {
|
|
self.set_error("Password is required", cx);
|
|
return;
|
|
}
|
|
|
|
let Ok(enc) = EncryptedSecretKey::from_bech32(&content) else {
|
|
self.set_error("Secret Key is invalid", cx);
|
|
return;
|
|
};
|
|
|
|
// Decrypt in the background to ensure it doesn't block the UI
|
|
let task = cx.background_spawn(async move {
|
|
if let Ok(content) = enc.decrypt(&password) {
|
|
Ok(Keys::new(content))
|
|
} else {
|
|
Err(anyhow!("Invalid password"))
|
|
}
|
|
});
|
|
|
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
|
match task.await {
|
|
Ok(keys) => {
|
|
nostr.update_in(cx, |this, window, cx| {
|
|
this.set_signer(keys, cx);
|
|
window.close_modal(cx);
|
|
})?;
|
|
}
|
|
Err(e) => {
|
|
this.update(cx, |this, cx| {
|
|
this.set_error(e.to_string(), cx);
|
|
})?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}));
|
|
}
|
|
|
|
fn bunker(&mut self, uri: NostrConnectUri, window: &mut Window, cx: &mut Context<Self>) {
|
|
let nostr = NostrRegistry::global(cx);
|
|
let master_keys = nostr.read(cx).get_master_key(cx);
|
|
let password = uri.to_string();
|
|
let save = cx.write_credentials(USER_KEYRING, "bunker", password.as_bytes());
|
|
|
|
cx.spawn_in(window, async move |_this, cx| {
|
|
let keys = master_keys.await;
|
|
let timeout = Duration::from_secs(30);
|
|
|
|
// Construct the nostr connect signer
|
|
let mut signer = NostrConnect::new(uri, keys, timeout, None)?;
|
|
|
|
// Handle auth url with the default browser
|
|
signer.auth_url_handler(CoopAuthUrlHandler);
|
|
|
|
nostr.update(cx, |this, cx| {
|
|
this.set_signer(signer, cx);
|
|
cx.background_spawn(async move { save.await.ok() }).detach();
|
|
cx.notify();
|
|
});
|
|
|
|
Ok::<(), anyhow::Error>(())
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
|
self.loading = status;
|
|
cx.notify();
|
|
}
|
|
|
|
fn set_error<S>(&mut self, message: S, cx: &mut Context<Self>)
|
|
where
|
|
S: Into<SharedString>,
|
|
{
|
|
self.set_loading(false, cx);
|
|
|
|
// Update error message
|
|
self.error.update(cx, |this, cx| {
|
|
*this = Some(message.into());
|
|
cx.notify();
|
|
});
|
|
|
|
// Clear the error message after 3 secs
|
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
cx.background_executor().timer(Duration::from_secs(3)).await;
|
|
|
|
this.update(cx, |this, cx| {
|
|
this.error.update(cx, |this, cx| {
|
|
*this = None;
|
|
cx.notify();
|
|
});
|
|
})?;
|
|
|
|
Ok(())
|
|
}));
|
|
}
|
|
}
|
|
|
|
impl Render for ImportIdentity {
|
|
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
const MSG: &str = "Coop won't store your identity key on the local device. You need to re-login again in the next session. You can use Nostr Connect for persistent login.";
|
|
|
|
let require_password = self.key_input.read(cx).value().starts_with("ncryptsec1");
|
|
let key_warning = self.key_input.read(cx).value().starts_with("nsec1") || require_password;
|
|
|
|
v_flex()
|
|
.size_full()
|
|
.gap_2()
|
|
.child(
|
|
v_flex()
|
|
.gap_1()
|
|
.text_color(cx.theme().text_muted)
|
|
.child("Continue with existing key or bunker connection")
|
|
.child(Input::new(&self.key_input)),
|
|
)
|
|
.when(require_password, |this| {
|
|
this.child(
|
|
v_flex()
|
|
.gap_1()
|
|
.text_color(cx.theme().text_muted)
|
|
.child("Decrypt Password:")
|
|
.child(Input::new(&self.pass_input)),
|
|
)
|
|
})
|
|
.when(key_warning, |this| {
|
|
this.child(
|
|
div()
|
|
.v_flex()
|
|
.text_xs()
|
|
.text_color(cx.theme().text_warning)
|
|
.child(div().font_semibold().child("Warning"))
|
|
.child(div().child(MSG)),
|
|
)
|
|
})
|
|
.child(
|
|
Button::new("login")
|
|
.label("Continue")
|
|
.primary()
|
|
.loading(self.loading)
|
|
.disabled(self.loading)
|
|
.on_click(cx.listener(move |this, _, window, cx| {
|
|
this.login(window, cx);
|
|
})),
|
|
)
|
|
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
|
this.child(
|
|
div()
|
|
.text_xs()
|
|
.text_center()
|
|
.text_color(cx.theme().text_danger)
|
|
.child(error.clone()),
|
|
)
|
|
})
|
|
}
|
|
}
|