attachments) {
+ this.attachments = attachments;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof EmailBean)) return false;
+ EmailBean emailBean = (EmailBean) o;
+ return Objects.equals(getFrom(), emailBean.getFrom())
+ && Objects.equals(getTo(), emailBean.getTo())
+ && Objects.equals(getReplyTo(), emailBean.getReplyTo())
+ && Objects.equals(getMimeType(), emailBean.getMimeType())
+ && Objects.equals(getBody(), emailBean.getBody())
+ && Objects.equals(getSubject(), emailBean.getSubject())
+ && Objects.equals(getAttachments(), emailBean.getAttachments());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ getFrom(), getTo(), getReplyTo(), getMimeType(), getBody(), getSubject(), getAttachments());
+ }
+
+ @Override
+ public String toString() {
+ return "EmailBean{"
+ + "from='"
+ + from
+ + '\''
+ + ", to='"
+ + to
+ + '\''
+ + ", replyTo='"
+ + replyTo
+ + '\''
+ + ", mimeType='"
+ + mimeType
+ + '\''
+ + ", body='"
+ + body
+ + '\''
+ + ", subject='"
+ + subject
+ + '\''
+ + ", attachments="
+ + attachments
+ + '}';
+ }
+}
diff --git a/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/MailSender.java b/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/MailSender.java
new file mode 100644
index 0000000000..a6a374ccf9
--- /dev/null
+++ b/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/MailSender.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (c) 2012-2017 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+package org.eclipse.che.mail;
+
+import static java.util.concurrent.Executors.newFixedThreadPool;
+
+import com.google.common.io.Files;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.io.File;
+import java.io.IOException;
+import java.util.Base64;
+import java.util.Date;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.mail.Message;
+import javax.mail.Multipart;
+import javax.mail.Transport;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeBodyPart;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMultipart;
+import org.apache.commons.io.FileUtils;
+import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provides email sending capability
+ *
+ * @author Alexander Garagatyi
+ * @author Sergii Kabashniuk
+ */
+@Singleton
+public class MailSender {
+ private static final Logger LOG = LoggerFactory.getLogger(MailSender.class);
+
+ private final ExecutorService executor;
+ private final MailSessionProvider mailSessionProvider;
+
+ @Inject
+ public MailSender(MailSessionProvider mailSessionProvider) {
+ this.mailSessionProvider = mailSessionProvider;
+ this.executor =
+ newFixedThreadPool(
+ 2 * Runtime.getRuntime().availableProcessors(),
+ new ThreadFactoryBuilder()
+ .setNameFormat("MailNotificationsPool-%d")
+ .setDaemon(false)
+ .setUncaughtExceptionHandler(LoggingUncaughtExceptionHandler.getInstance())
+ .build());
+ }
+
+ public void sendAsync(EmailBean emailBean) {
+ executor.execute(
+ () -> {
+ try {
+ sendMail(emailBean);
+ } catch (Exception ex) {
+ LOG.warn(
+ "Failed to send email notification for {} with subject {}. Cause: '{}'",
+ emailBean.getTo(),
+ emailBean.getSubject(),
+ ex.getLocalizedMessage());
+ }
+ });
+ }
+
+ public void sendMail(EmailBean emailBean) throws SendMailException {
+ File tempDir = null;
+ try {
+ MimeMessage message = new MimeMessage(mailSessionProvider.get());
+ Multipart contentPart = new MimeMultipart();
+
+ MimeBodyPart bodyPart = new MimeBodyPart();
+ bodyPart.setText(emailBean.getBody(), "UTF-8", getSubType(emailBean.getMimeType()));
+ contentPart.addBodyPart(bodyPart);
+
+ if (emailBean.getAttachments() != null) {
+ tempDir = Files.createTempDir();
+ for (Attachment attachment : emailBean.getAttachments()) {
+ // Create attachment file in temporary directory
+ byte[] attachmentContent = Base64.getDecoder().decode(attachment.getContent());
+ File attachmentFile = new File(tempDir, attachment.getFileName());
+ Files.write(attachmentContent, attachmentFile);
+
+ // Attach the attachment file to email
+ MimeBodyPart attachmentPart = new MimeBodyPart();
+ attachmentPart.attachFile(attachmentFile);
+ attachmentPart.setContentID("<" + attachment.getContentId() + ">");
+ contentPart.addBodyPart(attachmentPart);
+ }
+ }
+
+ message.setContent(contentPart);
+ message.setSubject(emailBean.getSubject(), "UTF-8");
+ message.setFrom(new InternetAddress(emailBean.getFrom(), true));
+ message.setSentDate(new Date());
+ message.addRecipients(Message.RecipientType.TO, InternetAddress.parse(emailBean.getTo()));
+
+ if (emailBean.getReplyTo() != null) {
+ message.setReplyTo(InternetAddress.parse(emailBean.getReplyTo()));
+ }
+ LOG.info(
+ "Sending from {} to {} with subject {}",
+ emailBean.getFrom(),
+ emailBean.getTo(),
+ emailBean.getSubject());
+
+ Transport.send(message);
+ LOG.debug("Mail sent");
+ } catch (Exception e) {
+ LOG.error(e.getLocalizedMessage());
+ throw new SendMailException(e.getLocalizedMessage(), e);
+ } finally {
+ if (tempDir != null) {
+ try {
+ FileUtils.deleteDirectory(tempDir);
+ } catch (IOException exception) {
+ LOG.error(exception.getMessage());
+ }
+ }
+ }
+ }
+
+ /**
+ * Get the specified MIME subtype from given primary MIME type.
+ *
+ * It is needed for setText method in MimeBodyPar because it works only with text MimeTypes.
+ * setText method in MimeBodyPar already adds predefined "text/" to given subtype.
+ *
+ * @param mimeType primary MIME type
+ * @return MIME subtype
+ */
+ private String getSubType(String mimeType) {
+ return mimeType.substring(mimeType.lastIndexOf("/") + 1);
+ }
+
+ @PreDestroy
+ public void shutdown() throws InterruptedException {
+ executor.shutdown();
+ try {
+ if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
+ executor.shutdownNow();
+ if (!executor.awaitTermination(60, TimeUnit.SECONDS)) LOG.warn("Pool did not terminate");
+ }
+ } catch (InterruptedException ie) {
+ executor.shutdownNow();
+ Thread.currentThread().interrupt();
+ }
+ }
+}
diff --git a/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/MailSessionProvider.java b/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/MailSessionProvider.java
new file mode 100644
index 0000000000..29121ae989
--- /dev/null
+++ b/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/MailSessionProvider.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2012-2017 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+package org.eclipse.che.mail;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.util.Map;
+import java.util.Properties;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+import javax.mail.Authenticator;
+import javax.mail.PasswordAuthentication;
+import javax.mail.Session;
+import org.eclipse.che.inject.ConfigurationProperties;
+
+/** Provider of {@link Session} */
+@Singleton
+public class MailSessionProvider implements Provider {
+
+ private final Session session;
+ /**
+ * Configuration can be injected from container with help of {@lin ConfigurationProperties} class.
+ * In this case all properties that starts with 'che.mail.' will be used to create {@link
+ * Session}. First 4 letters 'che.' from property names will be removed.
+ */
+ @Inject
+ public MailSessionProvider(ConfigurationProperties configurationProperties) {
+
+ this(
+ configurationProperties
+ .getProperties("che.mail.*")
+ .entrySet()
+ .stream()
+ .collect(Collectors.toMap(e -> e.getKey().substring(4), Map.Entry::getValue)));
+ }
+
+ @VisibleForTesting
+ MailSessionProvider(Map mailConfiguration) {
+ if (mailConfiguration != null && !mailConfiguration.isEmpty()) {
+ Properties props = new Properties();
+ mailConfiguration.forEach(props::setProperty);
+
+ if (Boolean.parseBoolean(props.getProperty("mail.smtp.auth"))) {
+ final String username = props.getProperty("mail.smtp.auth.username");
+ final String password = props.getProperty("mail.smtp.auth.password");
+
+ // remove useless properties
+ props.remove("mail.smtp.auth.username");
+ props.remove("mail.smtp.auth.password");
+
+ this.session =
+ Session.getInstance(
+ props,
+ new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ } else {
+ this.session = Session.getInstance(props);
+ }
+ } else {
+ this.session = null;
+ }
+ }
+
+ @Override
+ public Session get() {
+ if (session == null) {
+ throw new RuntimeException("SMTP is not configured");
+ }
+ return session;
+ }
+}
diff --git a/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/SendMailException.java b/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/SendMailException.java
new file mode 100644
index 0000000000..b44e384d16
--- /dev/null
+++ b/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/SendMailException.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2012-2017 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+package org.eclipse.che.mail;
+
+/** Exception happened during mail sending * */
+public class SendMailException extends Exception {
+
+ public SendMailException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/core/commons/che-core-commons-mail/src/test/java/org/eclipse/che/mail/MailSenderTest.java b/core/commons/che-core-commons-mail/src/test/java/org/eclipse/che/mail/MailSenderTest.java
new file mode 100644
index 0000000000..fb36c70bb3
--- /dev/null
+++ b/core/commons/che-core-commons-mail/src/test/java/org/eclipse/che/mail/MailSenderTest.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2012-2017 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+package org.eclipse.che.mail;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import com.dumbster.smtp.SimpleSmtpServer;
+import com.dumbster.smtp.SmtpMessage;
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Map;
+import org.testng.ITestContext;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+public class MailSenderTest {
+ private MailSender mailSender;
+ private SimpleSmtpServer server;
+
+ public static void assertMail(
+ SimpleSmtpServer server,
+ String from,
+ String to,
+ String replyTo,
+ String subject,
+ String mimeType,
+ String body,
+ String attachmentContentID,
+ String attachmentFileName) {
+ assertEquals(server.getReceivedEmails().size(), 1);
+ SmtpMessage email = server.getReceivedEmails().iterator().next();
+
+ assertEquals(email.getHeaderValue("Subject"), subject);
+ assertEquals(email.getHeaderValue("From"), from);
+ assertEquals(email.getHeaderValue("Reply-To"), replyTo);
+ assertEquals(email.getHeaderValue("To"), to);
+ assertTrue(email.getBody().contains("Content-Type: " + mimeType));
+ assertTrue(email.getBody().contains(body));
+ if (attachmentFileName != null && attachmentContentID != null) {
+ assertTrue(email.getBody().contains("filename=" + attachmentFileName));
+ assertTrue(email.getBody().contains("Content-ID: <" + attachmentContentID + ">"));
+ }
+ }
+
+ @BeforeMethod
+ public void setup(ITestContext context) throws IOException {
+ server = SimpleSmtpServer.start(SimpleSmtpServer.AUTO_SMTP_PORT);
+
+ Map mailConfiguration =
+ ImmutableMap.of(
+ "mail.smtp.host",
+ "localhost",
+ "mail.smtp.port",
+ server.getPort() + "",
+ "mail.transport.protocol",
+ "smtp",
+ " mail.smtp.auth",
+ "false");
+
+ mailSender = new MailSender(new MailSessionProvider(mailConfiguration));
+ }
+
+ @AfterMethod
+ public void stop() {
+ server.stop();
+ }
+
+ @Test
+ public void shouldBeAbleToSendMessage() throws SendMailException {
+ EmailBean emailBean =
+ new EmailBean()
+ .withFrom("noreply@cloud-ide.com")
+ .withTo("dev-test@cloud-ide.com")
+ .withReplyTo("dev-test@cloud-ide.com")
+ .withSubject("Subject")
+ .withMimeType("text/html")
+ .withBody("hello user");
+ mailSender.sendMail(emailBean);
+ assertMail(
+ server,
+ "noreply@cloud-ide.com",
+ "dev-test@cloud-ide.com",
+ "dev-test@cloud-ide.com",
+ "Subject",
+ "text/html",
+ "hello user",
+ null,
+ null);
+ }
+
+ @Test
+ public void shouldBeAbleToSendMessageWithFormattedFields() throws SendMailException {
+ EmailBean emailBean =
+ new EmailBean()
+ .withFrom("Exo IDE ")
+ .withTo("dev-test@cloud-ide.com")
+ .withReplyTo("Developers to reply ")
+ .withSubject("Subject")
+ .withMimeType("text/html")
+ .withBody("hello user");
+ mailSender.sendMail(emailBean);
+ assertMail(
+ server,
+ "Exo IDE ",
+ "dev-test@cloud-ide.com",
+ "Developers to reply ",
+ "Subject",
+ "text/html",
+ "hello user",
+ null,
+ null);
+ }
+
+ @Test
+ public void shouldBeAbleToSendMessageToFewEmails() throws SendMailException {
+ EmailBean emailBean =
+ new EmailBean()
+ .withFrom("noreply@cloud-ide.com")
+ .withTo("dev-test@cloud-ide.com, dev-test1@cloud-ide.com, dev-test2@cloud-ide.com")
+ .withReplyTo("dev-test@cloud-ide.com")
+ .withSubject("Subject")
+ .withMimeType("text/html")
+ .withBody("hello user");
+ mailSender.sendMail(emailBean);
+
+ assertMail(
+ server,
+ "noreply@cloud-ide.com",
+ "dev-test@cloud-ide.com, dev-test1@cloud-ide.com, dev-test2@cloud-ide.com",
+ "dev-test@cloud-ide.com",
+ "Subject",
+ "text/html",
+ "hello user",
+ null,
+ null);
+ }
+
+ @Test
+ public void shouldBeAbleToSendMessageWithAttachment() throws SendMailException {
+ EmailBean emailBean =
+ new EmailBean()
+ .withFrom("noreply@cloud-ide.com")
+ .withTo("dev-test@cloud-ide.com")
+ .withReplyTo("dev-test@cloud-ide.com")
+ .withSubject("Subject")
+ .withMimeType("text/html")
+ .withBody("hello user");
+
+ Attachment attachment =
+ new Attachment()
+ .withContentId("attachmentId")
+ .withFileName("attachment.txt")
+ .withContent(Base64.getEncoder().encodeToString("attachmentContent".getBytes(UTF_8)));
+
+ emailBean.setAttachments(Collections.singletonList(attachment));
+
+ mailSender.sendMail(emailBean);
+ assertMail(
+ server,
+ "noreply@cloud-ide.com",
+ "dev-test@cloud-ide.com",
+ "dev-test@cloud-ide.com",
+ "Subject",
+ "text/html",
+ "hello user",
+ "attachmentId",
+ "attachment.txt");
+ }
+}
diff --git a/core/commons/che-core-commons-mail/src/test/resources/logback-test.xml b/core/commons/che-core-commons-mail/src/test/resources/logback-test.xml
new file mode 100644
index 0000000000..92391f09e8
--- /dev/null
+++ b/core/commons/che-core-commons-mail/src/test/resources/logback-test.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ %-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n
+
+
+
+
+
+
+
+
diff --git a/core/commons/pom.xml b/core/commons/pom.xml
index 74a31883e1..873b2babfc 100644
--- a/core/commons/pom.xml
+++ b/core/commons/pom.xml
@@ -24,6 +24,7 @@
Che Core :: Commons :: Parent
che-core-commons-annotations
+ che-core-commons-auth
che-core-commons-lang
che-core-commons-inject
che-core-commons-json
@@ -31,5 +32,6 @@
che-core-commons-schedule
che-core-commons-test
che-core-commons-j2ee
+ che-core-commons-mail
diff --git a/dashboard/bower.json b/dashboard/bower.json
index 5268c24fb8..9ba94a659f 100644
--- a/dashboard/bower.json
+++ b/dashboard/bower.json
@@ -14,6 +14,7 @@
"angular-charts": "0.2.6",
"angular-cookies": "1.4.8",
"angular-dropdowns": "1.0.0",
+ "angular-gravatar": "0.2.4",
"angular-filter": "0.5.4",
"angular-material": "1.0.1",
"angular-moment": "0.9.0",
diff --git a/dashboard/gulp/proxy.js b/dashboard/gulp/proxy.js
index ea7dc1d0ab..6b0ef4aa23 100644
--- a/dashboard/gulp/proxy.js
+++ b/dashboard/gulp/proxy.js
@@ -22,10 +22,9 @@ var serverOptions = {
var options = minimist(process.argv.slice(2), serverOptions);
-var patterns = ['/api', '/ext', '/ws', '/datasource', '/java-ca', '/im', '/che', '/admin'];
-
-var proxies = []
+var patterns = ['/api', '/ext', '/ws', '/datasource', '/java-ca', '/im', '/che', '/admin', '/wsmaster'];
+var proxies = [];
patterns.forEach(function(pattern) {
var proxyOptions = url.parse(options.server + pattern);
@@ -37,6 +36,8 @@ patterns.forEach(function(pattern) {
proxyOptions.route = '/admin';
} else if (pattern === '/ext') {
proxyOptions.route = '/ext';
+ } else if (pattern === '/wsmaster') {
+ proxyOptions.route = '/wsmaster';
} else {
proxyOptions.route = '/api';
}
diff --git a/dashboard/src/app/admin/admin-config.ts b/dashboard/src/app/admin/admin-config.ts
index b089d0114e..e06893708f 100644
--- a/dashboard/src/app/admin/admin-config.ts
+++ b/dashboard/src/app/admin/admin-config.ts
@@ -11,15 +11,16 @@
'use strict';
import {AdminsPluginsConfig} from './plugins/plugins-config';
+import {AdminsUserManagementConfig} from './user-management/user-management-config';
/**
* @author Florent Benoit
*/
export class AdminsConfig {
- constructor(register) {
+ constructor(register: che.IRegisterService) {
new AdminsPluginsConfig(register);
-
+ new AdminsUserManagementConfig(register);
}
}
diff --git a/dashboard/src/app/admin/user-management/account-profile/account-profile.directive.ts b/dashboard/src/app/admin/user-management/account-profile/account-profile.directive.ts
new file mode 100644
index 0000000000..181e6356a9
--- /dev/null
+++ b/dashboard/src/app/admin/user-management/account-profile/account-profile.directive.ts
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2015-2017 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+'use strict';
+
+interface IAccountProfileScope extends ng.IScope {
+ profileAttributes: {
+ phone?: string;
+ country?: string;
+ employer?: string;
+ jobtitle?: string;
+ lastName?: string;
+ firstName?: string;
+ };
+ profileInformationForm: ng.IFormController;
+ countries?: Array<{ 'name': string, 'code': string }>;
+ jobs?: Array<{ 'name': string }>;
+}
+
+/**
+ * @ngdoc directive
+ * @name account.profile.directive:accountProfile
+ * @restrict E
+ * @element
+ *
+ * @description
+ * ` for displaying account profile.
+ *
+ * @usage
+ *
+ *
+ * @author Florent Benoit
+ */
+export class AccountProfile implements ng.IDirective {
+ restrict = 'E';
+ templateUrl = 'app/account/account-profile/account-profile.html';
+ replace = true;
+ scope = {
+ profileAttributes: '=profileAttributes',
+ profileInformationForm: '=?profileInformationForm'
+ };
+
+ jsonCountries: string;
+ jsonJobs: string;
+
+ /**
+ * Default constructor that is using resource
+ * @ngInject for Dependency injection
+ */
+ constructor(jsonCountries: string, jsonJobs: string) {
+ this.jsonCountries = jsonCountries;
+ this.jsonJobs = jsonJobs;
+ }
+
+ link($scope: IAccountProfileScope) {
+ $scope.countries = angular.fromJson(this.jsonCountries);
+ $scope.jobs = angular.fromJson(this.jsonJobs);
+ }
+}
diff --git a/dashboard/src/app/admin/user-management/account-profile/account-profile.html b/dashboard/src/app/admin/user-management/account-profile/account-profile.html
new file mode 100644
index 0000000000..573bfb10cd
--- /dev/null
+++ b/dashboard/src/app/admin/user-management/account-profile/account-profile.html
@@ -0,0 +1,67 @@
+
+
+
+
+ A name is required.
+ The name has to be less than 128 characters long.
+
+
+
+
+
+ A last name is required.
+ The name has to be less than 128 characters long.
+
+
+
+
+
+ Should be numbers and may start with a '+'.
+ The phone number has to be more than 7 characters long.
+ The phone number has to be less than 15 characters long.
+
+
+
+
+
+
+
+
+
+
+ The name has to be less than 128 characters long.
+
+
+
+
+
+
+
+
+
diff --git a/dashboard/src/app/admin/user-management/account-profile/account-profile.styl b/dashboard/src/app/admin/user-management/account-profile/account-profile.styl
new file mode 100644
index 0000000000..2ba6ba3599
--- /dev/null
+++ b/dashboard/src/app/admin/user-management/account-profile/account-profile.styl
@@ -0,0 +1,8 @@
+.account-profile
+ padding 0 14px
+
+ .che-input-desktop
+ margin-top -1px
+
+ .che-select
+ margin-top -2px
diff --git a/dashboard/src/app/admin/user-management/add-user/add-user.controller.ts b/dashboard/src/app/admin/user-management/add-user/add-user.controller.ts
new file mode 100644
index 0000000000..c1fa184fd0
--- /dev/null
+++ b/dashboard/src/app/admin/user-management/add-user/add-user.controller.ts
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2015-2017 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+'use strict';
+import {AdminsUserManagementCtrl} from '../user-management.controller';
+
+/**
+ * This class is handling the controller for the add user
+ * @author Oleksii Orel
+ */
+export class AdminsAddUserController {
+ private $mdDialog: ng.material.IDialogService;
+ private lodash: any;
+ private cheNotification: any;
+ private cheUser: any;
+ private callbackController: AdminsUserManagementCtrl;
+ private newUserName: string;
+ private newUserEmail: string;
+ private newUserPassword: string;
+ private organizations: Array;
+ private organization: string;
+ private cheOrganization: che.api.ICheOrganization;
+ private chePermissions: che.api.IChePermissions;
+ private organizationRoles: che.resource.ICheOrganizationRoles;
+
+ /**
+ * Default constructor.
+ * @ngInject for Dependency injection
+ */
+ constructor($mdDialog: ng.material.IDialogService,
+ cheUser: any,
+ cheNotification: any,
+ lodash: any,
+ cheOrganization: che.api.ICheOrganization,
+ chePermissions: che.api.IChePermissions,
+ resourcesService: che.service.IResourcesService) {
+ this.$mdDialog = $mdDialog;
+ this.lodash = lodash;
+ this.cheUser = cheUser;
+ this.cheNotification = cheNotification;
+ this.cheOrganization = cheOrganization;
+ this.chePermissions = chePermissions;
+ this.organizationRoles = resourcesService.getOrganizationRoles();
+
+ this.organizations = [];
+
+ this.cheOrganization.fetchOrganizations().then(() => {
+ let organizations = this.cheOrganization.getOrganizations();
+ let rootOrganizations = organizations.filter((organization: any) => {
+ return !organization.parent;
+ });
+ this.organizations = lodash.pluck(rootOrganizations, 'name');
+ if (this.organizations.length > 0) {
+ this.organization = this.organizations[0];
+ }
+ });
+ }
+
+ /**
+ * Callback of the cancel button of the dialog.
+ */
+ abort(): void {
+ this.$mdDialog.hide();
+ }
+
+ /**
+ * Callback of the add button of the dialog(create new user).
+ */
+ createUser(): void {
+ let promise = this.cheUser.createUser(this.newUserName, this.newUserEmail, this.newUserPassword);
+
+ promise.then((data: any) => {
+ if (this.organization) {
+ this.addUserToOrganization(data.id);
+ } else {
+ this.finish();
+ }
+ }, (error: any) => {
+ this.cheNotification.showError(error.data.message ? error.data.message : 'Failed to create user.');
+ });
+ }
+
+ /**
+ * Finish user creation.
+ */
+ private finish(): void {
+ this.$mdDialog.hide();
+ this.callbackController.updateUsers();
+ this.cheNotification.showInfo('User successfully created.');
+ }
+
+ /**
+ * Adds user to chosen organization.
+ *
+ * @param userId
+ */
+ private addUserToOrganization(userId: string): void {
+ let organizations = this.cheOrganization.getOrganizations();
+ let organization = this.lodash.find(organizations, (organization: any) => {
+ return organization.name === this.organization;
+ });
+
+ let actions = this.organizationRoles.MEMBER.actions;
+ let permissions = {
+ instanceId: organization.id,
+ userId: userId,
+ domainId: 'organization',
+ actions: actions
+ };
+ this.chePermissions.storePermissions(permissions).then(() => {
+ this.finish();
+ }, (error: any) => {
+ this.cheNotification.showError(error.data.message ? error.data.message : 'Failed to add user to organization' + this.organization + '.');
+ });
+ }
+}
diff --git a/dashboard/src/app/admin/user-management/add-user/add-user.html b/dashboard/src/app/admin/user-management/add-user/add-user.html
new file mode 100644
index 0000000000..6989bfcdf4
--- /dev/null
+++ b/dashboard/src/app/admin/user-management/add-user/add-user.html
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dashboard/src/app/admin/user-management/add-user/add-user.styl b/dashboard/src/app/admin/user-management/add-user/add-user.styl
new file mode 100644
index 0000000000..7383dc934c
--- /dev/null
+++ b/dashboard/src/app/admin/user-management/add-user/add-user.styl
@@ -0,0 +1,48 @@
+.admins-add-user-form
+ min-width 520px
+
+ .form-input-fields
+ margin 20px 0px
+
+ che-filter-selector
+ margin-bottom 20px
+ line-height 33px
+
+ .che-input-box-desktop
+ width 520px
+ max-width 520px
+
+ label
+ max-width 23% !important
+ width 23% !important
+ margin-top 10px
+
+ input
+ width 400px
+ float right
+
+.admins-add-user-form .che-input-desktop-label
+ margin-top 1px
+
+.admins-add-user-form .password-prompt,
+.admins-add-user-form .che-input-desktop-label
+ min-width 125px
+
+.admins-add-user-form .password-prompt
+ font-size 12px
+ color $disabled-color
+ margin-bottom 20px
+
+.admins-add-user-form .password-prompt p
+ margin-top 5px
+ margin-bottom 5px
+
+.admins-add-user-form .password-prompt
+ .password-prompt-text
+ min-width 125px
+
+ .pass-strength
+ width 100%
+
+ & > div
+ margin-bottom 10px
diff --git a/dashboard/src/app/admin/user-management/user-details/user-details.controller.ts b/dashboard/src/app/admin/user-management/user-details/user-details.controller.ts
new file mode 100644
index 0000000000..253bd10124
--- /dev/null
+++ b/dashboard/src/app/admin/user-management/user-details/user-details.controller.ts
@@ -0,0 +1,243 @@
+/*
+ * Copyright (c) 2015-2017 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+'use strict';
+
+enum Tab {Profile, Organization}
+
+interface IScope extends ng.IScope {
+ profileInformationForm: ng.IFormController;
+}
+
+interface IProfileAttributes {
+ firstName?: string;
+ lastName?: string;
+ phone?: string;
+ country?: string;
+ employer?: string;
+ jobtitle?: string;
+}
+
+const MAX_ITEMS = 12;
+
+/**
+ * Controller for user details.
+ *
+ * @author Oleksii Orel
+ */
+export class AdminUserDetailsController {
+ tab: Object = Tab;
+
+ /**
+ * Angular Location service.
+ */
+ private $location: ng.ILocationService;
+ /**
+ * User profile service.
+ */
+ private cheProfile: any;
+ /**
+ * Notification service.
+ */
+ private cheNotification: any;
+ /**
+ * Index of the selected tab.
+ */
+ private selectedTabIndex: number = 0;
+ /**
+ * User profile.
+ */
+ private profile: che.IProfile;
+ /**
+ * Profile attributes.
+ */
+ private profileAttributes: IProfileAttributes;
+ /**
+ * Loading state of the page.
+ */
+ private isLoading: boolean;
+ /**
+ * User ID.
+ */
+ private userId: string;
+ /**
+ * User Name.
+ */
+ private userName: string;
+
+ private cheOrganization: che.api.ICheOrganization;
+
+ private userOrganizations: Array;
+ /**
+ * User's page info.
+ */
+ private pageInfo: che.IPageInfo;
+
+ /**
+ * Default constructor that is using resource injection
+ * @ngInject for Dependency injection
+ */
+ constructor(cheProfile: any, $location: ng.ILocationService, $timeout: ng.ITimeoutService, $scope: ng.IScope, cheNotification: any, cheOrganization: che.api.ICheOrganization, initData: {userId; userName}) {
+ this.cheOrganization = cheOrganization;
+ this.$location = $location;
+ this.cheProfile = cheProfile;
+ this.cheNotification = cheNotification;
+ this.userId = initData.userId;
+ this.userName = initData.userName;
+
+ this.updateSelectedTab(this.$location.search().tab);
+ let deRegistrationFn = $scope.$watch(() => {
+ return $location.search().tab;
+ }, (tab: string) => {
+ if (!angular.isUndefined(tab)) {
+ this.updateSelectedTab(tab);
+ }
+ }, true);
+
+ let timeoutPromise: ng.IPromise;
+ $scope.$watch(() => {
+ return angular.isUndefined(this.profileAttributes) || this.profileAttributes;
+ }, () => {
+ if (!this.profileAttributes || !($scope).profileInformationForm || ($scope).profileInformationForm.$invalid) {
+ return;
+ }
+ if (timeoutPromise) {
+ $timeout.cancel(timeoutPromise);
+ }
+ timeoutPromise = $timeout(() => {
+ this.setProfileAttributes();
+ }, 500);
+ }, true);
+
+ $scope.$on('$destroy', () => {
+ deRegistrationFn();
+ if (timeoutPromise) {
+ $timeout.cancel(timeoutPromise);
+ }
+ });
+
+ this.updateData();
+ }
+
+ /**
+ * Update user's data.
+ */
+ updateData(): void {
+ this.isLoading = true;
+ this.cheProfile.fetchProfileById(this.userId).then(() => {
+ this.profile = this.cheProfile.getProfileById(this.userId);
+ this.profileAttributes = angular.copy(this.profile.attributes);
+ this.fetchOrganizations();
+ }, (error: any) => {
+ this.isLoading = false;
+ this.cheNotification.showError(error && error.data && error.data.message !== null ? error.data.message : 'Failed to retrieve user\'s profile.');
+ });
+ }
+
+ /**
+ * Request the list of the user's organizations (first page).
+ *
+ * @returns {ng.IPromise}
+ */
+ fetchOrganizations(): void {
+ this.isLoading = true;
+ this.cheOrganization.fetchUserOrganizations(this.userId, MAX_ITEMS).then((userOrganizations: Array) => {
+ this.userOrganizations = userOrganizations;
+ }, (error: any) => {
+ this.cheNotification.showError(error && error.data && error.data.message !== null ? error.data.message : 'Failed to retrieve organizations.');
+ }).finally(() => {
+ this.isLoading = false;
+ this.pageInfo = this.cheOrganization.getUserOrganizationPageInfo(this.userId);
+ });
+ }
+
+ /**
+ * Returns the array of user's organizations.
+ *
+ * @returns {Array}
+ */
+ getUserOrganizations(): Array {
+ return this.userOrganizations;
+ }
+
+ /**
+ * Returns the the user's page info.
+ *
+ * @returns {che.IPageInfo}
+ */
+ getPagesInfo(): che.IPageInfo {
+ return this.pageInfo;
+ }
+
+ /**
+ * Request the list of the user's organizations for a page depends on page key('first', 'prev', 'next', 'last').
+ * @param key {string}
+ */
+ fetchOrganizationPageObjects(key: string): void {
+ this.isLoading = true;
+ this.cheOrganization.fetchUserOrganizationPageObjects(this.userId, key).then((userOrganizations: Array) => {
+ this.userOrganizations = userOrganizations;
+ }).finally(() => {
+ this.isLoading = false;
+ });
+ }
+
+ /**
+ * Check if profile attributes have changed
+ * @returns {boolean}
+ */
+ isAttributesChanged(): boolean {
+ return !angular.equals(this.profile.attributes, this.profileAttributes);
+ }
+
+ /**
+ * Set profile attributes
+ */
+ setProfileAttributes(): void {
+ if (angular.equals(this.profile.attributes, this.profileAttributes)) {
+ return;
+ }
+ let promise = this.cheProfile.setAttributes(this.profileAttributes, this.userId);
+
+ promise.then(() => {
+ this.cheNotification.showInfo('Profile successfully updated.');
+ this.updateData();
+ }, (error: any) => {
+ this.profileAttributes = angular.copy(this.profile.attributes);
+ this.cheNotification.showError(error.data.message ? error.data.message : 'Profile update failed.');
+ });
+ }
+
+ /**
+ * Update selected tab index by search part of URL.
+ *
+ * @param {string} tab
+ */
+ updateSelectedTab(tab: string): void {
+ this.selectedTabIndex = parseInt(this.tab[tab], 10);
+ }
+
+ /**
+ * Changes search part of URL.
+ *
+ * @param {number} tabIndex
+ */
+ onSelectTab(tabIndex?: number): void {
+ let param: { tab?: string } = {};
+ if (!angular.isUndefined(tabIndex)) {
+ param.tab = Tab[tabIndex];
+ }
+ if (angular.isUndefined(this.$location.search().tab)) {
+ this.$location.replace();
+ }
+ this.$location.search(param);
+ }
+
+}
diff --git a/dashboard/src/app/admin/user-management/user-details/user-details.html b/dashboard/src/app/admin/user-management/user-details/user-details.html
new file mode 100644
index 0000000000..8cd58b10dc
--- /dev/null
+++ b/dashboard/src/app/admin/user-management/user-details/user-details.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+ Profile
+
+
+
+
+
+
+
+
+
+
+
+
+ Organizations
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dashboard/src/app/admin/user-management/user-management-config.ts b/dashboard/src/app/admin/user-management/user-management-config.ts
new file mode 100644
index 0000000000..0375f9528e
--- /dev/null
+++ b/dashboard/src/app/admin/user-management/user-management-config.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2015-2017 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+'use strict';
+
+import {AdminsAddUserController} from './add-user/add-user.controller';
+import {AdminsUserManagementCtrl} from './user-management.controller';
+import {AdminUserDetailsController} from './user-details/user-details.controller';
+import {AccountProfile} from './account-profile/account-profile.directive';
+
+export class AdminsUserManagementConfig {
+
+ constructor(register: che.IRegisterService) {
+ register.controller('AdminUserDetailsController', AdminUserDetailsController);
+ register.controller('AdminsAddUserController', AdminsAddUserController);
+ register.controller('AdminsUserManagementCtrl', AdminsUserManagementCtrl);
+ register.directive('accountProfile', AccountProfile);
+
+ const userDetailLocationProvider = {
+ title: 'User Details',
+ reloadOnSearch: false,
+ templateUrl: 'app/admin/user-management/user-details/user-details.html',
+ controller: 'AdminUserDetailsController',
+ controllerAs: 'adminUserDetailsController',
+ resolve: {
+ initData: ['$q', 'cheUser', '$route', 'chePermissions', ($q: ng.IQService, cheUser: any, $route: any, chePermissions: che.api.IChePermissions) => {
+ const userId = $route.current.params.userId;
+ let defer = $q.defer();
+ chePermissions.fetchSystemPermissions().finally(() => {
+ cheUser.fetchUserId(userId).then((user: che.IUser) => {
+ if (!chePermissions.getUserServices().hasAdminUserService) {
+ defer.reject();
+ }
+ defer.resolve({userId: userId, userName: user.name});
+ }, (error: any) => {
+ defer.reject(error);
+ });
+ });
+ return defer.promise;
+ }]
+ }
+ };
+
+ // configure routes
+ register.app.config(($routeProvider: che.route.IRouteProvider) => {
+ $routeProvider.accessWhen('/admin/usermanagement', {
+ title: 'Users',
+ templateUrl: 'app/admin/user-management/user-management.html',
+ controller: 'AdminsUserManagementCtrl',
+ controllerAs: 'adminsUserManagementCtrl',
+ resolve: {
+ check: ['$q', 'chePermissions', ($q: ng.IQService, chePermissions: che.api.IChePermissions) => {
+ let defer = $q.defer();
+ chePermissions.fetchSystemPermissions().finally(() => {
+ if (chePermissions.getUserServices().hasUserService) {
+ defer.resolve();
+ } else {
+ defer.reject();
+ }
+ });
+ return defer.promise;
+ }]
+ }
+ })
+ .accessWhen('/admin/userdetails/:userId', userDetailLocationProvider);
+ });
+
+ }
+}
diff --git a/dashboard/src/app/admin/user-management/user-management.controller.ts b/dashboard/src/app/admin/user-management/user-management.controller.ts
new file mode 100644
index 0000000000..304bd2a3d1
--- /dev/null
+++ b/dashboard/src/app/admin/user-management/user-management.controller.ts
@@ -0,0 +1,298 @@
+/*
+ * Copyright (c) 2015-2017 Red Hat, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+'use strict';
+
+const MAX_ITEMS = 12;
+
+/**
+ * This class is handling the controller for the admins user management
+ * @author Oleksii Orel
+ */
+export class AdminsUserManagementCtrl {
+ $q: ng.IQService;
+ $log: ng.ILogService;
+ $mdDialog: ng.material.IDialogService;
+ $location: ng.ILocationService;
+ cheUser: any;
+ cheNotification: any;
+ pagesInfo: any;
+ users: Array;
+ usersMap: Map;
+ userFilter: {name: string};
+ userOrderBy: string;
+ isLoading: boolean;
+
+ private confirmDialogService: any;
+ private cheOrganization: che.api.ICheOrganization;
+ private userOrganizationCount: {[userId: string]: number} = {};
+ private cheListHelper: che.widget.ICheListHelper;
+
+ /**
+ * Default constructor.
+ * @ngInject for Dependency injection
+ */
+ constructor($q: ng.IQService,
+ $rootScope: che.IRootScopeService,
+ $log: ng.ILogService,
+ $mdDialog: ng.material.IDialogService,
+ cheUser: any,
+ $location: ng.ILocationService,
+ cheNotification: any,
+ confirmDialogService: any,
+ cheOrganization: che.api.ICheOrganization,
+ $scope: ng.IScope,
+ cheListHelperFactory: che.widget.ICheListHelperFactory) {
+ this.$q = $q;
+ this.$log = $log;
+ this.$mdDialog = $mdDialog;
+ this.$location = $location;
+ this.cheUser = cheUser;
+ this.cheOrganization = cheOrganization;
+ this.cheNotification = cheNotification;
+ this.confirmDialogService = confirmDialogService;
+
+ $rootScope.showIDE = false;
+
+ this.isLoading = false;
+
+ this.users = [];
+ this.usersMap = this.cheUser.getUsersMap();
+
+ this.userOrderBy = 'name';
+ this.userFilter = {name: ''};
+
+ const helperId = 'user-management';
+ this.cheListHelper = cheListHelperFactory.getHelper(helperId);
+ $scope.$on('$destroy', () => {
+ cheListHelperFactory.removeHelper(helperId);
+ });
+
+ if (this.usersMap && this.usersMap.size > 1) {
+ this.updateUsers();
+ } else {
+ this.isLoading = true;
+ this.cheUser.fetchUsers(MAX_ITEMS, 0).then(() => {
+ this.isLoading = false;
+ this.updateUsers();
+ }, (error: any) => {
+ this.isLoading = false;
+ if (error && error.status !== 304) {
+ this.cheNotification.showError(error.data && error.data.message ? error.data.message : 'Failed to retrieve the list of users.');
+ }
+ });
+ }
+
+ this.pagesInfo = this.cheUser.getPagesInfo();
+ }
+
+ /**
+ * Callback when name is changed.
+ *
+ * @param str {string} a string to filter user names.
+ */
+ onSearchChanged(str: string): void {
+ this.userFilter.name = str;
+ this.cheListHelper.applyFilter('name', this.userFilter);
+ }
+
+ /**
+ * Redirect to user details
+ * @param userId {string}
+ * @param tab {string}
+ */
+ redirectToUserDetails(userId: string, tab?: string): void {
+ this.$location.path('/admin/userdetails/' + userId).search(!tab ? {} : {tab: tab});
+ }
+
+ /**
+ * Update user's organizations count
+ * @param userId {string}
+ */
+ updateUserOrganizationsCount(userId: string): void {
+ this.cheOrganization.fetchUserOrganizations(userId, 1).then((userOrganizations: Array) => {
+ if (!angular.isArray(userOrganizations) || userOrganizations.length === 0) {
+ return;
+ }
+ this.userOrganizationCount[userId] = this.cheOrganization.getUserOrganizationPageInfo(userId).countPages;
+ });
+ }
+
+ /**
+ * User clicked on the - action to remove the user. Show the dialog
+ * @param event {MouseEvent} - the $event
+ * @param user {any} - the selected user
+ */
+ removeUser(event: MouseEvent, user: any): void {
+ let content = 'Are you sure you want to remove \'' + user.email + '\'?';
+ let promise = this.confirmDialogService.showConfirmDialog('Remove user', content, 'Delete', 'Cancel');
+
+ promise.then(() => {
+ this.isLoading = true;
+ let promise = this.cheUser.deleteUserById(user.id);
+ promise.then(() => {
+ this.isLoading = false;
+ this.updateUsers();
+ }, (error: any) => {
+ this.isLoading = false;
+ this.cheNotification.showError(error.data && error.data.message ? error.data.message : 'Delete user failed.');
+ });
+ });
+ }
+
+ /**
+ * Delete all selected users
+ */
+ deleteSelectedUsers(): void {
+ const selectedUsers = this.cheListHelper.getSelectedItems(),
+ selectedUserIds = selectedUsers.map((user: che.IUser) => {
+ return user.id;
+ });
+
+ const queueLength = selectedUserIds.length;
+ if (!queueLength) {
+ this.cheNotification.showError('No such user.');
+ return;
+ }
+
+ const confirmationPromise = this.showDeleteUsersConfirmation(queueLength);
+ confirmationPromise.then(() => {
+ const numberToDelete = queueLength;
+ const deleteUserPromises = [];
+ let isError = false;
+ let currentUserId;
+
+ selectedUserIds.forEach((userId: string) => {
+ currentUserId = userId;
+ this.cheListHelper.itemsSelectionStatus[userId] = false;
+
+ let promise = this.cheUser.deleteUserById(userId);
+ promise.catch((error: any) => {
+ isError = true;
+ this.$log.error('Cannot delete user: ', error);
+ });
+ deleteUserPromises.push(promise);
+ });
+
+ this.$q.all(deleteUserPromises).finally(() => {
+ this.isLoading = true;
+
+ const promise = this.cheUser.fetchUsersPage(this.pagesInfo.currentPageNumber);
+ promise.then(() => {
+ this.isLoading = false;
+ this.updateUsers();
+ }, (error: any) => {
+ this.isLoading = false;
+ this.$log.error(error);
+ });
+
+ if (isError) {
+ this.cheNotification.showError('Delete failed.');
+ } else {
+ if (numberToDelete === 1) {
+ this.cheNotification.showInfo('Selected user has been removed.');
+ } else {
+ this.cheNotification.showInfo('Selected users have been removed.');
+ }
+ }
+ });
+ });
+ }
+
+ /**
+ * Show confirmation popup before delete
+ * @param numberToDelete {number}
+ * @returns {angular.IPromise}
+ */
+ showDeleteUsersConfirmation(numberToDelete: number): angular.IPromise {
+ let content = 'Are you sure you want to remove ' + numberToDelete + ' selected ';
+ if (numberToDelete > 1) {
+ content += 'users?';
+ } else {
+ content += 'user?';
+ }
+
+ return this.confirmDialogService.showConfirmDialog('Remove users', content, 'Delete', 'Cancel');
+ }
+
+ /**
+ * Update users array
+ */
+ updateUsers(): void {
+ // update users array
+ this.users.length = 0;
+ this.usersMap.forEach((user: any) => {
+ this.users.push(user);
+ });
+
+ this.cheListHelper.setList(this.users, 'id');
+ }
+
+ /**
+ * Ask for loading the users page in asynchronous way
+ * @param pageKey {string} - the key of page
+ */
+ fetchUsersPage(pageKey: string): void {
+ this.isLoading = true;
+ let promise = this.cheUser.fetchUsersPage(pageKey);
+
+ promise.then(() => {
+ this.isLoading = false;
+ this.updateUsers();
+ }, (error: any) => {
+ this.isLoading = false;
+ if (error.status === 304) {
+ this.updateUsers();
+ } else {
+ this.cheNotification.showError(error.data && error.data.message ? error.data.message : 'Update information failed.');
+ }
+ });
+ }
+
+ /**
+ * Returns true if the next page is exist.
+ * @returns {boolean}
+ */
+ hasNextPage(): boolean {
+ return this.pagesInfo.currentPageNumber < this.pagesInfo.countOfPages;
+ }
+
+ /**
+ * Returns true if the previous page is exist.
+ * @returns {boolean}
+ */
+ hasPreviousPage(): boolean {
+ return this.pagesInfo.currentPageNumber > 1;
+ }
+
+ /**
+ * Returns true if we have more then one page.
+ * @returns {boolean}
+ */
+ isPagination(): boolean {
+ return this.pagesInfo.countOfPages > 1;
+ }
+
+ /**
+ * Add a new user. Show the dialog
+ * @param event {MouseEvent} - the $event
+ */
+ showAddUserDialog(event: MouseEvent): void {
+ this.$mdDialog.show({
+ targetEvent: event,
+ bindToController: true,
+ clickOutsideToClose: true,
+ controller: 'AdminsAddUserController',
+ controllerAs: 'adminsAddUserController',
+ locals: {callbackController: this},
+ templateUrl: 'app/admin/user-management/add-user/add-user.html'
+ });
+ }
+}
diff --git a/dashboard/src/app/admin/user-management/user-management.html b/dashboard/src/app/admin/user-management/user-management.html
new file mode 100644
index 0000000000..49329e16f1
--- /dev/null
+++ b/dashboard/src/app/admin/user-management/user-management.html
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
{{user.email}}
+
+
+
+ {{user.name}}
+
+
+
+ {{adminsUserManagementCtrl.userOrganizationCount[user.id] ? adminsUserManagementCtrl.userOrganizationCount[user.id] : '-'}}
+
+
+
+
+
+
+
+ <<
+
+
+ <
+
+
+ {{adminsUserManagementCtrl.pagesInfo.currentPageNumber}}
+
+
+ >
+
+
+ >>
+
+
+
+
+
+ No users found.
+
+ There are no users.
+
+
+
diff --git a/dashboard/src/app/admin/user-management/user-management.styl b/dashboard/src/app/admin/user-management/user-management.styl
new file mode 100644
index 0000000000..1d30dab23a
--- /dev/null
+++ b/dashboard/src/app/admin/user-management/user-management.styl
@@ -0,0 +1,17 @@
+.admins-user-management
+ .user-email,
+ .user-description
+ max-width 100%
+ .user-description
+ color $label-info-color
+ .user-face
+ che-developers-face()
+ height 16px
+ width 16px
+ margin-right 5px
+
+ .che-list-item
+ outline none
+ user-select none
+ *
+ outline inherit
diff --git a/dashboard/src/app/index.module.ts b/dashboard/src/app/index.module.ts
index 94c048f8be..5b65788aad 100755
--- a/dashboard/src/app/index.module.ts
+++ b/dashboard/src/app/index.module.ts
@@ -36,12 +36,81 @@ import IdeIFrameSvc from './ide/ide-iframe/ide-iframe.service';
import {CheIdeFetcher} from '../components/ide-fetcher/che-ide-fetcher.service';
import {RouteHistory} from '../components/routing/route-history.service';
import {CheUIElementsInjectorService} from '../components/service/injector/che-ui-elements-injector.service';
-import {WorkspaceDetailsService} from './workspaces/workspace-details/workspace-details.service';
+import {OrganizationsConfig} from './organizations/organizations-config';
+import {TeamsConfig} from './teams/teams-config';
// init module
const initModule = angular.module('userDashboard', ['ngAnimate', 'ngCookies', 'ngTouch', 'ngSanitize', 'ngResource', 'ngRoute',
'angular-websocket', 'ui.bootstrap', 'ui.codemirror', 'ngMaterial', 'ngMessages', 'angularMoment', 'angular.filter',
- 'ngDropdowns', 'ngLodash', 'angularCharts', 'uuid4', 'angularFileUpload']);
+ 'ngDropdowns', 'ngLodash', 'angularCharts', 'uuid4', 'angularFileUpload', 'ui.gravatar']);
+
+declare const Keycloak: Function;
+function buildKeycloakConfig(keycloakSettings: any) {
+ return {
+ url: keycloakSettings['che.keycloak.auth_server_url'],
+ realm: keycloakSettings['che.keycloak.realm'],
+ clientId: keycloakSettings['che.keycloak.client_id']
+ };
+}
+interface IResolveFn {
+ (value: T | PromiseLike): Promise;
+}
+interface IRejectFn {
+ (reason: any): Promise;
+}
+function keycloakLoad(keycloakSettings: any) {
+ return new Promise((resolve: IResolveFn, reject: IRejectFn) => {
+ const script = document.createElement('script');
+ script.async = true;
+ script.src = keycloakSettings['che.keycloak.auth_server_url'] + '/js/keycloak.js';
+ script.addEventListener('load', resolve);
+ script.addEventListener('error', () => reject('Error loading script.'));
+ script.addEventListener('abort', () => reject('Script loading aborted.'));
+ document.head.appendChild(script);
+ });
+}
+function keycloakInit(keycloakConfig: any) {
+ return new Promise((resolve: IResolveFn, reject: IRejectFn) => {
+ const keycloak = Keycloak(keycloakConfig);
+ keycloak.init({
+ onLoad: 'login-required', checkLoginIframe: false
+ }).success(() => {
+ resolve(keycloak);
+ }).error((error: any) => {
+ reject(error);
+ });
+ });
+}
+
+const keycloakAuth = {
+ isPresent: false,
+ keycloak: null,
+ config: null
+};
+initModule.constant('keycloakAuth', keycloakAuth);
+
+const promise = new Promise((resolve: IResolveFn, reject: IRejectFn) => {
+ angular.element.get('/api/keycloak/settings').then(resolve, reject);
+});
+promise.then((keycloakSettings: any) => {
+ keycloakAuth.config = buildKeycloakConfig(keycloakSettings);
+
+ // load Keycloak
+ return keycloakLoad(keycloakSettings).then(() => {
+ // init Keycloak
+ return keycloakInit(keycloakAuth.config);
+ }).then((keycloak: any) => {
+ keycloakAuth.isPresent = true;
+ keycloakAuth.keycloak = keycloak;
+ /* tslint:disable */
+ window['_keycloak'] = keycloak;
+ /* tslint:enable */
+ });
+}).catch((error: any) => {
+ console.error('Keycloak initialization failed with error: ', error);
+}).then(() => {
+ angular.bootstrap(document.body, ['userDashboard'], {strictDi: true}); // manually bootstrap Angular
+});
// add a global resolve flag on all routes (user needs to be resolved first)
initModule.config(['$routeProvider', ($routeProvider: che.route.IRouteProvider) => {
@@ -92,7 +161,7 @@ initModule.config(['$routeProvider', ($routeProvider: che.route.IRouteProvider)
const DEV = false;
// configs
-initModule.config(['$routeProvider', ($routeProvider: che.route.IRouteProvider) => {
+initModule.config(['$routeProvider', ($routeProvider) => {
// config routes (add demo page)
if (DEV) {
$routeProvider.accessWhen('/demo-components', {
@@ -113,13 +182,11 @@ initModule.config(['$routeProvider', ($routeProvider: che.route.IRouteProvider)
* Setup route redirect module
*/
initModule.run(['$rootScope', '$location', '$routeParams', 'routingRedirect', '$timeout', 'ideIFrameSvc', 'cheIdeFetcher', 'routeHistory', 'cheUIElementsInjectorService', 'workspaceDetailsService',
- ($rootScope: che.IRootScopeService, $location: ng.ILocationService, $routeParams: ng.route.IRouteParamsService, routingRedirect: RoutingRedirect, $timeout: ng.ITimeoutService, ideIFrameSvc: IdeIFrameSvc, cheIdeFetcher: CheIdeFetcher, routeHistory: RouteHistory, cheUIElementsInjectorService: CheUIElementsInjectorService, workspaceDetailsService: WorkspaceDetailsService) => {
+ ($rootScope: che.IRootScopeService, $location: ng.ILocationService, $routeParams: ng.route.IRouteParamsService, routingRedirect: RoutingRedirect, $timeout: ng.ITimeoutService, ideIFrameSvc: IdeIFrameSvc, cheIdeFetcher: CheIdeFetcher, routeHistory: RouteHistory, cheUIElementsInjectorService: CheUIElementsInjectorService) => {
$rootScope.hideLoader = false;
$rootScope.waitingLoaded = false;
$rootScope.showIDE = false;
- workspaceDetailsService.addPage('SSH', '', 'icon-ic_vpn_key_24px');
-
// here only to create instances of these components
/* tslint:disable */
cheIdeFetcher;
@@ -172,45 +239,6 @@ initModule.run(['$rootScope', '$location', '$routeParams', 'routingRedirect', '$
}
]);
-// add interceptors
-initModule.factory('ETagInterceptor', ($window: ng.IWindowService, $cookies: ng.cookies.ICookiesService, $q: ng.IQService) => {
-
- const etagMap = {};
-
- return {
- request: (config: any) => {
- // add IfNoneMatch request on the che api if there is an existing eTag
- if ('GET' === config.method) {
- if (config.url.indexOf('/api') === 0) {
- const eTagURI = etagMap[config.url];
- if (eTagURI) {
- config.headers = config.headers || {};
- angular.extend(config.headers, {'If-None-Match': eTagURI});
- }
- }
- }
- return config || $q.when(config);
- },
- response: (response: any) => {
-
- // if response is ok, keep ETag
- if ('GET' === response.config.method) {
- if (response.status === 200) {
- const responseEtag = response.headers().etag;
- if (responseEtag) {
- if (response.config.url.indexOf('/api') === 0) {
-
- etagMap[response.config.url] = responseEtag;
- }
- }
- }
-
- }
- return response || $q.when(response);
- }
- };
-});
-
initModule.config(($mdThemingProvider: ng.material.IThemingProvider, jsonColors: any) => {
const cheColors = angular.fromJson(jsonColors);
@@ -357,11 +385,6 @@ initModule.constant('userDashboardConfig', {
developmentMode: DEV
});
-initModule.config(['$routeProvider', '$httpProvider', ($routeProvider: che.route.IRouteProvider, $httpProvider: ng.IHttpProvider) => {
- // add the ETag interceptor for Che API
- $httpProvider.interceptors.push('ETagInterceptor');
-}]);
-
const instanceRegister = new Register(initModule);
if (DEV) {
@@ -386,4 +409,6 @@ new WorkspacesConfig(instanceRegister);
new DashboardConfig(instanceRegister);
new StacksConfig(instanceRegister);
new FactoryConfig(instanceRegister);
+new OrganizationsConfig(instanceRegister);
+new TeamsConfig(instanceRegister);
/* tslint:enable */
diff --git a/dashboard/src/app/navbar/navbar.controller.ts b/dashboard/src/app/navbar/navbar.controller.ts
index 47767dcd69..e0ba472285 100644
--- a/dashboard/src/app/navbar/navbar.controller.ts
+++ b/dashboard/src/app/navbar/navbar.controller.ts
@@ -10,9 +10,10 @@
*/
'use strict';
import {CheAPI} from '../../components/api/che-api.factory';
+import {CheKeycloak, keycloakUserInfo} from '../../components/api/che-keycloak.factory';
export class CheNavBarController {
- menuItemUrl = {
+ private menuItemUrl = {
dashboard: '#/',
workspaces: '#/workspaces',
administration: '#/administration',
@@ -20,9 +21,26 @@ export class CheNavBarController {
plugins: '#/admin/plugins',
factories: '#/factories',
account: '#/account',
- stacks: '#/stacks'
+ stacks: '#/stacks',
+ organizations: '#/organizations',
+ usermanagement: '#/admin/usermanagement'
};
+ accountItems = [
+ {
+ name: 'Go to Profile',
+ onclick: () => {
+ this.gotoProfile();
+ }
+ },
+ {
+ name: 'Logout',
+ onclick: () => {
+ this.logout();
+ }
+ }
+ ];
+
private $mdSidenav: ng.material.ISidenavService;
private $scope: ng.IScope;
private $window: ng.IWindowService;
@@ -30,21 +48,39 @@ export class CheNavBarController {
private $route: ng.route.IRouteService;
private cheAPI: CheAPI;
private profile: che.IProfile;
+ private chePermissions: che.api.IChePermissions;
+ private userServices: che.IUserServices;
+ private hasPersonalAccount: boolean;
+ private organizations: Array;
+ private cheKeycloak: CheKeycloak;
+ private userInfo: keycloakUserInfo;
/**
* Default constructor
* @ngInject for Dependency injection
*/
- constructor($mdSidenav: ng.material.ISidenavService, $scope: ng.IScope, $location: ng.ILocationService, $route: ng.route.IRouteService, cheAPI: CheAPI, $window: ng.IWindowService) {
+ constructor($mdSidenav: ng.material.ISidenavService,
+ $scope: ng.IScope,
+ $location: ng.ILocationService,
+ $route: ng.route.IRouteService,
+ cheAPI: CheAPI,
+ $window: ng.IWindowService,
+ chePermissions: che.api.IChePermissions,
+ cheKeycloak: CheKeycloak) {
this.$mdSidenav = $mdSidenav;
this.$scope = $scope;
this.$location = $location;
this.$route = $route;
this.cheAPI = cheAPI;
this.$window = $window;
+ this.chePermissions = chePermissions;
+ this.cheKeycloak = cheKeycloak;
+ this.userInfo = null;
this.profile = cheAPI.getProfile().getProfile();
+ this.userServices = this.chePermissions.getUserServices();
+
// highlight navbar menu item
$scope.$on('$locationChangeStart', () => {
let path = '#' + $location.path();
@@ -53,6 +89,34 @@ export class CheNavBarController {
cheAPI.getWorkspace().fetchWorkspaces();
cheAPI.getFactory().fetchFactories();
+
+ if (this.cheKeycloak.isPresent()) {
+ this.cheKeycloak.fetchUserInfo().then((userInfo: keycloakUserInfo) => {
+ this.userInfo = userInfo;
+ });
+ }
+
+ if (this.chePermissions.getSystemPermissions()) {
+ this.updateData();
+ } else {
+ this.chePermissions.fetchSystemPermissions().finally(() => {
+ this.updateData();
+ });
+ }
+ }
+
+ /**
+ * Update data.
+ */
+ updateData(): void {
+ const organization = this.cheAPI.getOrganization();
+ organization.fetchOrganizations().then(() => {
+ this.organizations = organization.getOrganizations();
+ const user = this.cheAPI.getUser().getUser();
+ organization.fetchOrganizationByName(user.name).finally(() => {
+ this.hasPersonalAccount = angular.isDefined(organization.getOrganizationByName(user.name));
+ });
+ });
}
reload(): void {
@@ -66,15 +130,63 @@ export class CheNavBarController {
this.$mdSidenav('left').toggle();
}
+ /**
+ * Returns number of workspaces.
+ *
+ * @return {number}
+ */
getWorkspacesNumber(): number {
return this.cheAPI.getWorkspace().getWorkspaces().length;
}
+ /**
+ * Returns number of factories.
+ *
+ * @return {number}
+ */
getFactoriesNumber(): number {
return this.cheAPI.getFactory().getPageFactories().length;
}
+ /**
+ * Returns number of all organizations.
+ *
+ * @return {number}
+ */
+ getOrganizationsNumber(): number {
+ if (!this.organizations) {
+ return 0;
+ }
+
+ return this.organizations.length;
+ }
+
openLinkInNewTab(url: string): void {
this.$window.open(url, '_blank');
}
+
+ /**
+ * Returns true if Keycloak is present.
+ *
+ * @returns {boolean}
+ */
+ isKeycloakPresent(): boolean {
+ return this.cheKeycloak.isPresent();
+ }
+
+ /**
+ * Opens user profile in new browser page.
+ */
+ gotoProfile(): void {
+ const url = this.cheKeycloak.getProfileUrl();
+ this.$window.open(url);
+ }
+
+ /**
+ * Logout.
+ */
+ logout(): void {
+ this.cheKeycloak.logout();
+ }
+
}
diff --git a/dashboard/src/app/navbar/navbar.directive.ts b/dashboard/src/app/navbar/navbar.directive.ts
index 0baa7dbb76..338666e395 100644
--- a/dashboard/src/app/navbar/navbar.directive.ts
+++ b/dashboard/src/app/navbar/navbar.directive.ts
@@ -30,7 +30,7 @@ export class CheNavBar {
this.replace = false;
this.templateUrl = 'app/navbar/navbar.html';
this.controller = 'CheNavBarController';
- this.controllerAs = 'navbarCtrl';
+ this.controllerAs = 'navbarController';
}
}
diff --git a/dashboard/src/app/navbar/navbar.html b/dashboard/src/app/navbar/navbar.html
index a4b4922d1f..9376750635 100644
--- a/dashboard/src/app/navbar/navbar.html
+++ b/dashboard/src/app/navbar/navbar.html
@@ -12,8 +12,9 @@
-->