Merge pull request #963 from menakaj/application-mgt

Application Management Publisher UI
merge-requests/7/head
sinthuja 7 years ago committed by GitHub
commit 034fe9313e

@ -0,0 +1,192 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
~
~ WSO2 Inc. 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.wso2.carbon.devicemgt</groupId>
<artifactId>application-mgt</artifactId>
<version>3.0.46-SNAPSHOT</version>
</parent>
<artifactId>org.wso2.carbon.device.application.mgt.authhandler</artifactId>
<version>3.0.46-SNAPSHOT</version>
<packaging>war</packaging>
<name>WSO2 Carbon - Application Management Authentication Handler API</name>
<description>Proxy Service for Authentication Handling in WSO2 App Manager.</description>
<url>http://wso2.org</url>
<build>
<plugins>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<packagingExcludes>WEB-INF/lib/*cxf*.jar</packagingExcludes>
<warName>auth#application-mgt#v1.0</warName>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>deploy</id>
<build>
<defaultGoal>compile</defaultGoal>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<tasks>
<copy todir="${basedir}/../../../repository/deployment/server/webapps" overwrite="true">
<fileset dir="${basedir}/target">
<include name="auth#application-mgt#v1.0.war" />
</fileset>
</copy>
</tasks>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>client</id>
<build>
<defaultGoal>test</defaultGoal>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<executions>
<execution>
<phase>test</phase>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<dependencies>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxws</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxrs</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-jaxrs</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-asl</artifactId>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.wso2.carbon</groupId>
<artifactId>org.wso2.carbon.utils</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.wso2.carbon</groupId>
<artifactId>org.wso2.carbon.logging</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.json.wso2</groupId>
<artifactId>json</artifactId>
</dependency>
<dependency>
<groupId>commons-codec.wso2</groupId>
<artifactId>commons-codec</artifactId>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-core -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<version>9.5.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-jackson -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jackson</artifactId>
<version>9.5.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-jackson -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jaxrs</artifactId>
<version>9.5.0</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.wso2.orbit.com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
</dependency>
</dependencies>
</project>

@ -0,0 +1,52 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.wso2.carbon.device.application.mgt.auth.handler.service;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@Path("/auth")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public interface AuthHandlerService {
@POST
@Path("/login")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
Response login(@QueryParam("userName") String userName, @QueryParam("password") String password);
@POST
@Path("/refresh")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
Response refresh(@QueryParam("refreshToken") String refreshToken, @QueryParam("clientId") String clientId,
@QueryParam("clientSecret") String clientSecret);
@POST
@Path("/logout")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
Response logout(@QueryParam("token") String token, @QueryParam("clientId") String clientId,
@QueryParam("clientSecret") String clientSecret);
}

@ -0,0 +1,164 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.wso2.carbon.device.application.mgt.auth.handler.service.impl;
import feign.Client;
import feign.Feign;
import feign.auth.BasicAuthRequestInterceptor;
import feign.jackson.JacksonDecoder;
import feign.jackson.JacksonEncoder;
import feign.jaxrs.JAXRSContract;
import org.json.JSONObject;
import org.wso2.carbon.device.application.mgt.auth.handler.service.AuthHandlerService;
import org.wso2.carbon.device.application.mgt.auth.handler.util.Constants;
import org.wso2.carbon.device.application.mgt.auth.handler.util.dto.AccessTokenInfo;
import org.wso2.carbon.device.application.mgt.auth.handler.util.dto.ApiApplicationKey;
import org.wso2.carbon.device.application.mgt.auth.handler.util.dto.ApiApplicationRegistrationService;
import org.wso2.carbon.device.application.mgt.auth.handler.util.dto.ApiRegistrationProfile;
import org.wso2.carbon.device.application.mgt.auth.handler.util.dto.TokenIssuerService;
import org.wso2.carbon.device.application.mgt.auth.handler.util.dto.TokenRevokeService;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
@Path("/auth")
public class AuthHandlerServiceImpl implements AuthHandlerService {
private TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(
java.security.cert.X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(
java.security.cert.X509Certificate[] certs, String authType) {
}
}
};
private Client disableHostnameVerification = new Client.Default(getTrustedSSLSocketFactory(),
new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
return true;
}
}
);
@POST
@Path("/login")
@Produces(MediaType.APPLICATION_JSON)
@Override
public Response login(@QueryParam("userName") String userName, @QueryParam("password") String password) {
try {
ApiApplicationRegistrationService apiApplicationRegistrationService = Feign.builder()
.client(disableHostnameVerification)
.requestInterceptor(new BasicAuthRequestInterceptor(userName, password))
.contract(new JAXRSContract()).encoder(new JacksonEncoder()).decoder(new JacksonDecoder())
.target(ApiApplicationRegistrationService.class, Constants.API_APPLICATION_ENDPOINT);
ApiRegistrationProfile apiRegistrationProfile = new ApiRegistrationProfile();
apiRegistrationProfile.setApplicationName(Constants.APPLICATION_NAME);
apiRegistrationProfile.setIsAllowedToAllDomains(false);
apiRegistrationProfile.setIsMappingAnExistingOAuthApp(false);
apiRegistrationProfile.setTags(Constants.TAGS);
ApiApplicationKey apiApplicationKey = apiApplicationRegistrationService.register(apiRegistrationProfile);
//PasswordGrantType
TokenIssuerService tokenIssuerService = Feign.builder().client(disableHostnameVerification)
.requestInterceptor(new BasicAuthRequestInterceptor(apiApplicationKey.getConsumerKey(),
apiApplicationKey.getConsumerSecret()))
.contract(new JAXRSContract()).encoder(new JacksonEncoder()).decoder(new JacksonDecoder())
.target(TokenIssuerService.class, Constants.TOKEN_ENDPOINT);
AccessTokenInfo accessTokenInfo = tokenIssuerService.getToken(Constants.PASSWORD_GRANT_TYPE,
userName, password, Constants.SCOPES);
JSONObject loginInfo = new JSONObject(accessTokenInfo);
loginInfo.append(Constants.USER_NAME, userName);
loginInfo.append(Constants.APPLICATION_INFO, new JSONObject(apiApplicationKey));
System.out.println(loginInfo);
return Response.status(200).entity(loginInfo.toString()).build();
} catch (Exception e) {
//return Response.status(500).build();
}
return Response.status(200).build();
}
@POST
@Path("/refresh")
@Produces(MediaType.APPLICATION_JSON)
@Override
public Response refresh(@QueryParam("refreshToken") String refreshToken, @QueryParam("clientId") String clientId,
@QueryParam("clientSecret") String clientSecret) {
try {
TokenIssuerService tokenIssuerService = Feign.builder().client(disableHostnameVerification)
.requestInterceptor(new BasicAuthRequestInterceptor(clientId, clientSecret))
.contract(new JAXRSContract()).encoder(new JacksonEncoder()).decoder(new JacksonDecoder())
.target(TokenIssuerService.class, Constants.TOKEN_ENDPOINT);
AccessTokenInfo accessTokenInfo = tokenIssuerService.getRefreshToken(Constants.REFRESH_GRANT_TYPE,
refreshToken);
return Response.status(200).entity(new JSONObject(accessTokenInfo)).build();
} catch (Exception e) {
return Response.status(500).build();
}
}
@POST
@Path("/logout")
@Override
public Response logout(@QueryParam("token") String token, @QueryParam("clientId") String clientId,
@QueryParam("clientSecret") String clientSecret) {
try {
TokenRevokeService tokenRevokeService = Feign.builder().client(disableHostnameVerification)
.requestInterceptor(new BasicAuthRequestInterceptor(clientId, clientSecret))
.contract(new JAXRSContract()).encoder(new JacksonEncoder()).decoder(new JacksonDecoder())
.target(TokenRevokeService.class, Constants.TOKEN_ENDPOINT);
tokenRevokeService.revoke(token);
return Response.status(200).build();
} catch (Exception e) {
return Response.status(500).build();
}
}
private SSLSocketFactory getTrustedSSLSocketFactory() {
try {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
return sc.getSocketFactory();
} catch (KeyManagementException | NoSuchAlgorithmException e) {
return null;
}
}
}

@ -0,0 +1,35 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.wso2.carbon.device.application.mgt.auth.handler.util;
//TODO: Remove hardcoded localhost and ports
public class Constants {
public static String SCOPES = "perm:application:get perm:application:create perm:application:update " +
"perm:application-mgt:login perm:application:delete perm:platform:add perm:platform:remove " +
"perm:roles:view perm:devices:view perm:platform:get";
public static String[] TAGS = {"device_management"};
public static String USER_NAME = "userName";
public static String APPLICATION_NAME = "applicationmgt_publisher";
public static String TOKEN_ENDPOINT = "https://localhost:8243";
public static String PASSWORD_GRANT_TYPE = "password";
public static String REFRESH_GRANT_TYPE = "refresh_token";
public static String API_APPLICATION_ENDPOINT = "https://localhost:9443/api-application-registration/";
public static String APPLICATION_INFO = "application_info";
}

@ -0,0 +1,83 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.wso2.carbon.device.application.mgt.auth.handler.util.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* This hold access token info that returned from the api call
*/
@XmlRootElement(name = "AccessTokenInfo")
@JsonIgnoreProperties(ignoreUnknown = true)
public class AccessTokenInfo {
@XmlElement(required = true, name = "tokenType")
private String tokenType;
@XmlElement(required = true, name = "expiresIn")
private String expiresIn;
@XmlElement(required = true, name = "refreshToken")
private String refreshToken;
@XmlElement(required = true, name = "accessToken")
private String accessToken;
public AccessTokenInfo() {}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public String getExpiresIn() {
return expiresIn;
}
public void setExpiresIn(String expiresIn) {
this.expiresIn = expiresIn;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
@Override
public String toString() {
return accessToken + " " + tokenType + " " + refreshToken + " ";
}
}

@ -0,0 +1,49 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.wso2.carbon.device.application.mgt.auth.handler.util.dto;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* This holds api application consumer key and secret.
*/
@XmlRootElement
public class ApiApplicationKey {
@XmlElement
private String clientId;
@XmlElement
private String clientSecret;
public String getConsumerKey() {
return this.clientId;
}
public void setClientId(String consumerKey) {
this.clientId = consumerKey;
}
public String getConsumerSecret() {
return this.clientSecret;
}
public void setClientSecret(String consumerSecret) {
this.clientSecret = consumerSecret;
}
}

@ -0,0 +1,43 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.wso2.carbon.device.application.mgt.auth.handler.util.dto;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
/**
* This is the application registration service that exposed for apimApplicationRegistration
*/
@Path("/register")
public interface ApiApplicationRegistrationService {
/**
* This method is used to register api application
*
* @param registrationProfile contains the necessary attributes that are needed in order to register an app.
*/
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
ApiApplicationKey register(ApiRegistrationProfile registrationProfile);
}

@ -0,0 +1,82 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.wso2.carbon.device.application.mgt.auth.handler.util.dto;
/**
* This class represents the data that are required to register
* the oauth application.
*/
public class ApiRegistrationProfile {
public String applicationName;
public String tags[];
public boolean isAllowedToAllDomains;
public String consumerKey;
public String consumerSecret;
public boolean isMappingAnExistingOAuthApp;
public String getApplicationName() {
return applicationName;
}
public void setApplicationName(String applicationName) {
this.applicationName = applicationName;
}
public String[] getTags() {
return tags;
}
public void setTags(String[] tags) {
this.tags = tags;
}
public boolean isAllowedToAllDomains() {
return isAllowedToAllDomains;
}
public void setIsAllowedToAllDomains(boolean isAllowedToAllDomains) {
this.isAllowedToAllDomains = isAllowedToAllDomains;
}
public boolean isMappingAnExistingOAuthApp() {
return isMappingAnExistingOAuthApp;
}
public void setIsMappingAnExistingOAuthApp(boolean isMappingAnExistingOAuthApp) {
this.isMappingAnExistingOAuthApp = isMappingAnExistingOAuthApp;
}
public String getConsumerKey() {
return consumerKey;
}
public void setConsumerKey(String consumerKey) {
this.consumerKey = consumerKey;
}
public String getConsumerSecret() {
return consumerSecret;
}
public void setConsumerSecret(String consumerSecret) {
this.consumerSecret = consumerSecret;
}
}

@ -0,0 +1,47 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.wso2.carbon.device.application.mgt.auth.handler.util.dto;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import static feign.Util.checkNotNull;
/**
* This is a request interceptor to add oauth token header.
*/
public class OAuthRequestInterceptor implements RequestInterceptor {
private final String headerValue;
/**
* Creates an interceptor that authenticates all requests with the specified OAUTH token
*
* @param token the access token to use for authentication
*/
public OAuthRequestInterceptor(String token) {
checkNotNull(token, "access_token");
headerValue = "Bearer " + token;
}
@Override
public void apply(RequestTemplate template) {
template.header("Authorization", headerValue);
}
}

@ -0,0 +1,83 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.wso2.carbon.device.application.mgt.auth.handler.util.dto;
/**
* This class represents the data that are required to register
* the oauth application.
*/
public class RegistrationProfile {
public String callbackUrl;
public String clientName;
public String tokenScope;
public String owner;
public String grantType;
public String applicationType;
private static final String TAG = RegistrationProfile.class.getSimpleName();
public String getCallbackUrl() {
return callbackUrl;
}
public void setCallbackUrl(String callBackUrl) {
this.callbackUrl = callBackUrl;
}
public String getClientName() {
return clientName;
}
public void setClientName(String clientName) {
this.clientName = clientName;
}
public String getTokenScope() {
return tokenScope;
}
public void setTokenScope(String tokenScope) {
this.tokenScope = tokenScope;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public String getGrantType() {
return grantType;
}
public void setGrantType(String grantType) {
this.grantType = grantType;
}
public String getApplicationType() {
return applicationType;
}
public void setApplicationType(String applicationType) {
this.applicationType = applicationType;
}
}

@ -0,0 +1,42 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.wso2.carbon.device.application.mgt.auth.handler.util.dto;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
/**
* This hold the api definition that is used as a contract with netflix feign.
*/
@Path("/token")
public interface TokenIssuerService {
@POST
@Produces(MediaType.APPLICATION_JSON)
AccessTokenInfo getToken(@QueryParam("grant_type") String grant, @QueryParam("username") String username,
@QueryParam("password") String password, @QueryParam("scope") String scope);
@POST
@Produces(MediaType.APPLICATION_JSON)
AccessTokenInfo getRefreshToken(@QueryParam("grant_type") String grantType,
@QueryParam("refreshToken") String refreshToken);
}

@ -16,6 +16,20 @@
* under the License.
*/
package org.wso2.carbon.device.application.mgt.auth.handler.util.dto;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
/**
* Defines the list of App Manager APIs.
* Api definition for token revoke that will be used as Feign contract.
* */
@Path("/revoke")
public interface TokenRevokeService {
@POST
Response revoke(@QueryParam("token")String accessToken);
}

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
~
~ WSO2 Inc. 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.
-->
<!-- This file contains the list of permissions that are associated with URL end points
of the web app. Each permission should contain the name, permission path ,API path
(URL) , HTTP method and OAUTH2 authorization scope (not-required).
When defining dynamic paths for APIs, path variables are denoted by '*' notation.
For ex:
Actual API endpoint: devicemgt_admin/1.0.0/devices/{device-id}
URL to be represented here: /devices/*
NOTE: All the endpoints of the web app should be available in this file. Otherwise
it will result 403 error at the runtime.
-->
<PermissionConfiguration>
<APIVersion></APIVersion>
<!-- Application related permissions -->
</PermissionConfiguration>

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
~ Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
~
~ WSO2 Inc. 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.
-->
<!--
This file defines class loading policy of the whole container. But this behaviour can be overridden by individual webapps by putting this file into the META-INF/ directory.
-->
<Classloading xmlns="http://wso2.org/projects/as/classloading">
<!-- Parent-first or child-first. Default behaviour is child-first.-->
<ParentFirst>false</ParentFirst>
<!--
Default environments that contains provides to all the webapps. This can be overridden by individual webapps by specifing required environments
Tomcat environment is the default and every webapps gets it even if they didn't specify it.
e.g. If a webapps requires CXF, they will get both Tomcat and CXF.
-->
<Environments>CXF,Carbon</Environments>
</Classloading>

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
~
~ WSO2 Inc. 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.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jaxrs="http://cxf.apache.org/jaxrs"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd">
<jaxrs:server id="applicationMgtAuthService" address="/">
<jaxrs:serviceBeans>
<ref bean="applicationMgtAuthServiceBean"/>
</jaxrs:serviceBeans>
</jaxrs:server>
<bean id="applicationMgtAuthServiceBean"
class="org.wso2.carbon.device.application.mgt.auth.handler.service.impl.AuthHandlerServiceImpl"/>
</beans>

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
~
~ WSO2 Inc. 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.
-->
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<display-name>Application Management Auth Webapp</display-name>
<servlet>
<description>JAX-WS/JAX-RS Application Management Endpoint</description>
<display-name>JAX-WS/JAX-RS Servlet</display-name>
<servlet-name>CXFServlet</servlet-name>
<servlet-class>
org.apache.cxf.transport.servlet.CXFServlet
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>CXFServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>60</session-timeout>
</session-config>
<context-param>
<param-name>doAuthentication</param-name>
<param-value>false</param-value>
</context-param>
<!--publish to apim-->
<context-param>
<param-name>managed-api-enabled</param-name>
<param-value>false</param-value>
</context-param>
<context-param>
<param-name>managed-api-owner</param-name>
<param-value>admin</param-value>
</context-param>
<context-param>
<param-name>isSharedWithAllTenants</param-name>
<param-value>true</param-value>
</context-param>
<filter>
<filter-name>CorsFilter</filter-name>
<filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
<init-param>
<param-name>cors.allowed.origins</param-name>
<param-value>*</param-value>
</init-param>
<init-param>
<param-name>cors.allowed.methods</param-name>
<param-value>GET,POST,DELETE,PUT</param-value>
</init-param>
<init-param>
<param-name>cors.allowed.headers</param-name>
<param-value>Content-Type</param-value>
</init-param>
</filter>
<filter>
<filter-name>HttpHeaderSecurityFilter</filter-name>
<filter-class>org.apache.catalina.filters.HttpHeaderSecurityFilter</filter-class>
<init-param>
<param-name>hstsEnabled</param-name>
<param-value>false</param-value>
</init-param>
</filter>
<filter>
<filter-name>ContentTypeBasedCachePreventionFilter</filter-name>
<filter-class>org.wso2.carbon.ui.filters.cache.ContentTypeBasedCachePreventionFilter</filter-class>
<init-param>
<param-name>patterns</param-name>
<param-value>text/html" ,application/json" ,text/plain</param-value>
</init-param>
<init-param>
<param-name>filterAction</param-name>
<param-value>enforce</param-value>
</init-param>
<init-param>
<param-name>httpHeaders</param-name>
<param-value>Cache-Control: no-store, no-cache, must-revalidate, private</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>HttpHeaderSecurityFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>ContentTypeBasedCachePreventionFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>CorsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

@ -19,15 +19,19 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.wso2.carbon.devicemgt</groupId>
<artifactId>application-mgt</artifactId>
<version>3.0.46-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>org.wso2.carbon.device.application.mgt.publisher.ui</artifactId>
<version>3.0.46-SNAPSHOT</version>
<packaging>war</packaging>
<name>WSO2 Carbon - Application Management Publisher UI</name>
<description>WSO2 Carbon - Application Management Publisher UI React Application</description>
<url>http://wso2.org</url>
<build>
<plugins>

@ -11,38 +11,40 @@
"dependencies": {
"axios": "^0.16.2",
"flux": "^3.1.3",
"history": "^4.6.3",
"history": "^4.7.2",
"latest-version": "^3.1.0",
"material-ui": "^0.19.0",
"material-ui": "^0.19.1",
"prop-types": "^15.5.10",
"qs": "^6.5.0",
"react": "^15.6.1",
"react-dom": "^15.6.1",
"react-dropzone": "^4.1.0",
"react-dropzone": "^4.1.2",
"react-images-uploader": "^1.1.0",
"react-material-ui-form-validator": "^0.5.0",
"react-modal": "^2.2.2",
"react-router": "^4.1.2",
"react-router-dom": "^4.1.2",
"react-material-ui-form-validator": "^0.5.1",
"react-modal": "^2.3.2",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
"react-scripts": "1.0.10",
"react-sliding-pane": "^1.2.3",
"react-tap-event-plugin": "^2.0.1"
},
"devDependencies": {
"babel-core": "^6.24.1",
"babel-loader": "^7.0.0",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-preset-es2015": "^6.24.1",
"chai": "^4.0.2",
"babel-preset-react": "^6.24.1",
"babel-register": "^6.24.1",
"css-loader": "^0.28.2",
"babel-register": "^6.26.0",
"chai": "^4.1.2",
"css-loader": "^0.28.7",
"less": "^2.7.2",
"less-loader": "^4.0.4",
"mocha": "^3.4.1",
"mock-local-storage": "^1.0.2",
"node-sass": "^4.5.3",
"sass-loader": "^6.0.6",
"style-loader": "^0.18.1",
"webpack": "^2.5.0"
"webpack": "^2.7.0"
},
"scripts": {
"start": "react-scripts start",

@ -66,3 +66,8 @@
.createplatformdropzonep {
margin: 70px 40px 70px 70px
}
.createPlatformTagWrapper {
display: flex;
flex-wrap: wrap;
}

@ -16,8 +16,10 @@
* under the License.
*/
import './App.css';
import './App.scss';
import Theme from './theme';
import React, {Component} from 'react';
import AuthHandler from './api/authHandler';
import createHistory from 'history/createBrowserHistory';
import {BrowserRouter as Router, Redirect, Route, Switch} from 'react-router-dom'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
@ -31,12 +33,10 @@ import {
PlatformCreate,
PlatformListing
} from './components';
import Theme from './theme';
const history = createHistory({basename: '/publisher'});
/**
* This component defines the layout and the routes for the app.
* All the content will be loaded inside the Base component.
@ -54,15 +54,32 @@ class Base extends Component {
constructor() {
super();
this.state = {
user: "admin"
user: null
}
}
componentWillMount() {
let user = AuthHandler.getUser();
if (user) {
if (!AuthHandler.isTokenExpired()) {
this.setState({user: user});
} else {
console.log("expired!");
this.setState({user: null});
}
}
}
componentDidMount() {
}
render() {
if (this.state.user) {
if (this.state.user !== null) {
console.log("Have User.");
return (
<div className="container">
<BaseLayout>
<BaseLayout user={this.state.user}>
<Switch>
<Redirect exact path={"/"} to={"/assets/apps"}/>
<Route exact path={"/assets/apps"} component={ApplicationListing}/>
@ -80,9 +97,12 @@ class Base extends Component {
</BaseLayout>
</div>
)
}
} else {
console.log("No user");
return (<Redirect to={"/login"}/>)
}
}
}
/**

@ -1,17 +0,0 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.
*/

@ -0,0 +1,138 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.
*/
'use strict';
import Axios from 'axios';
import AuthHandler from './authHandler';
import Constants from '../common/constants';
import Helper from './helpers/appMgtApiHelpers';
/**
* Api definitions related to application management.
* TODO: Work to be done on Application release.
* */
export default class ApplicationMgtApi {
/**
* Api for create an application.
* @param: applicationData: The application data object. This contains an object array of each step data from
* application creation wizard.
*
* From applicationData, the proper application object will be created and send it to the api.
* */
static createApplication(applicationData) {
let {application, images} = Helper.buildApplication(applicationData);
const headers = AuthHandler.createAuthenticationHeaders("application/json");
console.log(application);
console.log(images);
Axios.post(Constants.appManagerEndpoints.CREATE_APP, application, {headers: headers});
}
/**
* Upload the image artifacts (banner, icon, screenshots) related to the application.
* @param appId: The application uuid of the application which the images should be uploaded to.
* @param images: The images object. This contains icon, banner and screenshots.
* */
static uploadImageArtifacts(appId, images) {
let formData = new FormData();
formData.append('icon', images.icon);
formData.append('banner', images.banner);
formData.append('screenshot', images.screenshots);
console.log("Image", formData);
const headers = AuthHandler.createAuthenticationHeaders("multipart/form-data");
return Axios.post(Constants.appManagerEndpoints.UPLOAD_IMAGE_ARTIFACTS + appId, formData, {headers: headers});
}
/**
* Method to handle application release process.
* */
static releaseApplication(appId) {
}
/**
* Promote the current life cycle state of the application.
* @param appId: The uuid of the application which the state should be updated.
* @param nextState: The next lifecycle state that the application can be updated to.
*
* URL Pattern : /application/1.0/
* */
static updateLifeCycleState(appId, nextState) {
}
/**
* Get the next possible state, which the application can be promoted to.
* @param appId: The application uuid.
*/
static getNextLifeCycleState(appId) {
}
/**
* Edit created application.
* @param applicationData: The modified application data.
* */
static editApplication(applicationData) {
let app = Helper.buildApplication(applicationData).application;
const headers = AuthHandler.createAuthenticationHeaders("application/json");
return Axios.put(Constants.appManagerEndpoints.CREATE_APP, app, {headers: headers});
}
static getApplicationArtifacts(appId, artifactName) {
const headers = AuthHandler.createAuthenticationHeaders("image/png");
return Axios.get(Constants.appManagerEndpoints.GET_IMAGE_ARTIFACTS + appId + "?name=" + artifactName,
{headers: headers});
}
static editApplicationArtifacts(appId, images) {
let formData = new FormData();
formData.append('icon', images.icon);
formData.append('banner', images.banner);
formData.append('screenshot', images.screenshots);
const headers = AuthHandler.createAuthenticationHeaders("application/json");
return Axios.put(Constants.appManagerEndpoints.UPLOAD_IMAGE_ARTIFACTS + appId, formData, {headers: headers});
}
/**
* Get all the created applications for the user.
* @return Object: The response object from the axios post.
* */
static getApplications() {
const headers = AuthHandler.createAuthenticationHeaders("application/json");
return Axios.get(Constants.appManagerEndpoints.GET_ALL_APPS, {headers: headers});
}
/**
* Get specific application.
* @param appId: The application Id.
* */
static getApplication(appId) {
const headers = AuthHandler.createAuthenticationHeaders("application/json");
return Axios.get(Constants.appManagerEndpoints.GET_ALL_APPS + appId, {headers: headers});
}
/**
* Delete specified application.
* @param appId: The id of the application which is to be deleted.
* */
static deleteApplication(appId) {
const headers = AuthHandler.createAuthenticationHeaders("application/json");
return Axios.delete(Constants.appManagerEndpoints.GET_ALL_APPS + appId, {headers: headers});
}
}

@ -0,0 +1,148 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.
*/
'use strict';
import Axios from 'axios';
import User from './data/user';
import Utils from './data/utils';
import Constants from "../common/constants";
/**
* Handles all tasks related to Authentication and Authorization.
* Generate access tokens, verify the user has necessary permissions etc.
* */
class AuthHandler {
/**
* Sends a request to the auth handler endpoint (auth/application-mgt/v1.0/auth/login) and generate token pair.
* @param userName: The user name of the user.
* @param password: The user password.
* @return Object: The response object from the axios post.
* */
static login(userName, password) {
const headers = {"Content-type": "application/json"};
let login_promise =
Axios.post(Constants.userConstants.LOGIN_URL+"?userName=" + userName+ "&password=" + password,
null, {headers: headers});
login_promise.then(response => {
console.log(response);
const userName = response.data.userName;
const validityPeriod = response.data.expiresIn; // In seconds
const WSO2_IOT_TOKEN = response.data.accessToken;
const refreshToken = response.data.refreshToken;
const clientId = response.data.application_info[0].consumerKey;
const clientSecret = response.data.application_info[0].consumerSecret;
const user = new User(userName, clientId, clientSecret, validityPeriod);
console.log(user);
user.setAuthToken(WSO2_IOT_TOKEN, validityPeriod);
let expiresIn = Date.now() + (validityPeriod * 1000);
localStorage.setItem("expiresIn", expiresIn);
AuthHandler.setUser(user);
}
);
return login_promise;
};
/**
* Persists the user object in browser's local storage.
* @param user: The user object.
* */
static setUser(user) {
if (!user instanceof User) {
throw "Invalid user object";
}
user.created = Date.now();
localStorage.setItem(Constants.userConstants.WSO2_USER, JSON.stringify(user.toJson()));
/* TODO: IMHO it's better to get this key (`wso2_user`) from configs */
}
static unauthorizedErrorHandler(error_response) {
if (error_response.status !== 401) { /* Skip unrelated response code to handle in unauthorizedErrorHandler*/
throw error_response;
/* re throwing the error since we don't handle it here and propagate to downstream error handlers in catch chain*/
}
let message = "The session has expired" + ".<br/> You will be redirect to the login page ...";
if (true) {
alert(message);
} else {
throw error_response;
}
}
/**
* Get the logged in user.
* @return User: The logged in user object.
* */
static getUser() {
const userData = localStorage.getItem(Constants.userConstants.WSO2_USER);
const partialToken = Utils.getCookie(Constants.userConstants.PARTIAL_TOKEN);
if (!(userData && partialToken)) {
return null;
}
return User.fromJson(JSON.parse(userData));
}
isLoggedIn() {
}
static logout() {
const user = AuthHandler.getUser();
const clientId = user.getClientId();
const clientSecret = user.getClientSecret();
const token = user.getAuthToken();
const headers = {"Content-type": "application/json"};
let login_promise = Axios.post(Constants.userConstants.LOGOUT_URL+"?token=" + token + "&clientId=" + clientId
+ "&clientSecret=" + clientSecret,
null, {headers: headers});
login_promise.then(
(response) => {
Utils.delete_cookie(Constants.userConstants.PARTIAL_TOKEN);
localStorage.removeItem(Constants.userConstants.WSO2_USER);
window.location = "/";
}
).catch(
(err) => {
AuthHandler.unauthorizedErrorHandler(err);
}
)
}
/**
* Checks whether the access token is expired.
* @return boolean: True if expired. False otherwise.
* */
static isTokenExpired() {
const expiresIn = localStorage.getItem("expiresIn");
return (expiresIn < Date.now());
}
static createAuthenticationHeaders(contentType) {
return {
"Authorization": "Bearer " + AuthHandler.getUser().getAuthToken(),
"Content-Type": contentType,
};
};
}
export default AuthHandler;

@ -0,0 +1,118 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.
*/
"use strict";
import Utils from './utils'
import Constants from '../../common/constants';
/**
* Represent an user logged in to the application, There will be allays one user per session and
* this user details will be persist in browser localstorage.
*/
export default class User {
constructor(name, clientId, clientSecret, validityPeriod) {
if (User._instance) {
return User._instance;
}
this._userName = name;
this._clientId = clientId;
this._clientSecret = clientSecret;
this._expires = validityPeriod;
User._instance = this;
}
/**
* OAuth scopes which are available for use by this user
* @returns {Array} : An array of scopes
*/
get scopes() {
return this._scopes;
}
/**
* Set OAuth scopes available to be used by this user
* @param {Array} newScopes : An array of scopes
*/
set scopes(newScopes) {
Object.assign(this.scopes, newScopes);
}
/**
* Get the JS accessible access token fragment from cookie storage.
* @returns {String|null}
*/
getAuthToken() {
return Utils.getCookie(Constants.userConstants.PARTIAL_TOKEN);
}
getClientId() {
return this._clientId;
}
getClientSecret() {
return this._clientSecret;
}
/**
* Store the JavaScript accessible access token segment in cookie storage
* @param {String} newToken : Part of the access token which needs when accessing REST API
* @param {Number} validityPeriod : Validity period of the cookie in seconds
*/
setAuthToken(newToken, validityPeriod) {
Utils.delete_cookie(Constants.userConstants.PARTIAL_TOKEN);
Utils.setCookie(Constants.userConstants.PARTIAL_TOKEN, newToken, validityPeriod);
}
/**
* Get the user name of logged in user.
* @return String: User name
* */
getUserName() {
return this._userName;
}
/**
* Provide user data in JSON structure.
* @returns {JSON} : JSON representation of the user object
*/
toJson() {
return {
name: this._userName,
clientId: this._clientId,
clientSecret: this._clientSecret,
expires: this._expires
};
}
/**
* User utility method to create an user from JSON object.
* @param {JSON} userJson : Need to provide user information in JSON structure to create an user object
* @returns {User} : An instance of User(this) class.
*/
static fromJson(userJson) {
const _user = new User(userJson.name);
_user._clientId = userJson.clientId;
_user._clientSecret = userJson.clientSecret;
_user._expires = userJson.expires;
console.log(_user);
return _user;
}
}
User._instance = null; // A private class variable to preserve the single instance of a swaggerClient

@ -0,0 +1,92 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.
*/
/**
* Utility class for Publisher application
*/
class PublisherUtils {
/**
* TODO: Remove this method one the initial phase is done, This is used to continue the API class until the login page is create
* @returns {promise}
*/
// static autoLogin() {
// let auth = new AuthManager();
// return auth.authenticateUser('admin', 'admin');
// }
/**
* Get JavaScript accessible cookies saved in browser, by giving the cooke name.
* @param {String} name : Name of the cookie which need to be retrived
* @returns {String|null} : If found a cookie with given name , return its value,Else null value is returned
*/
static getCookie(name) {
let pairs = document.cookie.split(";");
let cookie = null;
for (let pair of pairs) {
pair = pair.split("=");
let cookie_name = pair[0].trim();
let value = encodeURIComponent(pair[1]);
if (cookie_name === name) {
cookie = value;
break;
}
}
return cookie;
}
/**
* Delete a browser cookie given its name
* @param {String} name : Name of the cookie which need to be deleted
*/
static delete_cookie(name) {
document.cookie = name + '=; Path=' + "/" + '; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
}
/**
* Set a cookie with given name and value assigned to it. Cookies can be only set to the same origin,
* which the script is running
* @param {String} name : Name of the cookie which need to be set
* @param {String} value : Value of the cookie, expect it to be URLEncoded
* @param {number} validityPeriod : (Optional) Validity period of the cookie in seconds
* @param {String} path : Path which needs to set the given cookie
* @param {boolean} secured : secured parameter is set
*/
static setCookie(name, value, validityPeriod, path = "/", secured = true) {
let expires = "";
const securedDirective = secured ? "; Secure" : "";
if (validityPeriod) {
const date = new Date();
date.setTime(date.getTime() + validityPeriod * 1000);
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + value + expires + "; path=" + path + securedDirective + validityPeriod
}
/**
* Given an object returns whether the object is empty or not
* @param {Object} object : Any JSON object
* @returns {boolean}
*/
static isEmptyObject(object) {
return Object.keys(object).length === 0 && object.constructor === Object
}
}
export default PublisherUtils;

@ -0,0 +1,61 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.
*/
'use strict';
/**
* Helper methods for app publisher.
* */
export default class Helper {
/**
* Generate application object from form data passed.
* @param appData: Application data from the application creation form.
* @return {Object, Object}: The application object and the set of images related to the application.
* */
static buildApplication(appData) {
let application = {};
let images = {};
for (let step in appData) {
let tmpData = appData[step].data.step;
for (let prop in tmpData) {
if (prop === 'banner' || prop === 'screenshots' || prop === 'icon') {
images[prop] = tmpData[prop];
} else if(prop === 'tags') {
application[prop] = Helper.stringifyTags(tmpData[prop]);
} else {
application[prop] = tmpData[prop];
}
}
}
return {application, images};
}
static stringifyTags(tags) {
let tmpTags = [];
for (let tag in tags) {
console.log(tag);
tmpTags.push(tags[tag].value);
}
return tmpTags;
}
}

@ -0,0 +1,68 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.
*/
'use strict';
import Axios from 'axios';
import AuthHandler from './authHandler';
import Constants from '../common/constants';
/**
* Api definitions for Platform management.
* */
export default class PlatformMgtApi{
/**
* Create a new Platform
* @param platformData: The platform data object.
* */
static createPlatform(platformData) {
const headers = AuthHandler.createAuthenticationHeaders("application/json");
Axios.post(Constants.platformManagerEndpoints.CREATE_PLATFORM, platformData, {headers: headers}).then(
function (response) {
console.log(response);
}
).catch(function (err) {
console.log(err);
});
}
/**
* Get available platforms
* */
static getPlatforms() {
const headers = AuthHandler.createAuthenticationHeaders("application/json");
return Axios.get(Constants.platformManagerEndpoints.GET_ENABLED_PLATFORMS, {headers: headers});
}
/**
* Get the user specified platform
* @param platformId: The identifier of the platform
* */
static getPlatform(platformId) {
const headers = AuthHandler.createAuthenticationHeaders("application/json");
return Axios.get(Constants.platformManagerEndpoints.GET_PLATFORM + platformId, {headers: headers});
}
/**
* Delete specified platform
* @param platformId: The id of the platform which is to be deleted.
* */
static deletePlatform(platformId) {
const headers = AuthHandler.createAuthenticationHeaders("application/json");
return Axios.delete(Constants.platformManagerEndpoints.GET_PLATFORM + platformId, {headers: headers});
}
}

@ -15,3 +15,33 @@
* specific language governing permissions and limitations
* under the License.
*/
'use strict';
//TODO: Replace the server address with response from auth endpoint and remove hardcoded ids etc.
export default class Constants {
static scopes = 'perm:application:get perm:application:create perm:application:update perm:application-mgt:login' +
' perm:application:delete perm:platform:add perm:platform:remove perm:roles:view perm:devices:view';
static appManagerEndpoints = {
GET_ALL_APPS: 'https://localhost:8243/api/application-mgt/v1.0/applications/1.0.0/',
CREATE_APP: 'https://localhost:8243/api/application-mgt/v1.0/applications/1.0.0/',
UPLOAD_IMAGE_ARTIFACTS: 'https://localhost:8243/api/application-mgt/v1.0/applications/1.0.0/upload-image-artifacts/', //+appId
GET_IMAGE_ARTIFACTS: "https://localhost:8243/api/application-mgt/v1.0/applications/1.0.0/image-artifacts/"
};
static platformManagerEndpoints = {
CREATE_PLATFORM: 'https://localhost:8243/api/application-mgt/v1.0/platforms/1.0.0',
GET_ENABLED_PLATFORMS: 'https://localhost:8243/api/application-mgt/v1.0/platforms/1.0.0?status=ENABLED',
GET_PLATFORM: 'https://localhost:8243/api/application-mgt/v1.0/platforms/1.0.0/'
};
static userConstants = {
LOGIN_URL:"https://localhost:9443/auth/application-mgt/v1.0/auth/login",
LOGOUT_URL: "https://localhost:9443/auth/application-mgt/v1.0/auth/logout",
REFRESH_TOKEN_URL: "",
WSO2_USER: 'wso2_user',
PARTIAL_TOKEN: 'WSO2_IOT_TOKEN'
}
}

@ -16,15 +16,16 @@
* under the License.
*/
import Theme from '../../theme';
import React, {Component} from 'react';
import Dialog from 'material-ui/Dialog';
import {withRouter} from 'react-router-dom';
import FlatButton from 'material-ui/FlatButton';
import {Step1, Step2, Step3} from './CreateSteps';
import RaisedButton from 'material-ui/RaisedButton';
import ApplicationMgtApi from '../../api/applicationMgtApi';
import {Card, CardActions, CardTitle} from 'material-ui/Card';
import {Step, StepLabel, Stepper,} from 'material-ui/Stepper';
import Theme from '../../theme';
/**
* The App Create Component.
@ -38,12 +39,14 @@ class ApplicationCreate extends Component {
constructor() {
super();
this.scriptId = "application-create";
this.setStepData.bind(this);
this.removeStepData.bind(this);
this.handleSubmit.bind(this);
this.handleCancel.bind(this);
this.handleYes.bind(this);
this.handleNo.bind(this);
this.setStepData = this.setStepData.bind(this);
this.removeStepData = this.removeStepData.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.handleYes = this.handleYes.bind(this);
this.handleNo = this.handleNo.bind(this);
this.handlePrev = this.handlePrev.bind(this);
this.handleNext = this.handleNext.bind(this);
this.state = {
finished: false,
stepIndex: 0,
@ -66,7 +69,7 @@ class ApplicationCreate extends Component {
/**
* Handles next button click event.
* */
handleNext = () => {
handleNext() {
console.log("Handle Next");
const {stepIndex} = this.state;
this.setState({
@ -78,15 +81,24 @@ class ApplicationCreate extends Component {
/**
* Handles form submit.
* */
handleSubmit = () => {
console.log(this.state.stepData);
handleSubmit() {
let stepData = this.state.stepData;
let applicationCreationPromise = ApplicationMgtApi.createApplication(stepData);
applicationCreationPromise.then( response => {
this.handleYes();
}
).catch(
function (err) {
console.log(err);
}
);
};
/**
* Handles cancel button click event.
* This will show a confirmation dialog to cancel the application creation process.
* */
handleCancel = () => {
handleCancel() {
this.setState({isDialogOpen: true});
};
@ -94,7 +106,7 @@ class ApplicationCreate extends Component {
* Handled [ < Prev ] button click.
* This clears the data in the current step and returns to the previous step.
* */
handlePrev = () => {
handlePrev() {
const {stepIndex} = this.state;
if (stepIndex > 0) {
this.removeStepData();
@ -104,8 +116,10 @@ class ApplicationCreate extends Component {
/**
* Saves form data in each step in to the state.
* @param step: The step number of the step data.
* @param data: The form data of the step.
* */
setStepData = (step, data) => {
setStepData(step, data) {
console.log(step, data, this.state.stepData);
let tmpStepData = this.state.stepData;
tmpStepData.push({step: step, data: data});
@ -116,7 +130,7 @@ class ApplicationCreate extends Component {
/**
* Remove the last data point
* */
removeStepData = () => {
removeStepData() {
let tempData = this.state.stepData;
tempData.pop();
this.setState({stepData: tempData});
@ -126,7 +140,7 @@ class ApplicationCreate extends Component {
* Handles the Yes button in app creation cancellation dialog.
* Clears all the form data and reset the wizard.
* */
handleYes = () => {
handleYes() {
this.setState({finished: false, stepIndex: 0, stepData: [], isDialogOpen: false});
};
@ -134,7 +148,7 @@ class ApplicationCreate extends Component {
* Handles No button in app creation cancellation dialog.
* Returns to the same step.
* */
handleNo = () => {
handleNo() {
this.setState({isDialogOpen: false});
};
@ -150,28 +164,38 @@ class ApplicationCreate extends Component {
getStepContent(stepIndex) {
switch (stepIndex) {
case 0:
return <Step1 handleNext={this.handleNext}
return (
<Step1
handleNext={this.handleNext}
setData={this.setStepData}
removeData={this.removeStepData}/>;
removeData={this.removeStepData}
/>
);
case 1:
return <Step2 handleNext={this.handleNext}
return (
<Step2
handleNext={this.handleNext}
handlePrev={this.handlePrev}
setData={this.setStepData}
removeData={this.removeStepData}/>;
removeData={this.removeStepData}
/>
);
case 2:
return <Step3 handleFinish={this.handleNext}
return (
<Step3
handleFinish={this.handleNext}
handlePrev={this.handlePrev}
setData={this.setStepData}
removeData={this.removeStepData}/>;
removeData={this.removeStepData}
/>
);
default:
return 'You\'re a long way from home sonny jim!';
return <div/>;
}
}
render() {
const {finished, stepIndex} = this.state;
const contentStyle = {margin: '0 16px'};
/**
* Defines the dialog box actions. [Yes][No]
@ -189,7 +213,6 @@ class ApplicationCreate extends Component {
/>,
];
return (
<div className="middle createapplicationmiddle">
<Card className="creataapplicationcard">

@ -16,12 +16,14 @@
* under the License.
*/
import Theme from '../../theme';
import React, {Component} from 'react';
import {withRouter} from 'react-router-dom';
import TextField from 'material-ui/TextField';
import AuthHandler from "../../api/authHandler";
import DataTable from '../UIComponents/DataTable';
import ApplicationMgtApi from '../../api/applicationMgtApi';
import {Card, CardActions, CardTitle} from 'material-ui/Card';
import Theme from '../../theme';
/**
* The App Create Component.
@ -34,69 +36,39 @@ import Theme from '../../theme';
class ApplicationListing extends Component {
constructor() {
super();
this.searchApplications = this.searchApplications.bind(this);
this.onRowClick = this.onRowClick.bind(this);
this.setData = this.setData.bind(this);
this.sortData = this.sortData.bind(this);
this.compare = this.compare.bind(this);
this.state = {
data: [],
searchedApplications: [],
applications: [],
asc: true
};
this.scriptId = "application-listing";
}
data = [
{
id: Math.random(),
applicationName:"Cne",
platform:'Android',
category:"Public",
status: "Created"
},
{
id: Math.random(),
applicationName:"Gone",
platform:'IOS',
category:"Public",
status: "Created"
},
{
id: Math.random(),
applicationName:"Ane",
platform:'Android',
category:"Public",
status: "Created"
},
{
id: Math.random(),
applicationName:"one",
platform:'Android',
category:"Public",
status: "Created"
},
{
id: Math.random(),
applicationName:"one",
platform:'Android',
category:"Public",
status: "Created"
},
];
headers = [
{
data_id: "image",
data_type: "image",
sortable: false,
label: ""},
label: ""
},
{
data_id: "applicationName",
data_type: "string",
sortable: true,
label: "Application Name",
sort: this._sortData.bind(this)
sort: this.sortData
},
{
data_id: "platform",
data_type: "image_array",
sortable: false,
label: "Platform"},
label: "Platform"
},
{
data_id: "category",
data_type: "string",
@ -112,8 +84,6 @@ class ApplicationListing extends Component {
];
componentWillMount() {
//Fetch all the applications from backend and create application objects.
this.setState({data: this.data});
/**
*Loading the theme files based on the the user-preference.
@ -123,38 +93,70 @@ class ApplicationListing extends Component {
componentWillUnmount() {
Theme.removeThemingScripts(this.scriptId);
// this.setState({data: this.data});
}
componentDidMount() {
let getApps = ApplicationMgtApi.getApplications();
getApps.then(response => {
let apps = this.setData(response.data.applications);
console.log(apps);
this.setState({searchedApplications: apps});
// console.log(this.setState({data: response.data}), console.log(this.state));
}).catch(err => {
AuthHandler.unauthorizedErrorHandler(err);
});
}
/**
* Extract application from application list and update the state.
* */
setData(applications) {
let apps = [];
for (let app in applications) {
let application = {};
application.id = applications[app].uuid;
application.applicationName = applications[app].name;
application.platform = applications[app].platform.name;
application.category = applications[app].category.id;
application.status = applications[app].currentLifecycle.lifecycleState.name;
apps.push(application);
}
this.setState({searchedApplications: apps});
}
/**
* Handles the search action.
* When typing in the search bar, this method will be invoked.
* @param event: The event triggered from typing in the search box.
* @param searchText: The text that typed in the search box.
* */
_searchApplications(event, word) {
searchApplications(event, searchText) {
let searchedData;
if (word){
searchedData = this.data.filter((dataItem) => {
return dataItem.applicationName.includes(word);
if (searchText) {
searchedData = this.state.applications.filter((dataItem) => {
return dataItem.applicationName.includes(searchText);
});
} else {
searchedData = this.data;
searchedData = this.state.applications;
}
this.setState({data: searchedData}, console.log("Searched data ", this.state.data));
this.setState({searchedApplications: searchedData}, console.log("Searched data ", this.state.searchedApplications));
}
/**
* Handles sort data function and toggles the asc state.
* asc: true : sort in ascending order.
* */
_sortData() {
sortData() {
console.log(this.state);
let isAsc = this.state.asc;
let datas = isAsc?this.data.sort(this._compare):this.data.reverse();
this.setState({data: datas, asc: !isAsc});
let sortedData = isAsc ? this.state.searchedApplications.sort(this.compare) : this.data.reverse();
this.setState({searchedApplications: sortedData, asc: !isAsc});
}
_compare(a, b) {
compare(a, b) {
if (a.applicationName < b.applicationName)
return -1;
if (a.applicationName > b.applicationName)
@ -162,27 +164,33 @@ class ApplicationListing extends Component {
return 0;
}
_onRowClick(id) {
this.props.history.push("apps/"+id);
onRowClick(id) {
ApplicationMgtApi.getApplication(id).then(response => {
console.log(response);
}).catch(err => {
console.log(err)
});
// this.props.history.push("apps/" + id);
}
render() {
return (
<div className="middle applicationListingMiddle">
<Card className="applicationListingCard">
<TextField hintText="Search" className="applicationListingSearch"
onChange={this._searchApplications.bind(this)}/>
<TextField
hintText="Search"
className="applicationListingSearch"
onChange={this.searchApplications}/>
<CardTitle title="Applications" className="applicationListTitle"/>
<CardActions>
</CardActions>
<DataTable headers={this.headers}
data={this.state.data}
handleRowClick={this._onRowClick.bind(this)}
noDataMessage={{type: 'button', text: 'Create Application'}}/>
<DataTable
headers={this.headers}
data={this.state.searchedApplications}
handleRowClick={this.onRowClick}
noDataMessage={{type: 'button', text: 'Create Application'}}
/>
</Card>
</div>);
</div>
);
}
}

@ -16,12 +16,14 @@
* under the License.
*/
import Theme from '../../../theme';
import PropTypes from 'prop-types';
import React, {Component} from 'react';
import MenuItem from 'material-ui/MenuItem';
import SelectField from 'material-ui/SelectField';
import AuthHandler from "../../../api/authHandler";
import RaisedButton from 'material-ui/RaisedButton';
import Theme from '../../../theme';
import PlatformMgtApi from "../../../api/platformMgtApi";
/**
* The first step of the application creation wizard.
@ -39,11 +41,15 @@ import Theme from '../../../theme';
class Step1 extends Component {
constructor() {
super();
this.setPlatforms = this.setPlatforms.bind(this);
this.platforms = [];
this.state = {
finished: false,
stepIndex: 0,
store: 1,
platform: 1,
platformSelectedIndex: 0,
platform: "",
platforms: [],
stepData: [],
title: "",
titleError: ""
@ -61,22 +67,40 @@ class Step1 extends Component {
componentWillUnmount() {
Theme.removeThemingScripts(this.scriptId);
}
componentDidMount() {
//Get the list of available platforms and set to the state.
PlatformMgtApi.getPlatforms().then(response => {
console.log(response);
this.setPlatforms(response.data);
}).catch(err => {
AuthHandler.unauthorizedErrorHandler(err);
})
}
/**
* Invokes the handleNext function in Create component.
* Extract the platforms from the response data and populate the state.
* @param platforms: The array returned as the response.
* */
_handleNext = () => {
this.props.handleNext();
};
setPlatforms(platforms) {
let tmpPlatforms = [];
for (let index in platforms) {
let platform = {};
platform = platforms[index];
tmpPlatforms.push(platform);
}
this.setState({platforms: tmpPlatforms, platformSelectedIndex: 0, platform: tmpPlatforms[0].identifier})
}
/**
* Persist the current form data to the state.
* */
_setStepData() {
var step = {
setStepData() {
console.log("Platforms",this.state.platforms);
let step = {
store: this.state.store,
platform: this.state.platform
platform: this.state.platforms[this.state.platformSelectedIndex]
};
console.log(step);
this.props.setData("step1", {step: step});
}
@ -86,32 +110,25 @@ class Step1 extends Component {
* Sets the data to the state.
* Invokes the handleNext method of Create component.
* */
_handleClick() {
this._setStepData();
handleClick() {
this.setStepData();
}
/**
* Triggers when changing the Platform selection.
* */
_onChangePlatform = (event, index, value) => {
console.log(value);
this.setState({platform: value});
onChangePlatform(event, index, value) {
console.log(this.state.platforms[index]);
this.setState({platform: this.state.platforms[index].identifier, platformSelectedIndex: index});
};
/**
* Triggers when changing the Store selection.
* */
_onChangeStore = (event, index, value) => {
onChangeStore(event, index, value) {
this.setState({store: value});
};
/**
* Triggers when user types on Title text field.
* */
_onChangeTitle = (event, value) => {
this.setState({title: value});
};
render() {
return (
<div>
@ -122,30 +139,38 @@ class Step1 extends Component {
floatingLabelText="Store Type*"
value={this.state.store}
floatingLabelFixed={true}
onChange={this._onChangeStore.bind(this)}
onChange={this.onChangeStore.bind(this)}
>
<MenuItem value={1} primaryText="Enterprise"/>
<MenuItem value={2} primaryText="Public"/>
</SelectField> <br/>
<MenuItem value={0} primaryText="Enterprise"/>
<MenuItem value={1} primaryText="Public"/>
</SelectField>
<br/>
<SelectField
floatingLabelText="Platform*"
value={this.state.platform}
floatingLabelFixed={true}
onChange={this._onChangePlatform.bind(this)}
onChange={this.onChangePlatform.bind(this)}
>
<MenuItem value={1} primaryText="Android"/>
<MenuItem value={2} primaryText="iOS"/>
<MenuItem value={{name: "Web", id:3}} primaryText="Web"/>
{this.state.platforms.length > 0 ? this.state.platforms.map(platform => {
return (
<MenuItem
key={Math.random()}
value={platform.identifier}
primaryText={platform.name}
/>
)
}) : <div/>}
</SelectField>
</div>
<br/>
<br/>
<div className="nextButton">
<RaisedButton
label="Next >"
primary={true}
onClick={this._handleClick.bind(this)}
onClick={this.handleClick.bind(this)}
/>
</div>
</div>

@ -17,6 +17,7 @@
*/
import PropTypes from 'prop-types';
import Theme from '../../../theme';
import Chip from 'material-ui/Chip';
import Dropzone from 'react-dropzone';
import React, {Component} from 'react';
@ -28,7 +29,6 @@ import SelectField from 'material-ui/SelectField';
import RaisedButton from 'material-ui/RaisedButton';
import Clear from 'material-ui/svg-icons/content/clear';
import {GridList, GridTile} from 'material-ui/GridList';
import Theme from '../../../theme';
/**
* The Second step of application create wizard.
@ -56,16 +56,17 @@ class Step2 extends Component {
super();
this.state = {
tags: [],
icon: [],
title: "",
errors: {},
banner: [],
defValue: "",
category: 0,
visibility: 0,
errors: {},
title: "",
shortDescription: "",
description: "",
banner: [],
screenshots: [],
icon: []
identifier: "",
shortDescription: ""
};
this.scriptId = "application-create-step2";
}
@ -86,19 +87,19 @@ class Step2 extends Component {
* Clears the tags text field.
* Chip gets two parameters: Key and value.
* */
_addTags(event) {
addTags(event) {
let tags = this.state.tags;
if (event.charCode === 13) {
event.preventDefault();
tags.push({key: Math.floor(Math.random() * 1000), value: event.target.value});
this.setState({tags, defValue: ""});
this.setState({tags, defValue: ""}, console.log(tags));
}
}
/**
* Set the value for tag.
* */
_handleTagChange(event) {
handleTagChange(event) {
let defaultValue = this.state.defValue;
defaultValue = event.target.value;
this.setState({defValue: defaultValue})
@ -107,21 +108,21 @@ class Step2 extends Component {
/**
* Invokes the handleNext function in Create component.
* */
_handleNext() {
handleNext() {
let fields = [{name: "Title", value: this.state.title},
{name: "Short Description", value: this.state.shortDescription},
{name: "Description", value: this.state.description},
{name: "Banner", value: this.state.banner},
{name: "Screenshots", value: this.state.screenshots},
{name: "Identifier", value: this.state.identifier},
{name: "Icon", value: this.state.icon}];
this._validate(fields);
// this.props.handleNext();
this.validate(fields);
}
/**
* Invokes the handlePrev function in Create component.
* */
_handlePrev() {
handlePrev() {
this.props.handlePrev();
}
@ -129,7 +130,7 @@ class Step2 extends Component {
* Handles Chip delete function.
* Removes the tag from state.tags
* */
_handleRequestDelete = (key) => {
handleRequestDelete(key) {
this.chipData = this.state.tags;
const chipToDelete = this.chipData.map((chip) => chip.key).indexOf(key);
this.chipData.splice(chipToDelete, 1);
@ -139,18 +140,18 @@ class Step2 extends Component {
/**
* Creates Chip array from state.tags.
* */
_renderChip(data) {
renderChip(data) {
return (
<Chip
key={data.key}
onRequestDelete={() => this._handleRequestDelete(data.key)}
onRequestDelete={() => this.handleRequestDelete(data.key)}
className="applicationCreateChip">
{data.value}
</Chip>
);
}
_onVisibilitySelect = (event, index, value) => {
onVisibilitySelect(event, index, value) {
console.log(value);
let comp = <SelectField> <MenuItem value={0} primaryText="Public"/>
<MenuItem value={1} primaryText="Roles"/>
@ -167,7 +168,7 @@ class Step2 extends Component {
/**
* Validate the form.
* */
_validate(fields) {
validate(fields) {
let errors = {};
let errorsPresent = false;
fields.forEach(function (field) {
@ -181,6 +182,15 @@ class Step2 extends Component {
}
break;
}
case 'Identifier': {
if (field.value === "") {
errors[field.name] = field.name + " is required!";
errorsPresent = true;
} else {
errorsPresent = false;
}
break;
}
case 'Short Description': {
if (field.value === "") {
errors[field.name] = field.name + " is required!";
@ -229,40 +239,40 @@ class Step2 extends Component {
}
});
console.log(errorsPresent);
if (!errorsPresent) {
this._setStepData();
this.setStepData();
} else {
this.setState({errors: errors}, console.log(errors));
}
}
/**
* Creates an object with the current step data and persist in the parent.
* */
_setStepData() {
setStepData() {
let stepData = {
title: this.state.title,
description: this.state.description,
shortDescription: this.state.shortDescription,
icon: this.state.icon,
name: this.state.name,
tags: this.state.tags,
banner: this.state.banner,
category: this.categories[this.state.category],
identifier: this.state.identifier,
screenshots: this.state.screenshots,
icon: this.state.icon
description: this.state.description,
shortDescription: this.state.shortDescription
};
this.props.setData("step2", {step: stepData});
}
};
/**
* Set text field values to state.
* */
_onTextFieldChange(event, value) {
onTextFieldChange(event, value) {
let field = event.target.id;
switch (field) {
case "title": {
this.setState({title: value});
case "name": {
this.setState({name: value});
break;
}
case "shortDescription": {
@ -273,30 +283,34 @@ class Step2 extends Component {
this.setState({description: value});
break;
}
case "identifier": {
this.setState({identifier: value});
break;
}
}
};
/**
* Removed user uploaded banner.
* */
_removeBanner(event, d) {
removeBanner(event, d) {
console.log(event, d);
this.setState({banner: []});
}
};
/**
* Removes uploaded icon.
* */
_removeIcon(event) {
removeIcon(event) {
this.setState({icon: []});
}
};
/**
* Removes selected screenshot.
* */
_removeScreenshot(event) {
removeScreenshot(event) {
console.log(event.target)
}
};
render() {
console.log(this.state.visibilityComponent);
@ -305,13 +319,23 @@ class Step2 extends Component {
<div>
<div>
<TextField
id="title"
hintText="Enter a title for your application."
id="name"
hintText="Enter a name for your application."
errorText={this.state.errors["Title"]}
floatingLabelText="Title*"
floatingLabelText="Name*"
floatingLabelFixed={true}
onChange={this._onTextFieldChange.bind(this)}
/><br/>
onChange={this.onTextFieldChange.bind(this)}
/>
<br/>
<TextField
id="identifier"
hintText="Unique Identifier for Application."
errorText={this.state.errors["Identifier"]}
floatingLabelText="Identifier*"
floatingLabelFixed={true}
onChange={this.onTextFieldChange.bind(this)}
/>
<br/>
<TextField
id="shortDescription"
hintText="Enter a short description for your application."
@ -320,9 +344,9 @@ class Step2 extends Component {
floatingLabelFixed={true}
multiLine={true}
rows={2}
onChange={this._onTextFieldChange.bind(this)}
/><br/>
onChange={this.onTextFieldChange.bind(this)}
/>
<br/>
<TextField
id="description"
errorText={this.state.errors["Description"]}
@ -331,30 +355,33 @@ class Step2 extends Component {
floatingLabelFixed={true}
multiLine={true}
rows={4}
onChange={this._onTextFieldChange.bind(this)}
/><br/>
onChange={this.onTextFieldChange.bind(this)}
/>
<br/>
<SelectField
floatingLabelText="Visibility*"
value={this.state.visibility}
floatingLabelFixed={true}
onChange={this._onVisibilitySelect.bind(this)}
onChange={this.onVisibilitySelect.bind(this)}
>
<MenuItem value={0} primaryText="Public"/>
<MenuItem value={1} primaryText="Roles"/>
<MenuItem value={2} primaryText="Devices"/>
</SelectField><br/>
</SelectField>
<br/>
<TextField
id="tags"
errorText={this.state.errors["tags"]}
hintText="Enter application tags.."
hintText="Enter Application tags.."
floatingLabelText="Tags*"
floatingLabelFixed={true}
value={this.state.defValue}
onChange={this._handleTagChange.bind(this)}
onKeyPress={this._addTags.bind(this)}
/><br/>
onChange={this.handleTagChange.bind(this)}
onKeyPress={this.addTags.bind(this)}
/>
<br/>
<div className="applicationCreateWrapper">
{this.state.tags.map(this._renderChip, this)}
{this.state.tags.map(this.renderChip, this)}
</div>
<br/>
<SelectField
@ -363,7 +390,8 @@ class Step2 extends Component {
floatingLabelFixed={true}
>
<MenuItem value={0} primaryText="Business"/>
</SelectField> <br/>
</SelectField>
<br/>
{/*Platform Specific Properties.*/}
<div className="platformSpecificPropertyDiv">
<p className="platformSpecificPropertyP">Platform Specific Properties</p>
@ -374,24 +402,28 @@ class Step2 extends Component {
<p className="applicationCreateBannerTitle">Banner*:</p>
<GridList className="applicationCreateGrid" cols={1.1}>
{this.state.banner.map((tile) => (
<GridTile key={Math.floor(Math.random() * 1000)}
<GridTile
key={Math.floor(Math.random() * 1000)}
title={tile.name}
actionIcon={
<IconButton onClick={this._removeBanner.bind(this)}>
<IconButton onClick={this.removeBanner.bind(this)}>
<Clear/>
</IconButton>}>
<img src={tile.preview}/></GridTile>
<img src={tile.preview}/>
</GridTile>
))}
{this.state.banner.length === 0 ?
<Dropzone className="applicationCreateBannerDropZone" accept="image/jpeg, image/png"
<Dropzone
className="applicationCreateBannerDropZone"
accept="image/jpeg, image/png"
onDrop={(banner, rejected) => {
this.setState({banner, rejected});
}}>
}}
>
<p className="applicationCreateBannerp">+</p>
</Dropzone> : <div />}
</Dropzone> : <div/>
}
</GridList>
</div>
<br/>
<div>
@ -399,23 +431,27 @@ class Step2 extends Component {
<p className="applicationCreateScreenshotTitle">Screenshots*:</p>
<GridList className="applicationCreateScreenshotGrid" cols={1.1}>
{this.state.screenshots.map((file) => (
<GridTile key={Math.floor(Math.random() * 1000)}
<GridTile
key={Math.floor(Math.random() * 1000)}
title={file[0].name}
actionIcon={
<IconButton onClick={this._removeScreenshot.bind(this)}>
<IconButton onClick={this.removeScreenshot.bind(this)}>
<Clear/>
</IconButton>}>
<img src={file[0].preview}/></GridTile>
))}
{this.state.screenshots.length < 3 ?
<Dropzone className="applicationCreateScreenshotDropZone"
<Dropzone
className="applicationCreateScreenshotDropZone"
accept="image/jpeg, image/png"
onDrop={(screenshots, rejected) => {
let tmpScreenshots = this.state.screenshots;
tmpScreenshots.push(screenshots);
this.setState({
screenshots: tmpScreenshots});
}}>
screenshots: tmpScreenshots
});
}}
>
<p className="applicationCreateScreenshotp">+</p>
</Dropzone> : <div/>}
</GridList>
@ -426,38 +462,43 @@ class Step2 extends Component {
<p className="applicationCreateIconTitle">Icon*:</p>
<GridList className="applicationCreateIconGrid" cols={1.1}>
{this.state.icon.map((tile) => (
<GridTile key={Math.floor(Math.random() * 1000)}
<GridTile
key={Math.floor(Math.random() * 1000)}
title={tile.name}
actionIcon={
<IconButton onClick={this._removeIcon.bind(this)}>
<IconButton onClick={this.removeIcon.bind(this)}>
<Clear/>
</IconButton>}>
<img src={tile.preview}/></GridTile>
<img src={tile.preview}/>
</GridTile>
))}
{this.state.icon.length === 0 ?
<Dropzone className="applicationCreateIconDropZone"
<Dropzone
className="applicationCreateIconDropZone"
accept="image/jpeg, image/png"
onDrop={(icon, rejected) => {this.setState({icon, rejected});}}>
onDrop={(icon, rejected) => {
this.setState({icon, rejected});
}}
>
<p className="applicationCreateIconp">+</p>
</Dropzone> : <div/>}
</GridList>
</div>
<br/>
</div>
<br/>
<br/>
<div className="applicationCreateBackAndNext">
<FlatButton
label="< Back"
disabled={false}
onClick={this._handlePrev.bind(this)}
className="applicationCreateBack"
onClick={this.handlePrev.bind(this)}
style={{marginRight: 12}}
/>
<RaisedButton
label="Next >"
primary={true}
onClick={this._handleNext.bind(this)}
onClick={this.handleNext.bind(this)}
/>
</div>
</div>

@ -51,6 +51,10 @@ import Theme from '../../../theme';
class Step3 extends Component {
constructor() {
super();
this.handleToggle = this.handleToggle.bind(this);
this.handlePrev = this.handlePrev.bind(this);
this.handleToggle = this.handleToggle.bind(this);
this.handleFinish = this.handleFinish.bind(this);
this.state = {
showForm: false,
releaseChannel: 1,
@ -74,21 +78,21 @@ class Step3 extends Component {
* Handles finish button click.
* This invokes handleNext function in parent component.
* */
_handleFinish() {
handleFinish() {
this.props.handleFinish();
}
/**
* Invokes Prev button click.
* */
_handlePrev() {
handlePrev() {
this.props.handlePrev();
}
/**
* Handles release application selection.
* */
_handleToggle() {
handleToggle() {
let hide = this.state.showForm;
this.setState({showForm: !hide});
}
@ -100,11 +104,12 @@ class Step3 extends Component {
<Toggle
label="Release the Application"
labelPosition="right"
onToggle={this._handleToggle.bind(this)}
onToggle={this.handleToggle}
defaultToggled={this.state.showForm}
/>
{/*If toggle is true, the release form will be shown.*/}
{!this.state.showForm ? <div/> : <div>
{!this.state.showForm ? <div/> :
<div>
<SelectField
floatingLabelText="Select Release Channel*"
value={this.state.releaseChannel}
@ -113,7 +118,8 @@ class Step3 extends Component {
<MenuItem value={1} primaryText="Alpha"/>
<MenuItem value={2} primaryText="Beta"/>
<MenuItem value={3} primaryText="GA"/>
</SelectField> <br/>
</SelectField>
<br/>
<TextField
hintText="1.0.0"
floatingLabelText="Version*"
@ -121,15 +127,19 @@ class Step3 extends Component {
floatingLabelFixed={true}
/><br/>
</div>}
<div className="applicationCreateBackAndFinish">
<FlatButton label="< Back" disabled={false} onClick={this._handlePrev.bind(this)}
className="applicationCreateFinish"/>
<FlatButton
label="< Back"
disabled={false}
onClick={this.handlePrev}
className="applicationCreateFinish"
/>
<RaisedButton
label="Finish"
primary={true}
onClick={this._handleFinish.bind(this)}
onClick={this.handleFinish}
/>
</div>
</div>
</div>

@ -16,12 +16,17 @@
* under the License.
*/
import Theme from '../../theme';
import PropTypes from 'prop-types';
import Badge from 'material-ui/Badge';
import React, {Component} from 'react';
import AppBar from 'material-ui/AppBar';
import Drawer from 'material-ui/Drawer';
import IconMenu from 'material-ui/IconMenu';
import MenuItem from 'material-ui/MenuItem';
import {withRouter} from 'react-router-dom';
import AuthHandler from "../../api/authHandler";
import FlatButton from 'material-ui/FlatButton';
import IconButton from 'material-ui/IconButton';
import {List, ListItem} from 'material-ui/List';
import Apps from 'material-ui/svg-icons/navigation/apps';
@ -30,7 +35,6 @@ import Feedback from 'material-ui/svg-icons/action/feedback';
import DevicesOther from 'material-ui/svg-icons/hardware/devices-other';
import NotificationsIcon from 'material-ui/svg-icons/social/notifications';
import ActionAccountCircle from 'material-ui/svg-icons/action/account-circle';
import Theme from '../../theme';
/**
@ -48,6 +52,7 @@ class BaseLayout extends Component {
user: 'Admin'
};
this.scriptId = "basic-layout";
this.logout = this.logout.bind(this);
}
componentWillMount() {
@ -93,11 +98,16 @@ class BaseLayout extends Component {
this.props.history.push(to);
}
logout(event, index, value) {
AuthHandler.logout();
}
render() {
return (
<div>
<AppBar title="App Publisher"
<AppBar
title="App Publisher"
iconElementRight={
<div>
<Badge
@ -109,18 +119,30 @@ class BaseLayout extends Component {
<NotificationsIcon/>
</IconButton>
</Badge>
<IconButton onClick={() => {
console.log("Clicked")
}}>
<ActionAccountCircle/>
</IconButton>
<IconMenu
iconButtonElement={<FlatButton
icon={<ActionAccountCircle/>}
label="sdfdsf"
/>}
anchorOrigin={{horizontal: 'left', vertical: 'top'}}
targetOrigin={{horizontal: 'left', vertical: 'top'}}
onChange={this.logout}
>
<MenuItem value={0} primaryText="Logout" />
</IconMenu>
{/*<FlatButton*/}
{/*icon={<ActionAccountCircle/>}*/}
{/*onClick={() => {console.log("Clicked")}}*/}
{/*label={this.props.user.getUserName()}*/}
{/*/>*/}
</div>
}
/>
<div>
<Drawer containerStyle={{height: 'calc(100% - 64px)', width: '15%', top: '10%'}} open={true}>
<List>
<ListItem primaryText="Applications"
<ListItem
primaryText="Applications"
leftIcon={<Apps/>}
initiallyOpen={false}
primaryTogglesNestedList={true}
@ -131,9 +153,11 @@ class BaseLayout extends Component {
primaryText="Create"
onClick={this.handleApplicationCreateClick.bind(this)}
leftIcon={<Add/>}
/>]}
/>
<ListItem primaryText="Platforms"
]}
/>
<ListItem
primaryText="Platforms"
leftIcon={<DevicesOther/>}
initiallyOpen={false}
primaryTogglesNestedList={true}
@ -144,20 +168,23 @@ class BaseLayout extends Component {
primaryText="Create"
onClick={this.handlePlatformCreateClick.bind(this)}
leftIcon={<Add/>}
/>]}
/>
<ListItem primaryText="Reviews"
]}
/>
<ListItem
primaryText="Reviews"
onClick={this.handleReviewClick.bind(this)}
leftIcon={<Feedback/>}/>
leftIcon={<Feedback/>}
/>
</List>
</Drawer>
</div>
<div className="basicLayoutDiv">
{this.props.children}
</div>
</div>);
</div>
);
}
}
BaseLayout.propTypes = {

@ -1,49 +0,0 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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 React, {Component} from 'react';
import DataTable from '../UIComponents/DataTable';
/**
*
* ***NEW***
* The Publisher overview component.
* This component could be used to view app analytics.
* i.e number of overall downloads, ratings ect.
* */
class PublisherOverview extends Component {
constructor() {
super();
}
componentWillMount() {
}
render() {
return (
<div>
Overview
</div>
);
}
}
export default PublisherOverview;

@ -16,7 +16,9 @@
* under the License.
*/
import Theme from '../../theme';
import PropTypes from 'prop-types';
import Chip from 'material-ui/Chip';
import Dropzone from 'react-dropzone';
import React, {Component} from 'react';
import Toggle from 'material-ui/Toggle';
@ -26,12 +28,12 @@ import FlatButton from 'material-ui/FlatButton';
import IconButton from 'material-ui/IconButton';
import SelectField from 'material-ui/SelectField';
import RaisedButton from 'material-ui/RaisedButton';
import PlatformMgtApi from '../../api/platformMgtApi';
import Clear from 'material-ui/svg-icons/content/clear';
import {GridList, GridTile} from 'material-ui/GridList';
import Close from 'material-ui/svg-icons/navigation/close';
import {Card, CardActions, CardTitle} from 'material-ui/Card';
import AddCircleOutline from 'material-ui/svg-icons/content/add-circle-outline';
import Theme from '../../theme';
/**
* Platform Create component.
@ -47,7 +49,20 @@ class PlatformCreate extends Component {
constructor() {
super();
this.onCreatePlatform = this.onCreatePlatform.bind(this);
this.handleToggle = this.handleToggle.bind(this);
this.addProperty = this.addProperty.bind(this);
this.addTags = this.addTags.bind(this);
this.clearForm = this.clearForm.bind(this);
this.onPropertySelect = this.onPropertySelect.bind(this);
this.handleTagChange = this.handleTagChange.bind(this);
this.removeIcon = this.removeIcon.bind(this);
this.onTextChange = this.onTextChange.bind(this);
this.renderChip = this.renderChip.bind(this);
this.removeProperty = this.removeProperty.bind(this);
this.state = {
tags: [],
defValue: "",
enabled: true,
allTenants: false,
files: [],
@ -57,6 +72,7 @@ class PlatformCreate extends Component {
description: "",
property: "",
icon: [],
identifier: "",
propertyTypes: [
{key: 0, value: 'String'},
{key: 1, value: 'Number'},
@ -81,7 +97,7 @@ class PlatformCreate extends Component {
* Handles toggle button actions.
* One method is used for all the toggle buttons and, each toggle is identified by the id.
* */
_handleToggle(event) {
handleToggle(event) {
switch (event.target.id) {
case "enabled" : {
let enabled = this.state.enabled;
@ -99,15 +115,64 @@ class PlatformCreate extends Component {
/**
* Triggers the onChange action on property type selection.
* */
_onPropertySelect = (event, index, value) => {
onPropertySelect(event, index, value) {
console.log(this.state.propertyTypes[value]);
this.setState({selectedProperty: value});
};
}
/**
* Handles Chip delete function.
* Removes the tag from state.tags
* */
handleTagDelete(key) {
this.chipData = this.state.tags;
const chipToDelete = this.chipData.map((chip) => chip.key).indexOf(key);
this.chipData.splice(chipToDelete, 1);
this.setState({tags: this.chipData});
}
/**
* Create a tag on Enter key press and set it to the state.
* Clears the tags text field.
* Chip gets two parameters: Key and value.
* */
addTags(event) {
let tags = this.state.tags;
if (event.charCode === 13) {
event.preventDefault();
tags.push({key: Math.floor(Math.random() * 1000), value: event.target.value});
this.setState({tags, defValue: ""});
}
}
/**
* Creates Chip array from state.tags.
* */
renderChip(data) {
return (
<Chip
key={data.key}
onRequestDelete={() => this.handleTagDelete(data.key)}
style={this.styles.chip}
>
{data.value}
</Chip>
);
}
/**
* Set the value for tag.
* */
handleTagChange(event) {
let defaultValue = this.state.defValue;
defaultValue = event.target.value;
this.setState({defValue: defaultValue})
}
/**
* Remove the selected property from the property list.
* */
_removeProperty(property) {
removeProperty(property) {
let properties = this.state.platformProperties;
properties.splice(properties.indexOf(property), 1);
this.setState({platformProperties: properties});
@ -116,28 +181,31 @@ class PlatformCreate extends Component {
/**
* Add a new platform property.
* */
_addProperty() {
addProperty() {
let property = this.state.property;
let selected = this.state.selectedProperty;
this.setState({platformProperties:
this.setState({
platformProperties:
this.state.platformProperties.concat([
{
key: property,
value: this.state.propertyTypes[selected].value
}]),
property: "",
selectedProperty: 0});
selectedProperty: 0
});
}
/**
* Triggers in onChange event of text fields.
* Text fields are identified by their ids and the value will be persisted in the component state.
* */
_onTextChange = (event, value) => {
onTextChange(event, value) {
let property = this.state.property;
let name = this.state.name;
let description = this.state.description;
let identifier = this.state.identifier;
switch (event.target.id) {
case "name": {
@ -157,32 +225,57 @@ class PlatformCreate extends Component {
this.setState({property: property});
break;
}
case "identifier": {
identifier = value;
this.setState({identifier: identifier});
}
}
};
_onCreatePlatform() {
/**
* Create platform object and call the create platform api.
* */
onCreatePlatform(event) {
//Call the platform create api.
event.preventDefault();
let platform = {};
platform.identifier = this.state.identifier;
platform.name = this.state.name;
platform.description = this.state.description;
platform.tags = this.state.tags;
platform.properties = this.state.platformProperties;
platform.icon = this.state.icon;
platform.enabled = this.state.enabled;
platform.allTenants = this.state.allTenants;
platform.defaultTenantMapping = true;
PlatformMgtApi.createPlatform(platform);
}
/**
* Remove the uploaded icon.
* */
_removeIcon(event) {
removeIcon(event) {
event.preventDefault();
this.setState({icon: []});
}
/**
* Clears the user entered values in the form.
* */
_clearForm() {
this.setState({enabled: true,
clearForm(event) {
event.preventDefault();
this.setState({
enabled: true,
allTenants: false,
files: [],
platformProperties: [],
selectedProperty: 0,
name: "",
description: "",
property: "",})
property: "",
})
}
render() {
@ -193,25 +286,38 @@ class PlatformCreate extends Component {
selectedProperty,
propertyTypes,
name,
tags,
defValue,
description,
property} = this.state;
identifier,
property
} = this.state;
return (
<div className="middle createplatformmiddle">
<Card>
<CardTitle title="Create Platform"/>
<CardActions>
<div className="createplatformcardaction">
<form>
<TextField
hintText="Unique Identifier for Platform."
id="identifier"
floatingLabelText="Identifier*"
floatingLabelFixed={true}
value={identifier}
onChange={this.onTextChange}
/>
<br/>
<TextField
hintText="Enter the Platform Name."
id="name"
floatingLabelText="Name*"
floatingLabelFixed={true}
value={name}
onChange={this._onTextChange.bind(this)}
/><br/>
onChange={this.onTextChange}
/>
<br/>
<TextField
id="description"
hintText="Enter the Platform Description."
@ -220,28 +326,46 @@ class PlatformCreate extends Component {
multiLine={true}
rows={2}
value={description}
onChange={this._onTextChange.bind(this)}
/><br/><br/>
onChange={this.onTextChange}
/>
<br/>
<br/>
<Toggle
id="tenant"
label="Shared with all Tenants"
labelPosition="right"
onToggle={this._handleToggle.bind(this)}
onToggle={this.handleToggle}
toggled={allTenants}
/> <br/>
/>
<br/>
<Toggle
id="enabled"
label="Enabled"
labelPosition="right"
onToggle={this._handleToggle.bind(this)}
onToggle={this.handleToggle}
toggled={enabled}
/> <br/>
/>
<br/>
<TextField
id="tags"
hintText="Enter Platform tags.."
floatingLabelText="Tags*"
floatingLabelFixed={true}
value={defValue}
onChange={this.handleTagChange}
onKeyPress={this.addTags}
/>
<br/>
<div className="createPlatformTagWrapper">
{tags.map(this.renderChip, this)}
</div>
<br/>
<div>
<p className="createplatformproperties">Platform Properties</p>
<div id="property-container">
{platformProperties.map((p) => {
return <div key={p.key}>{p.key} : {p.value}
<IconButton onClick={this._removeProperty.bind(this, p)}>
<IconButton onClick={this.removeProperty.bind(this, p)}>
<Close className="createplatformpropertyclose"/>
</IconButton>
</div>
@ -254,21 +378,21 @@ class PlatformCreate extends Component {
floatingLabelText="Platform Property*"
floatingLabelFixed={true}
value={this.state.property}
onChange={this._onTextChange.bind(this)}
onChange={this.onTextChange}
/> <em/>
<SelectField
className="createplatformpropertyselect"
floatingLabelText="Property Type"
value={selectedProperty}
floatingLabelFixed={true}
onChange={this._onPropertySelect.bind(this)}>
onChange={this.onPropertySelect}>
{propertyTypes.map((type) => {
return <MenuItem key={type.key}
value={type.key}
primaryText={type.value}/>
})}
</SelectField>
<IconButton onClick={this._addProperty.bind(this)}>
<IconButton onClick={this.addProperty}>
<AddCircleOutline/>
</IconButton>
<br/>
@ -278,27 +402,33 @@ class PlatformCreate extends Component {
<p className="createplatformiconp">Platform Icon*:</p>
<GridList className="createplatformicon" cols={1.1}>
{this.state.icon.map((tile) => (
<GridTile key={Math.floor(Math.random() * 1000)}
<GridTile
key={Math.floor(Math.random() * 1000)}
title={tile.name}
actionIcon={
<IconButton onClick={this._removeIcon.bind(this)}>
<IconButton onClick={this.removeIcon}>
<Clear/>
</IconButton>}>
<img src={tile.preview}/>
</GridTile>
))}
{this.state.icon.length === 0 ?
<Dropzone className="createplatformdropzone"
<Dropzone
className="createplatformdropzone"
accept="image/jpeg, image/png"
onDrop={(icon, rejected) => {this.setState({icon, rejected})}}>
onDrop={(icon, rejected) => {
this.setState({icon, rejected})
}}
>
<p className="createplatformdropzonep">+</p>
</Dropzone> : <div/>}
</GridList>
</div>
<br/>
<RaisedButton primary={true} label="Create"
onClick={this._onCreatePlatform.bind(this)}/>
<FlatButton label="Cancel" onClick={this._clearForm.bind(this)}/>
<RaisedButton
primary={true} label="Create"
onClick={this.onCreatePlatform}/>
<FlatButton label="Cancel" onClick={this.clearForm}/>
</form>
</div>
</CardActions>
@ -308,7 +438,6 @@ class PlatformCreate extends Component {
}
}
PlatformCreate.prototypes = {
};
PlatformCreate.prototypes = {};
export default PlatformCreate;

@ -16,12 +16,12 @@
* under the License.
*/
import Theme from '../../theme';
import React, {Component} from 'react';
import {withRouter} from 'react-router-dom';
import TextField from 'material-ui/TextField';
import DataTable from '../UIComponents/DataTable';
import {Card, CardActions, CardTitle} from 'material-ui/Card';
import Theme from '../../theme';
/**
* The App Create Component.
@ -56,7 +56,7 @@ class PlatformListing extends Component {
* Handles the search action.
* When typing in the search bar, this method will be invoked.
* */
_searchApplications(word) {
searchApplications(word) {
let searchedData = [];
}
@ -64,13 +64,13 @@ class PlatformListing extends Component {
* Handles sort data function and toggles the asc state.
* asc: true : sort in ascending order.
* */
_sortData() {
sortData() {
let isAsc = this.state.asc;
let datas = isAsc?this.data.sort(this._compare):this.data.reverse();
let datas = isAsc ? this.data.sort(this.compare) : this.data.reverse();
this.setState({data: datas, asc: !isAsc});
}
_compare(a, b) {
compare(a, b) {
if (a.applicationName < b.applicationName)
return -1;
if (a.applicationName > b.applicationName)
@ -78,7 +78,7 @@ class PlatformListing extends Component {
return 0;
}
_onRowClick(id) {
onRowClick(id) {
console.log(id)
}
@ -86,19 +86,20 @@ class PlatformListing extends Component {
return (
<div className='middle listingplatformmiddle'>
<Card className='listingplatformcard'>
<TextField hintText="Search" onChange={this._searchApplications.bind(this)}
<TextField hintText="Search" onChange={this.searchApplications.bind(this)}
className='listingplatformsearch'/>
<CardTitle title="Platforms" className='listingplatformTitle'/>
<CardActions>
</CardActions>
<DataTable headers={this.headers}
<DataTable
headers={this.headers}
data={this.data}
handleRowClick={this._onRowClick.bind(this)}
handleRowClick={this.onRowClick.bind(this)}
noDataMessage={{type: 'button', text: 'Create Platform'}}/>
</Card>
</div>);
</div>
);
}
}

@ -16,13 +16,13 @@
* under the License.
*/
import Theme from '../../theme';
import PropTypes from 'prop-types';
import React, {Component} from 'react';
import DataTableRow from './DataTableRow';
import DataTableHeader from './DataTableHeader';
import RaisedButton from 'material-ui/RaisedButton';
import {Table, TableBody, TableHeader, TableRow} from 'material-ui/Table';
import Theme from '../../theme';
/**
* The Custom Table Component.
@ -53,6 +53,7 @@ class DataTable extends Component {
constructor() {
super();
this.handleRowClick = this.handleRowClick.bind(this);
this.state = {
data: [],
headers: [],
@ -83,7 +84,7 @@ class DataTable extends Component {
* Triggers when user click on table row.
* This method invokes the parent method handleRowClick, which is passed via props.
* */
_handleRowClick(id) {
handleRowClick(id) {
this.props.handleRowClick(id);
}
@ -101,21 +102,28 @@ class DataTable extends Component {
if (data) {
return (<Table
selectable={false}>
<TableHeader displaySelectAll={ false }
adjustForCheckbox={ false }>
<TableHeader displaySelectAll={false} adjustForCheckbox={false}>
<TableRow>
{headers.map((header) => {
return (<DataTableHeader key={header.data_id} className="datatableRowColumn"
header={header}/>)
}
return (
<DataTableHeader
key={header.data_id}
className="datatableRowColumn"
header={header}
/>
)}
)}
</TableRow>
</TableHeader>
<TableBody>
{data.map((dataItem) => {
return (<DataTableRow key={dataItem.id}
return (
<DataTableRow
key={dataItem.id}
dataItem={dataItem}
handleClick={this._handleRowClick.bind(this)}/>)
handleClick={this.handleRowClick}
/>
)
})}
</TableBody>
</Table>)

@ -16,11 +16,11 @@
* under the License.
*/
import Theme from '../../theme';
import PropTypes from 'prop-types';
import React, {Component} from 'react';
import FlatButton from 'material-ui/FlatButton';
import {TableHeaderColumn} from 'material-ui/Table';
import Theme from '../../theme';
/**
* Data Table header component.
@ -30,6 +30,7 @@ class DataTableHeader extends Component {
constructor() {
super();
this.tableHeaderClick = this.tableHeaderClick.bind(this);
this.scriptId = "data-table";
}
@ -42,13 +43,14 @@ class DataTableHeader extends Component {
componentWillUnmount() {
Theme.removeThemingScripts(this.scriptId);
}
/**
* The onClick function of the table header.
* Invokes the function passed in the header object.
* */
_tableHeaderClick() {
tableHeaderClick() {
this.props.header.sort();
}
@ -60,8 +62,12 @@ class DataTableHeader extends Component {
* else create a span element with label as the table header.
* */
if (this.props.header.sortable) {
headerCell = <FlatButton label={this.props.header.label}
onClick={this._tableHeaderClick.bind(this)} className="sortableHeaderCell"/>;
headerCell =
<FlatButton
label={this.props.header.label}
onClick={this.tableHeaderClick}
className="sortableHeaderCell"
/>
} else {
headerCell = <span className="notsortableHeaderCell">{this.props.header.label}</span>;
}

@ -16,10 +16,10 @@
* under the License.
*/
import Theme from '../../theme';
import PropTypes from 'prop-types';
import React, {Component} from 'react';
import {TableRow, TableRowColumn} from 'material-ui/Table';
import Theme from '../../theme';
/**
* Data table row component.
@ -51,18 +51,26 @@ class DataTableRow extends Component {
/**
* Triggers the click event on the data table row.
* */
_handleClick() {
handleClick() {
this.props.handleClick(this.state.dataItem.id);
}
render() {
const {dataItem} = this.state;
return (
<TableRow key={this.props.key} onClick={this._handleClick.bind(this)} >
<TableRow
key={this.props.key}
onClick={this.handleClick.bind(this)}
>
{Object.keys(dataItem).map((key) => {
if (key !== 'id') {
return <TableRowColumn className = "datatableRowColumn"
key={key}>{dataItem[key]}</TableRowColumn>
return (
<TableRowColumn
className="datatableRowColumn"
key={key}
>
{dataItem[key]}
</TableRowColumn>)
} else {
return <TableRowColumn key={key}/>
}

@ -20,13 +20,11 @@ import qs from 'qs';
import PropTypes from 'prop-types';
import React, {Component} from 'react';
import Checkbox from 'material-ui/Checkbox';
import TextField from 'material-ui/TextField';
import {Redirect, Switch} from 'react-router-dom';
import AuthHandler from '../../../api/authHandler';
import RaisedButton from 'material-ui/RaisedButton';
import {Card, CardActions, CardTitle} from 'material-ui/Card';
import {TextValidator, ValidatorForm} from 'react-material-ui-form-validator';
//todo: remove the {TextValidator, ValidatorForm} and implement it manually.
/**
* The Login Component.
@ -40,36 +38,43 @@ class Login extends Component {
constructor() {
super();
this.state = {
isLoggedIn: true,
isLoggedIn: false,
referrer: "/",
userName: "",
password: "",
rememberMe: true
rememberMe: true,
errors: {}
}
}
componentDidMount() {
let queryString = this.props.location.search;
console.log(queryString);
queryString = queryString.replace(/^\?/, '');
/* With QS version up we can directly use {ignoreQueryPrefix: true} option */
let params = qs.parse(queryString);
if (params.referrer) {
this.setState({referrer: params.referrer});
componentWillMount() {
console.log("IN Login")
}
componentDidMount() {
console.log("in Login")
// let queryString = this.props.location.search;
// console.log(queryString);
// queryString = queryString.replace(/^\?/, '');
// /* With QS version up we can directly use {ignoreQueryPrefix: true} option */
// let params = qs.parse(queryString);
// if (params.referrer) {
// this.setState({referrer: params.referrer});
// }
}
handleLogin(event) {
event.preventDefault();
this.validateForm();
}
/**
* Handles the username field change event.
* */
onUserNameChange(event) {
onUserNameChange(event, value) {
this.setState(
{
userName: event.target.value
userName: value
}
);
}
@ -77,10 +82,10 @@ class Login extends Component {
/**
* Handles the password field change event.
* */
onPasswordChange(event) {
onPasswordChange(event, value) {
this.setState(
{
password: event.target.value
password: value
}
);
}
@ -96,48 +101,77 @@ class Login extends Component {
);
}
/**
* Validate the login form.
* */
validateForm() {
let errors = {};
let validationFailed = true;
if (!this.state.password) {
errors["passwordError"] = "Password is Required";
validationFailed = true;
} else {
validationFailed = false;
}
if (!this.state.userName) {
errors["userNameError"] = "User Name is Required";
validationFailed = true;
} else {
validationFailed = false;
}
if (validationFailed) {
this.setState({errors: errors}, console.log(errors));
} else {
let loginPromis = AuthHandler.login(this.state.userName, this.state.password);
loginPromis.then(response => {
console.log(AuthHandler.getUser());
this.setState({isLoggedIn: AuthHandler.getUser()});
})
}
}
render() {
if (!this.state.isLoggedIn) {
return (
<div>
{/*TODO: Style the components.*/}
<Card>
<CardTitle title="WSO2 IoT App Publisher"/>
<CardActions>
<ValidatorForm
ref="form"
onSubmit={this.handleLogin.bind(this)}
onError={errors => console.log(errors)}>
<TextValidator
floatingLabelText="User Name"
<form onSubmit={this.handleLogin.bind(this)}>
<TextField
hintText="Enter the User Name."
id="username"
errorText={this.state.errors["userNameError"]}
floatingLabelText="User Name*"
floatingLabelFixed={true}
onChange={this.onUserNameChange.bind(this)}
name="userName"
validators={['required']}
errorMessages={['User Name is required']}
value={this.state.userName}
onChange={this.onUserNameChange.bind(this)}
/>
<br/>
<TextValidator
floatingLabelText="Password"
floatingLabelFixed={true}
onChange={this.onPasswordChange.bind(this)}
name="password"
<TextField
hintText="Enter the Password."
id="password"
type="password"
errorText={this.state.errors["passwordError"]}
floatingLabelText="Password*"
floatingLabelFixed={true}
value={this.state.password}
validators={['required']}
errorMessages={['Password is required']}
onChange={this.onPasswordChange.bind(this)}
/>
<br/>
<Checkbox label="Remember me."
<Checkbox
label="Remember me."
onCheck={this.handleRememberMe.bind(this)}
checked={this.state.rememberMe}/>
checked={this.state.rememberMe}
/>
<br/>
<RaisedButton type="submit" label="Login"/>
</ValidatorForm>
</form>
</CardActions>
</Card>
</div>);

@ -21,7 +21,6 @@ import NotFound from './Error/NotFound';
import BaseLayout from './Base/BaseLayout';
import PlatformCreate from './Platform/PlatformCreate';
import PlatformListing from './Platform/PlatformListing';
import PublisherOverview from './Overview/PublisherOverview';
import ApplicationCreate from './Application/ApplicationCreate';
import ApplicationListing from './Application/ApplicationListing';
@ -29,5 +28,4 @@ import ApplicationListing from './Application/ApplicationListing';
* Contains all UI components related to Application, Login and Platform
*/
export {Login, BaseLayout, ApplicationCreate, ApplicationListing, PlatformListing, NotFound, PublisherOverview,
PlatformCreate};
export {Login, BaseLayout, ApplicationCreate, ApplicationListing, PlatformListing, NotFound, PlatformCreate};

@ -35,10 +35,10 @@ class Theme {
//TODO Need to get the app context properly when the server is ready
this.baseURL = window.location.origin;
this.appContext = window.location.pathname.split("/")[1];
this.loadThemeConfigs.bind(this);
this.loadThemeFiles.bind(this);
this.insertThemingScripts.bind(this);
this.removeThemingScripts.bind(this);
this.loadThemeConfigs = this.loadThemeConfigs.bind(this);
this.loadThemeFiles = this.loadThemeFiles.bind(this);
this.insertThemingScripts = this.insertThemingScripts.bind(this);
this.removeThemingScripts = this.removeThemingScripts.bind(this);
}
/**

@ -38,6 +38,7 @@
<module>org.wso2.carbon.device.application.mgt.common</module>
<module>org.wso2.carbon.device.application.mgt.api</module>
<module>org.wso2.carbon.device.application.mgt.publisher.ui</module>
<module>org.wso2.carbon.device.application.mgt.authhandler</module>
</modules>
<build>

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
~
~ WSO2 Inc. 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.wso2.carbon.devicemgt</groupId>
<artifactId>application-mgt-feature</artifactId>
<version>3.0.46-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>org.wso2.carbon.device.application.mgt.auth.handler.feature</artifactId>
<version>3.0.46-SNAPSHOT</version>
<packaging>pom</packaging>
<name>WSO2 Carbon - Application Management Authentication Handler Feature</name>
<description>This feature contains the Authentication Handler implementation for Publisher and Store.</description>
<url>http://wso2.org</url>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy</id>
<phase>package</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.wso2.carbon.devicemgt</groupId>
<artifactId>org.wso2.carbon.device.application.mgt.authhandler
</artifactId>
<version>${project.version}</version>
<type>war</type>
<overWrite>true</overWrite>
<outputDirectory>
${project.build.directory}/maven-shared-archive-resources/webapps
</outputDirectory>
<destFileName>auth#application-mgt#v1.0.war</destFileName>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-resources</id>
<phase>generate-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>src/main/resources</outputDirectory>
<resources>
<resource>
<directory>resources</directory>
<includes>
<include>build.properties</include>
<include>p2.inf</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.wso2.maven</groupId>
<artifactId>carbon-p2-plugin</artifactId>
<executions>
<execution>
<id>p2-feature-generation</id>
<phase>package</phase>
<goals>
<goal>p2-feature-gen</goal>
</goals>
<configuration>
<id>org.wso2.carbon.device.application.mgt.auth.handler</id>
<propertiesFile>../../../features/etc/feature.properties
</propertiesFile>
<adviceFile>
<properties>
<propertyDef>org.wso2.carbon.p2.category.type:server
</propertyDef>
<propertyDef>org.eclipse.equinox.p2.type.group:false
</propertyDef>
</properties>
</adviceFile>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,3 @@
instructions.configure = \
org.eclipse.equinox.p2.touchpoint.natives.mkdir(path:${installFolder}/../../deployment/server/webapps/);\
org.eclipse.equinox.p2.touchpoint.natives.copy(source:${installFolder}/../features/org.wso2.carbon.device.application.mgt.auth.handler_${feature.version}/webapps/auth#application-mgt#v1.0.war,target:${installFolder}/../../deployment/server/webapps/auth#application-mgt#v1.0.war,overwrite:true);\

@ -42,7 +42,7 @@
</dependency>
<dependency>
<groupId>org.wso2.carbon.devicemgt</groupId>
<artifactId>org.wso2.carbon.device.application.mgt.ui.feature</artifactId>
<artifactId>org.wso2.carbon.device.application.mgt.publisher.ui.feature</artifactId>
<type>zip</type>
</dependency>
<dependency>

@ -35,6 +35,7 @@
<modules>
<module>org.wso2.carbon.device.application.mgt.api.feature</module>
<module>org.wso2.carbon.device.application.mgt.auth.handler.feature</module>
<!--<module>org.wso2.carbon.device.application.mgt.ui.feature</module>-->
<module>org.wso2.carbon.device.application.mgt.feature</module>
<!--<module>org.wso2.carbon.device.application.mgt.extensions.feature</module>-->

Loading…
Cancel
Save