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

1926
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
cargo-features = ["codegen-backend"]
# cargo-features = ["codegen-backend"]
[package]
name = "openworm"
@@ -23,11 +23,14 @@ clap = { version = "4.5.53", features = ["derive"] }
scrypt = "0.11.0"
ed25519-dalek = { version = "3.0.0-pre.2", features = ["rand_core"] }
rand = { version = "0.10.0-rc.5", features = ["chacha", "sys_rng"] }
keyring = { version = "3.6.3", features = ["apple-native", "sync-secret-service", "windows-native"] }
keyring = { version = "4.0.0-rc.3" }
bitcode = "0.6.9"
dashmap = "6.1.0"
fjall = "3.0.1"
time = { version = "0.3.47", features = ["local-offset"] }
serde = { version = "1.0.228", features = ["derive"] }
derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] }
keyring-core = "0.7.2"
[[bin]]
name = "openworm-client"
@@ -37,8 +40,8 @@ path = "src/bin/client/main.rs"
name = "openworm-server"
path = "src/bin/server/main.rs"
[profile.dev]
codegen-backend = "cranelift"
# [profile.dev]
# codegen-backend = "cranelift"
[profile.dev.package."*"]
opt-level = 1
# [profile.dev.package."*"]
# opt-level = 1

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();

View File

@@ -9,6 +9,7 @@ impl From<ClientMsg> for ClientMsgInst {
ClientMsg::CreateAccount(v) => Self::CreateAccountV0(v),
ClientMsg::RequestMsgs => Self::RequestMsgsV0,
ClientMsg::SendMsg(v) => Self::SendMsgV0(v),
ClientMsg::Login(v) => Self::LoginV0(v),
}
}
}
@@ -19,6 +20,7 @@ impl From<ClientMsgInst> for ClientMsg {
ClientMsgInst::CreateAccountV0(v) => Self::CreateAccount(v),
ClientMsgInst::RequestMsgsV0 => Self::RequestMsgs,
ClientMsgInst::SendMsgV0(v) => Self::SendMsg(v),
ClientMsgInst::LoginV0(v) => Self::Login(v),
}
}
}
@@ -26,10 +28,11 @@ impl From<ClientMsgInst> for ClientMsg {
impl From<ServerMsg> for ServerMsgInst {
fn from(value: ServerMsg) -> Self {
match value {
ServerMsg::CreateAccount(v) => Self::CreateAccountV0(v),
ServerMsg::CreateAccountResp(v) => Self::CreateAccountRespV0(v),
ServerMsg::LoadMsg(v) => Self::LoadMsgV0(v),
ServerMsg::LoadMsgs(v) => Self::LoadMsgsV0(v),
ServerMsg::ServerError(v) => Self::ServerErrorV0(v),
ServerMsg::LoginResp(v) => Self::LoginRespV0(v),
}
}
}
@@ -37,10 +40,11 @@ impl From<ServerMsg> for ServerMsgInst {
impl From<ServerMsgInst> for ServerMsg {
fn from(value: ServerMsgInst) -> Self {
match value {
ServerMsgInst::CreateAccountV0(v) => Self::CreateAccount(v),
ServerMsgInst::CreateAccountRespV0(v) => Self::CreateAccountResp(v),
ServerMsgInst::LoadMsgV0(v) => Self::LoadMsg(v),
ServerMsgInst::LoadMsgsV0(v) => Self::LoadMsgs(v),
ServerMsgInst::ServerErrorV0(v) => Self::ServerError(v),
ServerMsgInst::LoginRespV0(v) => Self::LoginResp(v),
}
}
}

View File

@@ -4,17 +4,19 @@ use rand::TryRng;
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
pub enum ClientMsgInst {
CreateAccountV0(CreateAccountV0) = 0,
RequestMsgsV0 = 1,
SendMsgV0(SendMsgV0) = 2,
LoginV0(LoginV0) = 1,
RequestMsgsV0 = 2,
SendMsgV0(SendMsgV0) = 3,
}
#[repr(u32)]
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
pub enum ServerMsgInst {
CreateAccountV0(CreateAccountRespV0) = 0,
LoadMsgV0(LoadMsgV0) = 1,
LoadMsgsV0(Vec<LoadMsgV0>) = 2,
ServerErrorV0(ServerErrorV0) = 3,
CreateAccountRespV0(CreateAccountRespV0) = 0,
LoginRespV0(LoginRespV0) = 1,
LoadMsgV0(LoadMsgV0) = 2,
LoadMsgsV0(Vec<LoadMsgV0>) = 3,
ServerErrorV0(ServerErrorV0) = 4,
}
pub type UserIdV0 = u64;
@@ -24,7 +26,12 @@ pub struct CreateAccountV0 {
pub username: String,
pub password: String,
pub token: String,
pub login_key: LoginKeyV0,
}
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
pub struct LoginV0 {
pub username: String,
pub password: String,
}
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
@@ -34,6 +41,13 @@ pub enum CreateAccountRespV0 {
InvalidToken,
}
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
pub enum LoginRespV0 {
Ok { id: UserIdV0 },
UnknownUsername,
InvalidPassword,
}
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
pub struct LoginKeyV0(Vec<u8>);
impl LoginKeyV0 {
@@ -47,6 +61,10 @@ impl LoginKeyV0 {
.expect("failed to generate random key");
Self(key.to_vec())
}
pub fn bytes(&self) -> &[u8] {
&self.0
}
}
impl From<Vec<u8>> for LoginKeyV0 {
fn from(value: Vec<u8>) -> Self {

View File

@@ -3,13 +3,15 @@ use super::data::*;
#[derive(Debug)]
pub enum ClientMsg {
CreateAccount(CreateAccount),
Login(Login),
RequestMsgs,
SendMsg(SendMsg),
}
#[derive(Debug)]
pub enum ServerMsg {
CreateAccount(CreateAccountResp),
CreateAccountResp(CreateAccountResp),
LoginResp(LoginResp),
LoadMsg(LoadMsg),
LoadMsgs(Vec<LoadMsg>),
ServerError(ServerError),
@@ -21,6 +23,8 @@ pub type LoadMsg = LoadMsgV0;
pub type ServerError = ServerErrorV0;
pub type CreateAccount = CreateAccountV0;
pub type CreateAccountResp = CreateAccountRespV0;
pub type Login = LoginV0;
pub type LoginResp = LoginRespV0;
pub type UserId = UserIdV0;
impl From<CreateAccount> for ClientMsg {
@@ -29,6 +33,12 @@ impl From<CreateAccount> for ClientMsg {
}
}
impl From<Login> for ClientMsg {
fn from(value: Login) -> Self {
Self::Login(value)
}
}
impl From<ServerError> for ServerMsg {
fn from(value: ServerError) -> Self {
Self::ServerError(value)
@@ -43,6 +53,12 @@ impl From<LoadMsg> for ServerMsg {
impl From<CreateAccountResp> for ServerMsg {
fn from(value: CreateAccountResp) -> Self {
Self::CreateAccount(value)
Self::CreateAccountResp(value)
}
}
impl From<LoginResp> for ServerMsg {
fn from(value: LoginResp) -> Self {
Self::LoginResp(value)
}
}

View File

@@ -1,50 +1,103 @@
use std::{
fs::{self, File},
io::Write,
path::Path,
path::PathBuf,
};
use directories_next::ProjectDirs;
#[derive(Clone)]
pub struct DataDir {
dirs: ProjectDirs,
}
impl Default for DataDir {
fn default() -> Self {
Self {
dirs: ProjectDirs::from("", "", "openworm").unwrap(),
}
}
}
pub trait DataRsc: bitcode::Encode + bitcode::DecodeOwned + Default {
fn path() -> &'static str;
pub path: PathBuf,
}
impl DataDir {
pub fn get(&self) -> &Path {
self.dirs.data_local_dir()
}
pub fn load<T: DataRsc>(&self) -> T {
let path = self.get().join(T::path());
match fs::read(path) {
Ok(bytes) => match bitcode::decode(&bytes) {
Ok(data) => data,
Err(_) => todo!(),
},
Err(_) => T::default(),
pub fn new(dir: Option<&str>) -> Self {
let dirs = ProjectDirs::from("", "", "openworm").unwrap();
let mut path = dirs.data_local_dir().to_path_buf();
if let Some(dir) = dir {
path = path.join(dir);
}
Self { path }
}
}
pub trait DataRsc: serde::Serialize + Default {
fn name() -> &'static str;
fn path() -> String {
Self::name().to_string() + ".ron"
}
fn parse_version(text: &str, version: u32) -> Result<Self, String>;
fn version() -> u32;
}
pub struct DataGuard<T: DataRsc> {
path: PathBuf,
val: T,
}
impl DataDir {
pub fn load<T: DataRsc>(&self) -> DataGuard<T> {
let path = self.path.join(T::path());
let invalid = |info: &str| {
println!("warning: invalid config @ {path:?}: {info}");
DataGuard {
path: path.clone(),
val: T::default(),
}
};
match fs::read_to_string(&path) {
Ok(text) => {
let mut lines = text.lines();
let Some(first) = lines.next() else {
return invalid("empty file");
};
let version_str: String = first
.chars()
.skip_while(|c| *c != 'v')
.skip(1)
.take_while(|c| !c.is_whitespace())
.collect();
let Ok(version): Result<u32, _> = version_str.parse() else {
return invalid("invalid version");
};
let text: String = lines.collect();
match T::parse_version(&text, version) {
Ok(val) => DataGuard { path, val },
Err(e) => invalid(&e),
}
}
Err(_) => DataGuard {
path,
val: T::default(),
},
}
}
}
impl<T: DataRsc> std::ops::Deref for DataGuard<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.val
}
}
impl<T: DataRsc> std::ops::DerefMut for DataGuard<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.val
}
}
impl<T: DataRsc> Drop for DataGuard<T> {
fn drop(&mut self) {
let dir = self.path.parent().unwrap();
fs::create_dir_all(dir).unwrap();
let mut file = File::create(dir.join(T::path())).unwrap();
let ron = ron::to_string(&self.val).unwrap();
let data = format!("// v{}\n{}\n", T::version(), ron);
if let Err(e) = file.write_all(data.as_bytes()) {
println!("Failed to write config @ {:?}: {e}", self.path);
}
}
pub fn save<T: DataRsc>(&self, data: &T) {
let dir = self.get();
fs::create_dir_all(dir).unwrap();
let mut file = File::create(dir.join(T::path())).unwrap();
// TODO: used to use encode_into_std_write from bincode here
let data = bitcode::encode(data);
file.write_all(&data).unwrap();
}
}