ACCOUNT CREATION AND LOGIN

This commit is contained in:
2026-02-16 23:56:07 -05:00
parent 79da5e1146
commit 61e9c2ac5c
17 changed files with 2322 additions and 326 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

View File

@@ -1,48 +0,0 @@
use openworm::{
net::LoginKey,
rsc::{DataDir, DataRsc},
};
pub struct ClientData {
pub dir: DataDir,
pub cache: Cache,
}
impl ClientData {
pub fn load() -> Self {
let dir = DataDir::default();
Self {
cache: dir.load(),
dir,
}
}
pub fn save(&self) {
self.dir.save(&self.cache);
}
pub fn login_key(&self, user: &str) -> LoginKey {
let entry = keyring::Entry::new("openworm", user).expect("failed to open keyring entry");
if let Ok(secret) = entry.get_secret() {
LoginKey::from(secret)
} else {
LoginKey::new()
}
}
}
#[derive(Debug, Default, bitcode::Encode, bitcode::Decode)]
pub struct Cache {
pub ip: String,
pub username: String,
/// TODO: not store this as plain string?
/// need to figure out crypto stuff
/// or store session token
pub password: String,
}
impl DataRsc for Cache {
fn path() -> &'static str {
"client_data"
}
}

View File

@@ -0,0 +1,42 @@
use openworm::rsc::{DataDir, DataGuard};
mod ver;
pub struct ClientData {
pub data: DataDir,
}
pub type AccountList = ver::AccountListV0;
pub type AccountInfo = ver::AccountInfoV0;
pub type ServerList = ver::ServerListV0;
pub type ServerInfo = ver::ServerInfoV0;
impl ClientData {
pub fn load() -> Self {
let data = DataDir::new(None);
Self { data }
}
pub fn create_account(&self, server: ServerInfo, info: AccountInfo, password: &str) {
keyring::use_native_store(true).unwrap();
let user_path = info.path();
let entry =
keyring_core::Entry::new("openworm", &user_path).expect("failed to open keyring entry");
entry.set_password(password).unwrap();
self.data
.load::<ServerList>()
.insert(info.url.clone(), server);
self.data.load::<AccountList>().push(info);
}
pub fn password(&self, info: &AccountInfo) -> String {
let user_path = info.path();
let entry =
keyring_core::Entry::new("openworm", &user_path).expect("failed to open keyring entry");
entry.get_password().unwrap()
}
pub fn accounts(&self) -> DataGuard<AccountList> {
self.data.load::<AccountList>()
}
}

View File

@@ -0,0 +1,58 @@
use derive_more::{Deref, DerefMut};
use iris::core::util::HashMap;
use openworm::rsc::DataRsc;
#[derive(Default, serde::Serialize, serde::Deserialize, Deref, DerefMut)]
pub struct AccountListV0(Vec<AccountInfoV0>);
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct AccountInfoV0 {
pub url: String,
pub username: String,
}
impl AccountInfoV0 {
pub fn path(&self) -> String {
self.username.clone() + "@" + &self.url
}
}
impl DataRsc for AccountListV0 {
fn name() -> &'static str {
"account_list"
}
fn parse_version(text: &str, version: u32) -> Result<Self, String> {
match version {
0 => ron::from_str(text).map_err(|e| e.to_string()),
_ => Err(format!("unknown version {version}")),
}
}
fn version() -> u32 {
0
}
}
#[derive(Debug, Default, serde::Serialize, serde::Deserialize, Deref, DerefMut)]
pub struct ServerListV0(HashMap<String, ServerInfoV0>);
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct ServerInfoV0 {
pub cert_hex: String,
}
impl DataRsc for ServerListV0 {
fn name() -> &'static str {
"server_list"
}
fn parse_version(text: &str, version: u32) -> Result<Self, String> {
match version {
0 => ron::from_str(text).map_err(|e| e.to_string()),
_ => Err(format!("unknown version {version}")),
}
}
fn version() -> u32 {
0
}
}

View File

@@ -62,11 +62,12 @@ impl DefaultAppState for Client {
.stack()
.set_root(rsc, &mut ui_state);
ui::start(rsc).set_ptr(main_ui, rsc);
let data = ClientData::load();
ui::start(rsc, &data).set_ptr(main_ui, rsc);
Self {
ui_state,
data: ClientData::load(),
data,
state: Default::default(),
main_ui,
notif,
@@ -90,7 +91,6 @@ impl DefaultAppState for Client {
fn exit(&mut self, _rsc: &mut DefaultRsc<Self>, _render: &mut UiRenderState) {
self.state.exit();
self.data.save();
}
fn window_event(

View File

@@ -1,8 +1,8 @@
use crate::ClientEvent;
use dashmap::DashMap;
use openworm::net::{
ClientMsg, ClientRequestMsg, CreateAccount, CreateAccountResp, RecvHandler, RequestId,
SERVER_NAME, ServerMsg, ServerRespMsg, SkipServerVerification, recv_uni, send_uni,
ClientMsg, ClientRequestMsg, CreateAccount, CreateAccountResp, Login, LoginResp, RecvHandler,
RequestId, SERVER_NAME, ServerMsg, ServerRespMsg, SkipServerVerification, recv_uni, send_uni,
};
use quinn::{
ClientConfig, Connection, Endpoint, IdleTimeout, TransportConfig,
@@ -10,6 +10,7 @@ use quinn::{
};
use std::{
net::{Ipv6Addr, SocketAddr, SocketAddrV6, ToSocketAddrs},
str::FromStr,
sync::Arc,
time::Duration,
};
@@ -83,7 +84,7 @@ impl RequestMsg for CreateAccount {
type Result = CreateAccountResp;
fn result(msg: ServerMsg) -> Option<Self::Result> {
if let ServerMsg::CreateAccount(res) = msg {
if let ServerMsg::CreateAccountResp(res) = msg {
Some(res)
} else {
None
@@ -91,19 +92,41 @@ impl RequestMsg for CreateAccount {
}
}
async fn connection_cert(addr: SocketAddr, cert: CertificateDer) -> NetResult<Connection> {
impl RequestMsg for Login {
type Result = LoginResp;
fn result(msg: ServerMsg) -> Option<Self::Result> {
if let ServerMsg::LoginResp(res) = msg {
Some(res)
} else {
None
}
}
}
async fn connection_cert(
addr: SocketAddr,
cert: CertificateDer<'_>,
) -> NetResult<(Endpoint, Connection)> {
let mut roots = quinn::rustls::RootCertStore::empty();
roots.add(cert);
roots
.add(cert)
.map_err(|e| format!("Invalid Certificate: {e:?}"))?;
let client_crypto = quinn::rustls::ClientConfig::builder()
.with_root_certificates(roots)
.with_no_client_auth();
let client_config = ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?));
let mut endpoint = quinn::Endpoint::client(SocketAddr::from_str("[::]:0").unwrap())?;
let client_config = ClientConfig::new(Arc::new(
QuicClientConfig::try_from(client_crypto).map_err(|e| e.to_string())?,
));
let mut endpoint = quinn::Endpoint::client(SocketAddr::from_str("[::]:0").unwrap())
.map_err(|e| e.to_string())?;
endpoint.set_default_client_config(client_config);
endpoint
.connect(addr, SERVER_NAME)?
let conn = endpoint
.connect(addr, SERVER_NAME)
.map_err(|e| e.to_string())?
.await
.map_err(|e| format!("failed to connect: {}", e))
.map_err(|e| format!("failed to connect: {}", e))?;
Ok((endpoint, conn))
}
async fn connection_no_cert(addr: SocketAddr) -> NetResult<(Endpoint, Connection)> {
@@ -148,7 +171,7 @@ impl NetHandle {
.map_err(|e| e.to_string())?
.next()
.ok_or("no addresses found".to_string())?;
let (endpoint, conn) = connection_cert(addr).await?;
let (endpoint, conn) = connection_cert(addr, cert).await?;
let conn_ = conn.clone();
let mut req_id = RequestId::first();

View File

@@ -2,3 +2,4 @@ use super::*;
pub const MODAL_BG: UiColor = UiColor::BLACK.brighter(0.05);
pub const GREEN: UiColor = UiColor::rgb(0, 150, 0);
pub const DARK: UiColor = UiColor::BLACK.brighter(0.02);

View File

@@ -1,20 +1,101 @@
use openworm::net::{CreateAccount, CreateAccountResp};
use openworm::net::{CreateAccount, CreateAccountResp, Login, LoginResp};
use crate::net::{ConnectInfo, NetHandle};
use crate::{
data::{AccountInfo, ClientData, ServerInfo, ServerList},
net::{ConnectInfo, NetHandle},
};
use super::*;
pub fn start(rsc: &mut Rsc) -> WeakWidget {
pub fn start(rsc: &mut Rsc, data: &ClientData) -> WeakWidget {
let mut accounts = Span::empty(Dir::DOWN);
accounts.push(
wtext("no accounts")
.size(20)
.center_text()
.color(Color::GRAY)
.height(60)
.add_strong(rsc),
);
let accts = data.accounts();
if accts.is_empty() {
accounts.push(
wtext("no accounts")
.size(20)
.center_text()
.color(Color::GRAY)
.height(60)
.add_strong(rsc),
);
} else {
for account in accts.iter() {
let button = Button::new_fg(
wtext(&account.username)
.size(20)
.center_text()
.height(60)
.add(rsc),
color::DARK,
rsc,
)
.add(rsc);
let account = account.clone();
let cert_hex = data
.data
.load::<ServerList>()
.get(&account.url)
.unwrap()
.cert_hex
.clone();
let cert = decode_hex(&cert_hex).unwrap();
keyring::use_native_store(true).unwrap();
rsc.events.register(button, Submit, move |ctx, rsc| {
let account = account.clone();
let cert = cert.clone();
let password = ctx.state.data.password(&account);
rsc.spawn_task(async move |mut ctx| {
let mut fail = |reason: &str| {
let reason = reason.to_string();
ctx.update(move |ctx, rsc| {
rsc[ctx.notif].inner = Some(werror(&reason, rsc));
})
};
let net = match NetHandle::connect(
async |msg| {
println!("msg recv :joy:");
},
ConnectInfo {
url: account.url.clone(),
cert,
},
)
.await
{
Ok(v) => v,
Err(e) => {
return fail(&e);
}
};
let Ok(resp) = net
.request(Login {
username: account.username.clone(),
password: password.clone(),
})
.await
else {
return fail("failed to create account");
};
let id = match resp {
LoginResp::Ok { id } => id,
LoginResp::UnknownUsername => {
return fail("unknown username");
}
LoginResp::InvalidPassword => {
return fail("invalid password");
}
};
ctx.update(move |ctx, rsc| {
main_view(rsc).set_ptr(ctx.main_ui, rsc);
});
});
});
accounts.push(button.add_strong(rsc));
}
}
let connect = Button::submit("connect", rsc);
let create = Button::submit("create", rsc);
@@ -47,41 +128,48 @@ pub fn create_account(rsc: &mut Rsc) -> WeakWidget {
let create = Button::submit("create", rsc);
rsc.events.register(create, Submit, move |ctx, rsc| {
let url = rsc[url].content();
let token = rsc[token].content();
let cert = rsc[cert].content();
let Ok(cert) = decode_hex(&cert) else {
let url = rsc[url].content().trim().to_string();
let token = rsc[token].content().trim().to_string();
let cert_hex = rsc[cert].content().trim().to_string();
let Some(cert) = decode_hex(&cert_hex) else {
rsc[ctx.state.notif].inner = Some(werror("Invalid certificate hex", rsc));
return;
};
let username = rsc[username].content();
let password = rsc[password].content();
let login_key = ctx.state.data.login_key(&username);
create.disable(rsc);
rsc.spawn_task(async move |mut ctx| {
let mut fail = move |reason| {
let mut fail = |reason: &str| {
let reason = reason.to_string();
ctx.update(move |ctx, rsc| {
rsc[ctx.notif].inner = Some(werror(reason, rsc));
rsc[ctx.notif].inner = Some(werror(&reason, rsc));
create.enable(rsc);
})
};
let Ok(net) = NetHandle::connect(
keyring::use_native_store(true).unwrap();
let net = match NetHandle::connect(
async |msg| {
println!("msg recv :joy:");
},
ConnectInfo { url, cert },
ConnectInfo {
url: url.clone(),
cert,
},
)
.await
else {
return fail("failed to connect");
{
Ok(v) => v,
Err(e) => {
return fail(&e);
}
};
let Ok(resp) = net
.request(CreateAccount {
username,
password,
username: username.clone(),
password: password.clone(),
token,
login_key,
})
.await
else {
@@ -96,7 +184,14 @@ pub fn create_account(rsc: &mut Rsc) -> WeakWidget {
return fail("invalid account token");
}
};
println!("account created!!!!");
ctx.update(move |ctx, rsc| {
main_view(rsc).set_ptr(ctx.main_ui, rsc);
ctx.data.create_account(
ServerInfo { cert_hex },
AccountInfo { url, username },
&password,
);
});
});
});
@@ -115,115 +210,12 @@ pub fn create_account(rsc: &mut Rsc) -> WeakWidget {
.add(rsc)
}
pub fn decode_hex(s: &str) -> Result<Vec<u8>, std::num::ParseIntError> {
pub fn decode_hex(s: &str) -> Option<Vec<u8>> {
if !s.len().is_multiple_of(2) {
return None;
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16))
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
.collect()
}
// pub fn connect_screen(client: &mut Client, ui: &mut Ui, state: &UiState) -> WeakWidget {
// let Client { data, proxy, .. } = client;
// let ip = field_widget(&data.ip, "ip", ui);
// let ip_ = ip.clone();
// let handle = AppHandle {
// proxy: proxy.clone(),
// window: state.window.clone(),
// };
//
// let submit = Button::submit("connect", ui);
//
// submit.on(Submit, move |ctx| {
// let ClientState::Connect(state) = &mut ctx.state.state else {
// return;
// };
// let ip = ip_.get().content();
// state.handle = Some(connect(handle.clone(), ConnectInfo { ip }));
// });
//
// (
// wtext("connect to a server")
// .text_align(Align::CENTER)
// .size(30),
// field_box(
// // NOTE: should probably do this on submit
// ip.on(Edited, |ctx| {
// ctx.state.data.ip = ctx.widget.get().content();
// })
// .add(ui),
// ui,
// ),
// submit,
// )
// .span(Dir::DOWN)
// .gap(10)
// .pad(15)
// .background(rect(Color::BLACK.brighter(0.2)).radius(15))
// .width(400)
// .align(Align::CENTER)
// .add(ui)
// }
// pub fn login_screen(client: &mut Client, ui: &mut Ui) -> WeakWidget {
// let Client { data, .. } = client;
// let username = field_widget(&data.username, "username", ui);
// let password = field_widget(&data.password, "password", ui);
// let username_ = username.clone();
// let password_ = password.clone();
// let submit = Button::submit("login", ui);
// submit
// .on(move |client, _ui| {
// let ClientState::Login(state) = &mut client.state else {
// return;
// };
// let username = username_.get().content();
// let password = password_.get().content();
// state.handle.send(ClientMsg::Login { username, password });
// })
// .add(ui);
// let username_ = username.clone();
// let password_ = password.clone();
// let create_button = Button::submit(
// "create account",
// move |client, _ui| {
// let ClientState::Login(state) = &mut client.state else {
// return;
// };
// let username = username_.get().content();
// let password = password_.get().content();
// state
// .handle
// .send(ClientMsg::CreateAccount { username, password });
// },
// ui,
// );
// create_button.on()
// (
// wtext("login to server").text_align(Align::CENTER).size(30),
// field_box(
// username
// .on(Edited, |ctx| {
// ctx.state.data.username = ctx.widget.get().content();
// })
// .add(ui),
// ui,
// ),
// field_box(
// password
// .on(Edited, |ctx| {
// ctx.state.data.password = ctx.widget.get().content();
// })
// .add(ui),
// ui,
// ),
// submit,
// create_button,
// )
// .span(Dir::DOWN)
// .gap(10)
// .pad(15)
// .background(rect(Color::BLACK.brighter(0.2)).radius(15))
// .width(400)
// .align(Align::CENTER)
// .add(ui)
// }

View File

@@ -38,7 +38,7 @@ pub struct Button {
}
impl Button {
pub fn new(text: &str, color: UiColor, rsc: &mut Rsc) -> Self {
pub fn new_fg(fg: WeakWidget, color: UiColor, rsc: &mut Rsc) -> Self {
let rect = rect(color).radius(15).add(rsc);
let enabled = rsc.create_state(rect, true);
let root = rect
@@ -58,7 +58,7 @@ impl Button {
rsc[ctx.widget].color = color;
})
.height(60)
.foreground(wtext(text).size(25).text_align(Align::CENTER))
.foreground(fg)
.add(rsc);
rect.on(CursorSense::click(), move |ctx, rsc: &mut Rsc| {
if !rsc[enabled] {
@@ -76,6 +76,14 @@ impl Button {
}
}
pub fn new(text: &str, color: UiColor, rsc: &mut Rsc) -> Self {
Self::new_fg(
wtext(text).size(25).text_align(Align::CENTER).add(rsc),
color,
rsc,
)
}
pub fn submit(text: &str, rsc: &mut Rsc) -> Self {
Self::new(text, color::GREEN, rsc)
}

View File

@@ -7,14 +7,14 @@ use net::{ClientSender, ConAccepter, listen};
use openworm::{
net::{
ClientMsg, ClientRequestMsg, CreateAccount, CreateAccountResp, DisconnectReason, LoadMsg,
RecvHandler, ServerError, ServerMsg, install_crypto_provider,
Login, LoginResp, RecvHandler, ServerError, ServerMsg, install_crypto_provider,
},
rsc::DataDir,
};
use rand::distr::{Alphanumeric, SampleString};
use scrypt::{
Scrypt,
password_hash::{PasswordHasher, SaltString, rand_core::OsRng},
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
};
use std::{
collections::HashMap,
@@ -41,9 +41,8 @@ fn main() {
#[tokio::main]
pub async fn run_server(port: u16) {
let dir = DataDir::default();
let path = dir.get().join("server");
let db = Db::open(path.join("db"));
let dir = DataDir::new(Some("server"));
let db = Db::open(dir.path.join("db"));
let handler = ServerListener {
senders: Default::default(),
count: 0.into(),
@@ -53,7 +52,7 @@ pub async fn run_server(port: u16) {
let token = account_token(&db, ServerPerms::ACCOUNT_TOKENS);
println!("no users found, token for admin: {token}");
}
let (endpoint, handle) = listen(port, &path, handler);
let (endpoint, handle) = listen(port, &dir.path, handler);
let _ = ctrl_c().await;
println!("stopping server");
println!("closing connections...");
@@ -171,7 +170,6 @@ impl RecvHandler<ClientRequestMsg> for ClientHandler {
token,
username,
password,
login_key,
} = &info;
let salt = SaltString::generate(&mut OsRng);
let params = scrypt::Params::new(11, 8, 1, 32).unwrap();
@@ -214,24 +212,26 @@ impl RecvHandler<ClientRequestMsg> for ClientHandler {
println!("account created: \"{username}\"");
*self.state.write().await = ClientState::Authed(id);
let _ = replier.send(CreateAccountResp::Ok { id }).await;
} // ClientMsgType::Login { username, password } => {
// let Some(id) = self.db.usernames.get(&username) else {
// let _ = self.send.send(ServerError::UnknownUsername).await;
// return;
// };
// let Some(user) = self.db.users.get(&id) else {
// panic!("invalid state! (should be a user)");
// };
// let hash = PasswordHash::new(&user.password_hash).unwrap();
// if Scrypt.verify_password(password.as_bytes(), &hash).is_err() {
// println!("invalid password: \"{username}\"");
// let _ = self.send.send(ServerError::InvalidPassword).await;
// return;
// }
// println!("login: \"{username}\"");
// *self.state.write().await = ClientState::Authed(id);
// let _ = self.send.send(ServerMsgType::Login { username }).await;
// }
}
ClientMsg::Login(info) => {
let Login { username, password } = &info;
let Some(id) = self.db.usernames.get(username) else {
let _ = replier.send(LoginResp::UnknownUsername).await;
return;
};
let Some(user) = self.db.users.get(&id) else {
panic!("invalid state! (should be a user)");
};
let hash = PasswordHash::new(&user.password_hash).unwrap();
if Scrypt.verify_password(password.as_bytes(), &hash).is_err() {
println!("invalid password: \"{username}\"");
let _ = replier.send(LoginResp::InvalidPassword).await;
return;
}
println!("login: \"{username}\"");
*self.state.write().await = ClientState::Authed(id);
let _ = replier.send(LoginResp::Ok { id }).await;
}
}
}

View File

@@ -39,7 +39,7 @@ pub fn init_endpoint(port: u16, data_path: &Path) -> Endpoint {
};
print!("cert hex: ");
for x in cert.iter() {
print!("{:x}", x);
print!("{:02x}", x);
}
println!();
let server_config = ServerConfig::with_single_cert(vec![cert], key).unwrap();