diff --git a/rust/agama-lib/src/proxies.rs b/rust/agama-lib/src/proxies.rs index 5a64753915..d33efa6921 100644 --- a/rust/agama-lib/src/proxies.rs +++ b/rust/agama-lib/src/proxies.rs @@ -178,3 +178,14 @@ trait Issues { #[dbus_proxy(property)] fn all(&self) -> zbus::Result>; } + +#[dbus_proxy(interface = "org.opensuse.Agama1.Validation", assume_defaults = true)] +trait Validation { + /// Errors property + #[dbus_proxy(property)] + fn errors(&self) -> zbus::Result>; + + /// Valid property + #[dbus_proxy(property)] + fn valid(&self) -> zbus::Result; +} diff --git a/rust/agama-lib/src/users.rs b/rust/agama-lib/src/users.rs index a7dc878639..9ee6a72b5a 100644 --- a/rust/agama-lib/src/users.rs +++ b/rust/agama-lib/src/users.rs @@ -1,7 +1,7 @@ //! Implements support for handling the users settings mod client; -mod proxies; +pub mod proxies; mod settings; mod store; diff --git a/rust/agama-lib/src/users/client.rs b/rust/agama-lib/src/users/client.rs index 437ff3f21d..979b788e3c 100644 --- a/rust/agama-lib/src/users/client.rs +++ b/rust/agama-lib/src/users/client.rs @@ -3,11 +3,12 @@ use super::proxies::{FirstUser as FirstUserFromDBus, Users1Proxy}; use crate::error::ServiceError; use agama_settings::{settings::Settings, SettingValue, SettingsError}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use zbus::Connection; /// Represents the settings for the first user -#[derive(Serialize, Debug, Default)] +#[derive(Serialize, Deserialize, Clone, Debug, Default, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct FirstUser { /// First user's full name pub full_name: String, @@ -66,6 +67,7 @@ impl Settings for FirstUser { } /// D-Bus client for the users service +#[derive(Clone)] pub struct UsersClient<'a> { users_proxy: Users1Proxy<'a>, } @@ -91,6 +93,10 @@ impl<'a> UsersClient<'a> { Ok(self.users_proxy.set_root_password(value, encrypted).await?) } + pub async fn remove_root_password(&self) -> Result { + Ok(self.users_proxy.remove_root_password().await?) + } + /// Whether the root password is set or not pub async fn is_root_password(&self) -> Result { Ok(self.users_proxy.root_password_set().await?) @@ -121,4 +127,8 @@ impl<'a> UsersClient<'a> { ) .await } + + pub async fn remove_first_user(&self) -> zbus::Result { + Ok(self.users_proxy.remove_first_user().await? == 0) + } } diff --git a/rust/agama-lib/src/users/settings.rs b/rust/agama-lib/src/users/settings.rs index 39b31c1484..afc75ec317 100644 --- a/rust/agama-lib/src/users/settings.rs +++ b/rust/agama-lib/src/users/settings.rs @@ -17,7 +17,7 @@ pub struct UserSettings { /// First user settings /// /// Holds the settings for the first user. -#[derive(Debug, Default, Settings, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Settings, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FirstUserSettings { /// First user's full name diff --git a/rust/agama-server/src/lib.rs b/rust/agama-server/src/lib.rs index d73b21f9c9..8c2602f7b9 100644 --- a/rust/agama-server/src/lib.rs +++ b/rust/agama-server/src/lib.rs @@ -5,5 +5,6 @@ pub mod manager; pub mod network; pub mod questions; pub mod software; +pub mod users; pub mod web; pub use web::service; diff --git a/rust/agama-server/src/users.rs b/rust/agama-server/src/users.rs new file mode 100644 index 0000000000..76ddbc68ee --- /dev/null +++ b/rust/agama-server/src/users.rs @@ -0,0 +1,2 @@ +pub mod web; +pub use web::{users_service, users_streams}; diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs new file mode 100644 index 0000000000..161d9a2bd5 --- /dev/null +++ b/rust/agama-server/src/users/web.rs @@ -0,0 +1,229 @@ +//! +//! The module offers two public functions: +//! +//! * `users_service` which returns the Axum service. +//! * `users_stream` which offers an stream that emits the users events coming from D-Bus. + +use crate::{ + error::Error, + web::{ + common::{service_status_router, validation_router}, + Event, + }, +}; +use agama_lib::{ + error::ServiceError, + users::{proxies::Users1Proxy, FirstUser, UsersClient}, +}; +use axum::{extract::State, routing::get, Json, Router}; +use serde::{Deserialize, Serialize}; +use std::pin::Pin; +use tokio_stream::{Stream, StreamExt}; + +#[derive(Clone)] +struct UsersState<'a> { + users: UsersClient<'a>, +} + +/// Returns streams that emits users related events coming from D-Bus. +/// +/// It emits the Event::RootPasswordChange, Event::RootSSHKeyChanged and Event::FirstUserChanged events. +/// +/// * `connection`: D-Bus connection to listen for events. +pub async fn users_streams( + dbus: zbus::Connection, +) -> Result + Send>>)>, Error> { + const FIRST_USER_ID: &str = "first_user"; + const ROOT_PASSWORD_ID: &str = "root_password"; + const ROOT_SSHKEY_ID: &str = "root_sshkey"; + // here we have three streams, but only two events. Reason is + // that we have three streams from dbus about property change + // and unify two root user properties into single event to http API + let result: Vec<(&str, Pin + Send>>)> = vec![ + ( + FIRST_USER_ID, + Box::pin(first_user_changed_stream(dbus.clone()).await?), + ), + ( + ROOT_PASSWORD_ID, + Box::pin(root_password_changed_stream(dbus.clone()).await?), + ), + ( + ROOT_SSHKEY_ID, + Box::pin(root_ssh_key_changed_stream(dbus.clone()).await?), + ), + ]; + + Ok(result) +} + +async fn first_user_changed_stream( + dbus: zbus::Connection, +) -> Result + Send, Error> { + let proxy = Users1Proxy::new(&dbus).await?; + let stream = proxy + .receive_first_user_changed() + .await + .then(|change| async move { + if let Ok(user) = change.get().await { + let user_struct = FirstUser { + full_name: user.0, + user_name: user.1, + password: user.2, + autologin: user.3, + data: user.4, + }; + return Some(Event::FirstUserChanged(user_struct)); + } + None + }) + .filter_map(|e| e); + Ok(stream) +} + +async fn root_password_changed_stream( + dbus: zbus::Connection, +) -> Result + Send, Error> { + let proxy = Users1Proxy::new(&dbus).await?; + let stream = proxy + .receive_root_password_set_changed() + .await + .then(|change| async move { + if let Ok(is_set) = change.get().await { + return Some(Event::RootChanged { + password: Some(is_set), + sshkey: None, + }); + } + None + }) + .filter_map(|e| e); + Ok(stream) +} + +async fn root_ssh_key_changed_stream( + dbus: zbus::Connection, +) -> Result + Send, Error> { + let proxy = Users1Proxy::new(&dbus).await?; + let stream = proxy + .receive_root_sshkey_changed() + .await + .then(|change| async move { + if let Ok(key) = change.get().await { + return Some(Event::RootChanged { + password: None, + sshkey: Some(key), + }); + } + None + }) + .filter_map(|e| e); + Ok(stream) +} + +/// Sets up and returns the axum service for the users module. +pub async fn users_service(dbus: zbus::Connection) -> Result { + const DBUS_SERVICE: &str = "org.opensuse.Agama.Manager1"; + const DBUS_PATH: &str = "/org/opensuse/Agama/Users1"; + + let users = UsersClient::new(dbus.clone()).await?; + let state = UsersState { users }; + let validation_router = validation_router(&dbus, DBUS_SERVICE, DBUS_PATH).await?; + let status_router = service_status_router(&dbus, DBUS_SERVICE, DBUS_PATH).await?; + let router = Router::new() + .route( + "/first", + get(get_user_config) + .put(set_first_user) + .delete(remove_first_user), + ) + .route("/root", get(get_root_config).patch(patch_root)) + .merge(validation_router) + .merge(status_router) + .with_state(state); + Ok(router) +} + +/// Removes the first user settings +#[utoipa::path(delete, path = "/users/first", responses( + (status = 200, description = "Removes the first user"), + (status = 400, description = "The D-Bus service could not perform the action"), +))] +async fn remove_first_user(State(state): State>) -> Result<(), Error> { + state.users.remove_first_user().await?; + Ok(()) +} + +#[utoipa::path(put, path = "/users/first", responses( + (status = 200, description = "Sets the first user"), + (status = 400, description = "The D-Bus service could not perform the action"), +))] +async fn set_first_user( + State(state): State>, + Json(config): Json, +) -> Result<(), Error> { + state.users.set_first_user(&config).await?; + Ok(()) +} + +#[utoipa::path(get, path = "/users/first", responses( + (status = 200, description = "Configuration for the first user", body = FirstUser), + (status = 400, description = "The D-Bus service could not perform the action"), +))] +async fn get_user_config(State(state): State>) -> Result, Error> { + Ok(Json(state.users.first_user().await?)) +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct RootPatchSettings { + /// empty string here means remove ssh key for root + pub sshkey: Option, + /// empty string here means remove password for root + pub password: Option, + /// specify if patched password is provided in encrypted form + pub password_encrypted: Option, +} + +#[utoipa::path(patch, path = "/users/root", responses( + (status = 200, description = "Root configuration is modified", body = RootPatchSettings), + (status = 400, description = "The D-Bus service could not perform the action"), +))] +async fn patch_root( + State(state): State>, + Json(config): Json, +) -> Result<(), Error> { + if let Some(key) = config.sshkey { + state.users.set_root_sshkey(&key).await?; + } + if let Some(password) = config.password { + if password.is_empty() { + state.users.remove_root_password().await?; + } else { + state + .users + .set_root_password(&password, config.password_encrypted == Some(true)) + .await?; + } + } + Ok(()) +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RootConfig { + /// returns if password for root is set or not + password: bool, + /// empty string mean no sshkey is specified + sshkey: String, +} + +#[utoipa::path(get, path = "/users/root", responses( + (status = 200, description = "Configuration for the root user", body = RootConfig), + (status = 400, description = "The D-Bus service could not perform the action"), +))] +async fn get_root_config(State(state): State>) -> Result, Error> { + let password = state.users.is_root_password().await?; + let sshkey = state.users.root_ssh_key().await?; + let config = RootConfig { password, sshkey }; + Ok(Json(config)) +} diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index e52abc7efc..491716aca2 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -11,6 +11,7 @@ use crate::{ network::{web::network_service, NetworkManagerAdapter}, questions::web::{questions_service, questions_stream}, software::web::{software_service, software_stream}, + users::web::{users_service, users_streams}, web::common::{issues_stream, progress_stream, service_status_stream}, }; use axum::Router; @@ -61,7 +62,8 @@ where "/network", network_service(dbus.clone(), network_adapter).await?, ) - .add_service("/questions", questions_service(dbus).await?) + .add_service("/questions", questions_service(dbus.clone()).await?) + .add_service("/users", users_service(dbus.clone()).await?) .with_config(config) .build(); Ok(router) @@ -105,7 +107,9 @@ async fn run_events_monitor(dbus: zbus::Connection, events: EventsSender) -> Res ) .await?, ); - + for (id, user_stream) in users_streams(dbus.clone()).await? { + stream.insert(id, user_stream); + } stream.insert("software", software_stream(dbus.clone()).await?); stream.insert( "software-status", diff --git a/rust/agama-server/src/web/common.rs b/rust/agama-server/src/web/common.rs index 891a292f31..0c50cd7e95 100644 --- a/rust/agama-server/src/web/common.rs +++ b/rust/agama-server/src/web/common.rs @@ -5,7 +5,7 @@ use std::{pin::Pin, task::Poll}; use agama_lib::{ error::ServiceError, progress::Progress, - proxies::{IssuesProxy, ProgressProxy, ServiceStatusProxy}, + proxies::{IssuesProxy, ProgressProxy, ServiceStatusProxy, ValidationProxy}, }; use axum::{extract::State, routing::get, Json, Router}; use pin_project::pin_project; @@ -352,3 +352,108 @@ async fn build_issues_proxy<'a>( .await?; Ok(proxy) } + +/// Builds a router to the `org.opensuse.Agama1.Validation` interface of a given +/// D-Bus object. +/// +/// ```no_run +/// # use axum::{extract::State, routing::get, Json, Router}; +/// # use agama_lib::connection; +/// # use agama_server::web::common::validation_router; +/// # use tokio_test; +/// +/// # tokio_test::block_on(async { +/// async fn hello(state: State) {}; +/// +/// #[derive(Clone)] +/// struct HelloWorldState {}; +/// +/// let dbus = connection().await.unwrap(); +/// let issues_router = validation_router( +/// &dbus, "org.opensuse.HelloWorld", "/org/opensuse/hello" +/// ).await.unwrap(); +/// let router: Router = Router::new() +/// .route("/hello", get(hello)) +/// .merge(validation_router) +/// .with_state(HelloWorldState {}); +/// }); +/// ``` +/// +/// * `dbus`: D-Bus connection. +/// * `destination`: D-Bus service name. +/// * `path`: D-Bus object path. +pub async fn validation_router( + dbus: &zbus::Connection, + destination: &str, + path: &str, +) -> Result, ServiceError> { + let proxy = build_validation_proxy(dbus, destination, path).await?; + let state = ValidationState { proxy }; + Ok(Router::new() + .route("/validation", get(validation)) + .with_state(state)) +} + +#[derive(Clone, Serialize, utoipa::ToSchema)] +pub struct ValidationResult { + valid: bool, + errors: Vec, +} + +async fn validation( + State(state): State>, +) -> Result, Error> { + let validation = ValidationResult { + valid: state.proxy.valid().await?, + errors: state.proxy.errors().await?, + }; + Ok(Json(validation)) +} + +#[derive(Clone)] +struct ValidationState<'a> { + proxy: ValidationProxy<'a>, +} + +/// Builds a stream of the changes in the the `org.opensuse.Agama1.Validation` +/// interface of the given D-Bus object. +/// +/// * `dbus`: D-Bus connection. +/// * `destination`: D-Bus service name. +/// * `path`: D-Bus object path. +pub async fn validation_stream( + dbus: zbus::Connection, + destination: &'static str, + path: &'static str, +) -> Result + Send>>, Error> { + let proxy = build_validation_proxy(&dbus, destination, path).await?; + let stream = proxy + .receive_errors_changed() + .await + .then(move |change| async move { + if let Ok(errors) = change.get().await { + Some(Event::ValidationChanged { + service: destination.to_string(), + path: path.to_string(), + errors, + }) + } else { + None + } + }) + .filter_map(|e| e); + Ok(Box::pin(stream)) +} + +async fn build_validation_proxy<'a>( + dbus: &zbus::Connection, + destination: &str, + path: &str, +) -> Result, zbus::Error> { + let proxy = ValidationProxy::builder(dbus) + .destination(destination.to_string())? + .path(path.to_string())? + .build() + .await?; + Ok(proxy) +} diff --git a/rust/agama-server/src/web/docs.rs b/rust/agama-server/src/web/docs.rs index f61f9a23bf..8d625b0c7e 100644 --- a/rust/agama-server/src/web/docs.rs +++ b/rust/agama-server/src/web/docs.rs @@ -12,7 +12,6 @@ use utoipa::OpenApi; crate::network::web::connections, crate::software::web::get_config, crate::software::web::patterns, - crate::software::web::patterns, crate::software::web::set_config, crate::manager::web::probe_action, crate::manager::web::install_action, @@ -20,6 +19,11 @@ use utoipa::OpenApi; crate::manager::web::installer_status, crate::questions::web::list_questions, crate::questions::web::answer, + crate::users::web::get_root_config, + crate::users::web::get_user_config, + crate::users::web::set_first_user, + crate::users::web::remove_first_user, + crate::users::web::patch_root, super::http::ping, ), components( @@ -43,6 +47,9 @@ use utoipa::OpenApi; schemas(crate::questions::web::Answer), schemas(crate::questions::web::GenericAnswer), schemas(crate::questions::web::PasswordAnswer), + schemas(agama_lib::users::FirstUser), + schemas(crate::users::web::RootConfig), + schemas(crate::users::web::RootPatchSettings), schemas(super::http::PingResponse), ) )] diff --git a/rust/agama-server/src/web/event.rs b/rust/agama-server/src/web/event.rs index 848f10cae1..3f23090659 100644 --- a/rust/agama-server/src/web/event.rs +++ b/rust/agama-server/src/web/event.rs @@ -1,5 +1,7 @@ use crate::l10n::web::LocaleConfig; -use agama_lib::{manager::InstallationPhase, progress::Progress, software::SelectedBy}; +use agama_lib::{ + manager::InstallationPhase, progress::Progress, software::SelectedBy, users::FirstUser, +}; use serde::Serialize; use std::collections::HashMap; use tokio::sync::broadcast::{Receiver, Sender}; @@ -21,6 +23,11 @@ pub enum Event { ProductChanged { id: String, }, + FirstUserChanged(FirstUser), + RootChanged { + password: Option, + sshkey: Option, + }, // TODO: it should include the full software proposal or, at least, // all the relevant changes. SoftwareProposalChanged { @@ -42,6 +49,11 @@ pub enum Event { path: String, issues: Vec, }, + ValidationChanged { + service: String, + path: String, + errors: Vec, + }, } pub type EventsSender = Sender; diff --git a/web/README.md b/web/README.md index e903923adc..746d1012e6 100644 --- a/web/README.md +++ b/web/README.md @@ -35,6 +35,26 @@ running Agama server instance. This is especially useful if you use the Live ISO which does not contain any development tools, you can develop the web frontend easily from your workstation. +Example of running from different machine: + +``` + # backend machine + # using ip of machine instead of localhost is important to be network accessible + agama-web-server serve --address 10.100.1.1:3000 + + # frontend machine + # ESLINT=0 is useful to ignore linter problems during development + ESLINT=0 AGAMA_SERVER=10.100.1.1:3000 npm run server +``` + +### Debugging Hints + +There are several places to look when something does not work and requires debugging. +The first place is the browser's console which can give +some hints. The second location to check for errors or warnings is output of `npm run server` +where you can find issues when communicating with the backend. And last but on least is +journal on backend machine where is logged backend activity `journalctl -b`. + ### Special Environment Variables `AGAMA_SERVER` - When running the development server set up a proxy to diff --git a/web/src/client/http.js b/web/src/client/http.js index 3a78066236..844c6545a1 100644 --- a/web/src/client/http.js +++ b/web/src/client/http.js @@ -142,27 +142,36 @@ class HTTPClient { }, }); - try { - return await response.json(); - } catch (e) { - console.warn("Expecting a JSON response", e); - return response.status === 200; - } + return response; + } + + /** + * @param {string} url - Endpoint URL (e.g., "/l10n/config"). + * @return {Promise} Server response. + */ + async delete(url) { + const response = await fetch(`${this.baseUrl}/${url}`, { + method: "DELETE", + }); + + return response; } /** * @param {string} url - Endpoint URL (e.g., "/l10n/config"). * @param {object} data - Data to submit - * @return {Promise} Server response. + * @return {Promise} Server response. */ async patch(url, data) { - await fetch(`${this.baseUrl}/${url}`, { + const response = await fetch(`${this.baseUrl}/${url}`, { method: "PATCH", body: JSON.stringify(data), headers: { "Content-Type": "application/json", }, }); + + return response; } /** diff --git a/web/src/client/index.js b/web/src/client/index.js index 63b6e2b422..d86c0037e8 100644 --- a/web/src/client/index.js +++ b/web/src/client/index.js @@ -80,7 +80,7 @@ const createClient = (url) => { // const network = new NetworkClient(address); const software = new SoftwareClient(client); // const storage = new StorageClient(address); - // const users = new UsersClient(address); + const users = new UsersClient(client); // const questions = new QuestionsClient(address); /** @@ -141,7 +141,7 @@ const createClient = (url) => { // network, software, // storage, - // users, + users, // questions, // issues, onIssuesChange, diff --git a/web/src/client/mixins.js b/web/src/client/mixins.js index 927610784d..6e4fb3d851 100644 --- a/web/src/client/mixins.js +++ b/web/src/client/mixins.js @@ -21,11 +21,6 @@ // @ts-check -const ISSUES_IFACE = "org.opensuse.Agama1.Issues"; -const STATUS_IFACE = "org.opensuse.Agama1.ServiceStatus"; -const PROGRESS_IFACE = "org.opensuse.Agama1.Progress"; -const VALIDATION_IFACE = "org.opensuse.Agama1.Validation"; - /** * @typedef {new(...args: any[]) => T} GConstructor * @template {object} T @@ -245,26 +240,27 @@ const createError = (message) => { /** * Extends the given class with methods to get validation errors over D-Bus - * @param {string} object_path - object_path + * @template {!WithHTTPClient} T * @param {T} superclass - superclass to extend - * @template {!WithDBusClient} T + * @param {string} validation_path - status resource path (e.g., "/manager/status"). + * @param {string} service_name - service name (e.g., "org.opensuse.Agama.Manager1"). */ -const WithValidation = (superclass, object_path) => class extends superclass { +const WithValidation = (superclass, validation_path, service_name) => class extends superclass { /** * Returns the validation errors * * @return {Promise} */ async getValidationErrors() { - let errors; + let response; try { - errors = await this.client.getProperty(object_path, VALIDATION_IFACE, "Errors"); + response = await this.client.get(validation_path); } catch (error) { - console.error(`Could not get validation errors for ${object_path}`, error); + console.error(`Could not get validation errors for ${validation_path}`, error); } - return errors.map(createError); + return response.errors.map(createError); } /** @@ -274,8 +270,10 @@ const WithValidation = (superclass, object_path) => class extends superclass { * @return {import ("./dbus").RemoveFn} function to disable the callback */ onValidationChange(handler) { - return this.client.onObjectChanged(object_path, VALIDATION_IFACE, () => { - this.getValidationErrors().then(handler); + return this.client.onEvent("ValidationChange", ({ service, errors }) => { + if (service === service_name) { + handler(errors); + } }); } }; diff --git a/web/src/client/software.js b/web/src/client/software.js index eb4996b40d..e4f6989837 100644 --- a/web/src/client/software.js +++ b/web/src/client/software.js @@ -277,7 +277,7 @@ class SoftwareClient extends WithIssues( "/software/progress", SOFTWARE_SERVICE, ), - "software/issues/software", + "/software/issues/software", "/org/opensuse/Agama/Software1", ) {} @@ -334,6 +334,6 @@ class ProductBaseClient { } class ProductClient - extends WithIssues(ProductBaseClient, "software/issues/product", PRODUCT_PATH) {} + extends WithIssues(ProductBaseClient, "/software/issues/product", PRODUCT_PATH) {} export { ProductClient, SelectedBy, SoftwareClient }; diff --git a/web/src/client/users.js b/web/src/client/users.js index 2a2b46588e..ac570aad53 100644 --- a/web/src/client/users.js +++ b/web/src/client/users.js @@ -21,13 +21,8 @@ // @ts-check -import DBusClient from "./dbus"; import { WithValidation } from "./mixins"; -const USERS_SERVICE = "org.opensuse.Agama.Manager1"; -const USERS_IFACE = "org.opensuse.Agama.Users1"; -const USERS_PATH = "/org/opensuse/Agama/Users1"; - /** * @typedef {object} UserResult * @property {boolean} result - whether the action succeeded or not @@ -40,6 +35,7 @@ const USERS_PATH = "/org/opensuse/Agama/Users1"; * @property {string} userName - userName * @property {string} [password] - user password * @property {boolean} autologin - Whether autologin is enabled + * @property {object} data - additional user data */ /** @@ -56,10 +52,10 @@ const USERS_PATH = "/org/opensuse/Agama/Users1"; */ class UsersBaseClient { /** - * @param {string|undefined} address - D-Bus address; if it is undefined, it uses the system bus. + * @param {import("./http").HTTPClient} client - HTTP client. */ - constructor(address = undefined) { - this.client = new DBusClient(USERS_SERVICE, address); + constructor(client) { + this.client = client; } /** @@ -68,9 +64,13 @@ class UsersBaseClient { * @return {Promise} */ async getUser() { - const proxy = await this.client.proxy(USERS_IFACE); - const [fullName, userName, password, autologin] = proxy.FirstUser; - return { fullName, userName, password, autologin }; + const user = await this.client.get("/users/first"); + + if (user === null) { + return { fullName: "", userName: "", password: "", autologin: false, data: {} }; + } + + return user; } /** @@ -79,8 +79,8 @@ class UsersBaseClient { * @return {Promise} */ async isRootPasswordSet() { - const proxy = await this.client.proxy(USERS_IFACE); - return proxy.RootPasswordSet; + const proxy = await this.client.get("/users/root"); + return proxy.password; } /** @@ -90,16 +90,9 @@ class UsersBaseClient { * @return {Promise} returns an object with the result and the issues found if error */ async setUser(user) { - const proxy = await this.client.proxy(USERS_IFACE); - const [result, issues] = await proxy.SetFirstUser( - user.fullName, - user.userName, - user.password, - user.autologin, - {} - ); - - return { result, issues }; + const result = await this.client.put("/users/first", user); + + return { result: result.ok, issues: [] }; // TODO: check how to handle issues and result. Maybe separate call to validate? } /** @@ -108,9 +101,7 @@ class UsersBaseClient { * @return {Promise} whether the operation was successful or not */ async removeUser() { - const proxy = await this.client.proxy(USERS_IFACE); - const result = await proxy.RemoveFirstUser(); - return result === 0; + return (await this.client.delete("/users/first")).ok; } /** @@ -120,9 +111,8 @@ class UsersBaseClient { * @return {Promise} whether the operation was successful or not */ async setRootPassword(password) { - const proxy = await this.client.proxy(USERS_IFACE); - const result = await proxy.SetRootPassword(password, false); - return result === 0; + const response = await this.client.patch("/users/root", { password, password_encrypted: false }); + return response.ok; } /** @@ -131,9 +121,7 @@ class UsersBaseClient { * @return {Promise} whether the operation was successful or not */ async removeRootPassword() { - const proxy = await this.client.proxy(USERS_IFACE); - const result = await proxy.RemoveRootPassword(); - return result === 0; + return this.setRootPassword(""); } /** @@ -142,8 +130,8 @@ class UsersBaseClient { * @return {Promise} SSH public key or an empty string if it is not set */ async getRootSSHKey() { - const proxy = await this.client.proxy(USERS_IFACE); - return proxy.RootSSHKey; + const proxy = await this.client.get("/users/root"); + return proxy.sshkey; } /** @@ -153,9 +141,8 @@ class UsersBaseClient { * @return {Promise} whether the operation was successful or not */ async setRootSSHKey(key) { - const proxy = await this.client.proxy(USERS_IFACE); - const result = await proxy.SetRootSSHKey(key); - return result === 0; + const response = await this.client.patch("/users/root", { sshkey: key }); + return response.ok; } /** @@ -165,16 +152,21 @@ class UsersBaseClient { * @return {import ("./dbus").RemoveFn} function to disable the callback */ onUsersChange(handler) { - return this.client.onObjectChanged(USERS_PATH, USERS_IFACE, changes => { - if (changes.RootPasswordSet) { + return this.client.ws.onEvent((event) => { + if (event.type === "RootChanged") { + const res = {}; + if (event.password !== null) { + res.rootPasswordSet = event.password; + } + if (event.sshkey !== null) { + res.rootSSHKey = event.sshkey; + } // @ts-ignore - return handler({ rootPasswordSet: changes.RootPasswordSet.v }); - } else if (changes.RootSSHKey) { - return handler({ rootSSHKey: changes.RootSSHKey.v.toString() }); - } else if (changes.FirstUser) { + return handler(res); + } else if (event.type === "FirstUserChanged") { // @ts-ignore - const [fullName, userName, password, autologin] = changes.FirstUser.v; - return handler({ firstUser: { fullName, userName, password, autologin } }); + const { fullName, userName, password, autologin, data } = event; + return handler({ firstUser: { fullName, userName, password, autologin, data } }); } }); } @@ -183,6 +175,6 @@ class UsersBaseClient { /** * Client to interact with the Agama users service */ -class UsersClient extends WithValidation(UsersBaseClient, USERS_PATH) { } +class UsersClient extends WithValidation(UsersBaseClient, "/users/validation", "/org/opensuse/Agama/Users1") { } export { UsersClient }; diff --git a/web/src/components/overview/OverviewPage.jsx b/web/src/components/overview/OverviewPage.jsx index b51a6129d0..e41bd0fff4 100644 --- a/web/src/components/overview/OverviewPage.jsx +++ b/web/src/components/overview/OverviewPage.jsx @@ -49,7 +49,6 @@ export default function OverviewPage() { // > // // - // // // setShowErrors(true)} /> @@ -66,6 +65,7 @@ export default function OverviewPage() { + ); }