From 5a6c7ae9c5da14153365bd525f5b2a9bba6d2b2d Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 21 Jul 2024 11:50:22 +0300 Subject: [PATCH] feat(homeserver): add signin endpoint --- pubky-homeserver/src/database/tables/users.rs | 1 + pubky-homeserver/src/routes.rs | 1 + pubky-homeserver/src/routes/auth.rs | 86 +++++++++++-------- pubky/src/client.rs | 15 ++++ pubky/src/client_async.rs | 12 +++ pubky/src/lib.rs | 8 ++ 6 files changed, 88 insertions(+), 35 deletions(-) diff --git a/pubky-homeserver/src/database/tables/users.rs b/pubky-homeserver/src/database/tables/users.rs index e785245..9666637 100644 --- a/pubky-homeserver/src/database/tables/users.rs +++ b/pubky-homeserver/src/database/tables/users.rs @@ -15,6 +15,7 @@ pub type UsersTable = Database; pub const USERS_TABLE: &str = "users"; +// TODO: add more adminstration metadata like quota, invitation links, etc.. #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct User { pub created_at: u64, diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index dcf0b66..86120c2 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -16,6 +16,7 @@ pub fn create_app(state: AppState) -> Router { .route("/", get(root::handler)) .route("/:pubky", put(auth::signup)) .route("/:pubky/session", get(auth::session)) + .route("/:pubky/session", post(auth::signin)) .route("/:pubky/session", delete(auth::signout)) .route("/:pubky/*key", get(drive::put)) .layer(TraceLayer::new_for_http()) diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index b49f862..fceb6fe 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -34,41 +34,9 @@ pub async fn signup( pubky: Pubky, body: Bytes, ) -> Result { - let public_key = pubky.public_key(); - - state.verifier.verify(&body, public_key)?; - - let mut wtxn = state.db.env.write_txn()?; - let users: UsersTable = state.db.env.create_database(&mut wtxn, Some(USERS_TABLE))?; - - users.put( - &mut wtxn, - public_key, - &User { - created_at: Timestamp::now().into_inner(), - }, - )?; - - let session_secret = base32::encode(base32::Alphabet::Crockford, &random_bytes::<16>()); - - let sessions: SessionsTable = state - .db - .env - .open_database(&wtxn, Some(SESSIONS_TABLE))? - .expect("Sessions table already created"); - - // TODO: handle not having a user agent? - let mut session = Session::new(); - - session.set_user_agent(user_agent.to_string()); - - sessions.put(&mut wtxn, &session_secret, &session.serialize())?; - - cookies.add(Cookie::new(public_key.to_string(), session_secret)); - - wtxn.commit()?; - - Ok(()) + // TODO: Verify invitation link. + // TODO: add errors in case of already axisting user. + signin(State(state), TypedHeader(user_agent), cookies, pubky, body).await } pub async fn session( @@ -122,3 +90,51 @@ pub async fn signout( Err(Error::with_status(StatusCode::UNAUTHORIZED)) } + +pub async fn signin( + State(state): State, + TypedHeader(user_agent): TypedHeader, + cookies: Cookies, + pubky: Pubky, + body: Bytes, +) -> Result { + let public_key = pubky.public_key(); + + state.verifier.verify(&body, public_key)?; + + let mut wtxn = state.db.env.write_txn()?; + let users: UsersTable = state.db.env.create_database(&mut wtxn, Some(USERS_TABLE))?; + + if let Some(existing) = users.get(&wtxn, public_key)? { + users.put(&mut wtxn, public_key, &existing)?; + } else { + users.put( + &mut wtxn, + public_key, + &User { + created_at: Timestamp::now().into_inner(), + }, + )?; + } + + let session_secret = base32::encode(base32::Alphabet::Crockford, &random_bytes::<16>()); + + let sessions: SessionsTable = state + .db + .env + .open_database(&wtxn, Some(SESSIONS_TABLE))? + .expect("Sessions table already created"); + + // TODO: handle not having a user agent? + let mut session = Session::new(); + + session.set_user_agent(user_agent.to_string()); + + sessions.put(&mut wtxn, &session_secret, &session.serialize())?; + + cookies.add(Cookie::new(public_key.to_string(), session_secret)); + + wtxn.commit()?; + + Ok(()) +} diff --git a/pubky/src/client.rs b/pubky/src/client.rs index 95362c4..e353e98 100644 --- a/pubky/src/client.rs +++ b/pubky/src/client.rs @@ -95,6 +95,21 @@ impl PubkyClient { Ok(()) } + /// Signin to a homeserver. + pub fn signin(&self, keypair: &Keypair) -> Result<()> { + let pubky = keypair.public_key(); + + let (audience, mut url) = self.resolve_pubky_homeserver(&pubky)?; + + url.set_path(&format!("/{}/session", &pubky)); + + self.request(HttpMethod::Post, &url) + .send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes()) + .map_err(Box::new)?; + + Ok(()) + } + // === Private Methods === /// Publish the SVCB record for `_pubky.`. diff --git a/pubky/src/client_async.rs b/pubky/src/client_async.rs index ca0a308..de9012c 100644 --- a/pubky/src/client_async.rs +++ b/pubky/src/client_async.rs @@ -50,4 +50,16 @@ impl PubkyClientAsync { receiver.recv_async().await? } + + /// Async version of [PubkyClient::signin] + pub async fn signin(&self, keypair: &Keypair) -> Result<()> { + let (sender, receiver) = flume::bounded::>(1); + + let client = self.0.clone(); + let keypair = keypair.clone(); + + thread::spawn(move || sender.send(client.signin(&keypair))); + + receiver.recv_async().await? + } } diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 1876cda..b05d067 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -47,5 +47,13 @@ mod tests { _ => assert!(false, "expected NotSignedInt error"), } } + + client.signin(&keypair).await.unwrap(); + + { + let session = client.session(&keypair.public_key()).await.unwrap(); + + assert_eq!(session, Session { ..session.clone() }); + } } }