You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampipes.apache.org by ri...@apache.org on 2021/01/05 16:13:24 UTC
[incubator-streampipes] branch STREAMPIPES-272 updated:
[STREAMPIPES-276] Add simple token-based API authentication
This is an automated email from the ASF dual-hosted git repository.
riemer pushed a commit to branch STREAMPIPES-272
in repository https://gitbox.apache.org/repos/asf/incubator-streampipes.git
The following commit(s) were added to refs/heads/STREAMPIPES-272 by this push:
new 87a47b5 [STREAMPIPES-276] Add simple token-based API authentication
87a47b5 is described below
commit 87a47b51512c5e8ca6f6cdf52b0b9d00798c1a52
Author: Dominik Riemer <ri...@fzi.de>
AuthorDate: Tue Jan 5 17:13:05 2021 +0100
[STREAMPIPES-276] Add simple token-based API authentication
---
pom.xml | 2 +-
streampipes-backend/src/main/resources/shiro.ini | 4 +-
streampipes-model-client/pom.xml | 8 +-
.../model/client/user/RawUserApiToken.java | 65 +++++++++++++++
.../apache/streampipes/model/client/user/User.java | 32 ++++++++
.../model/client/user/UserApiToken.java | 62 ++++++++++++++
.../org/apache/streampipes/rest/impl/User.java | 23 ++++++
.../storage/couchdb/impl/UserStorage.java | 16 ++--
streampipes-user-management/pom.xml | 11 ++-
.../authentication/StreamPipesRealm.java | 40 +++++----
.../user/management/service/TokenService.java | 61 ++++++++++++++
.../user/management/util/TokenUtil.java | 62 ++++++++++++++
.../user/management/util/TestTokenUtil.java | 57 +++++++++++++
ui/deployment/app-routing.module.mst | 6 +-
ui/deployment/appng5.module.mst | 4 +-
ui/src/app/app-routing.module.ts | 6 +-
.../app/core-model/gen/streampipes-model-client.ts | 94 +++++++++++++++++++++-
ui/src/app/core-model/gen/streampipes-model.ts | 18 +++++
.../core/components/toolbar/toolbar.component.html | 10 ++-
.../core/components/toolbar/toolbar.component.scss | 13 ++-
.../core/components/toolbar/toolbar.component.ts | 11 ++-
.../profile/components/basic-profile-settings.ts | 58 +++++++++++++
.../general-profile-settings.component.html | 51 ++++++++++++
.../general-profile-settings.component.scss} | 19 ++++-
.../general/general-profile-settings.component.ts} | 26 +++++-
.../token/token-management-settings.component.html | 90 +++++++++++++++++++++
.../token-management-settings.component.scss} | 59 +++++++++++++-
.../token/token-management-settings.component.ts | 70 ++++++++++++++++
ui/src/app/profile/profile.component.html | 42 ++++++++++
.../profile.component.scss} | 7 +-
.../profile.component.ts} | 21 ++++-
ui/src/app/profile/profile.module.ts | 62 ++++++++++++++
ui/src/app/profile/profile.service.ts | 53 ++++++++++++
33 files changed, 1108 insertions(+), 55 deletions(-)
diff --git a/pom.xml b/pom.xml
index 719fd47..7735bd5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -92,7 +92,7 @@
<rdf4j.version>3.5.0</rdf4j.version>
<rendersnake.version>1.8</rendersnake.version>
<retrofit.version>2.5.0</retrofit.version>
- <shiro.version>1.2.3</shiro.version>
+ <shiro.version>1.7.0</shiro.version>
<siddhi.version>5.1.12</siddhi.version>
<slf4j.version>1.7.30</slf4j.version>
<snakeyaml.version>1.26</snakeyaml.version>
diff --git a/streampipes-backend/src/main/resources/shiro.ini b/streampipes-backend/src/main/resources/shiro.ini
index f6f60c6..50aaf79 100644
--- a/streampipes-backend/src/main/resources/shiro.ini
+++ b/streampipes-backend/src/main/resources/shiro.ini
@@ -72,5 +72,5 @@ securityManager.rememberMeManager.cookie.maxAge = 1000000000
/api/v2/connect/*/master/sources/*/streams = anon
/api/v2/connect/*/master/sources/*/streams/* = anon
/api/v2/connect/*/master/resolvable/*/configurations = anon
-/api/** = customFilter
-/** = customFilter
\ No newline at end of file
+/api/** = authcBearer, customFilter
+/** = authcBearer, customFilter
diff --git a/streampipes-model-client/pom.xml b/streampipes-model-client/pom.xml
index 3832689..8d72daf 100644
--- a/streampipes-model-client/pom.xml
+++ b/streampipes-model-client/pom.xml
@@ -37,6 +37,10 @@
<!-- External dependencies -->
<dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-annotations</artifactId>
+ </dependency>
+ <dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
@@ -51,7 +55,7 @@
<plugin>
<groupId>cz.habarta.typescript-generator</groupId>
<artifactId>typescript-generator-maven-plugin</artifactId>
- <version>2.24.612</version>
+ <version>2.27.744</version>
<executions>
<execution>
<id>generate</id>
@@ -87,4 +91,4 @@
</plugin>
</plugins>
</build>
-</project>
\ No newline at end of file
+</project>
diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/RawUserApiToken.java b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/RawUserApiToken.java
new file mode 100644
index 0000000..d70df9e
--- /dev/null
+++ b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/RawUserApiToken.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.streampipes.model.client.user;
+
+import org.apache.streampipes.model.shared.annotation.TsModel;
+
+@TsModel
+public class RawUserApiToken {
+
+ private String rawToken;
+ private String hashedToken;
+ private String tokenName;
+ private String tokenId;
+
+ public RawUserApiToken() {
+
+ }
+
+ public String getRawToken() {
+ return rawToken;
+ }
+
+ public void setRawToken(String rawToken) {
+ this.rawToken = rawToken;
+ }
+
+ public String getTokenName() {
+ return tokenName;
+ }
+
+ public void setTokenName(String tokenName) {
+ this.tokenName = tokenName;
+ }
+
+ public String getTokenId() {
+ return tokenId;
+ }
+
+ public void setTokenId(String tokenId) {
+ this.tokenId = tokenId;
+ }
+
+ public String getHashedToken() {
+ return hashedToken;
+ }
+
+ public void setHashedToken(String hashedToken) {
+ this.hashedToken = hashedToken;
+ }
+}
diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/User.java b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/User.java
index 1cc535f..79b8349 100644
--- a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/User.java
+++ b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/User.java
@@ -19,16 +19,21 @@
package org.apache.streampipes.model.client.user;
import com.google.gson.annotations.SerializedName;
+import org.apache.streampipes.model.shared.annotation.TsModel;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+@TsModel
public class User {
private @SerializedName("_id") String userId;
protected @SerializedName("_rev") String rev;
protected String email;
+
+ private String username;
+ private String fullName;
private String password;
private List<Element> ownSources;
@@ -39,12 +44,15 @@ public class User {
private List<String> preferredSepas;
private List<String> preferredActions;
+ private List<UserApiToken> userApiTokens;
+
private boolean hideTutorial;
private Set<Role> roles;
public User() {
this.hideTutorial = false;
+ this.userApiTokens = new ArrayList<>();
}
public User(String email, String password, Set<Role> roles, List<Element> ownSources, List<Element> ownSepas, List<Element> ownActions) {
@@ -202,6 +210,30 @@ public class User {
this.preferredActions.remove(elementId);
}
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getFullName() {
+ return fullName;
+ }
+
+ public void setFullName(String fullName) {
+ this.fullName = fullName;
+ }
+
+ public List<UserApiToken> getUserApiTokens() {
+ return userApiTokens;
+ }
+
+ public void setUserApiTokens(List<UserApiToken> userApiTokens) {
+ this.userApiTokens = userApiTokens;
+ }
+
private Element find(String elementId, List<Element> source)
{
return source.stream().filter(f -> f.getElementId().equals(elementId)).findFirst().orElseThrow(IllegalArgumentException::new);
diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserApiToken.java b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserApiToken.java
new file mode 100644
index 0000000..ee0ce0a
--- /dev/null
+++ b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/UserApiToken.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.streampipes.model.client.user;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+public class UserApiToken {
+
+ private String tokenId;
+ private String tokenName;
+
+ @JsonIgnore
+ private String hashedToken;
+
+ public UserApiToken() {
+ }
+
+ public UserApiToken(String tokenId, String tokenName, String hashedToken) {
+ this.tokenId = tokenId;
+ this.tokenName = tokenName;
+ this.hashedToken = hashedToken;
+ }
+
+ public String getTokenName() {
+ return tokenName;
+ }
+
+ public void setTokenName(String tokenName) {
+ this.tokenName = tokenName;
+ }
+
+ public String getHashedToken() {
+ return hashedToken;
+ }
+
+ public void setHashedToken(String hashedToken) {
+ this.hashedToken = hashedToken;
+ }
+
+ public String getTokenId() {
+ return tokenId;
+ }
+
+ public void setTokenId(String tokenId) {
+ this.tokenId = tokenId;
+ }
+}
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/User.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/User.java
index 4498e21..0cbc466 100644
--- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/User.java
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/User.java
@@ -18,11 +18,15 @@
package org.apache.streampipes.rest.impl;
+import org.apache.streampipes.model.client.user.RawUserApiToken;
import org.apache.streampipes.model.message.Notifications;
+import org.apache.streampipes.rest.shared.annotation.JacksonSerialized;
+import org.apache.streampipes.user.management.service.TokenService;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
+import java.util.stream.Collectors;
@Path("/v2/users/{email}")
public class User extends AbstractRestInterface {
@@ -47,6 +51,15 @@ public class User extends AbstractRestInterface {
if (user != null) {
org.apache.streampipes.model.client.user.User existingUser = getUser(user.getEmail());
user.setPassword(existingUser.getPassword());
+ user.setUserApiTokens(existingUser
+ .getUserApiTokens()
+ .stream()
+ .filter(existingToken -> user.getUserApiTokens()
+ .stream()
+ .anyMatch(updatedToken -> existingToken
+ .getTokenId()
+ .equals(updatedToken.getTokenId())))
+ .collect(Collectors.toList()));
user.setRev(existingUser.getRev());
getUserStorage().updateUser(user);
return ok(Notifications.success("User updated"));
@@ -58,4 +71,14 @@ public class User extends AbstractRestInterface {
private org.apache.streampipes.model.client.user.User getUser(String email) {
return getUserStorage().getUser(email);
}
+
+ @POST
+ @Path("tokens")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ @JacksonSerialized
+ public Response createNewApiToken(@PathParam("email") String email, RawUserApiToken rawToken) {
+ RawUserApiToken generatedToken = new TokenService().createAndStoreNewToken(email, rawToken);
+ return ok(generatedToken);
+ }
}
diff --git a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/UserStorage.java b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/UserStorage.java
index 503d7c9..e560b6e 100644
--- a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/UserStorage.java
+++ b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/UserStorage.java
@@ -18,16 +18,16 @@
package org.apache.streampipes.storage.couchdb.impl;
-import org.lightcouch.CouchDbClient;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.apache.streampipes.model.client.user.User;
import org.apache.streampipes.storage.api.IUserStorage;
import org.apache.streampipes.storage.couchdb.dao.AbstractDao;
import org.apache.streampipes.storage.couchdb.utils.Utils;
+import org.lightcouch.CouchDbClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.util.ArrayList;
import java.util.List;
-import java.util.stream.Collectors;
/**
* User Storage.
@@ -42,12 +42,12 @@ public class UserStorage extends AbstractDao<User> implements IUserStorage {
public UserStorage() {
super(Utils::getCouchDbUserClient, User.class);
}
-
+
@Override
public List<User> getAllUsers()
{
- List<User> users = findAll();
- return users.stream().collect(Collectors.toList());
+ List<User> users = findAll();
+ return new ArrayList<>(users);
}
@Override
@@ -70,7 +70,7 @@ public class UserStorage extends AbstractDao<User> implements IUserStorage {
public void updateUser(User user) {
update(user);
}
-
+
@Override
public boolean emailExists(String email)
{
diff --git a/streampipes-user-management/pom.xml b/streampipes-user-management/pom.xml
index 1d5006f..3708843 100644
--- a/streampipes-user-management/pom.xml
+++ b/streampipes-user-management/pom.xml
@@ -36,7 +36,7 @@
</dependency>
<dependency>
<groupId>org.apache.streampipes</groupId>
- <artifactId>streampipes-storage-couchdb</artifactId>
+ <artifactId>streampipes-storage-management</artifactId>
<version>0.68.0-SNAPSHOT</version>
</dependency>
@@ -49,6 +49,13 @@
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</dependency>
+
+ <!-- Test dependencies -->
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
-</project>
\ No newline at end of file
+</project>
diff --git a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authentication/StreamPipesRealm.java b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authentication/StreamPipesRealm.java
index 6cf1316..6bcd452 100644
--- a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authentication/StreamPipesRealm.java
+++ b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/authentication/StreamPipesRealm.java
@@ -19,17 +19,16 @@
package org.apache.streampipes.user.management.authentication;
-import org.apache.shiro.authc.AuthenticationException;
-import org.apache.shiro.authc.AuthenticationInfo;
-import org.apache.shiro.authc.AuthenticationToken;
-import org.apache.shiro.authc.SimpleAuthenticationInfo;
-import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.authc.*;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.subject.SimplePrincipalCollection;
+import org.apache.streampipes.model.client.user.User;
+import org.apache.streampipes.user.management.service.TokenService;
+import org.apache.streampipes.user.management.service.UserService;
+import org.apache.streampipes.user.management.util.TokenUtil;
import org.lightcouch.CouchDbException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.apache.streampipes.user.management.service.UserService;
/**
@@ -57,7 +56,7 @@ public class StreamPipesRealm implements Realm {
* So far we only support UsernamePasswordToken.
*/
public boolean supports(AuthenticationToken authenticationToken) {
- return authenticationToken instanceof UsernamePasswordToken;
+ return authenticationToken instanceof UsernamePasswordToken || authenticationToken instanceof BearerToken;
}
@Override
@@ -72,12 +71,7 @@ public class StreamPipesRealm implements Realm {
String email = ((UsernamePasswordToken) authenticationToken).getUsername();
UserService userService = new UserService(email);
- SimpleAuthenticationInfo info = new SimpleAuthenticationInfo();
- SimplePrincipalCollection principals = new SimplePrincipalCollection();
- principals.add(email, this.getName());
-
- LOG.info(principals.toString());
- info.setPrincipals(principals);
+ SimpleAuthenticationInfo info = makeInfo(email);
info.setCredentials(userService.getPassword());
if (credentialsMatcher.doCredentialsMatch(authenticationToken, info)) {
@@ -90,8 +84,26 @@ public class StreamPipesRealm implements Realm {
} catch (CouchDbException | NullPointerException e) {
e.printStackTrace();
}
+ } else if (authenticationToken instanceof BearerToken) {
+ BearerToken token = (BearerToken) authenticationToken;
+ String hashedToken = TokenUtil.hashToken(token.getToken());
+ User user = new TokenService().findUserForToken(hashedToken);
+ SimpleAuthenticationInfo info = makeInfo(user.getEmail());
+
+ return info;
}
return null;
}
-}
\ No newline at end of file
+
+ private SimpleAuthenticationInfo makeInfo(String email) {
+ SimpleAuthenticationInfo info = new SimpleAuthenticationInfo();
+ SimplePrincipalCollection principals = new SimplePrincipalCollection();
+ principals.add(email, this.getName());
+
+ LOG.info(principals.toString());
+ info.setPrincipals(principals);
+
+ return info;
+ }
+}
diff --git a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/service/TokenService.java b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/service/TokenService.java
new file mode 100644
index 0000000..76444e3
--- /dev/null
+++ b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/service/TokenService.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.streampipes.user.management.service;
+
+import com.google.gson.JsonObject;
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.streampipes.model.client.user.RawUserApiToken;
+import org.apache.streampipes.model.client.user.User;
+import org.apache.streampipes.storage.api.IUserStorage;
+import org.apache.streampipes.storage.couchdb.utils.Utils;
+import org.apache.streampipes.storage.management.StorageDispatcher;
+import org.apache.streampipes.user.management.util.TokenUtil;
+import org.lightcouch.CouchDbClient;
+
+import java.util.List;
+
+public class TokenService {
+
+ public RawUserApiToken createAndStoreNewToken(String email, RawUserApiToken baseToken) {
+ User user = getUserStorage().getUser(email);
+ RawUserApiToken generatedToken = TokenUtil.createToken(baseToken.getTokenName());
+ storeToken(user, generatedToken);
+ generatedToken.setHashedToken("");
+ return generatedToken;
+ }
+
+ private void storeToken(User user, RawUserApiToken generatedToken) {
+ user.getUserApiTokens().add(TokenUtil.toUserToken(generatedToken));
+ getUserStorage().updateUser(user);
+ }
+
+ private IUserStorage getUserStorage() {
+ return StorageDispatcher.INSTANCE
+ .getNoSqlStore()
+ .getUserStorageAPI();
+ }
+
+ public User findUserForToken(String token) {
+ CouchDbClient dbClient = Utils.getCouchDbUserClient();
+ List<JsonObject> users = dbClient.view("users/token").key(token).includeDocs(true).query(JsonObject.class);
+ if (users.size() != 1) {
+ throw new AuthenticationException("None or too many users with matching token");
+ }
+ return getUserStorage().getUser(users.get(0).get("email").getAsString());
+ }
+}
diff --git a/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/util/TokenUtil.java b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/util/TokenUtil.java
new file mode 100644
index 0000000..e0c873f
--- /dev/null
+++ b/streampipes-user-management/src/main/java/org/apache/streampipes/user/management/util/TokenUtil.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.streampipes.user.management.util;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.streampipes.model.client.user.RawUserApiToken;
+import org.apache.streampipes.model.client.user.UserApiToken;
+
+import java.util.UUID;
+
+public class TokenUtil {
+
+ private static final Integer TOKEN_LENGTH = 24;
+
+ public static RawUserApiToken createToken(String tokenName) {
+ RawUserApiToken rawToken = new RawUserApiToken();
+ rawToken.setTokenId(UUID.randomUUID().toString());
+ rawToken.setTokenName(tokenName);
+ rawToken.setRawToken(generateToken());
+ rawToken.setHashedToken(hashToken(rawToken.getRawToken()));
+
+ return rawToken;
+ }
+
+ public static UserApiToken toUserToken(RawUserApiToken rawToken) {
+ UserApiToken userApiToken = new UserApiToken();
+ userApiToken.setTokenId(rawToken.getTokenId());
+ userApiToken.setHashedToken(rawToken.getHashedToken());
+ userApiToken.setTokenName(rawToken.getTokenName());
+
+ return userApiToken;
+ }
+
+ public static boolean validateToken(String providedToken, String hashedToken) {
+ return hashToken(providedToken).equals(hashedToken);
+ }
+
+ private static String generateToken() {
+ return RandomStringUtils.randomAlphanumeric(TOKEN_LENGTH);
+ }
+
+ public static String hashToken(String token) {
+ return DigestUtils.sha256Hex(token);
+ }
+
+}
diff --git a/streampipes-user-management/src/test/java/org/apache/streampipes/user/management/util/TestTokenUtil.java b/streampipes-user-management/src/test/java/org/apache/streampipes/user/management/util/TestTokenUtil.java
new file mode 100644
index 0000000..38b8da0
--- /dev/null
+++ b/streampipes-user-management/src/test/java/org/apache/streampipes/user/management/util/TestTokenUtil.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.streampipes.user.management.util;
+
+import org.apache.streampipes.model.client.user.RawUserApiToken;
+import org.apache.streampipes.model.client.user.UserApiToken;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class TestTokenUtil {
+
+ @Test
+ public void testCreateToken() {
+ String tokenName = "test-token";
+
+ RawUserApiToken rawToken = TokenUtil.createToken(tokenName);
+ assertEquals("test-token", rawToken.getTokenName());
+ assertNotNull(rawToken.getTokenId());
+ assertNotNull(rawToken.getRawToken());
+ assertNotNull(rawToken.getHashedToken());
+ }
+
+ @Test
+ public void testToUserToken() {
+ RawUserApiToken rawToken = TokenUtil.createToken("test-token");
+ UserApiToken token = TokenUtil.toUserToken(rawToken);
+
+ assertEquals(rawToken.getHashedToken(), token.getHashedToken());
+ assertEquals(rawToken.getTokenId(), token.getTokenId());
+ assertEquals(rawToken.getTokenName(), token.getTokenName());
+ }
+
+ @Test
+ public void testTokenValidation() {
+ RawUserApiToken rawToken = TokenUtil.createToken("test-token");
+ String rawApiKey = rawToken.getRawToken();
+
+ assertTrue(TokenUtil.validateToken(rawApiKey, rawToken.getHashedToken()));
+ }
+
+}
diff --git a/ui/deployment/app-routing.module.mst b/ui/deployment/app-routing.module.mst
index b76a04d..ec50e00 100644
--- a/ui/deployment/app-routing.module.mst
+++ b/ui/deployment/app-routing.module.mst
@@ -31,6 +31,7 @@ import {AlreadyConfiguredCanActivateGuard} from "./_guards/already-configured.ca
import {LoggedInCanActivateGuard} from "./_guards/logged-in.can-activate.guard";
import {InfoComponent} from "./info/info.component";
import {NotificationsComponent} from "./notifications/notifications.component";
+import {ProfileComponent} from "./profile/profile.component";
{{#modulesActive}}
{{#ng5}}
@@ -53,7 +54,8 @@ const routes: Routes = [
{{/modulesActive}}
{ path: 'notifications', component: NotificationsComponent },
{ path: 'info', component: InfoComponent },
- { path: 'pipeline-details', component: PipelineDetailsComponent }
+ { path: 'pipeline-details', component: PipelineDetailsComponent },
+ { path: 'profile', component: ProfileComponent},
], canActivateChild: [AuthCanActivateChildrenGuard] }
];
@@ -67,4 +69,4 @@ const routes: Routes = [
LoggedInCanActivateGuard
]
})
-export class AppRoutingModule { }
\ No newline at end of file
+export class AppRoutingModule { }
diff --git a/ui/deployment/appng5.module.mst b/ui/deployment/appng5.module.mst
index 0d3494e..b1fdf9a 100644
--- a/ui/deployment/appng5.module.mst
+++ b/ui/deployment/appng5.module.mst
@@ -32,6 +32,7 @@ import { CoreModule } from './core/core.module';
import { LoginModule } from './login/login.module';
import { HomeModule } from './home/home.module';
import { InfoModule } from './info/info.module';
+import {ProfileModule} from "./profile/profile.module";
import { PipelineDetailsModule } from './pipeline-details/pipeline-details.module';
import { NotificationCountService } from "./services/notification-count-service";
import { AuthService } from "./services/auth.service";
@@ -80,6 +81,7 @@ import * as $ from 'jquery';
PlatformServicesModule,
PipelineDetailsModule,
ServicesModule,
+ ProfileModule,
{{#modulesActive}}
{{#ng5}}
{{{ng5_moduleName}}},
@@ -102,4 +104,4 @@ import * as $ from 'jquery';
})
export class AppModule {
-}
\ No newline at end of file
+}
diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts
index f5f773a..ed25a92 100644
--- a/ui/src/app/app-routing.module.ts
+++ b/ui/src/app/app-routing.module.ts
@@ -31,6 +31,7 @@ import {AlreadyConfiguredCanActivateGuard} from "./_guards/already-configured.ca
import {LoggedInCanActivateGuard} from "./_guards/logged-in.can-activate.guard";
import {InfoComponent} from "./info/info.component";
import {NotificationsComponent} from "./notifications/notifications.component";
+import {ProfileComponent} from "./profile/profile.component";
import { EditorComponent } from './editor/editor.component';
import { PipelinesComponent } from './pipelines/pipelines.component';
@@ -61,7 +62,8 @@ const routes: Routes = [
{ path: 'configuration', component: ConfigurationComponent },
{ path: 'notifications', component: NotificationsComponent },
{ path: 'info', component: InfoComponent },
- { path: 'pipeline-details', component: PipelineDetailsComponent }
+ { path: 'pipeline-details', component: PipelineDetailsComponent },
+ { path: 'profile', component: ProfileComponent},
], canActivateChild: [AuthCanActivateChildrenGuard] }
];
@@ -75,4 +77,4 @@ const routes: Routes = [
LoggedInCanActivateGuard
]
})
-export class AppRoutingModule { }
\ No newline at end of file
+export class AppRoutingModule { }
diff --git a/ui/src/app/core-model/gen/streampipes-model-client.ts b/ui/src/app/core-model/gen/streampipes-model-client.ts
index e9fdac0..b168ade 100644
--- a/ui/src/app/core-model/gen/streampipes-model-client.ts
+++ b/ui/src/app/core-model/gen/streampipes-model-client.ts
@@ -19,7 +19,22 @@
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
-// Generated using typescript-generator version 2.24.612 on 2020-09-14 21:40:09.
+// Generated using typescript-generator version 2.27.744 on 2021-01-05 12:29:12.
+
+export class Element {
+ elementId: string;
+ publicElement: boolean;
+
+ static fromData(data: Element, target?: Element): Element {
+ if (!data) {
+ return data;
+ }
+ const instance = target || new Element();
+ instance.elementId = data.elementId;
+ instance.publicElement = data.publicElement;
+ return instance;
+ }
+}
export class FileMetadata {
createdAt: number;
@@ -71,6 +86,83 @@ export class MatchingResultMessage {
}
}
+export class RawUserApiToken {
+ hashedToken: string;
+ rawToken: string;
+ tokenId: string;
+ tokenName: string;
+
+ static fromData(data: RawUserApiToken, target?: RawUserApiToken): RawUserApiToken {
+ if (!data) {
+ return data;
+ }
+ const instance = target || new RawUserApiToken();
+ instance.rawToken = data.rawToken;
+ instance.hashedToken = data.hashedToken;
+ instance.tokenName = data.tokenName;
+ instance.tokenId = data.tokenId;
+ return instance;
+ }
+}
+
+export class User {
+ email: string;
+ fullName: string;
+ hideTutorial: boolean;
+ ownActions: Element[];
+ ownSepas: Element[];
+ ownSources: Element[];
+ password: string;
+ preferredActions: string[];
+ preferredSepas: string[];
+ preferredSources: string[];
+ rev: string;
+ roles: Role[];
+ userApiTokens: UserApiToken[];
+ userId: string;
+ username: string;
+
+ static fromData(data: User, target?: User): User {
+ if (!data) {
+ return data;
+ }
+ const instance = target || new User();
+ instance.userId = data.userId;
+ instance.rev = data.rev;
+ instance.email = data.email;
+ instance.username = data.username;
+ instance.fullName = data.fullName;
+ instance.password = data.password;
+ instance.ownSources = __getCopyArrayFn(Element.fromData)(data.ownSources);
+ instance.ownSepas = __getCopyArrayFn(Element.fromData)(data.ownSepas);
+ instance.ownActions = __getCopyArrayFn(Element.fromData)(data.ownActions);
+ instance.preferredSources = __getCopyArrayFn(__identity<string>())(data.preferredSources);
+ instance.preferredSepas = __getCopyArrayFn(__identity<string>())(data.preferredSepas);
+ instance.preferredActions = __getCopyArrayFn(__identity<string>())(data.preferredActions);
+ instance.userApiTokens = __getCopyArrayFn(UserApiToken.fromData)(data.userApiTokens);
+ instance.hideTutorial = data.hideTutorial;
+ instance.roles = __getCopyArrayFn(__identity<Role>())(data.roles);
+ return instance;
+ }
+}
+
+export class UserApiToken {
+ tokenId: string;
+ tokenName: string;
+
+ static fromData(data: UserApiToken, target?: UserApiToken): UserApiToken {
+ if (!data) {
+ return data;
+ }
+ const instance = target || new UserApiToken();
+ instance.tokenId = data.tokenId;
+ instance.tokenName = data.tokenName;
+ return instance;
+ }
+}
+
+export type Role = "SYSTEM_ADMINISTRATOR" | "MANAGER" | "OPERATOR" | "DIMENSION_OPERATOR" | "USER_DEMO" | "BUSINESS_ANALYST";
+
function __getCopyArrayFn<T>(itemCopyFn: (item: T) => T): (array: T[]) => T[] {
return (array: T[]) => __copyArray(array, itemCopyFn);
}
diff --git a/ui/src/app/core-model/gen/streampipes-model.ts b/ui/src/app/core-model/gen/streampipes-model.ts
index 5af919b..716c1d7 100644
--- a/ui/src/app/core-model/gen/streampipes-model.ts
+++ b/ui/src/app/core-model/gen/streampipes-model.ts
@@ -1,3 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
diff --git a/ui/src/app/core/components/toolbar/toolbar.component.html b/ui/src/app/core/components/toolbar/toolbar.component.html
index c531d89..991b82c 100644
--- a/ui/src/app/core/components/toolbar/toolbar.component.html
+++ b/ui/src/app/core/components/toolbar/toolbar.component.html
@@ -60,6 +60,14 @@
</div>
</div>
<mat-menu #menu="matMenu" id="account">
+ <div class="current-user">
+ {{userEmail}}
+ </div>
+ <mat-divider></mat-divider>
+ <button mat-menu-item (click)="openProfile()" style="min-width:0px;">
+ <mat-icon aria-label="Info">perm_identity</mat-icon>
+ Profile
+ </button>
<button mat-menu-item (click)="openInfo()" style="min-width:0px;">
<mat-icon aria-label="Info">help</mat-icon>
Info
@@ -69,7 +77,7 @@
<mat-icon aria-label="Documentation">description</mat-icon>
Documentation
</button>
- <mat-menu-divider></mat-menu-divider>
+ <mat-divider></mat-divider>
<button mat-menu-item id="sp_logout" (click)="logout()"
style="min-width:0px;">
<mat-icon aria-label="Logout">exit_to_app</mat-icon>
diff --git a/ui/src/app/core/components/toolbar/toolbar.component.scss b/ui/src/app/core/components/toolbar/toolbar.component.scss
index 61efca3..ce59853 100644
--- a/ui/src/app/core/components/toolbar/toolbar.component.scss
+++ b/ui/src/app/core/components/toolbar/toolbar.component.scss
@@ -19,4 +19,15 @@
::ng-deep .cdk-overlay-pane .feedback-menu-content{
min-width:500px;
-}
\ No newline at end of file
+}
+
+.current-user {
+ display: block;
+ line-height: 48px;
+ height: 48px;
+ padding: 0 16px;
+ text-align: left;
+ text-decoration: none;
+ max-width: 100%;
+ position: relative;
+}
diff --git a/ui/src/app/core/components/toolbar/toolbar.component.ts b/ui/src/app/core/components/toolbar/toolbar.component.ts
index 7d84414..b4b77b0 100644
--- a/ui/src/app/core/components/toolbar/toolbar.component.ts
+++ b/ui/src/app/core/components/toolbar/toolbar.component.ts
@@ -35,6 +35,7 @@ export class ToolbarComponent extends BaseNavigationComponent implements OnInit
@ViewChild('accountMenuOpen') accountMenuOpen: MatMenuTrigger;
versionInfo: VersionInfo;
+ userEmail;
constructor(Router: Router,
private RestApi: RestApi,
@@ -43,8 +44,9 @@ export class ToolbarComponent extends BaseNavigationComponent implements OnInit
}
ngOnInit(): void {
+ this.userEmail = this.AuthStatusService.email;
super.onInit();
- this.getVersion()
+ this.getVersion();
}
closeFeedbackWindow() {
@@ -61,6 +63,11 @@ export class ToolbarComponent extends BaseNavigationComponent implements OnInit
this.activePage = "Info";
}
+ openProfile() {
+ this.Router.navigate(["profile"]);
+ this.activePage = "Profile";
+ }
+
logout() {
this.RestApi.logout().subscribe(() => {
this.AuthStatusService.user = undefined;
@@ -74,4 +81,4 @@ export class ToolbarComponent extends BaseNavigationComponent implements OnInit
this.versionInfo = response as VersionInfo;
})
}
-}
\ No newline at end of file
+}
diff --git a/ui/src/app/profile/components/basic-profile-settings.ts b/ui/src/app/profile/components/basic-profile-settings.ts
new file mode 100644
index 0000000..0f31f7d
--- /dev/null
+++ b/ui/src/app/profile/components/basic-profile-settings.ts
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import {ProfileService} from "../profile.service";
+import {User} from "../../core-model/gen/streampipes-model-client";
+import {Directive} from "@angular/core";
+
+@Directive()
+export abstract class BasicProfileSettings {
+
+ userData: User;
+ profileLoaded: boolean = false;
+ profileUpdating: boolean = false;
+ errorMessage: string;
+
+ constructor(protected profileService: ProfileService) {
+
+ }
+
+ receiveUserData() {
+ this.profileService
+ .getUserProfile()
+ .subscribe(userData => {
+ this.userData = userData;
+ this.onUserDataReceived();
+ this.profileLoaded = true;
+ });
+ }
+
+ saveProfileSettings() {
+ this.profileUpdating = true;
+ this.profileService.updateUserProfile(this.userData).subscribe(response => {
+ this.profileUpdating = false;
+ if (response.success) {
+ this.receiveUserData();
+ } else {
+ this.errorMessage = response.notifications[0].title;
+ }
+ });
+ }
+
+ abstract onUserDataReceived();
+}
diff --git a/ui/src/app/profile/components/general/general-profile-settings.component.html b/ui/src/app/profile/components/general/general-profile-settings.component.html
new file mode 100644
index 0000000..dc7bdb9
--- /dev/null
+++ b/ui/src/app/profile/components/general/general-profile-settings.component.html
@@ -0,0 +1,51 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You under the Apache License, Version 2.0
+ ~ (the "License"); you may not use this file except in compliance with
+ ~ the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~
+ -->
+
+<div fxLayout="row" class="page-container-padding">
+ <div fxFlex="100" fxLayout="column" fxLayoutAlign="start start" *ngIf="profileLoaded">
+ <div fxLayout="row" fxFlex="100">
+ <div fxFlex="300px" fxLayout="column" class="profile-section">
+ <div class="settings-title">Main Settings</div>
+ <div class="settings-description">Manage your basic profile settings here.</div>
+ </div>
+ <div fxFill fxLayout="column">
+ <mat-form-field fxFlex>
+ <mat-label>Email</mat-label>
+ <input [(ngModel)]="userData.email" matInput type="email" name="email" disabled/>
+ <mat-hint>Your mail address can't be changed currently.</mat-hint>
+ </mat-form-field>
+ <mat-form-field fxFlex>
+ <mat-label>Username</mat-label>
+ <input [(ngModel)]="userData.username" matInput/>
+ </mat-form-field>
+ <mat-form-field fxFlex>
+ <mat-label>Full Name</mat-label>
+ <input [(ngModel)]="userData.fullName" matInput/>
+ </mat-form-field>
+ <div>
+ <button mat-button mat-raised-button color="primary"
+ (click)="saveProfileSettings()">
+ Update profile
+ </button>
+ </div>
+ </div>
+ </div>
+ <mat-divider></mat-divider>
+ </div>
+
+</div>
diff --git a/ui/src/app/core/components/toolbar/toolbar.component.scss b/ui/src/app/profile/components/general/general-profile-settings.component.scss
similarity index 78%
copy from ui/src/app/core/components/toolbar/toolbar.component.scss
copy to ui/src/app/profile/components/general/general-profile-settings.component.scss
index 61efca3..965fae7 100644
--- a/ui/src/app/core/components/toolbar/toolbar.component.scss
+++ b/ui/src/app/profile/components/general/general-profile-settings.component.scss
@@ -16,7 +16,20 @@
*
*/
+.settings-title {
+ font-weight: bold;
+ font-size: 16pt;
+}
-::ng-deep .cdk-overlay-pane .feedback-menu-content{
- min-width:500px;
-}
\ No newline at end of file
+.settings-description {
+ font-size: 12pt;
+}
+
+.profile-section {
+ border-right: 2px solid gray;
+ margin-right: 15px;
+}
+
+.title-section {
+ width: 300px;
+}
diff --git a/ui/src/app/core/components/toolbar/toolbar.component.scss b/ui/src/app/profile/components/general/general-profile-settings.component.ts
similarity index 54%
copy from ui/src/app/core/components/toolbar/toolbar.component.scss
copy to ui/src/app/profile/components/general/general-profile-settings.component.ts
index 61efca3..bd0726b 100644
--- a/ui/src/app/core/components/toolbar/toolbar.component.scss
+++ b/ui/src/app/profile/components/general/general-profile-settings.component.ts
@@ -16,7 +16,27 @@
*
*/
+import {Component, OnInit} from "@angular/core";
+import {ProfileService} from "../../profile.service";
+import {User} from "../../../core-model/gen/streampipes-model-client";
+import {BasicProfileSettings} from "../basic-profile-settings";
-::ng-deep .cdk-overlay-pane .feedback-menu-content{
- min-width:500px;
-}
\ No newline at end of file
+@Component({
+ selector: 'general-profile-settings',
+ templateUrl: './general-profile-settings.component.html',
+ styleUrls: ['./general-profile-settings.component.scss']
+})
+export class GeneralProfileSettingsComponent extends BasicProfileSettings implements OnInit {
+
+ constructor(profileService: ProfileService) {
+ super(profileService);
+ }
+
+ ngOnInit(): void {
+ this.receiveUserData();
+ }
+
+ onUserDataReceived() {
+ }
+
+}
diff --git a/ui/src/app/profile/components/token/token-management-settings.component.html b/ui/src/app/profile/components/token/token-management-settings.component.html
new file mode 100644
index 0000000..724e2dc
--- /dev/null
+++ b/ui/src/app/profile/components/token/token-management-settings.component.html
@@ -0,0 +1,90 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You under the Apache License, Version 2.0
+ ~ (the "License"); you may not use this file except in compliance with
+ ~ the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~
+ -->
+
+<div fxLayout="row" class="page-container-padding">
+ <div fxFlex="100" fxLayout="column" fxLayoutAlign="start start" *ngIf="profileLoaded">
+ <div fxLayout="row" fxFlex="100">
+ <div fxFlex="300px" fxLayout="column" class="profile-section">
+ <div class="settings-title">API Keys</div>
+ <div class="settings-description">Manage your API keys for third-party application access to
+ StreamPipes.
+ </div>
+ </div>
+ <div fxFill fxLayout="column">
+ <div fxLayout="column" class="subsection">
+ <div class="subsection-title">New API key</div>
+ <mat-form-field fxFlex>
+ <mat-label>Name</mat-label>
+ <input [(ngModel)]="newTokenName" matInput/>
+ </mat-form-field>
+ <div>
+ <button mat-button mat-raised-button color="primary"
+ (click)="requestNewKey()">
+ Create new API key
+ </button>
+ </div>
+ </div>
+ <div fxLayout="column" class="subsection mt-10" *ngIf="newTokenCreated">
+ <div class="subsection-title">Key created</div>
+ <div class="subsection-small">Your new API key has been created. Please copy the key now - you won't be able to see the key again.</div>
+ <div fxFlex="100" fxLayout="row" fxLayoutAlign="start center" class="new-token">
+ <div class="token-name">{{newlyCreatedToken.tokenName}}</div>
+ <div class="displayed-token">
+ {{newlyCreatedToken.rawToken}}
+ </div>
+ <button mat-button mat-raised-button color="primary"
+ [cdkCopyToClipboard]="newlyCreatedToken.rawToken">
+ Copy
+ </button>
+ </div>
+ </div>
+ <mat-divider class="divider"></mat-divider>
+ <div fxLayout="column" class="subsection mt-10">
+ <div class="subsection-title">Existing API keys</div>
+ <div *ngIf="userData.userApiTokens.length == 0">(no keys available)</div>
+ <table mat-table [dataSource]="apiKeyDataSource" class="mat-elevation-z0" *ngIf="userData.userApiTokens.length > 0">
+ <ng-container matColumnDef="name">
+ <th mat-header-cell *matHeaderCellDef> Name</th>
+ <td mat-cell *matCellDef="let element"> {{element.tokenName}} </td>
+ </ng-container>
+
+ <!-- Name Column -->
+ <ng-container matColumnDef="action">
+ <th mat-header-cell *matHeaderCellDef> Action</th>
+ <td mat-cell *matCellDef="let element">
+ <div fxLayout="end end">
+ <button mat-button mat-raised-button color="warn"
+ (click)="revokeApiKey(element)">
+ Revoke
+ </button>
+ </div>
+ </td>
+ </ng-container>
+
+ <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
+ <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
+ </table>
+ </div>
+ </div>
+ <div fxFill fxLayout="column">
+ </div>
+ </div>
+ <mat-divider></mat-divider>
+ </div>
+
+</div>
diff --git a/ui/src/app/core/components/toolbar/toolbar.component.scss b/ui/src/app/profile/components/token/token-management-settings.component.scss
similarity index 52%
copy from ui/src/app/core/components/toolbar/toolbar.component.scss
copy to ui/src/app/profile/components/token/token-management-settings.component.scss
index 61efca3..4264dcf 100644
--- a/ui/src/app/core/components/toolbar/toolbar.component.scss
+++ b/ui/src/app/profile/components/token/token-management-settings.component.scss
@@ -16,7 +16,60 @@
*
*/
+.settings-title {
+ font-weight: bold;
+ font-size: 16pt;
+}
-::ng-deep .cdk-overlay-pane .feedback-menu-content{
- min-width:500px;
-}
\ No newline at end of file
+.settings-description {
+ font-size: 12pt;
+}
+
+.profile-section {
+ border-right: 2px solid gray;
+ margin-right: 15px;
+}
+
+.title-section {
+ width: 300px;
+}
+
+.subsection-title {
+ font-size: 12pt;
+ font-weight: bold;
+ margin-bottom: 10px;
+}
+
+.subsection {
+ margin-bottom: 15px;
+}
+
+.mt-10 {
+ margin-top: 10px;
+}
+
+.divider {
+ margin-top: 5px;
+ margin-bottom: 5px;
+}
+
+.new-token {
+
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+
+.token-name {
+ margin-right: 10px;
+ font-weight: bold;
+ padding: 10px;
+}
+
+.displayed-token {
+ background: #f6f6f6;
+ border: 1px solid #cccccc;
+ padding: 10px;
+ color: black;
+ margin-left: 10px;
+ margin-right: 10px;
+}
diff --git a/ui/src/app/profile/components/token/token-management-settings.component.ts b/ui/src/app/profile/components/token/token-management-settings.component.ts
new file mode 100644
index 0000000..dfced55
--- /dev/null
+++ b/ui/src/app/profile/components/token/token-management-settings.component.ts
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import {Component, OnInit} from "@angular/core";
+import {BasicProfileSettings} from "../basic-profile-settings";
+import {RawUserApiToken, UserApiToken} from "../../../core-model/gen/streampipes-model-client";
+import {MatTableDataSource} from "@angular/material/table";
+
+@Component({
+ selector: 'token-management-settings',
+ templateUrl: './token-management-settings.component.html',
+ styleUrls: ['./token-management-settings.component.scss']
+})
+export class TokenManagementSettingsComponent extends BasicProfileSettings implements OnInit {
+
+ newTokenName: string;
+ newTokenCreated: boolean = false;
+ newlyCreatedToken: RawUserApiToken;
+
+ displayedColumns: string[] = ['name', 'action'];
+ apiKeyDataSource: MatTableDataSource<UserApiToken>;
+
+ ngOnInit(): void {
+ this.receiveUserData();
+ }
+
+ requestNewKey() {
+ let baseToken: RawUserApiToken = this.makeBaseToken();
+ this.profileService.requestNewApiToken(baseToken).subscribe(result => {
+ this.newlyCreatedToken = result;
+ this.newTokenCreated = true;
+ this.newTokenName = "";
+ this.receiveUserData();
+ });
+ }
+
+ makeBaseToken(): RawUserApiToken {
+ let baseToken = new RawUserApiToken();
+ baseToken.tokenName = this.newTokenName;
+ return baseToken;
+ }
+
+ revokeApiKey(apiKey: UserApiToken) {
+ var removeIndex = this.userData.userApiTokens.map(token => token.tokenId).indexOf(apiKey.tokenId);
+ this.userData.userApiTokens.splice(removeIndex, 1);
+ this.profileService.updateUserProfile(this.userData).subscribe(response => {
+ this.receiveUserData();
+ })
+ }
+
+ onUserDataReceived() {
+ this.apiKeyDataSource = new MatTableDataSource<UserApiToken>(this.userData.userApiTokens);
+ }
+
+}
diff --git a/ui/src/app/profile/profile.component.html b/ui/src/app/profile/profile.component.html
new file mode 100644
index 0000000..329d064
--- /dev/null
+++ b/ui/src/app/profile/profile.component.html
@@ -0,0 +1,42 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You under the Apache License, Version 2.0
+ ~ (the "License"); you may not use this file except in compliance with
+ ~ the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~
+ -->
+
+
+<div fxLayout="column" class="page-container">
+ <div fxLayout="row" class="border" style="padding:0px;background-color:#f6f6f6;">
+ <div fxFlex="100" style="line-height:24px;border-bottom:1px solid #ccc">
+ <div fxFlex="100" fxLayout="row">
+ <div fxFlex fxLayoutAlign="start center" [attr.id]="'peType'">
+ <mat-tab-group [selectedIndex]="selectedIndex" (selectedIndexChange)="selectedIndexChange($event)">
+ <mat-tab label="General Settings"></mat-tab>
+ <mat-tab label="API Keys"></mat-tab>
+ </mat-tab-group>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="fixed-height page-container-padding-inner" fxLayout="column" fxFlex="100">
+ <div class="fixed-height page-container-padding-inner" fxLayout="column" fxFlex="100" *ngIf="selectedIndex == 0">
+ <general-profile-settings fxFlex="100"></general-profile-settings>
+ </div>
+ <div class="fixed-height page-container-padding-inner" fxLayout="column" fxFlex="100" *ngIf="selectedIndex == 1">
+ <token-management-settings fxFlex="100"></token-management-settings>
+ </div>
+ </div>
+</div>
diff --git a/ui/src/app/core/components/toolbar/toolbar.component.scss b/ui/src/app/profile/profile.component.scss
similarity index 91%
copy from ui/src/app/core/components/toolbar/toolbar.component.scss
copy to ui/src/app/profile/profile.component.scss
index 61efca3..d727140 100644
--- a/ui/src/app/core/components/toolbar/toolbar.component.scss
+++ b/ui/src/app/profile/profile.component.scss
@@ -16,7 +16,6 @@
*
*/
-
-::ng-deep .cdk-overlay-pane .feedback-menu-content{
- min-width:500px;
-}
\ No newline at end of file
+.page-container-padding-inner {
+ padding: 10px;
+}
diff --git a/ui/src/app/core/components/toolbar/toolbar.component.scss b/ui/src/app/profile/profile.component.ts
similarity index 69%
copy from ui/src/app/core/components/toolbar/toolbar.component.scss
copy to ui/src/app/profile/profile.component.ts
index 61efca3..0ee7c0f 100644
--- a/ui/src/app/core/components/toolbar/toolbar.component.scss
+++ b/ui/src/app/profile/profile.component.ts
@@ -16,7 +16,22 @@
*
*/
+import {Component, OnInit} from "@angular/core";
-::ng-deep .cdk-overlay-pane .feedback-menu-content{
- min-width:500px;
-}
\ No newline at end of file
+@Component({
+ selector: 'profile',
+ templateUrl: './profile.component.html',
+ styleUrls: ['./profile.component.scss']
+})
+export class ProfileComponent implements OnInit {
+
+ selectedIndex: number = 0;
+
+ ngOnInit(): void {
+ }
+
+ selectedIndexChange(index: number) {
+ this.selectedIndex = index;
+ }
+
+}
diff --git a/ui/src/app/profile/profile.module.ts b/ui/src/app/profile/profile.module.ts
new file mode 100644
index 0000000..f6242d5
--- /dev/null
+++ b/ui/src/app/profile/profile.module.ts
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import {NgModule} from "@angular/core";
+import {FlexLayoutModule} from "@angular/flex-layout";
+import {FormsModule} from "@angular/forms";
+import {MatTabsModule} from "@angular/material/tabs";
+import {MatButtonModule} from "@angular/material/button";
+import {CustomMaterialModule} from "../CustomMaterial/custom-material.module";
+import {CommonModule} from "@angular/common";
+import {ProfileComponent} from "./profile.component";
+import {TokenManagementSettingsComponent} from "./components/token/token-management-settings.component";
+import {GeneralProfileSettingsComponent} from "./components/general/general-profile-settings.component";
+import {ProfileService} from "./profile.service";
+import {MatDividerModule} from "@angular/material/divider";
+
+@NgModule({
+ imports: [
+ FlexLayoutModule,
+ FormsModule,
+ MatDividerModule,
+ MatTabsModule,
+ MatButtonModule,
+ CustomMaterialModule,
+ CommonModule,
+ ],
+ declarations: [
+ GeneralProfileSettingsComponent,
+ ProfileComponent,
+ TokenManagementSettingsComponent
+ ],
+ providers: [
+ ProfileService
+ ],
+ exports: [
+ ProfileComponent
+ ],
+ entryComponents: [
+ ProfileComponent
+ ]
+})
+export class ProfileModule {
+
+ constructor() {
+ }
+
+}
diff --git a/ui/src/app/profile/profile.service.ts b/ui/src/app/profile/profile.service.ts
new file mode 100644
index 0000000..89f5e5d
--- /dev/null
+++ b/ui/src/app/profile/profile.service.ts
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import {Injectable} from "@angular/core";
+import {PlatformServicesCommons} from "../platform-services/apis/commons.service";
+import {HttpClient} from "@angular/common/http";
+import {RawUserApiToken, User} from "../core-model/gen/streampipes-model-client";
+import {Observable} from "rxjs";
+import {map} from "rxjs/operators";
+import {Message} from "../core-model/gen/streampipes-model";
+
+@Injectable()
+export class ProfileService {
+
+ constructor(private http: HttpClient,
+ private platformServicesCommons: PlatformServicesCommons) {
+
+ }
+
+ getUserProfile(): Observable<User> {
+ return this.http.get(this.platformServicesCommons.authUserBasePath()).pipe(map(response => {
+ return User.fromData(response as any);
+ }));
+ }
+
+ updateUserProfile(userData: User): Observable<Message> {
+ return this.http.put(this.platformServicesCommons.authUserBasePath(), userData).pipe(map(response => {
+ return Message.fromData(response as any);
+ }))
+ }
+
+ requestNewApiToken(baseToken: RawUserApiToken): Observable<RawUserApiToken> {
+ return this.http.post(this.platformServicesCommons.authUserBasePath() + "/tokens", baseToken)
+ .pipe(map(response => {
+ return RawUserApiToken.fromData(response as any);
+ }))
+ }
+}