mirror of https://github.com/lldap/lldap.git
Compare commits
5 Commits
fee6580d2c
...
47b4ae8207
Author | SHA1 | Date |
---|---|---|
Austin Alvarado | 47b4ae8207 | |
RobertL | 254a168e78 | |
Austin Alvarado | 5b817980a9 | |
Austin Alvarado | 66097f1880 | |
Austin Alvarado | adf3577f0e |
|
@ -37,12 +37,16 @@ version = "0.3"
|
||||||
features = [
|
features = [
|
||||||
"Document",
|
"Document",
|
||||||
"Element",
|
"Element",
|
||||||
|
"Event",
|
||||||
"FileReader",
|
"FileReader",
|
||||||
|
"FormData",
|
||||||
"HtmlDocument",
|
"HtmlDocument",
|
||||||
|
"HtmlFormElement",
|
||||||
"HtmlInputElement",
|
"HtmlInputElement",
|
||||||
"HtmlOptionElement",
|
"HtmlOptionElement",
|
||||||
"HtmlOptionsCollection",
|
"HtmlOptionsCollection",
|
||||||
"HtmlSelectElement",
|
"HtmlSelectElement",
|
||||||
|
"SubmitEvent",
|
||||||
"console",
|
"console",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -12,5 +12,17 @@ query GetUserDetails($id: String!) {
|
||||||
id
|
id
|
||||||
displayName
|
displayName
|
||||||
}
|
}
|
||||||
|
attributes {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
schema {
|
||||||
|
name
|
||||||
|
attributeType
|
||||||
|
isList
|
||||||
|
isVisible
|
||||||
|
isEditable
|
||||||
|
isHardcoded
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
use crate::infra::schema::AttributeType;
|
||||||
|
use yew::{
|
||||||
|
function_component, html, virtual_dom::AttrValue, Callback, InputEvent, NodeRef, Properties,
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
<input
|
||||||
|
ref={&ctx.props().input_ref}
|
||||||
|
type="text"
|
||||||
|
class="input-component"
|
||||||
|
placeholder={placeholder}
|
||||||
|
onmouseover={ctx.link().callback(|_| Msg::Hover)}
|
||||||
|
/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
struct AttributeInputProps {
|
||||||
|
name: AttrValue,
|
||||||
|
attribute_type: AttributeType,
|
||||||
|
#[prop_or(None)]
|
||||||
|
value: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(AttributeInput)]
|
||||||
|
fn attribute_input(props: &AttributeInputProps) -> Html {
|
||||||
|
let input_type = match props.attribute_type {
|
||||||
|
AttributeType::String => "text",
|
||||||
|
AttributeType::Integer => "number",
|
||||||
|
AttributeType::DateTime => "datetime-local",
|
||||||
|
AttributeType::Jpeg => "file",
|
||||||
|
};
|
||||||
|
let accept = match props.attribute_type {
|
||||||
|
AttributeType::Jpeg => Some("image/jpeg"),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
<input
|
||||||
|
type={input_type}
|
||||||
|
accept={accept}
|
||||||
|
name={props.name.clone()}
|
||||||
|
class="form-control"
|
||||||
|
value={props.value.clone()} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct SingleAttributeInputProps {
|
||||||
|
pub name: String,
|
||||||
|
pub attribute_type: AttributeType,
|
||||||
|
#[prop_or(None)]
|
||||||
|
pub value: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(SingleAttributeInput)]
|
||||||
|
pub fn single_attribute_input(props: &SingleAttributeInputProps) -> Html {
|
||||||
|
html! {
|
||||||
|
<div class="row mb-3">
|
||||||
|
<label for={props.name.clone()}
|
||||||
|
class="form-label col-4 col-form-label">
|
||||||
|
{&props.name}{":"}
|
||||||
|
</label>
|
||||||
|
<div class="col-8">
|
||||||
|
<AttributeInput
|
||||||
|
attribute_type={props.attribute_type.clone()}
|
||||||
|
name={props.name.clone()}
|
||||||
|
value={props.value.clone()} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod attribute_input;
|
||||||
pub mod checkbox;
|
pub mod checkbox;
|
||||||
pub mod field;
|
pub mod field;
|
||||||
pub mod select;
|
pub mod select;
|
||||||
|
|
|
@ -4,8 +4,8 @@ use crate::{
|
||||||
remove_user_from_group::RemoveUserFromGroupComponent,
|
remove_user_from_group::RemoveUserFromGroupComponent,
|
||||||
router::{AppRoute, Link},
|
router::{AppRoute, Link},
|
||||||
user_details_form::UserDetailsForm,
|
user_details_form::UserDetailsForm,
|
||||||
},
|
}, infra::{schema::AttributeType, common_component::{CommonComponent, CommonComponentParts}},
|
||||||
infra::common_component::{CommonComponent, CommonComponentParts},
|
convert_attribute_type
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Error, Result};
|
use anyhow::{bail, Error, Result};
|
||||||
use graphql_client::GraphQLQuery;
|
use graphql_client::GraphQLQuery;
|
||||||
|
@ -22,6 +22,10 @@ pub struct GetUserDetails;
|
||||||
|
|
||||||
pub type User = get_user_details::GetUserDetailsUser;
|
pub type User = get_user_details::GetUserDetailsUser;
|
||||||
pub type Group = get_user_details::GetUserDetailsUserGroups;
|
pub type Group = get_user_details::GetUserDetailsUserGroups;
|
||||||
|
pub type Attribute = get_user_details::GetUserDetailsUserAttributes;
|
||||||
|
pub type AttributeSchema = get_user_details::GetUserDetailsUserAttributesSchema;
|
||||||
|
|
||||||
|
convert_attribute_type!(get_user_details::AttributeType);
|
||||||
|
|
||||||
pub struct UserDetails {
|
pub struct UserDetails {
|
||||||
common: CommonComponentParts<Self>,
|
common: CommonComponentParts<Self>,
|
||||||
|
|
|
@ -2,22 +2,25 @@ use std::str::FromStr;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{
|
components::{
|
||||||
form::{field::Field, static_value::StaticValue, submit::Submit},
|
form::{attribute_input::SingleAttributeInput, field::Field, static_value::StaticValue, submit::Submit},
|
||||||
user_details::User,
|
user_details::{AttributeSchema, User},
|
||||||
},
|
}, convert_attribute_type, infra::{common_component::{CommonComponent, CommonComponentParts}, schema::AttributeType}
|
||||||
infra::common_component::{CommonComponent, CommonComponentParts},
|
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Error, Result};
|
use anyhow::{anyhow, bail, Error, Ok, Result};
|
||||||
|
use gloo_console::log;
|
||||||
use gloo_file::{
|
use gloo_file::{
|
||||||
callbacks::{read_as_bytes, FileReader},
|
callbacks::{read_as_bytes, FileReader},
|
||||||
File,
|
File,
|
||||||
};
|
};
|
||||||
use graphql_client::GraphQLQuery;
|
use graphql_client::GraphQLQuery;
|
||||||
|
use validator::HasLen;
|
||||||
use validator_derive::Validate;
|
use validator_derive::Validate;
|
||||||
use web_sys::{FileList, HtmlInputElement, InputEvent};
|
use web_sys::{FileList, FormData, HtmlFormElement, HtmlInputElement, InputEvent};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_form_derive::Model;
|
use yew_form_derive::Model;
|
||||||
|
|
||||||
|
use super::user_details::Attribute;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct JsFile {
|
struct JsFile {
|
||||||
file: Option<File>,
|
file: Option<File>,
|
||||||
|
@ -73,6 +76,7 @@ pub struct UserDetailsForm {
|
||||||
/// True if we just successfully updated the user, to display a success message.
|
/// True if we just successfully updated the user, to display a success message.
|
||||||
just_updated: bool,
|
just_updated: bool,
|
||||||
user: User,
|
user: User,
|
||||||
|
form_ref: NodeRef,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
|
@ -150,7 +154,14 @@ impl CommonComponent<UserDetailsForm> for UserDetailsForm {
|
||||||
}
|
}
|
||||||
self.reader = None;
|
self.reader = None;
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
} // Msg::OnSubmit(e) => {
|
||||||
|
// e.prevent_default();
|
||||||
|
// let form: HtmlFormElement = e.target_unchecked_into();
|
||||||
|
// let data = FormData::new_with_form(&form).unwrap();
|
||||||
|
// log!(format!("form data{:#?}", data));
|
||||||
|
// log!(format!("form data data{:#?}", *data));
|
||||||
|
// Ok(true)
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,6 +188,7 @@ impl Component for UserDetailsForm {
|
||||||
just_updated: false,
|
just_updated: false,
|
||||||
reader: None,
|
reader: None,
|
||||||
user: ctx.props().user.clone(),
|
user: ctx.props().user.clone(),
|
||||||
|
form_ref: NodeRef::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,10 +287,11 @@ impl Component for UserDetailsForm {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{self.user.attributes.iter().map(get_custom_attribute_input).collect::<Vec<_>>()}
|
||||||
<Submit
|
<Submit
|
||||||
text="Save changes"
|
text="Save changes"
|
||||||
disabled={self.common.is_task_running()}
|
disabled={self.common.is_task_running()}
|
||||||
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitClicked})} />
|
onclick={link.callback(|e: MouseEvent| {Msg::SubmitClicked})} />
|
||||||
</form>
|
</form>
|
||||||
{
|
{
|
||||||
if let Some(e) = &self.common.error {
|
if let Some(e) = &self.common.error {
|
||||||
|
@ -297,6 +310,45 @@ impl Component for UserDetailsForm {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AttributeValue = (String, Vec<String>);
|
||||||
|
|
||||||
|
fn get_values_from_form_data(
|
||||||
|
schema: Vec<AttributeSchema>,
|
||||||
|
form: &FormData,
|
||||||
|
) -> Result<Vec<AttributeValue>> {
|
||||||
|
schema
|
||||||
|
.into_iter()
|
||||||
|
.map(|attr| -> Result<AttributeValue> {
|
||||||
|
let val = form
|
||||||
|
.get_all(attr.name.as_str())
|
||||||
|
.iter()
|
||||||
|
.map(|js_val| js_val.as_string().unwrap())
|
||||||
|
.filter(|val| !val.is_empty())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
if val.length() > 1 && !attr.is_list {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Multiple values supplied for non-list attribute {}",
|
||||||
|
attr.name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok((attr.name.clone(), val))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_custom_attribute_input(attribute: &Attribute) -> Html {
|
||||||
|
if attribute.schema.is_list {
|
||||||
|
html!{<p>{"list attr"}</p>}
|
||||||
|
} else {
|
||||||
|
let value = if attribute.value.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(attribute.value[0].clone())
|
||||||
|
};
|
||||||
|
html!{<SingleAttributeInput name={attribute.name.clone()} attribute_type={Into::<AttributeType>::into(attribute.schema.attribute_type.clone())} value={value}/>}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl UserDetailsForm {
|
impl UserDetailsForm {
|
||||||
fn submit_user_update_form(&mut self, ctx: &Context<Self>) -> Result<bool> {
|
fn submit_user_update_form(&mut self, ctx: &Context<Self>) -> Result<bool> {
|
||||||
if !self.form.validate() {
|
if !self.form.validate() {
|
||||||
|
@ -309,7 +361,40 @@ impl UserDetailsForm {
|
||||||
{
|
{
|
||||||
bail!("Image file hasn't finished loading, try again");
|
bail!("Image file hasn't finished loading, try again");
|
||||||
}
|
}
|
||||||
|
let form = self.form_ref.cast::<HtmlFormElement>().unwrap();
|
||||||
|
let form_data = FormData::new_with_form(&form)
|
||||||
|
.map_err(|e| anyhow!("Failed to get FormData: {:#?}", e.as_string()))?;
|
||||||
|
let mut all_values = get_values_from_form_data(
|
||||||
|
self.user
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
|
.map(|attr| attr.schema.clone())
|
||||||
|
.filter(|attr| !attr.is_hardcoded)
|
||||||
|
.filter(|attr| attr.is_editable)
|
||||||
|
.collect(),
|
||||||
|
&form_data,
|
||||||
|
)?;
|
||||||
let base_user = &self.user;
|
let base_user = &self.user;
|
||||||
|
let base_attrs = &self.user.attributes;
|
||||||
|
all_values.retain(|(name, val)| {
|
||||||
|
let name = name.clone();
|
||||||
|
let base_val = base_attrs
|
||||||
|
.into_iter()
|
||||||
|
.find(|base_val| base_val.name == name)
|
||||||
|
.unwrap();
|
||||||
|
let new_values = val.clone();
|
||||||
|
base_val.value != new_values
|
||||||
|
});
|
||||||
|
let remove_names: Option<Vec<String>> = if all_values.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(all_values.iter().map(|(name, _)| name.clone()).collect())
|
||||||
|
};
|
||||||
|
let insert_attrs: Option<Vec<update_user::AttributeValueInput>> = if remove_names.is_none() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(all_values.into_iter().map(|(name, value)| update_user::AttributeValueInput{name, value}).collect())
|
||||||
|
};
|
||||||
let mut user_input = update_user::UpdateUserInput {
|
let mut user_input = update_user::UpdateUserInput {
|
||||||
id: self.user.id.clone(),
|
id: self.user.id.clone(),
|
||||||
email: None,
|
email: None,
|
||||||
|
@ -317,8 +402,8 @@ impl UserDetailsForm {
|
||||||
firstName: None,
|
firstName: None,
|
||||||
lastName: None,
|
lastName: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
removeAttributes: None,
|
removeAttributes: remove_names,
|
||||||
insertAttributes: None,
|
insertAttributes: insert_attrs,
|
||||||
};
|
};
|
||||||
let default_user_input = user_input.clone();
|
let default_user_input = user_input.clone();
|
||||||
let model = self.form.model();
|
let model = self.form.model();
|
||||||
|
|
|
@ -2,7 +2,7 @@ use anyhow::Result;
|
||||||
use std::{fmt::Display, str::FromStr};
|
use std::{fmt::Display, str::FromStr};
|
||||||
use validator::ValidationError;
|
use validator::ValidationError;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum AttributeType {
|
pub enum AttributeType {
|
||||||
String,
|
String,
|
||||||
Integer,
|
Integer,
|
||||||
|
|
|
@ -54,7 +54,7 @@ services:
|
||||||
- ENABLE_OPENDMARC=0
|
- ENABLE_OPENDMARC=0
|
||||||
# >>> Postfix LDAP Integration
|
# >>> Postfix LDAP Integration
|
||||||
- ACCOUNT_PROVISIONER=LDAP
|
- ACCOUNT_PROVISIONER=LDAP
|
||||||
- LDAP_SERVER_HOST=lldap:3890
|
- LDAP_SERVER_HOST=ldap://lldap:3890
|
||||||
- LDAP_SEARCH_BASE=ou=people,dc=example,dc=com
|
- LDAP_SEARCH_BASE=ou=people,dc=example,dc=com
|
||||||
- LDAP_BIND_DN=uid=admin,ou=people,dc=example,dc=com
|
- LDAP_BIND_DN=uid=admin,ou=people,dc=example,dc=com
|
||||||
- LDAP_BIND_PW=adminpassword
|
- LDAP_BIND_PW=adminpassword
|
||||||
|
|
Loading…
Reference in New Issue