diff --git a/Cargo.lock b/Cargo.lock index 318c14b..0a07a7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -348,6 +348,18 @@ dependencies = [ "byteorder", ] +[[package]] +name = "bcrypt" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e7c93a3fb23b2fdde989b2c9ec4dd153063ec81f408507f84c090cd91c6641" +dependencies = [ + "base64 0.13.0", + "blowfish", + "getrandom 0.2.6", + "zeroize", +] + [[package]] name = "bdk" version = "0.19.0" @@ -496,6 +508,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "bumpalo" version = "3.9.1" @@ -552,6 +574,16 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" +[[package]] +name = "cipher" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "2.34.0" @@ -1502,6 +1534,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -2559,9 +2600,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0aeddcca1082112a6eeb43bf25fd7820b066aaf6eaef776e19d0a1febe38fe" +checksum = "68ef841a26fc5d040ced0417c6c6a64ee851f42489df11cdf0218e545b6f8d28" dependencies = [ "byteorder", "digest 0.9.0", @@ -2918,6 +2959,7 @@ version = "0.1.0" dependencies = [ "base32", "base64 0.13.0", + "bcrypt", "bdk", "bech32", "bitcoin", @@ -4359,9 +4401,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.3.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" dependencies = [ "zeroize_derive", ] diff --git a/entity/src/lib.rs b/entity/src/lib.rs index fac0331..efe2e28 100644 --- a/entity/src/lib.rs +++ b/entity/src/lib.rs @@ -14,6 +14,7 @@ pub mod peer; pub mod peer_address; pub mod script_pubkey; pub mod transaction; +pub mod user; pub mod utxo; pub mod seaql_migrations; diff --git a/entity/src/node.rs b/entity/src/node.rs index d987b28..65d1ec6 100644 --- a/entity/src/node.rs +++ b/entity/src/node.rs @@ -25,16 +25,13 @@ impl From for i16 { #[sea_orm(rs_type = "i16", db_type = "SmallInteger")] pub enum NodeRole { #[sea_orm(num_value = 0)] - Root, - #[sea_orm(num_value = 1)] Default, } impl From for i16 { fn from(role: NodeRole) -> i16 { match role { - NodeRole::Root => 0, - NodeRole::Default => 1, + NodeRole::Default => 0, } } } @@ -64,14 +61,9 @@ pub struct Model { } impl Model { - pub fn is_root(&self) -> bool { - self.role == 0 - } - pub fn get_role(&self) -> NodeRole { match self.role { - 0 => NodeRole::Root, - 1 => NodeRole::Default, + 0 => NodeRole::Default, _ => panic!("invalid role"), } } diff --git a/entity/src/user.rs b/entity/src/user.rs new file mode 100644 index 0000000..2cacf3f --- /dev/null +++ b/entity/src/user.rs @@ -0,0 +1,111 @@ +use sea_orm::{entity::prelude::*, ActiveValue}; +use serde::{Deserialize, Serialize}; + +use crate::seconds_since_epoch; + +#[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] +#[sea_orm(rs_type = "i16", db_type = "SmallInteger")] +pub enum UserRole { + #[sea_orm(num_value = 0)] + Default, +} + +impl From for i16 { + fn from(role: UserRole) -> i16 { + match role { + UserRole::Default => 0, + } + } +} + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn table_name(&self) -> &str { + "user" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Serialize, Deserialize)] +pub struct Model { + pub id: String, + pub role: i16, + pub username: String, + pub hashed_password: String, + pub created_at: i64, + pub updated_at: i64, +} + +impl Model { + pub fn get_role(&self) -> UserRole { + match self.role { + 0 => UserRole::Default, + _ => panic!("invalid role"), + } + } +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + Id, + Role, + Username, + HashedPassword, + CreatedAt, + UpdatedAt, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + Id, +} + +impl PrimaryKeyTrait for PrimaryKey { + type ValueType = String; + fn auto_increment() -> bool { + false + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation {} + +impl ColumnTrait for Column { + type EntityName = Entity; + fn def(&self) -> ColumnDef { + match self { + Self::Id => ColumnType::String(None).def().unique(), + Self::Role => ColumnType::SmallInteger.def(), + Self::Username => ColumnType::String(None).def().unique(), + Self::HashedPassword => ColumnType::String(None).def(), + Self::CreatedAt => ColumnType::BigInteger.def(), + Self::UpdatedAt => ColumnType::BigInteger.def(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + panic!("No RelationDef") + } +} + +impl ActiveModelBehavior for ActiveModel { + fn new() -> Self { + Self { + id: ActiveValue::Set(Uuid::new_v4().to_string()), + role: ActiveValue::Set(UserRole::Default.into()), + ..::default() + } + } + + fn before_save(mut self, insert: bool) -> Result { + let now: i64 = seconds_since_epoch(); + self.updated_at = ActiveValue::Set(now); + if insert { + self.created_at = ActiveValue::Set(now); + } + Ok(self) + } +} diff --git a/migration/src/lib.rs b/migration/src/lib.rs index 104456a..d14e88d 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -11,6 +11,7 @@ mod m20220428_000003_create_transactions_table; mod m20220428_000004_create_keychains_table; mod m20220616_000001_create_peers_table; mod m20220701_000001_create_peer_addresses_table; +mod m20220808_000001_create_users_table; pub struct Migrator; @@ -30,6 +31,7 @@ impl MigratorTrait for Migrator { Box::new(m20220428_000004_create_keychains_table::Migration), Box::new(m20220616_000001_create_peers_table::Migration), Box::new(m20220701_000001_create_peer_addresses_table::Migration), + Box::new(m20220808_000001_create_users_table::Migration), ] } } diff --git a/migration/src/m20220808_000001_create_users_table.rs b/migration/src/m20220808_000001_create_users_table.rs new file mode 100644 index 0000000..cf0f8ef --- /dev/null +++ b/migration/src/m20220808_000001_create_users_table.rs @@ -0,0 +1,50 @@ +use sea_schema::migration::prelude::*; +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20220808_000001_create_users_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(User::Table) + .if_not_exists() + .col(ColumnDef::new(User::Id).string().not_null().primary_key()) + .col(ColumnDef::new(User::Role).small_integer().not_null()) + .col( + ColumnDef::new(User::Username) + .string() + .unique_key() + .not_null(), + ) + .col(ColumnDef::new(User::HashedPassword).string().not_null()) + .col(ColumnDef::new(User::CreatedAt).big_integer().not_null()) + .col(ColumnDef::new(User::UpdatedAt).big_integer().not_null()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let mut stmt = Table::drop(); + stmt.table(User::Table); + manager.drop_table(stmt).await + } +} + +#[derive(Iden)] +enum User { + Table, + Id, + Role, + Username, + HashedPassword, + CreatedAt, + UpdatedAt, +} diff --git a/proto/sensei.proto b/proto/sensei.proto index a0cfed5..afbb205 100644 --- a/proto/sensei.proto +++ b/proto/sensei.proto @@ -3,7 +3,6 @@ package sensei; service Admin { rpc CreateAdmin (CreateAdminRequest) returns (CreateAdminResponse); - rpc StartAdmin (StartAdminRequest) returns (StartAdminResponse); rpc ListNodes (ListNodesRequest) returns (ListNodesResponse); rpc CreateNode (CreateNodeRequest) returns (CreateNodeResponse); rpc DeleteNode (DeleteNodeRequest) returns (DeleteNodeResponse); @@ -103,17 +102,11 @@ message ListTokensResponse { message CreateAdminRequest { string username = 1; - string alias = 2; - string passphrase = 3; - bool start = 4; + string passphrase = 2; } message CreateAdminResponse { - string pubkey = 1; - string macaroon = 2; - string id = 3; - uint32 role = 4; - string token = 5; + string token = 1; } message CreateNodeRequest { @@ -163,9 +156,9 @@ message GetStatusResponse { optional string pubkey = 3; optional string username = 4; optional uint32 role = 5; - bool created = 6; - bool running = 7; - bool authenticated = 8; + bool setup = 6; + bool authenticated_admin = 7; + bool authenticated_node = 8; } message StartNodeRequest { diff --git a/senseicore/Cargo.toml b/senseicore/Cargo.toml index 5651fef..4495df7 100644 --- a/senseicore/Cargo.toml +++ b/senseicore/Cargo.toml @@ -32,6 +32,7 @@ bitcoincore-rpc = "0.15" bdk = "0.19" pin-project = "1.0" hyper = "0.14" +bcrypt = "0.13.0" tindercrypt = { version = "0.3.2", default-features = false } uuid = { version = "0.8", features = ["serde", "v4"] } macaroon = "0.2" diff --git a/senseicore/src/database.rs b/senseicore/src/database.rs index 5826c10..758fa18 100644 --- a/senseicore/src/database.rs +++ b/senseicore/src/database.rs @@ -24,6 +24,8 @@ use entity::sea_orm; use entity::sea_orm::ActiveValue; use entity::sea_orm::QueryOrder; use entity::seconds_since_epoch; +use entity::user; +use entity::user::Entity as User; use migration::Condition; use migration::Expr; use rand::thread_rng; @@ -86,13 +88,6 @@ impl SenseiDatabase { .map(|_| ())?) } - pub async fn get_root_node(&self) -> Result, Error> { - Ok(Node::find() - .filter(node::Column::Role.eq(node::NodeRole::Root)) - .one(&self.connection) - .await?) - } - pub async fn get_node_by_pubkey(&self, pubkey: &str) -> Result, Error> { Ok(Node::find() .filter(node::Column::Pubkey.eq(pubkey)) @@ -159,6 +154,31 @@ impl SenseiDatabase { )) } + pub async fn create_user( + &self, + username: String, + passphrase: String, + ) -> Result { + let hashed_password = bcrypt::hash(passphrase, 10).unwrap(); + let user = user::ActiveModel { + username: ActiveValue::Set(username), + hashed_password: ActiveValue::Set(hashed_password), + ..Default::default() + }; + Ok(user.insert(&self.connection).await?) + } + + pub async fn verify_user(&self, username: String, passphrase: String) -> Result { + match User::find() + .filter(user::Column::Username.eq(username)) + .one(&self.connection) + .await? + { + Some(user) => Ok(bcrypt::verify(passphrase, &user.hashed_password).unwrap()), + None => Ok(false), + } + } + pub async fn get_root_access_token(&self) -> Result, Error> { Ok(AccessToken::find() .filter(access_token::Column::Scope.eq(String::from("*"))) diff --git a/senseicore/src/services/admin.rs b/senseicore/src/services/admin.rs index cb95177..f54dab2 100644 --- a/senseicore/src/services/admin.rs +++ b/senseicore/src/services/admin.rs @@ -65,15 +65,11 @@ pub struct NodeCreateResult { pub enum AdminRequest { GetStatus { - pubkey: String, + pubkey: Option, + authenticated_admin: bool, }, CreateAdmin { username: String, - alias: String, - passphrase: String, - start: bool, - }, - StartAdmin { passphrase: String, }, CreateNode { @@ -146,24 +142,15 @@ pub enum AdminRequest { pub enum AdminResponse { GetStatus { version: String, + setup: bool, + authenticated_node: bool, + authenticated_admin: bool, alias: Option, - created: bool, - running: bool, - authenticated: bool, pubkey: Option, username: Option, role: Option, }, CreateAdmin { - pubkey: String, - macaroon: String, - id: String, - role: i16, - token: String, - }, - StartAdmin { - pubkey: String, - macaroon: String, token: String, }, CreateNode { @@ -319,22 +306,25 @@ impl From for Error { impl AdminService { pub async fn call(&self, request: AdminRequest) -> Result { match request { - AdminRequest::GetStatus { pubkey } => { - let root_node = self.database.get_root_node().await?; - match root_node { - Some(_root_node) => { + AdminRequest::GetStatus { + pubkey, + authenticated_admin, + } => { + let setup = self.database.get_root_access_token().await?.is_some(); + match pubkey { + Some(pubkey) => { let pubkey_node = self.database.get_node_by_pubkey(&pubkey).await?; match pubkey_node { Some(pubkey_node) => { let directory = self.node_directory.lock().await; - let node_running = directory.contains_key(&pubkey); + let _node_running = directory.contains_key(&pubkey); Ok(AdminResponse::GetStatus { version: version::get_version(), alias: Some(pubkey_node.alias), - created: true, - running: node_running, - authenticated: true, + setup, + authenticated_admin, + authenticated_node: true, pubkey: Some(pubkey_node.pubkey), username: Some(pubkey_node.username), role: Some(pubkey_node.role), @@ -343,9 +333,9 @@ impl AdminService { None => Ok(AdminResponse::GetStatus { version: version::get_version(), alias: None, - created: true, - running: false, - authenticated: false, + setup, + authenticated_admin, + authenticated_node: false, pubkey: None, username: None, role: None, @@ -355,10 +345,10 @@ impl AdminService { None => Ok(AdminResponse::GetStatus { version: version::get_version(), alias: None, + setup, + authenticated_admin, + authenticated_node: false, pubkey: None, - created: false, - running: false, - authenticated: false, username: None, role: None, }), @@ -366,55 +356,19 @@ impl AdminService { } AdminRequest::CreateAdmin { username, - alias, passphrase, - start, } => { - let (node, macaroon) = self - .create_node(username, alias, passphrase.clone(), node::NodeRole::Root) - .await?; - let root_token = self.database.create_root_access_token().await.unwrap(); - - let macaroon = macaroon.serialize(macaroon::Format::V2)?; - - if start { - self.start_node(node.clone(), passphrase).await?; - } + let _user = self + .database + .create_user(username, passphrase) + .await + .unwrap(); Ok(AdminResponse::CreateAdmin { - pubkey: node.pubkey, - macaroon: hex_utils::hex_str(macaroon.as_slice()), - id: node.id, - role: node.role, token: root_token.token, }) } - AdminRequest::StartAdmin { passphrase } => { - let root_node = self.database.get_root_node().await?; - let access_token = self.database.get_root_access_token().await?; - - match root_node { - Some(node) => { - let macaroon = LightningNode::get_macaroon_for_node( - &node.id, - &passphrase, - self.database.clone(), - ) - .await?; - self.start_node(node.clone(), passphrase).await?; - let macaroon = macaroon.serialize(macaroon::Format::V2)?; - Ok(AdminResponse::StartAdmin { - pubkey: node.pubkey, - macaroon: hex_utils::hex_str(macaroon.as_slice()), - token: access_token.expect("no token in db").token, - }) - } - None => Err(Error::Generic(String::from( - "root node not found, you need to init your sensei instance", - ))), - } - } AdminRequest::StartNode { pubkey, passphrase } => { let node = self.database.get_node_by_pubkey(&pubkey).await?; match node { @@ -776,7 +730,6 @@ impl AdminService { let listen_addr = self.config.api_host.clone(); let listen_port: i32 = match role { - node::NodeRole::Root => self.config.root_node_port.into(), node::NodeRole::Default => { let mut available_ports = self.available_ports.lock().await; available_ports.pop_front().unwrap().into() diff --git a/senseicore/tests/smoke_test.rs b/senseicore/tests/smoke_test.rs index 03dc115..6800b75 100644 --- a/senseicore/tests/smoke_test.rs +++ b/senseicore/tests/smoke_test.rs @@ -95,33 +95,20 @@ mod test { handle.as_ref().unwrap().node.clone() } - async fn create_root_node( + async fn create_admin_account( admin_service: &AdminService, username: &str, passphrase: &str, - start: bool, - ) -> Arc { + ) -> String { match admin_service .call(AdminRequest::CreateAdmin { username: String::from(username), - alias: String::from(username), passphrase: String::from(passphrase), - start, }) .await .unwrap() { - AdminResponse::CreateAdmin { - pubkey, - macaroon: _, - id: _, - token: _, - role: _, - } => { - let directory = admin_service.node_directory.lock().await; - let handle = directory.get(&pubkey).unwrap().as_ref().unwrap(); - Some(handle.node.clone()) - } + AdminResponse::CreateAdmin { token } => Some(token), _ => None, } .unwrap() @@ -637,7 +624,8 @@ mod test { } async fn smoke_test(bitcoind: BitcoinD, admin_service: AdminService) { - let alice = create_root_node(&admin_service, "alice", "alice", true).await; + let admin_token = create_admin_account(&admin_service, "admin", "admin").await; + let alice = create_node(&admin_service, "alice", "alice", true).await; let bob = create_node(&admin_service, "bob", "bob", true).await; let charlie = create_node(&admin_service, "charlie", "charlie", true).await; fund_node(&bitcoind, alice.clone()).await; @@ -744,7 +732,8 @@ mod test { } async fn batch_open_channels_test(bitcoind: BitcoinD, admin_service: AdminService) { - let alice = create_root_node(&admin_service, "alice", "alice", true).await; + let admin_token = create_admin_account(&admin_service, "admin", "admin").await; + let alice = create_node(&admin_service, "alice", "alice", true).await; let bob = create_node(&admin_service, "bob", "bob", true).await; let charlie = create_node(&admin_service, "charlie", "charlie", true).await; let doug = create_node(&admin_service, "doug", "doug", true).await; diff --git a/src/cli.rs b/src/cli.rs index 1948572..62db3c2 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -23,7 +23,7 @@ use crate::sensei::{ CreateNodeRequest, GetUnusedAddressRequest, InfoRequest, KeysendRequest, ListChannelsRequest, ListNodesRequest, ListPaymentsRequest, ListPeersRequest, ListUnspentRequest, NetworkGraphInfoRequest, OpenChannelRequest, OpenChannelsRequest, PayInvoiceRequest, - SignMessageRequest, StartAdminRequest, StartNodeRequest, + SignMessageRequest, StartNodeRequest, }; pub mod sensei { @@ -202,7 +202,6 @@ async fn main() -> Result<(), Box> { let mut admin_client = AdminClient::new(channel); let username = command_args.value_of("username").unwrap(); - let alias = command_args.value_of("alias").unwrap(); let mut passphrase = String::new(); print!("set a passphrase: "); @@ -210,9 +209,7 @@ async fn main() -> Result<(), Box> { let request = tonic::Request::new(CreateAdminRequest { username: username.to_string(), - alias: alias.to_string(), passphrase, - start: false, }); let response = admin_client.create_admin(request).await?; println!("{:?}", response.into_inner()); @@ -251,15 +248,6 @@ async fn main() -> Result<(), Box> { }); match command { - "start" => { - let mut passphrase = String::new(); - println!("enter your passphrase: "); - io::stdin().read_line(&mut passphrase)?; - - let request = tonic::Request::new(StartAdminRequest { passphrase }); - let response = admin_client.start_admin(request).await?; - println!("{:?}", response.into_inner()); - } "listnodes" => { let request = tonic::Request::new(ListNodesRequest { pagination: None }); let response = admin_client.list_nodes(request).await?; diff --git a/src/grpc/admin.rs b/src/grpc/admin.rs index 1b470e7..8f8396c 100644 --- a/src/grpc/admin.rs +++ b/src/grpc/admin.rs @@ -19,8 +19,7 @@ use super::{ FindRouteRequest, FindRouteResponse, GetStatusRequest, GetStatusResponse, ListNode, ListNodesRequest, ListNodesResponse, ListTokensRequest, ListTokensResponse, NodeInfoRequest, NodeInfoResponse, PathFailedRequest, PathFailedResponse, - PathSuccessfulRequest, PathSuccessfulResponse, StartAdminRequest, StartAdminResponse, - Token, + PathSuccessfulRequest, PathSuccessfulResponse, Token, }, utils::raw_macaroon_from_metadata, }; @@ -167,9 +166,7 @@ impl From for AdminRequest { fn from(req: CreateAdminRequest) -> Self { AdminRequest::CreateAdmin { username: req.username, - alias: req.alias, passphrase: req.passphrase, - start: req.start, } } } @@ -179,19 +176,7 @@ impl TryFrom for CreateAdminResponse { fn try_from(res: AdminResponse) -> Result { match res { - AdminResponse::CreateAdmin { - pubkey, - macaroon, - id, - role, - token, - } => Ok(Self { - pubkey, - macaroon, - id, - role: role as u32, - token, - }), + AdminResponse::CreateAdmin { token } => Ok(Self { token }), _ => Err("impossible".to_string()), } } @@ -205,18 +190,18 @@ impl TryFrom for GetStatusResponse { AdminResponse::GetStatus { version, alias, - running, - created, - authenticated, + setup, + authenticated_node, + authenticated_admin, pubkey, username, role, } => Ok(Self { version, alias, - running, - created, - authenticated, + setup, + authenticated_admin, + authenticated_node, pubkey, username, role: role.map(|role| role as u32), @@ -226,33 +211,6 @@ impl TryFrom for GetStatusResponse { } } -impl From for AdminRequest { - fn from(req: StartAdminRequest) -> Self { - AdminRequest::StartAdmin { - passphrase: req.passphrase, - } - } -} - -impl TryFrom for StartAdminResponse { - type Error = String; - - fn try_from(res: AdminResponse) -> Result { - match res { - AdminResponse::StartAdmin { - pubkey, - macaroon, - token, - } => Ok(Self { - pubkey, - macaroon, - token, - }), - _ => Err("impossible".to_string()), - } - } -} - impl From for AdminRequest { fn from(req: AdminStartNodeRequest) -> Self { AdminRequest::StartNode { @@ -472,7 +430,6 @@ impl AdminService { request: AdminRequest, ) -> Result { let required_scope = get_scope_from_request(&request); - let token = self.raw_token_from_metadata(metadata)?; if self.is_valid_token(token, required_scope).await { @@ -505,12 +462,27 @@ impl Admin for AdminService { request: tonic::Request, ) -> Result, Status> { let macaroon_hex_string = raw_macaroon_from_metadata(request.metadata().clone())?; + let token = super::utils::raw_token_from_metadata(request.metadata().clone())?; + + let pubkey = match macaroon_hex_string { + None => None, + Some(macaroon_hex_string) => { + let (_macaroon, session) = + utils::macaroon_with_session_from_hex_str(&macaroon_hex_string) + .map_err(|_e| Status::unauthenticated("invalid macaroon"))?; + Some(session.pubkey) + } + }; - let (_macaroon, session) = utils::macaroon_with_session_from_hex_str(&macaroon_hex_string) - .map_err(|_e| Status::unauthenticated("invalid macaroon"))?; - let pubkey = session.pubkey.clone(); + let authenticated_admin = match token { + None => false, + Some(token) => self.is_valid_token(token, Some("*")).await, + }; - let request = AdminRequest::GetStatus { pubkey }; + let request = AdminRequest::GetStatus { + pubkey, + authenticated_admin, + }; match self.admin_service.call(request).await { Ok(response) => { let response: Result = response.try_into(); @@ -536,21 +508,6 @@ impl Admin for AdminService { Err(_err) => Err(Status::unknown("error")), } } - async fn start_admin( - &self, - request: tonic::Request, - ) -> Result, Status> { - let request: AdminRequest = request.into_inner().into(); - match self.admin_service.call(request).await { - Ok(response) => { - let response: Result = response.try_into(); - response - .map(Response::new) - .map_err(|_err| Status::unknown("err")) - } - Err(_err) => Err(Status::unknown("error")), - } - } async fn start_node( &self, request: tonic::Request, diff --git a/src/grpc/node.rs b/src/grpc/node.rs index 05cee6f..c214d8b 100644 --- a/src/grpc/node.rs +++ b/src/grpc/node.rs @@ -47,55 +47,63 @@ impl NodeService { metadata: MetadataMap, request: NodeRequest, ) -> Result { - let macaroon_hex_string = raw_macaroon_from_metadata(metadata)?; + match raw_macaroon_from_metadata(metadata)? { + None => Err(Status::unauthenticated("macaroon required")), + Some(macaroon_hex_string) => { + let (macaroon, session) = + utils::macaroon_with_session_from_hex_str(&macaroon_hex_string) + .map_err(|_e| Status::unauthenticated("invalid macaroon"))?; + let pubkey = session.pubkey.clone(); - let (macaroon, session) = utils::macaroon_with_session_from_hex_str(&macaroon_hex_string) - .map_err(|_e| Status::unauthenticated("invalid macaroon"))?; - let pubkey = session.pubkey.clone(); + let node_directory = self.admin_service.node_directory.lock().await; - let node_directory = self.admin_service.node_directory.lock().await; - - match node_directory.get(&session.pubkey) { - Some(Some(handle)) => { - handle - .node - .verify_macaroon(macaroon, session) - .await - .map_err(|_e| Status::unauthenticated("invalid macaroon: failed to verify"))?; - - match request { - NodeRequest::StopNode {} => { - drop(node_directory); - let admin_request = AdminRequest::StopNode { pubkey }; - let _ = self - .admin_service - .call(admin_request) + match node_directory.get(&session.pubkey) { + Some(Some(handle)) => { + handle + .node + .verify_macaroon(macaroon, session) .await - .map_err(|_e| Status::unknown("failed to stop node"))?; - Ok(NodeResponse::StopNode {}) + .map_err(|_e| { + Status::unauthenticated("invalid macaroon: failed to verify") + })?; + + match request { + NodeRequest::StopNode {} => { + drop(node_directory); + let admin_request = AdminRequest::StopNode { pubkey }; + let _ = self + .admin_service + .call(admin_request) + .await + .map_err(|_e| Status::unknown("failed to stop node"))?; + Ok(NodeResponse::StopNode {}) + } + _ => handle + .node + .call(request) + .await + .map_err(|_e| Status::unknown("error")), + } } - _ => handle - .node - .call(request) - .await - .map_err(|_e| Status::unknown("error")), + Some(None) => Err(Status::not_found("node is in process of being started")), + None => match request { + NodeRequest::StartNode { passphrase } => { + drop(node_directory); + let admin_request = AdminRequest::StartNode { + passphrase, + pubkey: session.pubkey, + }; + let _ = self.admin_service.call(admin_request).await.map_err(|_e| { + Status::unauthenticated( + "failed to start node, likely invalid passphrase", + ) + })?; + Ok(NodeResponse::StartNode {}) + } + _ => Err(Status::not_found("node with that pubkey not found")), + }, } } - Some(None) => Err(Status::not_found("node is in process of being started")), - None => match request { - NodeRequest::StartNode { passphrase } => { - drop(node_directory); - let admin_request = AdminRequest::StartNode { - passphrase, - pubkey: session.pubkey, - }; - let _ = self.admin_service.call(admin_request).await.map_err(|_e| { - Status::unauthenticated("failed to start node, likely invalid passphrase") - })?; - Ok(NodeResponse::StartNode {}) - } - _ => Err(Status::not_found("node with that pubkey not found")), - }, } } } diff --git a/src/grpc/utils.rs b/src/grpc/utils.rs index 3b504d5..8e2e228 100644 --- a/src/grpc/utils.rs +++ b/src/grpc/utils.rs @@ -1,13 +1,25 @@ use tonic::{metadata::MetadataMap, Status}; -pub fn raw_macaroon_from_metadata(metadata: MetadataMap) -> Result { +pub fn raw_macaroon_from_metadata(metadata: MetadataMap) -> Result, Status> { let macaroon_opt = metadata.get("macaroon"); match macaroon_opt { Some(macaroon) => macaroon .to_str() - .map(String::from) + .map(|s| Some(String::from(s))) .map_err(|_e| Status::unauthenticated("invalid macaroon: must be ascii")), - None => Err(Status::unauthenticated("macaroon is required")), + None => Ok(None), + } +} + +pub fn raw_token_from_metadata(metadata: MetadataMap) -> Result, Status> { + let token_opt = metadata.get("token"); + + match token_opt { + Some(token) => token + .to_str() + .map(|s| Some(String::from(s))) + .map_err(|_e| Status::unauthenticated("invalid token: must be ascii")), + None => Ok(None), } } diff --git a/src/http/admin.rs b/src/http/admin.rs index 7d474d9..66dad31 100644 --- a/src/http/admin.rs +++ b/src/http/admin.rs @@ -262,8 +262,6 @@ impl From for AdminRequest { pub struct CreateAdminParams { pub username: String, pub passphrase: String, - pub alias: String, - pub start: bool, } impl From for AdminRequest { @@ -271,21 +269,6 @@ impl From for AdminRequest { Self::CreateAdmin { username: params.username, passphrase: params.passphrase, - alias: params.alias, - start: params.start, - } - } -} - -#[derive(Deserialize)] -pub struct StartAdminParams { - pub passphrase: String, -} - -impl From for AdminRequest { - fn from(params: StartAdminParams) -> Self { - Self::StartAdmin { - passphrase: params.passphrase, } } } @@ -350,6 +333,7 @@ pub fn add_routes(router: Router) -> Router { .route("/v1/init", post(init_sensei)) .route("/v1/nodes", get(list_nodes)) .route("/v1/nodes", post(create_node)) + .route("/v1/nodes/login", post(login_node)) .route("/v1/nodes/batch", post(batch_create_nodes)) .route("/v1/nodes/start", post(start_node)) .route("/v1/nodes/stop", post(stop_node)) @@ -358,8 +342,7 @@ pub fn add_routes(router: Router) -> Router { .route("/v1/tokens", post(create_token)) .route("/v1/tokens", delete(delete_token)) .route("/v1/status", get(get_status)) - .route("/v1/start", post(start_sensei)) - .route("/v1/login", post(login)) + .route("/v1/login", post(login_admin)) .route("/v1/logout", post(logout)) .route("/v1/peers/connect", post(connect_gossip_peer)) .route("/v1/ldk/network/route", post(find_route)) @@ -690,7 +673,39 @@ pub async fn list_nodes( } } -pub async fn login( +pub async fn login_admin( + Extension(admin_service): Extension>, + cookies: Cookies, + Json(payload): Json, +) -> Result, StatusCode> { + let params: LoginNodeParams = + serde_json::from_value(payload).map_err(|_e| StatusCode::UNPROCESSABLE_ENTITY)?; + + let admin_user = admin_service + .database + .verify_user(params.username, params.passphrase) + .await + .map_err(|_e| StatusCode::UNAUTHORIZED)?; + if admin_user { + let token = admin_service + .database + .get_root_access_token() + .await + .map_err(|_e| StatusCode::UNAUTHORIZED)? + .unwrap(); + let token_cookie = Cookie::build("token", token.token.clone()) + .http_only(true) + .finish(); + cookies.add(token_cookie); + Ok(Json(json!({ + "token": token.token + }))) + } else { + Err(StatusCode::UNAUTHORIZED) + } +} + +pub async fn login_node( Extension(admin_service): Extension>, cookies: Cookies, Json(payload): Json, @@ -706,20 +721,15 @@ pub async fn login( match node { Some(node) => { - let request = match node.is_root() { - true => AdminRequest::StartAdmin { - passphrase: params.passphrase, - }, - false => AdminRequest::StartNode { - pubkey: node.pubkey.clone(), - passphrase: params.passphrase, - }, + let request = AdminRequest::StartNode { + pubkey: node.pubkey.clone(), + passphrase: params.passphrase, }; - match admin_service.call(request).await { Ok(response) => match response { AdminResponse::StartNode { macaroon } => { let macaroon_cookie = Cookie::build("macaroon", macaroon.clone()) + .path("/") .http_only(true) .finish(); cookies.add(macaroon_cookie); @@ -730,27 +740,6 @@ pub async fn login( "role": node.role as u16 }))) } - AdminResponse::StartAdmin { - pubkey: _, - macaroon, - token, - } => { - let macaroon_cookie = Cookie::build("macaroon", macaroon.clone()) - .http_only(true) - .finish(); - cookies.add(macaroon_cookie); - let token_cookie = Cookie::build("token", token.clone()) - .http_only(true) - .finish(); - cookies.add(token_cookie); - Ok(Json(json!({ - "pubkey": node.pubkey, - "alias": node.alias, - "macaroon": macaroon, - "role": node.role as u16, - "token": token - }))) - } _ => Err(StatusCode::UNPROCESSABLE_ENTITY), }, Err(_err) => Err(StatusCode::UNPROCESSABLE_ENTITY), @@ -779,30 +768,13 @@ pub async fn init_sensei( match admin_service.call(request).await { Ok(response) => match response { - AdminResponse::CreateAdmin { - pubkey, - macaroon, - id, - role, - token, - } => { - let macaroon_cookie = Cookie::build("macaroon", macaroon.clone()) - .http_only(true) - .finish(); - + AdminResponse::CreateAdmin { token } => { let token_cookie = Cookie::build("token", token.clone()) .http_only(true) .finish(); - cookies.add(macaroon_cookie); cookies.add(token_cookie); - Ok(Json(AdminResponse::CreateAdmin { - pubkey, - macaroon, - id, - role, - token, - })) + Ok(Json(AdminResponse::CreateAdmin { token })) } _ => Err(StatusCode::UNPROCESSABLE_ENTITY), }, @@ -817,72 +789,34 @@ pub async fn init_sensei( pub async fn get_status( Extension(admin_service): Extension>, cookies: Cookies, - AuthHeader { macaroon, token: _ }: AuthHeader, + AuthHeader { macaroon, token }: AuthHeader, ) -> Result, StatusCode> { let pubkey = { match get_macaroon_hex_str_from_cookies_or_header(&cookies, macaroon) { Ok(macaroon_hex) => match utils::macaroon_with_session_from_hex_str(&macaroon_hex) { - Ok((_macaroon, session)) => session.pubkey, - Err(_) => String::from(""), + Ok((_macaroon, session)) => Some(session.pubkey), + Err(_) => None, }, - Err(_) => String::from(""), + Err(_) => None, } }; - match admin_service.call(AdminRequest::GetStatus { pubkey }).await { + let authenticated_admin = authenticate_request(&admin_service, "*", &cookies, token) + .await + .unwrap_or(false); + + match admin_service + .call(AdminRequest::GetStatus { + pubkey, + authenticated_admin, + }) + .await + { Ok(response) => Ok(Json(response)), Err(err) => Ok(Json(AdminResponse::Error(err))), } } -pub async fn start_sensei( - Extension(admin_service): Extension>, - cookies: Cookies, - Json(payload): Json, -) -> Result, StatusCode> { - let params: Result = serde_json::from_value(payload); - let request = match params { - Ok(params) => Ok(params.into()), - Err(_) => Err(StatusCode::UNPROCESSABLE_ENTITY), - }?; - - match request { - AdminRequest::StartAdmin { passphrase } => { - match admin_service - .call(AdminRequest::StartAdmin { passphrase }) - .await - { - Ok(response) => match response { - AdminResponse::StartAdmin { - pubkey, - macaroon, - token, - } => { - let macaroon_cookie = Cookie::build("macaroon", macaroon.clone()) - .http_only(true) - .permanent() - .finish(); - cookies.add(macaroon_cookie); - let token_cookie = Cookie::build("token", token.clone()) - .http_only(true) - .permanent() - .finish(); - cookies.add(token_cookie); - Ok(Json(AdminResponse::StartAdmin { - pubkey, - macaroon, - token, - })) - } - _ => Err(StatusCode::UNPROCESSABLE_ENTITY), - }, - Err(_err) => Err(StatusCode::UNAUTHORIZED), - } - } - _ => Err(StatusCode::UNPROCESSABLE_ENTITY), - } -} - pub async fn create_node( Extension(admin_service): Extension>, Json(payload): Json, diff --git a/src/main.rs b/src/main.rs index 4a02ee0..bf5cb1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,8 +39,7 @@ use axum::{ extract::Extension, handler::Handler, http::StatusCode, - response::{Html, IntoResponse, Response}, - routing::get, + response::{IntoResponse, Response}, Router, }; use clap::Parser; @@ -318,12 +317,13 @@ fn main() { .await, ); - let router = Router::new() - .route("/admin/*path", static_handler.into_service()) - .fallback(get(not_found)); + let api_router = Router::new(); + let api_router = add_admin_routes(api_router); + let api_router = add_node_routes(api_router); - let router = add_admin_routes(router); - let router = add_node_routes(router); + let router = Router::new() + .nest("/api", api_router) + .fallback(static_handler.into_service()); let router = match args.development_mode { Some(_development_mode) => router.layer( @@ -406,26 +406,41 @@ fn main() { }); } -// We use a wildcard matcher ("/static/*file") to match against everything -// within our defined assets directory. This is the directory on our Asset -// struct below, where folder = "examples/public/". async fn static_handler(uri: Uri) -> impl IntoResponse { let mut path = uri.path().trim_start_matches('/').to_string(); + let paths_to_passthrough = ["static/", "images/"]; + let files_to_passthrough = [ + "favicon.ico", + "favicon-16x16.png", + "favicon-32x32.png", + "logo192.png", + "logo512.png", + "manifest.json", + ]; + let mut passthrough = false; + + paths_to_passthrough.iter().for_each(|pp| { + if path.starts_with(pp) { + passthrough = true; + } + }); + + if files_to_passthrough.contains(&path.as_str()) { + passthrough = true; + } if path.starts_with("admin/static/") { path = path.replace("admin/static/", "static/"); - } else { + passthrough = true; + } + + if !passthrough { path = String::from("index.html"); } StaticFile(path) } -// Finally, we use a fallback route for anything that didn't match. -async fn not_found() -> Html<&'static str> { - Html("

404

Not Found

") -} - #[derive(RustEmbed)] #[folder = "web-admin/build/"] struct Asset; diff --git a/system-tests/system-test.py b/system-tests/system-test.py index 4369ab2..bcafdf7 100755 --- a/system-tests/system-test.py +++ b/system-tests/system-test.py @@ -88,7 +88,7 @@ def mine(self, count=1): def grpc_admin_client(url, metadata): channel = grpc.insecure_channel(url) stub = AdminStub(channel) - stub.GetStatus(GetStatusRequest(), metadata=metadata, timeout=1) + # stub.GetStatus(GetStatusRequest(), metadata=metadata, timeout=1) return stub @@ -143,22 +143,21 @@ def run(): balance = btc.getbalance() assert balance > 0 - alice = fund_root_node(btc, metadata) - meta_a = metadata - bob, meta_b, id_b = fund_node(btc, metadata, senseid, 1) + alice, meta_a, id_a = fund_node(btc, metadata, senseid, 1) + bob, meta_b, id_b = fund_node(btc, metadata, senseid, 2) print('Create channel alice -> bob') - oc_res = alice.OpenChannels(OpenChannelsRequest(requests=[OpenChannelRequest(counterparty_pubkey=f"{id_b}", counterparty_host_port=f"127.0.0.1:10000", amount_sats=CHANNEL_VALUE_SAT, public=True)]), + oc_res = alice.OpenChannels(OpenChannelsRequest(requests=[OpenChannelRequest(counterparty_pubkey=f"{id_b}", counterparty_host_port=f"127.0.0.1:10001", amount_sats=CHANNEL_VALUE_SAT, public=True)]), metadata=meta_a) print(oc_res) wait_until('channel at bob', lambda: bob.ListChannels(ListChannelsRequest(), metadata=meta_b).channels[0]) assert not bob.ListChannels(ListChannelsRequest(), metadata=meta_b).channels[0].is_usable - charlie, meta_c, id_c = fund_node(btc, metadata, senseid, 2) + charlie, meta_c, id_c = fund_node(btc, metadata, senseid, 3) print('Create channel bob -> charlie') - bob.OpenChannels(OpenChannelsRequest(requests=[OpenChannelRequest(counterparty_pubkey=f"{id_c}", counterparty_host_port=f"127.0.0.1:10001", amount_sats=CHANNEL_VALUE_SAT, public=True)]), + bob.OpenChannels(OpenChannelsRequest(requests=[OpenChannelRequest(counterparty_pubkey=f"{id_c}", counterparty_host_port=f"127.0.0.1:10002", amount_sats=CHANNEL_VALUE_SAT, public=True)]), metadata=meta_b) wait_until('channel at charlie', lambda: charlie.ListChannels(ListChannelsRequest(), metadata=meta_c).channels[0]) assert not charlie.ListChannels(ListChannelsRequest(), metadata=meta_c).channels[0].is_usable @@ -308,13 +307,13 @@ def start_senseid(): p = Popen(cmd, stdout=stdout_log, stderr=subprocess.STDOUT) processes.append(p) time.sleep(2) - requests.get('http://localhost:3301/v1/status') + requests.get('http://localhost:3301/api/v1/status') print("Init sensei") - res = requests.post('http://localhost:3301/v1/init', - json={"alias": "Satoshi", "passphrase": "test", "username": "admin", "start": True}) + res = requests.post('http://localhost:3301/api/v1/init', + json={ "passphrase": "test", "username": "admin"}) json = res.json() - metadata = (('macaroon', json['macaroon']), ('token', json['token'])) + metadata = (('macaroon', 'invalid'), ('token', json['token'])) senseid = grpc_admin_client(f'localhost:3301', metadata) return senseid, metadata diff --git a/web-admin/package-lock.json b/web-admin/package-lock.json index 59f8bdf..30ddfac 100644 --- a/web-admin/package-lock.json +++ b/web-admin/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@headlessui/react": "^1.4.2", "@heroicons/react": "^1.0.5", - "@l2-technology/sensei-client": "0.1.22", + "@l2-technology/sensei-client": "0.1.23", "@tailwindcss/aspect-ratio": "^0.4.0", "@tailwindcss/forms": "^0.4.0", "@tailwindcss/typography": "^0.5.0", @@ -3189,9 +3189,9 @@ } }, "node_modules/@l2-technology/sensei-client": { - "version": "0.1.22", - "resolved": "https://registry.npmjs.org/@l2-technology/sensei-client/-/sensei-client-0.1.22.tgz", - "integrity": "sha512-QIBxCFwh/eUW2tMnv7XMFh3iEizubP2/H1rTNvXPPca+sn+mRTfwWO0zpBTMVgsPv0dDeQ+Kzvw4WnjJw2Pwww==" + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@l2-technology/sensei-client/-/sensei-client-0.1.23.tgz", + "integrity": "sha512-c9oReTnaK9UGlCgS2dMoxlhveWynVBtkeI4TuTHYjDsZV4fshV8bdWn73KQSmFOV7Jp/WjaD/hVzEa93U+S2iw==" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -25922,9 +25922,9 @@ } }, "@l2-technology/sensei-client": { - "version": "0.1.22", - "resolved": "https://registry.npmjs.org/@l2-technology/sensei-client/-/sensei-client-0.1.22.tgz", - "integrity": "sha512-QIBxCFwh/eUW2tMnv7XMFh3iEizubP2/H1rTNvXPPca+sn+mRTfwWO0zpBTMVgsPv0dDeQ+Kzvw4WnjJw2Pwww==" + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@l2-technology/sensei-client/-/sensei-client-0.1.23.tgz", + "integrity": "sha512-c9oReTnaK9UGlCgS2dMoxlhveWynVBtkeI4TuTHYjDsZV4fshV8bdWn73KQSmFOV7Jp/WjaD/hVzEa93U+S2iw==" }, "@nodelib/fs.scandir": { "version": "2.1.5", diff --git a/web-admin/package.json b/web-admin/package.json index 295cc42..dac093d 100644 --- a/web-admin/package.json +++ b/web-admin/package.json @@ -1,12 +1,12 @@ { "name": "web-admin", "version": "0.2.0", - "homepage": "http://localhost:5401/admin", + "homepage": "http://localhost:5401/", "private": true, "dependencies": { "@headlessui/react": "^1.4.2", "@heroicons/react": "^1.0.5", - "@l2-technology/sensei-client": "0.1.22", + "@l2-technology/sensei-client": "0.1.23", "@tailwindcss/aspect-ratio": "^0.4.0", "@tailwindcss/forms": "^0.4.0", "@tailwindcss/typography": "^0.5.0", diff --git a/web-admin/src/App.tsx b/web-admin/src/App.tsx index 7f45965..f3c0eec 100644 --- a/web-admin/src/App.tsx +++ b/web-admin/src/App.tsx @@ -2,9 +2,11 @@ import React from "react"; import "./App.css"; import { AuthProvider } from "./contexts/auth"; import AppLayout from "./layouts/AppLayout"; -import LoginPage from "./auth/pages/LoginPage"; +import AdminLoginPage from "./auth/pages/AdminLoginPage"; +import NodeLoginPage from "./auth/pages/NodeLoginPage"; import SetupPage from "./auth/pages/SetupPage"; -import RequireAuth from "./components/RequireAuth"; +import RequireNodeAuth from "./components/RequireNodeAuth"; +import RequireAdminAuth from "./components/RequireAdminAuth"; import { Routes, Route, Navigate } from "react-router-dom"; import { useQuery } from "react-query"; import getStatus from "./auth/queries/getStatus"; @@ -39,33 +41,45 @@ function App() { return ( - } /> + } /> + }/> + } /> + } /> } /> - } /> - + - + } > - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> } /> } /> } /> } /> - } /> + } /> + + + + + } + > + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + diff --git a/web-admin/src/auth/mutations/createAdmin.ts b/web-admin/src/auth/mutations/createAdmin.ts deleted file mode 100644 index 01a31d1..0000000 --- a/web-admin/src/auth/mutations/createAdmin.ts +++ /dev/null @@ -1,12 +0,0 @@ -import sensei from "../../utils/sensei"; - -const createAdmin = async ( - username: string, - alias: string, - passphrase: string, - start: boolean -) => { - return sensei.init({ username, alias, passphrase, start }); -}; - -export default createAdmin; diff --git a/web-admin/src/auth/mutations/init.ts b/web-admin/src/auth/mutations/init.ts new file mode 100644 index 0000000..8a47aa1 --- /dev/null +++ b/web-admin/src/auth/mutations/init.ts @@ -0,0 +1,10 @@ +import sensei from "../../utils/sensei"; + +const init = async ( + username: string, + passphrase: string, +) => { + return sensei.init({ username, passphrase }); +}; + +export default init; diff --git a/web-admin/src/auth/pages/AdminLoginPage.tsx b/web-admin/src/auth/pages/AdminLoginPage.tsx new file mode 100644 index 0000000..50f9b9e --- /dev/null +++ b/web-admin/src/auth/pages/AdminLoginPage.tsx @@ -0,0 +1,117 @@ +import React from "react"; +import { useNavigate, useLocation } from "react-router"; +import { AlertMsg } from "src/components/ErrorAlert"; +import Spinner from "src/components/Spinner"; +import { useAuth } from "../../contexts/auth"; +import logo from "../../images/Icon-Lightning@2x.png"; + +const AdminLoginPage = () => { + let [submitting, setSubmitting] = React.useState(false); + let [submitError, setSubmitError] = React.useState(null!); + let navigate = useNavigate(); + let location = useLocation(); + let auth = useAuth(); + + let from = location.state?.from?.pathname || "/admin/nodes"; + + async function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + setSubmitting(true); + setSubmitError(null); + + let formData = new FormData(event.currentTarget); + let username = formData.get("username") as string; + let passphrase = formData.get("passphrase") as string; + + try { + await auth.loginAdmin(username, passphrase); + // Send them back to the page they tried to visit when they were + // redirected to the login page. Use { replace: true } so we don't create + // another entry in the history stack for the login page. This means that + // when they get to the protected page and click the back button, they + // won't end up back on the login page, which is also really nice for the + // user experience. + navigate(from, { replace: true }); + } catch (e) { + setSubmitError("invalid passphrase"); + setSubmitting(false); + } + } + + return ( +
+
+
+
+ sensei logo + +

+ Login to the admin +

+
+ + {submitError && ( + + {submitError} + + )} + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+ +
+
+
+
+
+
+ ); +}; + +export default AdminLoginPage; diff --git a/web-admin/src/auth/pages/LogoutPage.tsx b/web-admin/src/auth/pages/LogoutPage.tsx index eb3bc02..921576a 100644 --- a/web-admin/src/auth/pages/LogoutPage.tsx +++ b/web-admin/src/auth/pages/LogoutPage.tsx @@ -11,7 +11,7 @@ const LogoutPage = () => { const logout = async () => { await sensei.logout(); queryClient.clear(); - navigate("/admin/login", { replace: true }); + navigate("/login", { replace: true }); }; logout(); diff --git a/web-admin/src/auth/pages/LoginPage.tsx b/web-admin/src/auth/pages/NodeLoginPage.tsx similarity index 95% rename from web-admin/src/auth/pages/LoginPage.tsx rename to web-admin/src/auth/pages/NodeLoginPage.tsx index 24f4817..32e0a1b 100644 --- a/web-admin/src/auth/pages/LoginPage.tsx +++ b/web-admin/src/auth/pages/NodeLoginPage.tsx @@ -5,14 +5,14 @@ import Spinner from "src/components/Spinner"; import { useAuth } from "../../contexts/auth"; import logo from "../../images/Icon-Lightning@2x.png"; -const LoginPage = () => { +const NodeLoginPage = () => { let [submitting, setSubmitting] = React.useState(false); let [submitError, setSubmitError] = React.useState(null!); let navigate = useNavigate(); let location = useLocation(); let auth = useAuth(); - let from = location.state?.from?.pathname || "/admin/chain"; + let from = location.state?.from?.pathname || "/chain"; async function handleSubmit(event: React.FormEvent) { event.preventDefault(); @@ -24,7 +24,7 @@ const LoginPage = () => { let passphrase = formData.get("passphrase") as string; try { - await auth.login(username, passphrase); + await auth.loginNode(username, passphrase); // Send them back to the page they tried to visit when they were // redirected to the login page. Use { replace: true } so we don't create // another entry in the history stack for the login page. This means that @@ -114,4 +114,4 @@ const LoginPage = () => { ); }; -export default LoginPage; +export default NodeLoginPage; diff --git a/web-admin/src/auth/pages/SetupPage.tsx b/web-admin/src/auth/pages/SetupPage.tsx index 588dab6..2e4db5a 100644 --- a/web-admin/src/auth/pages/SetupPage.tsx +++ b/web-admin/src/auth/pages/SetupPage.tsx @@ -16,11 +16,9 @@ const UsernamePassphraseStep = () => { let formData = new FormData(event.currentTarget); let username = formData.get("username") as string; let passphrase = formData.get("passphrase") as string; - let alias = formData.get("alias") as string; - - await auth.create(username, alias, passphrase, true); + await auth.init(username, passphrase); setSubmitting(false); - await navigate("/admin/chain"); + await navigate("/admin/nodes"); } return ( @@ -31,7 +29,7 @@ const UsernamePassphraseStep = () => { sensei logo

- Setup your node + Setup Admin Account

@@ -55,24 +53,6 @@ const UsernamePassphraseStep = () => { /> -
- -
- -
-