User Details view and some other enhancements for the Admin UI

Jens Pelzetter 2020-09-24 21:13:57 +02:00
parent fd6ea7e38b
commit f92a266edd
41 changed files with 1212 additions and 86 deletions

View File

@ -0,0 +1,35 @@
/*
* Copyright (C) 2020 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.api;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
public final class ApiConstants {
private ApiConstants() {
// Nothing
}
public static final String IDENTIFIER_PREFIX_ID = "ID-";
public static final String IDENTIFIER_PREFIX_UUID = "UUID-";
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (C) 2020 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.api;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
public class Identifier {
private final IdentifierType type;
private final String identifier;
protected Identifier(
final IdentifierType type, final String identifier
) {
this.type = type;
this.identifier = identifier;
}
public IdentifierType getType() {
return type;
}
public String getIdentifier() {
return identifier;
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (C) 2020 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.api;
import java.util.Objects;
import javax.enterprise.context.Dependent;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@Dependent
public class IdentifierParser {
public Identifier parseIdentifier(final String identifierParam) {
Objects.requireNonNull(identifierParam, "identifier param is null.");
if (identifierParam.startsWith(ApiConstants.IDENTIFIER_PREFIX_ID)) {
final String identifier = identifierParam
.substring(ApiConstants.IDENTIFIER_PREFIX_ID.length());
return new Identifier(IdentifierType.ID, identifier);
} else if (identifierParam.startsWith(
ApiConstants.IDENTIFIER_PREFIX_UUID)) {
final String identifier = identifierParam
.substring(ApiConstants.IDENTIFIER_PREFIX_UUID.length());
return new Identifier(IdentifierType.ID, identifier);
} else {
return new Identifier(
IdentifierType.PROPERTY, identifierParam
);
}
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2020 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.api;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
public enum IdentifierType {
ID,
UUID,
PROPERTY
}

View File

@ -18,8 +18,6 @@
*/
package org.libreccm.core;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotBlank;
import javax.persistence.Column;
import javax.persistence.Embeddable;
@ -33,6 +31,8 @@ import static org.libreccm.core.CoreConstants.CORE_XML_NS;
import javax.json.Json;
import javax.json.JsonObjectBuilder;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
/**
* An embeddable entity for storing email addresses.

View File

@ -0,0 +1,44 @@
/*
* Copyright (C) 2020 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.ui;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
public class Message {
private final String message;
private final MessageType messageType;
public Message(String message, MessageType messageType) {
this.message = message;
this.messageType = messageType;
}
public String getMessage() {
return message;
}
public MessageType getMessageType() {
return messageType;
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (C) 2020 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.ui;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
public enum MessageType {
PRIMARY,
SECONDARY,
SUCCESS,
DANGER,
WARNING,
INFO,
LIGHT,
DARK,
}

View File

@ -20,7 +20,9 @@ package org.libreccm.ui.admin;
import org.libreccm.l10n.GlobalizationHelper;
import java.text.MessageFormat;
import java.util.AbstractMap;
import java.util.List;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.stream.Collectors;
@ -43,8 +45,6 @@ public class AdminMessages extends AbstractMap<String, String> {
private ResourceBundle messages;
@PostConstruct
private void init() {
messages = ResourceBundle.getBundle(
@ -57,21 +57,37 @@ public class AdminMessages extends AbstractMap<String, String> {
if (messages.containsKey(key)) {
return messages.getString(key);
} else {
return "???key???";
return String.format("???%s???", key);
}
}
public String getMessage(
final String key, final List<Object> parameters
) {
return getMessage(key, parameters.toArray());
}
public String getMessage(
final String key, final Object[] parameters
) {
if (messages.containsKey(key)) {
return MessageFormat.format(messages.getString(key), parameters);
} else {
return String.format("???%s???", key);
}
}
public String get(final String key) {
return getMessage(key);
}
@Override
public Set<Entry<String, String>> entrySet() {
return messages
.keySet()
.stream()
.collect(
Collectors.toMap(key -> key, key-> messages.getString(key))
Collectors.toMap(key -> key, key -> messages.getString(key))
)
.entrySet();
}

View File

@ -18,9 +18,10 @@
*/
package org.libreccm.ui.admin;
import java.util.ResourceBundle;
import java.util.Set;
import javax.mvc.MvcContext;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
@ -35,14 +36,15 @@ public interface AdminPage {
Set<Class<?>> getControllerClasses();
/**
* The entry point for the admin module. Used to generate the URL for an
* admin page in the navigation etc. For example, with the return value will
* {@code systeminformation} the generated link is
* {@code #{contextPath}/@admin/#{path}}.
*
* @return The path fragment of the entry point of the admin page/module.
* A identifier to use by {@link MvcContext#uri(java.lang.String)} to
* generate the URI of the page. The identifier has the same format as used in JavaDoc:
* <pre>
* ControllerSimpleClassName#methodName
* </pre>
*
* @return The identifier to use for generating the URL of the page
*/
String getPath();
String getUriIdentifier();
/**
* Gets the resourcebundle which provides the label of the admin page.

View File

@ -24,7 +24,7 @@ package org.libreccm.ui.admin;
*/
public class AdminPageModel {
private String path;
private String uriIdentifier;
private String label;
@ -32,19 +32,19 @@ public class AdminPageModel {
private String icon;
public String getPath() {
return path;
public String getUriIdentifier() {
return uriIdentifier;
}
public void setPath(String path) {
this.path = path;
public void setUriIdentifier(final String uriIdentifier) {
this.uriIdentifier = uriIdentifier;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
public void setLabel(final String label) {
this.label = label;
}
@ -52,7 +52,7 @@ public class AdminPageModel {
return description;
}
public void setDescription(String description) {
public void setDescription(final String description) {
this.description = description;
}
@ -60,7 +60,7 @@ public class AdminPageModel {
return icon;
}
public void setIcon(String icon) {
public void setIcon(final String icon) {
this.icon = icon;
}

View File

@ -71,7 +71,7 @@ public class AdminPagesModel {
);
final AdminPageModel model = new AdminPageModel();
model.setPath(fromAdminPage.getPath());
model.setUriIdentifier(fromAdminPage.getUriIdentifier());
model.setLabel(labelBundle.getString(fromAdminPage.getLabelKey()));
model.setDescription(
descriptionBundle.getString(

View File

@ -40,7 +40,7 @@ public class ApplicationsController {
@Path("/")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
public String getPlaceholder() {
public String getApplications() {
return "org/libreccm/ui/admin/applications.xhtml";
}
}

View File

@ -41,8 +41,10 @@ public class ApplicationsPage implements AdminPage {
}
@Override
public String getPath() {
return "applications";
public String getUriIdentifier() {
return String.format(
"%s#getApplications", ApplicationsController.class.getSimpleName()
);
}
@Override

View File

@ -40,7 +40,7 @@ public class CategoriesController {
@Path("/")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
public String getPlaceholder() {
public String getCategories() {
return "org/libreccm/ui/admin/categories.xhtml";
}
}

View File

@ -41,8 +41,10 @@ public class CategoriesPage implements AdminPage {
}
@Override
public String getPath() {
return "categories";
public String getUriIdentifier() {
return String.format(
"%s#getCategories", CategoriesController.class.getSimpleName()
);
}
@Override

View File

@ -40,7 +40,7 @@ public class ConfigurationController {
@Path("/")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
public String getPlaceholder() {
public String getSettings() {
return "org/libreccm/ui/admin/configuration.xhtml";
}
}

View File

@ -20,7 +20,6 @@ package org.libreccm.ui.admin.configuration;
import org.libreccm.ui.admin.AdminConstants;
import org.libreccm.ui.admin.AdminPage;
import org.libreccm.ui.admin.categories.CategoriesController;
import java.util.HashSet;
import java.util.Set;
@ -41,8 +40,10 @@ public class ConfigurationPage implements AdminPage {
}
@Override
public String getPath() {
return "configuration";
public String getUriIdentifier() {
return String.format(
"%s#getSettings", ConfigurationController.class.getSimpleName()
);
}
@Override

View File

@ -40,7 +40,7 @@ public class DashboardController {
@Path("/")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
public String getPlaceholder() {
public String getDashboard() {
return "org/libreccm/ui/admin/dashboard.xhtml";
}
}

View File

@ -20,6 +20,7 @@ package org.libreccm.ui.admin.dashboard;
import org.libreccm.ui.admin.AdminConstants;
import org.libreccm.ui.admin.AdminPage;
import org.libreccm.ui.admin.configuration.ConfigurationController;
import java.util.HashSet;
import java.util.Set;
@ -40,8 +41,10 @@ public class DashboardPage implements AdminPage {
}
@Override
public String getPath() {
return "/";
public String getUriIdentifier() {
return String.format(
"%s#getDashboard", DashboardController.class.getSimpleName()
);
}
@Override

View File

@ -40,7 +40,7 @@ public class ImExportController {
@Path("/")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
public String getPlaceholder() {
public String getImExportDashboard() {
return "org/libreccm/ui/admin/imexport.xhtml";
}
}

View File

@ -40,8 +40,10 @@ public class ImExportPage implements AdminPage {
}
@Override
public String getPath() {
return "imexport";
public String getUriIdentifier() {
return String.format(
"%s#getImExportDashboard", ImExportController.class.getSimpleName()
);
}
@Override

View File

@ -40,7 +40,7 @@ public class SitesController {
@Path("/")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
public String getPlaceholder() {
public String getSites() {
return "org/libreccm/ui/admin/sites.xhtml";
}
}

View File

@ -40,8 +40,10 @@ public class SitesPage implements AdminPage {
}
@Override
public String getPath() {
return "sites";
public String getUriIdentifier() {
return String.format(
"%s#getSites", SitesController.class.getSimpleName()
);
}
@Override

View File

@ -23,6 +23,7 @@ import java.util.Set;
import org.libreccm.ui.admin.AdminConstants;
import org.libreccm.ui.admin.AdminPage;
import org.libreccm.ui.admin.imexport.ImExportController;
import javax.enterprise.context.ApplicationScoped;
@ -41,8 +42,11 @@ public class SystemInformationPage implements AdminPage {
}
@Override
public String getPath() {
return "systeminformation";
public String getUriIdentifier() {
return String.format(
"%s#getSystemInformation",
SystemInformationController.class.getSimpleName()
);
}
@Override

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2020 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.ui.admin.usersgroupsroles;
import org.libreccm.core.CoreConstants;
import org.libreccm.security.AuthorizationRequired;
import org.libreccm.security.RequiresPrivilege;
import javax.enterprise.context.RequestScoped;
import javax.mvc.Controller;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@RequestScoped
@Controller
@Path("/users-groups-roles/group")
public class GroupsController {
@GET
@Path("/")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
public String getGroups() {
return "org/libreccm/ui/admin/users-groups-roles/groups.xhtml";
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright (C) 2020 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.ui.admin.usersgroupsroles;
import org.libreccm.security.Role;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
public class PartyRoleMembership implements Comparable<PartyRoleMembership>{
private long roleId;
private String roleUuid;
private String roleName;
public PartyRoleMembership() {
// Nothing
}
public PartyRoleMembership(final Role role) {
roleId = role.getRoleId();
roleUuid = role.getUuid();
roleName = role.getName();
}
public long getRoleId() {
return roleId;
}
public void setRoleId(long roleId) {
this.roleId = roleId;
}
public String getRoleUuid() {
return roleUuid;
}
public void setRoleUuid(String roleUuid) {
this.roleUuid = roleUuid;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
@Override
public int compareTo(final PartyRoleMembership other) {
return roleName.compareTo(other.getRoleName());
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2020 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.ui.admin.usersgroupsroles;
import org.libreccm.core.CoreConstants;
import org.libreccm.security.AuthorizationRequired;
import org.libreccm.security.RequiresPrivilege;
import javax.enterprise.context.RequestScoped;
import javax.mvc.Controller;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@RequestScoped
@Controller
@Path("/users-groups-roles/roles")
public class RolesController {
@GET
@Path("/")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
public String getRoles() {
return "org/libreccm/ui/admin/users-groups-roles/roles.xhtml";
}
}

View File

@ -0,0 +1,156 @@
/*
* Copyright (C) 2020 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.ui.admin.usersgroupsroles;
import org.libreccm.core.EmailAddress;
import org.libreccm.security.GroupMembership;
import org.libreccm.security.RoleMembership;
import org.libreccm.security.User;
import org.libreccm.ui.Message;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
import javax.transaction.Transactional;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@RequestScoped
@Named("UserDetailsModel")
public class UserDetailsModel {
private long userId;
private String uuid;
private String name;
private String givenName;
private String familyName;
private EmailAddress primaryEmailAddress;
private List<EmailAddress> emailAddresses;
private boolean banned;
private boolean passwordResetRequired;
private List<UserGroupMembership> groupMemberships;
private List<PartyRoleMembership> roles;
private final List<Message> messages;
public UserDetailsModel() {
messages = new ArrayList<>();
}
@Transactional(Transactional.TxType.REQUIRED)
protected void setUser(final User user) {
Objects.requireNonNull(user);
userId = user.getPartyId();
uuid = user.getUuid();
name = user.getName();
givenName = user.getGivenName();
familyName = user.getFamilyName();
primaryEmailAddress = user.getPrimaryEmailAddress();
// Ensure that we don't get a lazyily initalized list.
emailAddresses = user
.getEmailAddresses()
.stream()
.collect(Collectors.toList());
banned = user.isBanned();
passwordResetRequired = user.isPasswordResetRequired();
groupMemberships = user
.getGroupMemberships()
.stream()
.map(GroupMembership::getGroup)
.map(UserGroupMembership::new)
.collect(Collectors.toList());
roles = user
.getRoleMemberships()
.stream()
.map(RoleMembership::getRole)
.map(PartyRoleMembership::new)
.collect(Collectors.toList());
}
public List<Message> getMessages() {
return Collections.unmodifiableList(messages);
}
public void addMessage(final Message message) {
messages.add(message);
}
public long getUserId() {
return userId;
}
public String getUuid() {
return uuid;
}
public String getName() {
return name;
}
public String getGivenName() {
return givenName;
}
public String getFamilyName() {
return familyName;
}
public EmailAddress getPrimaryEmailAddress() {
return primaryEmailAddress;
}
public List<EmailAddress> getEmailAddresses() {
return Collections.unmodifiableList(emailAddresses);
}
public boolean isBanned() {
return banned;
}
public boolean isPasswordResetRequired() {
return passwordResetRequired;
}
public List<UserGroupMembership> getGroupMemberships() {
return Collections.unmodifiableList(groupMemberships);
}
public List<PartyRoleMembership> getRoles() {
return Collections.unmodifiableList(roles);
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright (C) 2020 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.ui.admin.usersgroupsroles;
import org.libreccm.security.Group;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
public class UserGroupMembership implements Comparable<UserGroupMembership> {
private long groupId;
private String groupUuid;
private String groupName;
public UserGroupMembership() {
// Nothing
}
public UserGroupMembership(final Group group) {
this.groupId = group.getPartyId();
this.groupUuid = group.getUuid();
this.groupName = group.getName();
}
public long getGroupId() {
return groupId;
}
public void setGroupId(final long groupId) {
this.groupId = groupId;
}
public String getGroupUuid() {
return groupUuid;
}
public void setGroupUuid(final String groupUuid) {
this.groupUuid = groupUuid;
}
public String getGroupName() {
return groupName;
}
public void setGroupName(final String groupName) {
this.groupName = groupName;
}
@Override
public int compareTo(final UserGroupMembership other) {
return groupName.compareTo(other.getGroupName());
}
}

View File

@ -18,13 +18,24 @@
*/
package org.libreccm.ui.admin.usersgroupsroles;
import org.libreccm.api.Identifier;
import org.libreccm.api.IdentifierParser;
import org.libreccm.core.CoreConstants;
import org.libreccm.security.AuthorizationRequired;
import org.libreccm.security.RequiresPrivilege;
import org.libreccm.security.User;
import org.libreccm.security.UserRepository;
import org.libreccm.ui.Message;
import org.libreccm.ui.MessageType;
import org.libreccm.ui.admin.AdminMessages;
import java.util.Arrays;
import java.util.Optional;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.mvc.Controller;
import javax.transaction.Transactional;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
@ -41,6 +52,18 @@ import javax.ws.rs.QueryParam;
@Path("/users-groups-roles/users")
public class UsersController {
@Inject
private AdminMessages adminMessages;
@Inject
private IdentifierParser identifierParser;
@Inject
private UserDetailsModel userDetailsModel;
@Inject
private UserRepository userRepository;
@Inject
private UsersTableModel usersTableModel;
@ -59,11 +82,45 @@ public class UsersController {
@Path("/{userIdentifier}/details")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
public String getUser(
@PathParam("userIdentifier") final String userIdentifier
@Transactional(Transactional.TxType.REQUIRED)
public String getUserDetails(
@PathParam("userIdentifier") final String userIdentifierParam
) {
final Identifier identifier = identifierParser.parseIdentifier(
userIdentifierParam
);
final Optional<User> result;
switch (identifier.getType()) {
case ID:
result = userRepository.findById(
Long.parseLong(identifier.getIdentifier())
);
break;
case UUID:
result = userRepository.findByUuid(
identifier.getIdentifier()
);
break;
default:
result = userRepository.findByName(identifier.getIdentifier());
break;
}
throw new UnsupportedOperationException();
if (result.isPresent()) {
userDetailsModel.setUser(result.get());
return "org/libreccm/ui/admin/users-groups-roles/user-details.xhtml";
} else {
userDetailsModel.addMessage(
new Message(
adminMessages.getMessage(
"usersgroupsroles.users.not_found.message",
Arrays.asList(userIdentifierParam)
),
MessageType.WARNING
)
);
return "org/libreccm/ui/admin/users-groups-roles/user-not-found.xhtml";
}
}
@POST

View File

@ -36,7 +36,6 @@ import javax.ws.rs.Path;
@Path("/users-groups-roles")
public class UsersGroupsRolesController {
@GET
@Path("/")
@AuthorizationRequired
@ -45,23 +44,4 @@ public class UsersGroupsRolesController {
return "org/libreccm/ui/admin/users-groups-roles/overview.xhtml";
}
@GET
@Path("/groups")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
public String getGroups() {
return "org/libreccm/ui/admin/users-groups-roles/groups.xhtml";
}
@GET
@Path("/roles")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
public String getRoles() {
return "org/libreccm/ui/admin/users-groups-roles/roles.xhtml";
}
}

View File

@ -32,17 +32,23 @@ import javax.enterprise.context.ApplicationScoped;
*/
@ApplicationScoped
public class UsersGroupsRolesPage implements AdminPage {
@Override
@Override
public Set<Class<?>> getControllerClasses() {
final Set<Class<?>> classes = new HashSet<>();
classes.add(UsersGroupsRolesController.class);
classes.add(GroupsController.class);
classes.add(RolesController.class);
classes.add(UsersController.class);
return classes;
}
@Override
public String getPath() {
return "users-groups-roles";
public String getUriIdentifier() {
return String.format(
"%s#getOverview",
UsersGroupsRolesController.class.getSimpleName()
);
}
@Override
@ -57,7 +63,7 @@ public class UsersGroupsRolesPage implements AdminPage {
@Override
public String getDescriptionBundle() {
return AdminConstants.ADMIN_BUNDLE;
return AdminConstants.ADMIN_BUNDLE;
}
@Override
@ -74,4 +80,5 @@ public class UsersGroupsRolesPage implements AdminPage {
public int getPosition() {
return 10;
}
}

View File

@ -29,8 +29,8 @@
<ul class="navbar-nav mr-auto">
<c:forEach items="#{AdminPagesModel.adminPages}" var="page">
<li class="nav-item">
<a class="nav-link #{activePage == page.path?'active':''}"
href="#{request.contextPath}/@admin/#{page.path}">
<a class="nav-link #{activePage == page.uriIdentifier?'active':''}"
href="#{mvc.uri(page.uriIdentifier)}">
<svg class="bi"
width="1em"
height="1em"
@ -47,7 +47,7 @@
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="#{request.contextPath}/@admin/">
<a href="#{mvc.uri('DashboardController#getDashboard')}">
#{AdminMessages['breadcrumbs.start']}
</a>
</li>

View File

@ -20,19 +20,19 @@
<div class="row">
<nav class="col-md-2 list-group">
<a class="list-group-item list-group-item-action #{activePanel == 'overview' ? 'active' : ''}"
href="#{request.contextPath}/@admin/users-groups-roles">
href="#{mvc.uri('UsersGroupsRolesController#getOverview')}">
#{AdminMessages['usersgroupsroles.overview.label']}
</a>
<a class="list-group-item list-group-item-action #{activePanel == 'users' ? 'active' : ''}"
href="#{request.contextPath}/@admin/users-groups-roles/users">
href="#{mvc.uri('UsersController#getUsers')}">
#{AdminMessages['usersgroupsroles.users.label']}
</a>
<a class="list-group-item list-group-item-action #{activePanel == 'groups' ? 'active' : ''}"
href="#{request.contextPath}/@admin/users-groups-roles/groups">
href="#{mvc.uri('GroupsController#getGroups')}">
#{AdminMessages['usersgroupsroles.groups.label']}
</a>
<a class="list-group-item list-group-item-action #{activePanel == 'roles' ? 'active' : ''}"
href="#{request.contextPath}/@admin/users-groups-roles/roles">
href="#{mvc.uri('RolesController#getRoles')}">
#{AdminMessages['usersgroupsroles.roles.label']}
</a>
</nav>

View File

@ -11,7 +11,9 @@
<ui:define name="breadcrumb">
<li class="breadcrumb-item">
<a href="#{request.contextPath}/@admin/users-groups-roles/">#{AdminMessages['usersgroupsroles.label']}</a>
<a href="#{mvc.uri('UsersGroupsRolesController#getOverview')}">
#{AdminMessages['usersgroupsroles.label']}
</a>
</li>
<li class="breadcrumb-item active">
#{AdminMessages['usersgroupsroles.groups.label']}

View File

@ -11,7 +11,9 @@
<ui:define name="breadcrumb">
<li class="breadcrumb-item">
<a href="#{request.contextPath}/@admin/users-groups-roles/">#{AdminMessages['usersgroupsroles.label']}</a>
<a href="#{mvc.uri('UsersGroupsRolesController#getOverview')}">
#{AdminMessages['usersgroupsroles.label']}
</a>
</li>
<li class="breadcrumb-item active">
#{AdminMessages['usersgroupsroles.roles.label']}

View File

@ -0,0 +1,298 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:xlink="http://www.w3.org/1999/xlink">
<ui:composition template="/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles.xhtml">
<ui:param name="activePage" value="usersgroupsroles" />
<ui:param name="activePanel" value="users" />
<ui:param name="title"
value="#{AdminMessages.getMessage('usersgroupsroles.users.user_details.title', [UserDetailsModel.name])}" />
<ui:define name="breadcrumb">
<li class="breadcrumb-item">
<a href="#{mvc.uri('UsersGroupsRolesController#getOverview')}">
#{AdminMessages['usersgroupsroles.label']}
</a>
</li>
<li class="breadcrumb-item">
<a href="#{mvc.uri('UsersController#getUsers')}">
#{AdminMessages['usersgroupsroles.users.label']}
</a>
</li>
<li class="breadcrumb-item">
#{UserDetailsModel.name}
</li>
</ui:define>
<ui:define name="panel">
<dl>
<div>
<dt>#{AdminMessages['usergroupsroles.users.user_details.id']}</dt>
<dd>#{UserDetailsModel.userId}</dd>
</div>
<div>
<dt>#{AdminMessages['usergroupsroles.users.user_details.uuid']}</dt>
<dd>#{UserDetailsModel.uuid}</dd>
</div>
<div>
<dt>#{AdminMessages['usergroupsroles.users.user_details.name']}</dt>
<dd>#{UserDetailsModel.name}</dd>
</div>
<div>
<dt>#{AdminMessages['usergroupsroles.users.user_details.given_name']}</dt>
<dd>#{UserDetailsModel.givenName}</dd>
</div>
<div>
<dt>#{AdminMessages['usergroupsroles.users.user_details.family name']}</dt>
<dd>#{UserDetailsModel.familyName}</dd>
</div>
<div>
<dt>
#{AdminMessages['usergroupsroles.users.user_details.primary_email_address']}
</dt>
<dd>
<dl>
<dt>#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.cols.address']}</dt>
<dd>
#{UserDetailsModel.primaryEmailAddress.address}
</dd>
<dt>
#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.cols.boucing']}
</dt>
<dd>
<c:choose>
<c:when test="#{UserDetailsModel.primaryEmailAddress.bouncing}">
#{AdminMessages['usergroupsroles.users.user_details.email_address.bouncing.yes']}
</c:when>
<c:otherwise>
#{AdminMessages['usergroupsroles.users.user_details.email_address.bouncing.no']}
</c:otherwise>
</c:choose>
</dd>
<dt>
#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.cols.verified']}
</dt>
<dd>
<c:choose>
<c:when test="#{UserDetailsModel.primaryEmailAddress.verified}">
#{AdminMessages['usergroupsroles.users.user_details.email_address.verified.yes']}
</c:when>
<c:otherwise>
#{AdminMessages['usergroupsroles.users.user_details.email_address.verified.no']}
</c:otherwise>
</c:choose>
</dd>
</dl>
</dd>
</div>
<div>
<dt>
#{AdminMessages['usergroupsroles.users.user_details.disabled']}
</dt>
<dd>
<c:choose >
<c:when test="#{UserDetailsModel.banned}">
#{AdminMessages['usergroupsroles.users.user_details.disabled.yes']}
</c:when>
<c:otherwise>
#{AdminMessages['usergroupsroles.users.user_details.disabled.no']}
</c:otherwise>
</c:choose>
</dd>
</div>
<div>
<dt>
#{AdminMessages['usergroupsroles.users.user_details.password_reset_required']}
</dt>
<dd>
<c:choose >
<c:when test="#{UserDetailsModel.passwordResetRequired}">
#{AdminMessages['usergroupsroles.users.user_details.password_reset_required.yes']}
</c:when>
<c:otherwise>
#{AdminMessages['usergroupsroles.users.user_details.password_reset_required.no']}
</c:otherwise>
</c:choose>
</dd>
</div>
</dl>
<a class="btn btn-info" href="#">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#pen" />
</svg>
<span>
#{AdminMessages['usergroupsroles.users.user_details.edit_user']}
</span>
</a>
<c:if test="#{UserDetailsModel.emailAddresses.size() > 0}">
<h2>
#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.heading']}
</h2>
<a class="btn btn-secondary" href="#">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#plus-circle" />
</svg>
<span>#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.add']}</span>
</a>
<table class="table table-hover">
<thead class="thead-light">
<tr>
<th>
#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.cols.address']}
</th>
<th class="text-center">
#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.cols.boucing']}
</th>
<th class="text-center">
#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.cols.verified']}
</th>
<th class="text-center" colspan="2">
#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.cols.actions']}
</th>
</tr>
<c:forEach items="#{UserDetailsModel.emailAddresses}"
var="address">
<tr>
<td>
#{address.address}
</td>
<td>
<c:choose>
<c:when test="#{address.bouncing}">
#{AdminMessages['usergroupsroles.users.user_details.email_address.bouncing.yes']}
</c:when>
<c:otherwise>
#{AdminMessages['usergroupsroles.users.user_details.email_address.bouncing.no']}
</c:otherwise>
</c:choose>
</td>
<td>
<c:choose>
<c:when test="#{address.verified}">
#{AdminMessages['usergroupsroles.users.user_details.email_address.verified.yes']}
</c:when>
<c:otherwise>
#{AdminMessages['usergroupsroles.users.user_details.email_address.verified.no']}
</c:otherwise>
</c:choose>
</td>
<td>
<a class="btn btn-info" href="#">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#pen" />
</svg>
<span>
#{AdminMessages['usergroupsroles.users.user_details.email_addresses.edit']}
</span>
</a>
</td>
<td>
<a class="btn btn-danger" href="#">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#x-circle" />
</svg>
<span>
#{AdminMessages['usergroupsroles.users.user_details.email_addresses.remove']}
</span>
</a>
</td>
</tr>
</c:forEach>
</thead>
</table>
</c:if>
<c:if test="#{UserDetailsModel.groupMemberships.size() > 0}">
<h2>
#{AdminMessages['usergroupsroles.users.user_details.groups.heading']}
</h2>
<a class="btn btn-secondary" href="#">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#plus-circle" />
</svg>
<span>#{AdminMessages['usergroupsroles.users.user_details.groups.add']}</span>
</a>
<ul class="list-group mt-1">
<c:forEach items="#{UserDetailsModel.groupMemberships}"
var="group">
<li class="list-group-item d-flex justify-content-between align-items-center">
<a href="#">
#{group.groupName}
</a>
<a class="btn btn-danger" href="#">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#x-circle" />
</svg>
<span>
#{AdminMessages['usergroupsroles.users.user_details.groups.remove']}
</span>
</a>
</li>
</c:forEach>
</ul>
</c:if>
<c:if test="#{UserDetailsModel.roles.size() > 0}">
<h2>
#{AdminMessages['usergroupsroles.users.user_details.roles.heading']}
</h2>
<a class="btn btn-secondary" href="#">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#plus-circle" />
</svg>
<span>#{AdminMessages['usergroupsroles.users.user_details.roles.add']}</span>
</a>
<ul class="list-group mt-1">
<c:forEach items="#{UserDetailsModel.roles}"
var="role">
<li class="list-group-item d-flex justify-content-between align-items-center">
<a href="#">
#{role.roleName}
</a>
<a class="btn btn-danger" href="#">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#x-circle" />
</svg>
<span>
#{AdminMessages['usergroupsroles.users.user_details.roles.remove']}
</span>
</a>
</li>
</c:forEach>
</ul>
</c:if>
</ui:define>
</ui:composition>
</html>

View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
<ui:composition template="/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles.xhtml">
<ui:param name="activePage" value="usersgroupsroles" />
<ui:param name="activePanel" value="users" />
<ui:param name="title"
value="#{AdminMessages('usersgroupsroles.users.not_found.title')}" />
<ui:define name="breadcrumb">
<li class="breadcrumb-item">
<a href="#{mvc.uri('UsersGroupsRolesController#getOverview')}">
#{AdminMessages['usersgroupsroles.label']}
</a>
</li>
<li class="breadcrumb-item">
<a href="#{mvc.uri('UsersController#getUsers')}">
#{AdminMessages['usersgroupsroles.users.label']}
</a>
</li>
<li class="breadcrumb-item">
#{AdminMessages['usersgroupsroles.users.not_found.title']}
</li>
</ui:define>
<ui:define name="panel">
<c:forEach items="#{UserDetailsModel.messages}" var="message">
<div class="alert alert-#{message.type}" role="alert">
#{message}
</div>
</c:forEach>
</ui:define>
</ui:composition>
</html>

View File

@ -12,7 +12,9 @@
<ui:define name="breadcrumb">
<li class="breadcrumb-item">
<a href="#{request.contextPath}/@admin/users-groups-roles/">#{AdminMessages['usersgroupsroles.label']}</a>
<a href="#{mvc.uri('UsersGroupsRolesController#getOverview')}">
#{AdminMessages['usersgroupsroles.label']}
</a>
</li>
<li class="breadcrumb-item active">
#{AdminMessages['usersgroupsroles.users.label']}
@ -22,7 +24,7 @@
<ui:define name="panel">
<div class="row mb-2">
<div class="col-sm-9">
<form action="#{request.contextPath}/@admin/users-groups-roles/users"
<form action="#{mvc.uri('UsersController#getUsers')}"
class="form-inline"
method="get">
<div class="form-group">
@ -76,14 +78,16 @@
#{user.banned ? AdminMessages['usersgroupsroles.users.table.headers.disabled.true'] : AdminMessages['usersgroupsroles.users.table.headers.disabled.false']}
</td>
<td class="text-center action-col">
<a class="btn btn-info" href="#">
<a class="btn btn-info"
href="#{mvc.uri('UsersController#getUserDetails', { 'userIdentifier': user.name })}">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#eye" />
</svg>
<span>#{AdminMessages['usersgroupsroles.users.detailslink.label']}
<span>
#{AdminMessages['usersgroupsroles.users.detailslink.label']}
</span>
</a>
</td>

View File

@ -38,3 +38,35 @@ usersgroupsroles.users.disablebutton.label=Disable
usersgroupsroles.users.add=Add user
usersgroupsroles.users.filter.label=Filter users
usersgroupsroles.users.filter.submit=Submit
usersgroupsroles.users.not_found.title=User not found
usersgroupsroles.users.user_details.title=User {0} Details
usergroupsroles.users.user_details.id=User ID
usergroupsroles.users.user_details.uuid=UUID
usergroupsroles.users.user_details.name=Name
usergroupsroles.users.user_details.given_name=Given name
usergroupsroles.users.user_details.familyname=Familyname
usergroupsroles.users.user_details.primary_email_address=Primary email address
usergroupsroles.users.user_details.disabled=Disabled?
usergroupsroles.users.user_details.disabled.yes=Yes
usergroupsroles.users.user_details.disabled.no=No
usergroupsroles.users.user_details.password_reset_required=Password reset required?
usergroupsroles.users.user_details.password_reset_required.yes=Yes
usergroupsroles.users.user_details.password_reset_required.no=No
usergroupsroles.users.user_details.additional_email_addresses.heading=Additional email addresses
'usergroupsroles.users.user_details.additional_email_addresses.cols.address=Address
usergroupsroles.users.user_details.additional_email_addresses.cols.boucing=Bouncing?
usergroupsroles.users.user_details.additional_email_addresses.cols.verified=Verified
usergroupsroles.users.user_details.additional_email_addresses.cols.actions=Actions
usergroupsroles.users.user_details.email_address.bouncing.yes=Yes
usergroupsroles.users.user_details.email_address.bouncing.no=No
usergroupsroles.users.user_details.email_address.verified.yes=Yes
usergroupsroles.users.user_details.email_address.verified.no=No
usergroupsroles.users.user_details.email_addresses.remove=Remove
usergroupsroles.users.user_details.email_addresses.edit=Edit
usergroupsroles.users.user_details.groups.heading=Groups Memberships
usergroupsroles.users.user_details.groups.add=Add
usergroupsroles.users.user_details.groups.remove=Remove
usergroupsroles.users.user_details.roles.heading=Roles
usergroupsroles.users.user_details.roles.add=Add
usergroupsroles.users.user_details.roles.remove=Remove
usergroupsroles.users.user_details.edit_user=Edit

View File

@ -38,3 +38,35 @@ usersgroupsroles.users.disablebutton.label=Sperren
usersgroupsroles.users.add=Benutzer*in hinzuf\u00fcgen
usersgroupsroles.users.filter.label=Benutzer*innen filtern
usersgroupsroles.users.filter.submit=Anwenden
usersgroupsroles.users.not_found.title=Benutzer*in nicht gefunden
usersgroupsroles.users.user_details.title=Details Benutzer*in {0}
usergroupsroles.users.user_details.id=Benutzer*in ID
usergroupsroles.users.user_details.uuid=UUID
usergroupsroles.users.user_details.name=Name
usergroupsroles.users.user_details.given_name=Vorname
usergroupsroles.users.user_details.familyname=Familienname
usergroupsroles.users.user_details.primary_email_address=Prim\u00e4re E-Mail-Adresse
usergroupsroles.users.user_details.disabled=Inaktiv?
usergroupsroles.users.user_details.disabled.yes=Ja
usergroupsroles.users.user_details.disabled.no=Nein
usergroupsroles.users.user_details.password_reset_required=Neues Password erforderlich?
usergroupsroles.users.user_details.password_reset_required.yes=Ja
usergroupsroles.users.user_details.password_reset_required.no=Nein
usergroupsroles.users.user_details.additional_email_addresses.heading=Weitere E-Mail-Addressen
'usergroupsroles.users.user_details.additional_email_addresses.cols.address=Addresse
usergroupsroles.users.user_details.additional_email_addresses.cols.boucing=Wird zur\u00fcckgewiesen?
usergroupsroles.users.user_details.additional_email_addresses.cols.verified=Verifizizert
usergroupsroles.users.user_details.additional_email_addresses.cols.actions=Aktionen
usergroupsroles.users.user_details.email_address.bouncing.yes=Ja
usergroupsroles.users.user_details.email_address.bouncing.no=Nein
usergroupsroles.users.user_details.email_address.verified.yes=Ja
usergroupsroles.users.user_details.email_address.verified.no=Nein
usergroupsroles.users.user_details.email_addresses.remove=Entfernen
usergroupsroles.users.user_details.email_addresses.edit=Bearbeiten
usergroupsroles.users.user_details.groups.heading=Gruppenmitgliedschaften
usergroupsroles.users.user_details.groups.add=Hinzuf\u00fcgen
usergroupsroles.users.user_details.groups.remove=Entfernen
usergroupsroles.users.user_details.roles.heading=Rollen
usergroupsroles.users.user_details.roles.add=Hinzuf\u00fcgen
usergroupsroles.users.user_details.roles.remove=Entfernen
usergroupsroles.users.user_details.edit_user=Bearbeiten