diff --git a/.gitignore b/.gitignore index 02655632..59050da6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ *.launch .settings/ *.sublime-workspace +__pycache__/ # IDE - VSCode .vscode/* @@ -37,3 +38,5 @@ testem.log # System Files .DS_Store Thumbs.db + + diff --git a/login/backend/v1/login-api/Dockerfile b/login/backend/v1/login-api/Dockerfile index a70a90d3..f5f84987 100644 --- a/login/backend/v1/login-api/Dockerfile +++ b/login/backend/v1/login-api/Dockerfile @@ -2,7 +2,7 @@ # The selene-shared parent image contains all the common Docker configs for # all Selene apps and services see the "shared" directory in this repository. -FROM selene-shared:latest +FROM docker.mycroft.ai/selene-shared:latest LABEL description="Run the API for the Mycroft login screen" # Use pipenv to install the package's dependencies in the container diff --git a/login/backend/v1/login-api/Pipfile b/login/backend/v1/login-api/Pipfile index 6c6495b8..62766ff9 100644 --- a/login/backend/v1/login-api/Pipfile +++ b/login/backend/v1/login-api/Pipfile @@ -9,9 +9,10 @@ requests = "*" pyjwt = "*" flask-restful = "*" certifi = "*" -gunicorn = "*" +uwsgi = "*" [dev-packages] +selene-util = {path = "./../../../../shared"} [requires] python_version = "3.7" diff --git a/login/backend/v1/login-api/Pipfile.lock b/login/backend/v1/login-api/Pipfile.lock index c48cd4e8..0806d099 100644 --- a/login/backend/v1/login-api/Pipfile.lock +++ b/login/backend/v1/login-api/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "624569e9bc0207f58dca3a683d5868e374e78a5be00a34b442aae4a656e4eac2" + "sha256": "7cf1dde24d5a966645f3e49d93dde93dad42c6b6fa62f9b254b79d6b58e93e06" }, "pipfile-spec": 6, "requires": { @@ -40,10 +40,11 @@ }, "click": { "hashes": [ - "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", - "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" ], - "version": "==6.7" + "markers": "python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.1.*'", + "version": "==7.0" }, "flask": { "hashes": [ @@ -61,14 +62,6 @@ "index": "pypi", "version": "==0.3.6" }, - "gunicorn": { - "hashes": [ - "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471", - "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3" - ], - "index": "pypi", - "version": "==19.9.0" - }, "idna": { "hashes": [ "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", @@ -133,6 +126,13 @@ "markers": "python_version >= '2.6' and python_version != '3.1.*' and python_version != '3.3.*' and python_version < '4' and python_version != '3.0.*' and python_version != '3.2.*'", "version": "==1.23" }, + "uwsgi": { + "hashes": [ + "sha256:d2318235c74665a60021a4fc7770e9c2756f9fc07de7b8c22805efe85b5ab277" + ], + "index": "pypi", + "version": "==2.0.17.1" + }, "werkzeug": { "hashes": [ "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", @@ -141,5 +141,9 @@ "version": "==0.14.1" } }, - "develop": {} + "develop": { + "selene-util": { + "path": "./../../../../shared" + } + } } diff --git a/login/backend/v1/login-api/login_api/api.py b/login/backend/v1/login-api/login_api/api.py index 929fd37c..12bd1db0 100644 --- a/login/backend/v1/login-api/login_api/api.py +++ b/login/backend/v1/login-api/login_api/api.py @@ -1,17 +1,41 @@ -from flask import Flask +from flask import Flask, request from flask_restful import Api -from .authorize import AuthorizeAntisocialView +from .endpoints import ( + AuthenticateAntisocialEndpoint, + SocialLoginTokensEndpoint, + AuthorizeFacebookEndpoint, + AuthorizeGithubEndpoint, + AuthorizeGoogleEndpoint, + LogoutEndpoint +) from .config import get_config_location -from .logout import LogoutView - -BASE_URL = '/api/auth/' +# Initialize the Flask application and the Flask Restful API login = Flask(__name__) login.config.from_object(get_config_location()) login_api = Api(login, catch_all_404s=True) -antisocial_view_url = BASE_URL + 'antisocial' -login_api.add_resource(AuthorizeAntisocialView, antisocial_view_url) +# Define the endpoints +login_api.add_resource(AuthenticateAntisocialEndpoint, '/api/antisocial') +login_api.add_resource(AuthorizeFacebookEndpoint, '/api/social/facebook') +login_api.add_resource(AuthorizeGithubEndpoint, '/api/social/github') +login_api.add_resource(AuthorizeGoogleEndpoint, '/api/social/google') +login_api.add_resource(SocialLoginTokensEndpoint, '/api/social/tokens') +login_api.add_resource(LogoutEndpoint, '/api/logout') + + +def add_cors_headers(response): + """Allow any application to logout""" + # if 'logout' in request.url: + response.headers['Access-Control-Allow-Origin'] = '*' + if request.method == 'OPTIONS': + response.headers['Access-Control-Allow-Methods'] = ( + 'DELETE, GET, POST, PUT' + ) + headers = request.headers.get('Access-Control-Request-Headers') + if headers: + response.headers['Access-Control-Allow-Headers'] = headers + return response + -logout_view_url = BASE_URL + 'logout' -login_api.add_resource(LogoutView, logout_view_url) +login.after_request(add_cors_headers) diff --git a/login/backend/v1/login-api/login_api/authorize.py b/login/backend/v1/login-api/login_api/authorize.py deleted file mode 100644 index 88e0f64e..00000000 --- a/login/backend/v1/login-api/login_api/authorize.py +++ /dev/null @@ -1,76 +0,0 @@ -from datetime import datetime -from http import HTTPStatus -import json -from time import time - -from flask import current_app, request as frontend_request -from flask_restful import Resource -import jwt -import requests as service_request - -THIRTY_DAYS = 2592000 - - -def encode_selene_token(user_uuid): - """ - Generates the Auth Token - :return: string - """ - token_expiration = time() + THIRTY_DAYS - payload = dict(iat=datetime.utcnow(), exp=token_expiration, sub=user_uuid) - selene_token = jwt.encode( - payload, - current_app.config['SECRET_KEY'], - algorithm='HS256' - ) - - # before returning the token, convert it from bytes to string so that - # it can be included in a JSON response object - return selene_token.decode() - - -class AuthorizeAntisocialView(Resource): - """ - User Login Resource - """ - def __init__(self): - self.frontend_response = None - self.response_status_code = HTTPStatus.OK - self.tartarus_token = None - self.users_uuid = None - - def get(self): - self._authorize() - self._build_frontend_response() - - return self.frontend_response - - def _authorize(self): - basic_credentials = frontend_request.headers['authorization'] - service_request_headers = {'Authorization': basic_credentials} - auth_service_response = service_request.get( - current_app.config['TARTARUS_BASE_URL'] + '/auth/login', - headers=service_request_headers - ) - if auth_service_response.status_code == HTTPStatus.OK: - auth_service_response_content = json.loads( - auth_service_response.content - ) - self.users_uuid = auth_service_response_content['uuid'] - self.tartarus_token = auth_service_response_content['accessToken'] - else: - self.response_status_code = auth_service_response.status_code - - def _build_frontend_response(self): - if self.response_status_code == HTTPStatus.OK: - frontend_response_data = dict( - expiration=time() + THIRTY_DAYS, - seleneToken=encode_selene_token(self.users_uuid), - tartarusToken=self.tartarus_token, - ) - else: - frontend_response_data = {} - self.frontend_response = ( - frontend_response_data, - self.response_status_code - ) diff --git a/login/backend/v1/login-api/login_api/config.py b/login/backend/v1/login-api/login_api/config.py index 711a346b..1da52994 100644 --- a/login/backend/v1/login-api/login_api/config.py +++ b/login/backend/v1/login-api/login_api/config.py @@ -8,20 +8,30 @@ class LoginConfigException(Exception): class BaseConfig: """Base configuration.""" DEBUG = False + LOGIN_BASE_URL = os.environ['LOGIN_BASE_URL'] SECRET_KEY = os.environ['JWT_SECRET'] + SELENE_BASE_URL = os.environ['SELENE_BASE_URL'] + TARTARUS_BASE_URL = os.environ['TARTARUS_BASE_URL'] class DevelopmentConfig(BaseConfig): """Development configuration.""" DEBUG = True - TARTARUS_BASE_URL = 'https://api-test.mycroft.ai/v1' + + +class TestConfig(BaseConfig): + pass + + +class ProdConfig(BaseConfig): + pass def get_config_location(): environment_configs = dict( dev='login_api.config.DevelopmentConfig', - # test=TestConfig, - # prod=ProdConfig + test=TestConfig, + prod=ProdConfig ) try: diff --git a/login/backend/v1/login-api/login_api/endpoints/__init__.py b/login/backend/v1/login-api/login_api/endpoints/__init__.py new file mode 100644 index 00000000..894858af --- /dev/null +++ b/login/backend/v1/login-api/login_api/endpoints/__init__.py @@ -0,0 +1,6 @@ +from .authenticate_antisocial import AuthenticateAntisocialEndpoint +from .social_login_tokens import SocialLoginTokensEndpoint +from .facebook import AuthorizeFacebookEndpoint +from .github import AuthorizeGithubEndpoint +from .google import AuthorizeGoogleEndpoint +from .logout import LogoutEndpoint diff --git a/login/backend/v1/login-api/login_api/endpoints/authenticate_antisocial.py b/login/backend/v1/login-api/login_api/endpoints/authenticate_antisocial.py new file mode 100644 index 00000000..c6db90f8 --- /dev/null +++ b/login/backend/v1/login-api/login_api/endpoints/authenticate_antisocial.py @@ -0,0 +1,54 @@ +from http import HTTPStatus +import json +from time import time + +import requests as service_request + +from selene_util.api import SeleneEndpoint, APIError +from selene_util.auth import encode_auth_token, THIRTY_DAYS + + +class AuthenticateAntisocialEndpoint(SeleneEndpoint): + """ + User Login Resource + """ + def __init__(self): + super(AuthenticateAntisocialEndpoint, self).__init__() + self.response_status_code = HTTPStatus.OK + self.tartarus_token = None + self.users_uuid = None + + def get(self): + try: + self._authenticate_credentials() + except APIError: + pass + else: + self._build_response() + + return self.response + + def _authenticate_credentials(self): + basic_credentials = self.request.headers['authorization'] + service_request_headers = {'Authorization': basic_credentials} + auth_service_response = service_request.get( + self.config['TARTARUS_BASE_URL'] + '/auth/login', + headers=service_request_headers + ) + self._check_for_service_errors(auth_service_response) + auth_service_response_content = json.loads( + auth_service_response.content + ) + self.users_uuid = auth_service_response_content['uuid'] + self.tartarus_token = auth_service_response_content['accessToken'] + + def _build_response(self): + self.selene_token = encode_auth_token( + self.config['SECRET_KEY'], self.users_uuid + ) + response_data = dict( + expiration=time() + THIRTY_DAYS, + seleneToken=self.selene_token, + tartarusToken=self.tartarus_token, + ) + self.response = (response_data, HTTPStatus.OK) \ No newline at end of file diff --git a/login/backend/v1/login-api/login_api/endpoints/authenticate_social.py b/login/backend/v1/login-api/login_api/endpoints/authenticate_social.py new file mode 100644 index 00000000..89074695 --- /dev/null +++ b/login/backend/v1/login-api/login_api/endpoints/authenticate_social.py @@ -0,0 +1,37 @@ +from http import HTTPStatus + +from selene_util.api import SeleneEndpoint +from selene_util.auth import encode_auth_token, THIRTY_DAYS +from time import time +import json + +class AuthenticateSocialEndpoint(SeleneEndpoint): + def __init__(self): + super(AuthenticateSocialEndpoint, self).__init__() + self.response_status_code = HTTPStatus.OK + self.tartarus_token = None + self.users_uuid = None + + def get(self): + self._get_tartarus_token() + self._build_front_end_response() + return self.response + + def _get_tartarus_token(self): + args = self.request.args + if "data" in args: + self.tartarus_token = args['data'] + token_json = json.loads(self.tartarus_token) + self.users_uuid = token_json["uuid"] + + def _build_front_end_response(self): + self.selene_token = encode_auth_token( + self.config['SECRET_KEY'], self.users_uuid + ) + + response_data = dict( + expiration=time() + THIRTY_DAYS, + seleneToken=self.selene_token, + tartarusToken=self.tartarus_token, + ) + self.response = (response_data, HTTPStatus.OK) \ No newline at end of file diff --git a/login/backend/v1/login-api/login_api/endpoints/facebook.py b/login/backend/v1/login-api/login_api/endpoints/facebook.py new file mode 100644 index 00000000..d064e354 --- /dev/null +++ b/login/backend/v1/login-api/login_api/endpoints/facebook.py @@ -0,0 +1,17 @@ +"""Endpoint for single sign on through Facebook""" +from flask import redirect + +from selene_util.api import SeleneEndpoint + + +class AuthorizeFacebookEndpoint(SeleneEndpoint): + def get(self): + """Call a Tartarus endpoint that will redirect to Facebook login.""" + tartarus_auth_endpoint = ( + '{tartarus_url}/social/auth/facebook' + '?clientUri={login_url}&path=/social/login'.format( + tartarus_url=self.config['TARTARUS_BASE_URL'], + login_url=self.config['LOGIN_BASE_URL'] + ) + ) + return redirect(tartarus_auth_endpoint) diff --git a/login/backend/v1/login-api/login_api/endpoints/github.py b/login/backend/v1/login-api/login_api/endpoints/github.py new file mode 100644 index 00000000..27417297 --- /dev/null +++ b/login/backend/v1/login-api/login_api/endpoints/github.py @@ -0,0 +1,18 @@ +"""Endpoint for single sign on through Github""" +from flask import redirect + +from selene_util.api import SeleneEndpoint + + +class AuthorizeGithubEndpoint(SeleneEndpoint): + + def get(self): + """Call a Tartarus endpoint that will redirect to Github login.""" + tartarus_auth_endpoint = ( + '{tartarus_url}/social/auth/github' + '?clientUri={login_url}&path=/social/login'.format( + tartarus_url=self.config['TARTARUS_BASE_URL'], + login_url=self.config['LOGIN_BASE_URL'] + ) + ) + return redirect(tartarus_auth_endpoint) \ No newline at end of file diff --git a/login/backend/v1/login-api/login_api/endpoints/google.py b/login/backend/v1/login-api/login_api/endpoints/google.py new file mode 100644 index 00000000..62b501af --- /dev/null +++ b/login/backend/v1/login-api/login_api/endpoints/google.py @@ -0,0 +1,18 @@ +"""Endpoint for single sign on through Google""" +from flask import redirect + +from selene_util.api import SeleneEndpoint + + +class AuthorizeGoogleEndpoint(SeleneEndpoint): + + def get(self): + """Call a Tartarus endpoint that will redirect to Google login.""" + tartarus_auth_endpoint = ( + '{tartarus_url}/social/auth/google' + '?clientUri={login_url}&path=/social/login'.format( + login_url=self.config['LOGIN_BASE_URL'], + tartarus_url=self.config['TARTARUS_BASE_URL'] + ) + ) + return redirect(tartarus_auth_endpoint) diff --git a/login/backend/v1/login-api/login_api/endpoints/logout.py b/login/backend/v1/login-api/login_api/endpoints/logout.py new file mode 100644 index 00000000..52407c3a --- /dev/null +++ b/login/backend/v1/login-api/login_api/endpoints/logout.py @@ -0,0 +1,37 @@ +"""Log a user out of Mycroft web sites""" + +from http import HTTPStatus +from logging import getLogger + +import requests + +from selene_util.api import SeleneEndpoint, APIError + +_log = getLogger(__package__) + + +class LogoutEndpoint(SeleneEndpoint): + def __init__(self): + super(LogoutEndpoint, self).__init__() + + def put(self): + try: + self._authenticate() + self._logout() + except APIError: + pass + + return self.response + + def _logout(self): + service_request_headers = { + 'Authorization': 'Bearer ' + self.tartarus_token + } + service_url = self.config['TARTARUS_BASE_URL'] + '/auth/logout' + auth_service_response = requests.get( + service_url, + headers=service_request_headers + ) + self._check_for_service_errors(auth_service_response) + logout_response = auth_service_response.json() + self.response = (logout_response, HTTPStatus.OK) diff --git a/login/backend/v1/login-api/login_api/endpoints/social_login_tokens.py b/login/backend/v1/login-api/login_api/endpoints/social_login_tokens.py new file mode 100644 index 00000000..896e81d2 --- /dev/null +++ b/login/backend/v1/login-api/login_api/endpoints/social_login_tokens.py @@ -0,0 +1,32 @@ +from http import HTTPStatus +import json +from time import time + +from selene_util.api import SeleneEndpoint +from selene_util.auth import encode_auth_token, THIRTY_DAYS + + +class SocialLoginTokensEndpoint(SeleneEndpoint): + def post(self): + self._get_tartarus_token() + self._build_selene_token() + self._build_response() + return self.response + + def _get_tartarus_token(self): + request_data = json.loads(self.request.data) + self.tartarus_token = request_data['accessToken'] + self.user_uuid = request_data["uuid"] + + def _build_selene_token(self): + self.selene_token = encode_auth_token( + self.config['SECRET_KEY'], self.user_uuid + ) + + def _build_response(self): + response_data = dict( + expiration=time() + THIRTY_DAYS, + seleneToken=self.selene_token, + tartarusToken=self.tartarus_token, + ) + self.response = (response_data, HTTPStatus.OK) diff --git a/login/backend/v1/login-api/login_api/facebook.py b/login/backend/v1/login-api/login_api/facebook.py deleted file mode 100644 index 238b9126..00000000 --- a/login/backend/v1/login-api/login_api/facebook.py +++ /dev/null @@ -1,52 +0,0 @@ -# from http import HTTPStatus -# import json -# from time import time -# -# from flask import current_app, request as frontend_request -# from flask_restful import Resource -# import requests as service_request -# -# FACEBOOK_API_URL = 'https://graph.facebook.com/v3.1/me/?fields=name,email' -# THIRTY_DAYS = 2592000 -# -# -# class AuthorizeFacebookView(Resource): -# """ -# Check the authenticity Facebook token obtained by the frontend -# """ -# def __init__(self): -# self.frontend_response = None -# self.response_status_code = HTTPStatus.OK -# self.users_email = None -# self.users_name = None -# -# def get(self): -# self._validate_token() -# self._build_frontend_response() -# -# return self.frontend_response -# -# def _validate_token(self): -# facebook_token = frontend_request.headers['token'] -# service_request_headers = {'Authorization': 'Bearer ' + facebook_token} -# fb_service_response = service_request.get( -# FACEBOOK_API_URL, -# headers=service_request_headers -# ) -# if fb_service_response.status_code == HTTPStatus.OK: -# fb_service_response_content = json.loads(fb_service_response.content) -# self.users_name = fb_service_response_content['name'] -# self.users_email = fb_service_response_content['email'] -# else: -# self.response_status_code = fb_service_response.status_code -# -# def _build_frontend_response(self): -# if self.response_status_code == HTTPStatus.OK: -# frontend_response_data = dict( -# expiration=time() + THIRTY_DAYS, -# seleneToken=encode_selene_token(self.users_uuid), -# tartarusToken=self.tartarus_token, -# ) -# else: -# frontend_response_data = {} -# self.frontend_response = (frontend_response_data, self.response_status_code) diff --git a/login/backend/v1/login-api/login_api/logout.py b/login/backend/v1/login-api/login_api/logout.py deleted file mode 100644 index 9f00d424..00000000 --- a/login/backend/v1/login-api/login_api/logout.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Defines a view that will install a skill on a device running Mycroft core"""\ - -from http import HTTPStatus -from logging import getLogger -import json - -from flask import request, current_app -from flask_restful import Resource -import requests - -from selene_util.auth import decode_auth_token, AuthorizationError - -_log = getLogger(__package__) - - -class ServiceUrlNotFound(Exception): - """Exception to call when a HTTP 404 status is returned.""" - pass - - -class ServiceServerError(Exception): - """Catch all exception for errors not previously identified""" - pass - - -class LogoutView(Resource): - """Log a user out of the Mycroft web presence""" - def __init__(self): - self.service_response = None - self.frontend_response = None - self.user_uuid: str = None - self.tartarus_token: str = None - self.selene_token: str = None - self.device_uuid = None - self.installer_skill_settings = [] - - def put(self): - try: - self._get_auth_tokens() - self._validate_auth_token() - self._logout() - except AuthorizationError as ae: - self._build_unauthorized_response(str(ae)) - except ServiceUrlNotFound as nf: - self._build_server_error_response(str(nf)) - except ServiceServerError as se: - self._build_server_error_response(str(se)) - else: - self._build_success_response() - - return self.frontend_response - - def _get_auth_tokens(self): - try: - self.selene_token = request.cookies['seleneToken'] - self.tartarus_token = request.cookies['tartarusToken'] - except KeyError: - raise AuthorizationError( - 'no authentication tokens found in request' - ) - - def _validate_auth_token(self): - self.user_uuid = decode_auth_token( - self.selene_token, - current_app.config['SECRET_KEY'] - ) - - def _logout(self): - service_request_headers = { - 'Authorization': 'Bearer ' + self.tartarus_token - } - service_url = current_app.config['TARTARUS_BASE_URL'] + '/auth/logout' - service_response = requests.get( - service_url, - headers=service_request_headers - ) - self.check_for_tartarus_errors(service_response, service_url) - self.service_response_data = json.loads(service_response.content) - - def check_for_tartarus_errors(self, service_response, service_url): - if service_response.status_code == HTTPStatus.UNAUTHORIZED: - error_message = 'invalid Tartarus token' - _log.error(error_message) - raise AuthorizationError(error_message) - elif service_response.status_code == HTTPStatus.NOT_FOUND: - error_message = 'service url {} not found'.format(service_url) - _log.error(error_message) - raise ServiceUrlNotFound(error_message) - elif service_response.status_code != HTTPStatus.OK: - error_message = ( - 'error occurred during request to {service} URL {url}' - ) - _log.error(error_message.format( - service='tartarus', - url=service_url) - ) - raise ServiceServerError(error_message) - - def _build_unauthorized_response(self, error_message): - self.frontend_response = ( - dict(errorMessage=error_message), - HTTPStatus.UNAUTHORIZED - ) - - def _build_server_error_response(self, error_message): - self.frontend_response = ( - dict(errorMessage=error_message), - HTTPStatus.INTERNAL_SERVER_ERROR - ) - - def _build_success_response(self): - service_response_data = json.loads(self.service_response.content) - self.frontend_response = ( - dict(name=service_response_data.get('name')), - HTTPStatus.OK - ) diff --git a/login/frontend/v1/login-ui/Dockerfile b/login/frontend/v1/login-ui/Dockerfile index 3f359d95..19de0d16 100644 --- a/login/frontend/v1/login-ui/Dockerfile +++ b/login/frontend/v1/login-ui/Dockerfile @@ -6,7 +6,8 @@ WORKDIR /usr/src/app COPY package*.json ./ RUN npm install COPY . . -RUN npm run build +ARG selene_env +RUN npm run build-${selene_env} # STAGE TWO: build the web server and copy the compiled angular app to it. FROM nginx:latest diff --git a/login/frontend/v1/login-ui/angular.json b/login/frontend/v1/login-ui/angular.json index d115d62a..3cb15ca8 100644 --- a/login/frontend/v1/login-ui/angular.json +++ b/login/frontend/v1/login-ui/angular.json @@ -3,7 +3,7 @@ "version": 1, "newProjectRoot": "projects", "projects": { - "frontend": { + "mycroft-login": { "root": "", "sourceRoot": "src", "projectType": "application", diff --git a/login/frontend/v1/login-ui/package.json b/login/frontend/v1/login-ui/package.json index a6999974..32ff9d47 100644 --- a/login/frontend/v1/login-ui/package.json +++ b/login/frontend/v1/login-ui/package.json @@ -4,7 +4,9 @@ "scripts": { "ng": "ng", "start": "ng serve --open --proxy-config proxy.config.json", - "build": "ng build", + "build-dev": "ng build", + "build-test": "ng build", + "build-prod": "ng build --prod", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" @@ -16,7 +18,7 @@ "@angular/common": "^6.0.3", "@angular/compiler": "^6.0.3", "@angular/core": "^6.0.3", - "@angular/flex-layout": "^6.0.0-beta.16", + "@angular/flex-layout": "6.0.0-beta.16", "@angular/forms": "^6.0.3", "@angular/http": "^6.0.9", "@angular/material": "^6.4.1", @@ -29,7 +31,7 @@ "@fortawesome/free-regular-svg-icons": "^5.2.0", "@fortawesome/free-solid-svg-icons": "^5.2.0", "core-js": "^2.5.4", - "rxjs": "^6.0.0", + "rxjs": "6.2.2", "zone.js": "^0.8.26" }, "devDependencies": { diff --git a/login/frontend/v1/login-ui/src/app/app.component.html b/login/frontend/v1/login-ui/src/app/app.component.html index 167e6937..642d4ed7 100644 --- a/login/frontend/v1/login-ui/src/app/app.component.html +++ b/login/frontend/v1/login-ui/src/app/app.component.html @@ -1,3 +1,31 @@ -
-
- + +
+
+
+
+
+ +
+ +
+
diff --git a/login/frontend/v1/login-ui/src/app/app.component.scss b/login/frontend/v1/login-ui/src/app/app.component.scss index f9b4fb07..2718fdd3 100644 --- a/login/frontend/v1/login-ui/src/app/app.component.scss +++ b/login/frontend/v1/login-ui/src/app/app.component.scss @@ -2,24 +2,42 @@ /* Split the screen in half */ .split { - height: 50%; - left: 0; - overflow-x: hidden; - padding-top: 20px; - position: fixed; - width: 100%; - z-index: -1; + height: 50%; + left: 0; + overflow-x: hidden; + padding-top: 20px; + position: fixed; + width: 100%; + z-index: -1; } -/* Control the top side */ +/* Top Half */ .top { - top: 0; - background-color: $mycroft-primary; + top: 0; + background-color: $mycroft-primary; } -/* Control the bottom side */ +/* Bottom Half */ .bottom { - bottom: 0; - background-color: #e5e5e5; + bottom: 0; + background-color: #e5e5e5; } + +mat-tab-group { + height: 485px; + width: 320px; +} + +.login-options { + background-color: $mycroft-white; + border-radius: 10px; + box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.12); + width: 320px; +} + +img { + margin-bottom: 50px; + margin-top: 50px; + width: 600px; +} \ No newline at end of file diff --git a/login/frontend/v1/login-ui/src/app/app.component.ts b/login/frontend/v1/login-ui/src/app/app.component.ts index d3ec277b..63cf067d 100644 --- a/login/frontend/v1/login-ui/src/app/app.component.ts +++ b/login/frontend/v1/login-ui/src/app/app.component.ts @@ -1,10 +1,26 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; + @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) -export class AppComponent { +export class AppComponent implements OnInit { title = 'Mycroft Login'; + public socialLoginDataFound: boolean = false; + + constructor () { + } + + ngOnInit () { + let uriParams = decodeURIComponent(window.location.search); + + if (uriParams) { + this.socialLoginDataFound = true; + window.opener.postMessage(uriParams, window.location.origin); + window.close(); + } + } + } diff --git a/login/frontend/v1/login-ui/src/app/app.module.ts b/login/frontend/v1/login-ui/src/app/app.module.ts index f38b5c15..f9ba7c1a 100644 --- a/login/frontend/v1/login-ui/src/app/app.module.ts +++ b/login/frontend/v1/login-ui/src/app/app.module.ts @@ -1,6 +1,6 @@ import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { FlexModule } from "@angular/flex-layout"; +import { FlexLayoutModule } from "@angular/flex-layout"; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; @@ -12,7 +12,7 @@ import { AuthModule } from "./auth/auth.module"; BrowserModule, AuthModule, BrowserAnimationsModule, - FlexModule, + FlexLayoutModule ], providers: [ ], bootstrap: [ AppComponent ] diff --git a/login/frontend/v1/login-ui/src/app/auth/auth-antisocial/auth-antisocial.component.html b/login/frontend/v1/login-ui/src/app/auth/auth-antisocial/auth-antisocial.component.html deleted file mode 100644 index 0cc3d399..00000000 --- a/login/frontend/v1/login-ui/src/app/auth/auth-antisocial/auth-antisocial.component.html +++ /dev/null @@ -1,29 +0,0 @@ -
- - - - - - - - Forgot password? - - -
-
Invalid username/password combination; try again
diff --git a/login/frontend/v1/login-ui/src/app/auth/auth-antisocial/auth-antisocial.component.scss b/login/frontend/v1/login-ui/src/app/auth/auth-antisocial/auth-antisocial.component.scss deleted file mode 100644 index 9fb4d4f4..00000000 --- a/login/frontend/v1/login-ui/src/app/auth/auth-antisocial/auth-antisocial.component.scss +++ /dev/null @@ -1,33 +0,0 @@ -@import '../../../stylesheets/global'; - -button { - @include login-button; -} - -form { - background-color: $mycroft-white; - padding: 20px; - fa-icon { - color: $mycroft-dark-grey; - margin-right: 15px; - } - mat-form-field { - width: 230px; - } - mat-checkbox { - color: $mycroft-dark-grey; - } - .forgot-password { - margin-left: 30px; - } - button { - background-color: $mycroft-primary; - margin-top: 30px; - text-align: center; - } -} - -.mat-body-2 { - color: $mycroft-tertiary-red; - padding: 15px; -} diff --git a/login/frontend/v1/login-ui/src/app/auth/auth-antisocial/auth-antisocial.component.spec.ts b/login/frontend/v1/login-ui/src/app/auth/auth-antisocial/auth-antisocial.component.spec.ts deleted file mode 100644 index c0c8cb1b..00000000 --- a/login/frontend/v1/login-ui/src/app/auth/auth-antisocial/auth-antisocial.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { AuthAntisocialComponent } from './auth-antisocial.component'; - -describe('AuthAntisocialComponent', () => { - let component: AuthAntisocialComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ AuthAntisocialComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(AuthAntisocialComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/login/frontend/v1/login-ui/src/app/auth/auth-antisocial/auth-antisocial.component.ts b/login/frontend/v1/login-ui/src/app/auth/auth-antisocial/auth-antisocial.component.ts deleted file mode 100644 index fa11b846..00000000 --- a/login/frontend/v1/login-ui/src/app/auth/auth-antisocial/auth-antisocial.component.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Component, OnInit } from '@angular/core'; - -import { faUser, faLock } from "@fortawesome/free-solid-svg-icons"; - -import { AuthService, AuthResponse } from "../auth.service"; - -@Component({ - selector: 'login-auth-antisocial', - templateUrl: './auth-antisocial.component.html', - styleUrls: ['./auth-antisocial.component.scss'] -}) -export class AuthAntisocialComponent implements OnInit { - public authFailed = false; - public password: string; - public passwordIcon = faLock; - public username: string; - public usernameIcon = faUser; - - constructor(private authService: AuthService) { } - - ngOnInit() { } - - authorizeUser(): void { - this.authService.authorizeAntisocial(this.username, this.password).subscribe( - (response) => {this.onAuthSuccess(response)}, - (response) => {this.onAuthFailure(response)} - ); - } - - onAuthSuccess(authResponse: AuthResponse) { - this.authFailed = false; - let expirationDate = new Date(authResponse.expiration * 1000); - let domain = document.domain.replace('login.', ''); - document.cookie = 'seleneToken=' + authResponse.seleneToken + - '; expires=' + expirationDate.toUTCString() + - '; domain=' + domain; - document.cookie = 'tartarusToken=' + authResponse.tartarusToken + - '; expires=' + expirationDate.toUTCString() + - '; domain=' + domain; - window.parent.postMessage('loggedIn', '*') - } - - onAuthFailure(authorizeUserResponse) { - if (authorizeUserResponse.status === 401) { - this.authFailed = true; - } - } -} diff --git a/login/frontend/v1/login-ui/src/app/auth/auth-social/auth-social.component.html b/login/frontend/v1/login-ui/src/app/auth/auth-social/auth-social.component.html deleted file mode 100644 index 17260773..00000000 --- a/login/frontend/v1/login-ui/src/app/auth/auth-social/auth-social.component.html +++ /dev/null @@ -1,14 +0,0 @@ -
- - - -
diff --git a/login/frontend/v1/login-ui/src/app/auth/auth-social/auth-social.component.scss b/login/frontend/v1/login-ui/src/app/auth/auth-social/auth-social.component.scss deleted file mode 100644 index 243524f6..00000000 --- a/login/frontend/v1/login-ui/src/app/auth/auth-social/auth-social.component.scss +++ /dev/null @@ -1,33 +0,0 @@ -@import '../../../stylesheets/global'; - -button { - @include login-button; -} - -.social { - padding: 20px; - button { - margin-bottom: 15px; - } - fa-icon { - margin-right: 15px; - font-size: 28px; - } - .facebook-button { - background-color: #3b5998; - padding-left: 5px; - } - .github-button { - background-color: #333333; - margin-right: 12px; - padding-left: 5px; - } - .google-button { - background-color: #4285F4; - padding-left: 1px; - img { - margin-right: 10px; - width: 14%; - } - } -} diff --git a/login/frontend/v1/login-ui/src/app/auth/auth-social/auth-social.component.spec.ts b/login/frontend/v1/login-ui/src/app/auth/auth-social/auth-social.component.spec.ts deleted file mode 100644 index fd456262..00000000 --- a/login/frontend/v1/login-ui/src/app/auth/auth-social/auth-social.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { AuthSocialComponent } from './auth-social.component'; - -describe('AuthSocialComponent', () => { - let component: AuthSocialComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ AuthSocialComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(AuthSocialComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/login/frontend/v1/login-ui/src/app/auth/auth-social/auth-social.component.ts b/login/frontend/v1/login-ui/src/app/auth/auth-social/auth-social.component.ts deleted file mode 100644 index 0c8641e1..00000000 --- a/login/frontend/v1/login-ui/src/app/auth/auth-social/auth-social.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Component, OnInit } from '@angular/core'; - -import { faFacebook, faGithub } from '@fortawesome/free-brands-svg-icons'; - -@Component({ - selector: 'login-auth-social', - templateUrl: './auth-social.component.html', - styleUrls: ['./auth-social.component.scss'] -}) -export class AuthSocialComponent implements OnInit { - public facebookIcon = faFacebook; - public githubIcon = faGithub; - - constructor() {} - - ngOnInit() { } -} diff --git a/login/frontend/v1/login-ui/src/app/auth/auth.component.html b/login/frontend/v1/login-ui/src/app/auth/auth.component.html index 7b90bc64..13a4199b 100644 --- a/login/frontend/v1/login-ui/src/app/auth/auth.component.html +++ b/login/frontend/v1/login-ui/src/app/auth/auth.component.html @@ -1,19 +1,49 @@ -
-
- -
- + +
OR
+
+ + + + + + + + Forgot password? + + +
+
Invalid username/password combination; try again
diff --git a/login/frontend/v1/login-ui/src/app/auth/auth.component.scss b/login/frontend/v1/login-ui/src/app/auth/auth.component.scss index d6227fbd..8cb3e4a7 100644 --- a/login/frontend/v1/login-ui/src/app/auth/auth.component.scss +++ b/login/frontend/v1/login-ui/src/app/auth/auth.component.scss @@ -1,13 +1,70 @@ @import '../../stylesheets/global'; -mat-tab-group { - height: 485px; - width: 320px; +button { + @include login-button; } -.login-options { - border-radius: 10px; +.social { + padding: 20px; + button { + margin-bottom: 15px; + } + fa-icon { + margin-right: 15px; + font-size: 28px; + } + .facebook-button { + background-color: #3b5998; + padding-left: 5px; + } + .github-button { + background-color: #333333; + margin-right: 12px; + padding-left: 5px; + } + .google-button { + background-color: #4285F4; + padding-left: 1px; + img { + margin-right: 10px; + width: 14%; + } + } +} + +button { + @include login-button; +} + +form { background-color: $mycroft-white; + border-radius: 10px; + padding: 20px; + fa-icon { + color: $mycroft-dark-grey; + margin-right: 15px; + } + mat-checkbox { + color: $mycroft-dark-grey; + } + .forgot-password { + margin-left: 30px; + } + button { + background-color: $mycroft-primary; + margin-top: 30px; + text-align: center; + } + button:hover { + background-color: $mycroft-tertiary-green; + color: $mycroft-secondary; + } + +} + +.mat-body-2 { + color: $mycroft-tertiary-red; + padding: 15px; } .mat-subheading-2 { @@ -16,9 +73,3 @@ mat-tab-group { margin-top: -15px; text-align: center; } - -img { - margin-bottom: 50px; - margin-top: 50px; - width: 600px; -} diff --git a/login/frontend/v1/login-ui/src/app/auth/auth.component.ts b/login/frontend/v1/login-ui/src/app/auth/auth.component.ts index 47f92135..3912a95b 100644 --- a/login/frontend/v1/login-ui/src/app/auth/auth.component.ts +++ b/login/frontend/v1/login-ui/src/app/auth/auth.component.ts @@ -1,13 +1,55 @@ import { Component, OnInit } from '@angular/core'; +import { faFacebook, faGithub } from "@fortawesome/free-brands-svg-icons"; +import { faLock, faUser } from "@fortawesome/free-solid-svg-icons"; + +import { AuthResponse, AuthService } from "./auth.service"; + @Component({ selector: 'login-authenticate', templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'] }) export class AuthComponent implements OnInit { + public facebookIcon = faFacebook; + public githubIcon = faGithub; + public authFailed: boolean; + public password: string; + public passwordIcon = faLock; + public username: string; + public usernameIcon = faUser; - constructor() { } + constructor(private authService: AuthService) { } ngOnInit() { } + + authenticateFacebook(): void { + this.authService.authenticateWithFacebook() + } + + authenticateGithub(): void { + this.authService.authenticateWithGithub(); + } + + authenticateGoogle(): void { + this.authService.authenticateWithGoogle(); + } + authorizeUser(): void { + this.authService.authorizeAntisocial(this.username, this.password).subscribe( + (response) => {this.onAuthSuccess(response)}, + (response) => {this.onAuthFailure(response)} + ); + } + + onAuthSuccess(authResponse: AuthResponse) { + this.authFailed = false; + this.authService.generateTokenCookies(authResponse); + window.history.back(); + } + + onAuthFailure(authorizeUserResponse) { + if (authorizeUserResponse.status === 401) { + this.authFailed = true; + } + } } diff --git a/login/frontend/v1/login-ui/src/app/auth/auth.module.ts b/login/frontend/v1/login-ui/src/app/auth/auth.module.ts index c939fcc1..cce309e6 100644 --- a/login/frontend/v1/login-ui/src/app/auth/auth.module.ts +++ b/login/frontend/v1/login-ui/src/app/auth/auth.module.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from "@angular/forms"; -import { FlexModule } from "@angular/flex-layout"; +import { FlexLayoutModule } from "@angular/flex-layout"; import { HttpClientModule } from "@angular/common/http"; import { MatButtonModule, @@ -9,22 +9,20 @@ import { MatDividerModule, MatFormFieldModule, MatInputModule, - MatTabsModule + MatSnackBarModule } from "@angular/material"; import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; import { AuthComponent } from './auth.component'; import { AuthService } from "./auth.service"; -import { AuthSocialComponent } from './auth-social/auth-social.component'; -import { AuthAntisocialComponent } from './auth-antisocial/auth-antisocial.component'; @NgModule({ - declarations: [ AuthComponent, AuthSocialComponent, AuthAntisocialComponent ], + declarations: [ AuthComponent ], exports: [ AuthComponent ], imports: [ CommonModule, - FlexModule, + FlexLayoutModule, FontAwesomeModule, FormsModule, HttpClientModule, @@ -33,7 +31,7 @@ import { AuthAntisocialComponent } from './auth-antisocial/auth-antisocial.compo MatDividerModule, MatFormFieldModule, MatInputModule, - MatTabsModule + MatSnackBarModule ], providers: [ AuthService ] }) diff --git a/login/frontend/v1/login-ui/src/app/auth/auth.service.ts b/login/frontend/v1/login-ui/src/app/auth/auth.service.ts index 1ae37382..3176c70d 100644 --- a/login/frontend/v1/login-ui/src/app/auth/auth.service.ts +++ b/login/frontend/v1/login-ui/src/app/auth/auth.service.ts @@ -2,19 +2,33 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders} from "@angular/common/http"; import { Observable } from 'rxjs'; +import { isArray } from "util"; -export class AuthResponse { +import { MatSnackBar } from "@angular/material"; + +export interface AuthResponse { expiration: number; seleneToken: string; tartarusToken: string; } +export interface SocialLoginData { + uuid: string; + accessToken: string; + refreshToken: string; + expiration: string; +} + @Injectable() export class AuthService { - private antisocialAuthUrl = '/api/auth/antisocial'; - private facebookAuthUrl = '/api/auth/facebook'; + private antisocialAuthUrl = '/api/antisocial'; + private facebookAuthUrl = '/api/social/facebook'; + private githubAuthUrl = '/api/social/github'; + private googleAuthUrl = '/api/social/google'; + private generateTokensUrl = 'api/social/tokens'; - constructor(private http: HttpClient) { } + constructor(private http: HttpClient, public loginSnackbar: MatSnackBar) { + } authorizeAntisocial (username, password): Observable { let rawCredentials = `${username}:${password}`; @@ -25,8 +39,71 @@ export class AuthService { return this.http.get(this.antisocialAuthUrl, {headers: httpHeaders}) } - authorizeFacebook(userData: any) { - const httpHeaders = new HttpHeaders({'token': userData.token}); - return this.http.get(this.facebookAuthUrl, {headers: httpHeaders}) + authenticateWithFacebook() { + window.open(this.facebookAuthUrl); + window.onmessage = (event) => {this.generateSocialLoginTokens(event)}; + } + + authenticateWithGithub() { + window.open(this.githubAuthUrl); + window.onmessage = (event) => {this.generateSocialLoginTokens(event)}; + } + + authenticateWithGoogle() { + window.open(this.googleAuthUrl); + window.onmessage = (event) => {this.generateSocialLoginTokens(event)}; + } + + generateSocialLoginTokens(event: any) { + let socialLoginData = this.parseUriParams(event.data); + if (socialLoginData) { + this.http.post( + this.generateTokensUrl, + socialLoginData + ).subscribe( + (response) => {this.generateTokenCookies(response)} + ); + } + return this.http.post( + this.generateTokensUrl, + socialLoginData + ) + } + + parseUriParams (uriParams: string) { + let socialLoginData: SocialLoginData = null; + + if (uriParams.startsWith('?data=')) { + let parsedUriParams = JSON.parse(uriParams.slice(6)); + if (isArray(parsedUriParams)) { + let socialLoginErrorMsg = 'An account exists for the email ' + + 'address associated with the social network log in ' + + 'attempt. To enable log in using a social network, log ' + + 'in with your username and password and enable the ' + + 'social network in your account preferences.'; + this.loginSnackbar.open( + socialLoginErrorMsg, + null, + {duration: 30000} + ); + } else { + socialLoginData = parsedUriParams; + } + } + + return socialLoginData } + + generateTokenCookies(authResponse: AuthResponse) { + let expirationDate = new Date(authResponse.expiration * 1000); + let domain = document.domain.replace('login.', ''); + document.cookie = 'seleneToken=' + authResponse.seleneToken + + '; expires=' + expirationDate.toUTCString() + + '; domain=' + domain; + document.cookie = 'tartarusToken=' + authResponse.tartarusToken + + '; expires=' + expirationDate.toUTCString() + + '; domain=' + domain; + } + + } diff --git a/login/frontend/v1/login-ui/src/environments/environment.prod.ts b/login/frontend/v1/login-ui/src/environments/environment.prod.ts index 3612073b..dfe292cc 100644 --- a/login/frontend/v1/login-ui/src/environments/environment.prod.ts +++ b/login/frontend/v1/login-ui/src/environments/environment.prod.ts @@ -1,3 +1,15 @@ export const environment = { - production: true + production: true }; + +document.write( + '' +); +document.write( + '' +); \ No newline at end of file diff --git a/login/frontend/v1/login-ui/src/index.html b/login/frontend/v1/login-ui/src/index.html index 7c7ddffa..e60ea142 100644 --- a/login/frontend/v1/login-ui/src/index.html +++ b/login/frontend/v1/login-ui/src/index.html @@ -13,28 +13,6 @@ - - diff --git a/market/backend/v1/market-api/Dockerfile b/market/backend/v1/market-api/Dockerfile index 3f2d0069..1f11b58a 100644 --- a/market/backend/v1/market-api/Dockerfile +++ b/market/backend/v1/market-api/Dockerfile @@ -2,7 +2,7 @@ # The selene-shared parent image contains all the common Docker configs for # all Selene apps and services see the "shared" directory in this repository. -FROM selene-shared:latest +FROM docker.mycroft.ai/selene-shared:latest LABEL description="Run the API for the Mycroft marketplace" # Use pipenv to install the package's dependencies in the container diff --git a/market/backend/v1/market-api/Pipfile.lock b/market/backend/v1/market-api/Pipfile.lock index 92384441..d8682b7b 100644 --- a/market/backend/v1/market-api/Pipfile.lock +++ b/market/backend/v1/market-api/Pipfile.lock @@ -40,10 +40,11 @@ }, "click": { "hashes": [ - "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", - "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" ], - "version": "==6.7" + "markers": "python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.1.*'", + "version": "==7.0" }, "flask": { "hashes": [ @@ -82,11 +83,11 @@ }, "markdown": { "hashes": [ - "sha256:9ba587db9daee7ec761cfc656272be6aabe2ed300fece21208e4aab2e457bc8f", - "sha256:a856869c7ff079ad84a3e19cd87a64998350c2b94e9e08e44270faef33400f81" + "sha256:c00429bd503a47ec88d5e30a751e147dcb4c6889663cd3e2ba0afe858e009baa", + "sha256:d02e0f9b04c500cde6637c11ad7c72671f359b87b9fe924b2383649d8841db7c" ], "index": "pypi", - "version": "==2.6.11" + "version": "==3.0.1" }, "markupsafe": { "hashes": [ diff --git a/market/backend/v1/market-api/market_api/api.py b/market/backend/v1/market-api/market_api/api.py index 50ace663..bc19f3b6 100644 --- a/market/backend/v1/market-api/market_api/api.py +++ b/market/backend/v1/market-api/market_api/api.py @@ -3,10 +3,10 @@ from .config import get_config_location from market_api.endpoints import ( - SkillSummaryView, - SkillDetailView, - SkillInstallView, - UserView + SkillSummaryEndpoint, + SkillDetailEndpoint, + SkillInstallEndpoint, + UserEndpoint ) @@ -14,7 +14,7 @@ marketplace.config.from_object(get_config_location()) marketplace_api = Api(marketplace) -marketplace_api.add_resource(SkillSummaryView, '/api/skills') -marketplace_api.add_resource(SkillDetailView, '/api/skill/') -marketplace_api.add_resource(SkillInstallView, '/api/install-skill') -marketplace_api.add_resource(UserView, '/api/user') +marketplace_api.add_resource(SkillSummaryEndpoint, '/api/skills') +marketplace_api.add_resource(SkillDetailEndpoint, '/api/skill/') +marketplace_api.add_resource(SkillInstallEndpoint, '/api/install') +marketplace_api.add_resource(UserEndpoint, '/api/user') diff --git a/market/backend/v1/market-api/market_api/endpoints/__init__.py b/market/backend/v1/market-api/market_api/endpoints/__init__.py index 6637a934..036e1e7c 100644 --- a/market/backend/v1/market-api/market_api/endpoints/__init__.py +++ b/market/backend/v1/market-api/market_api/endpoints/__init__.py @@ -1,4 +1,4 @@ -from .skill_detail import SkillDetailView -from .skill_install import SkillInstallView -from .skill_summary import SkillSummaryView -from .user import UserView \ No newline at end of file +from .skill_detail import SkillDetailEndpoint +from .skill_install import SkillInstallEndpoint +from .skill_summary import SkillSummaryEndpoint +from .user import UserEndpoint diff --git a/market/backend/v1/market-api/market_api/endpoints/skill_detail.py b/market/backend/v1/market-api/market_api/endpoints/skill_detail.py index ad304637..34d78e01 100644 --- a/market/backend/v1/market-api/market_api/endpoints/skill_detail.py +++ b/market/backend/v1/market-api/market_api/endpoints/skill_detail.py @@ -1,37 +1,47 @@ """View to return detailed information about a skill""" +from http import HTTPStatus + from markdown import markdown import requests as service_request -from selene_util.api import SeleneBaseView, AuthorizationError +from selene_util.api import SeleneEndpoint, APIError + +class SkillDetailEndpoint(SeleneEndpoint): + authentication_required = False -class SkillDetailView(SeleneBaseView): def __init__(self): - super(SkillDetailView, self).__init__() + super(SkillDetailEndpoint, self).__init__() self.skill_id = None + self.response_skill = None def get(self, skill_id): - """Handle and HTTP GET request.""" self.skill_id = skill_id try: self._authenticate() - except AuthorizationError: + self._get_skill_details() + except APIError: pass - self._build_response() + else: + self._build_response_data() + self.response = (self.response_skill, HTTPStatus.OK) return self.response - def _build_response_data(self): + def _get_skill_details(self): """Build the data to include in the response.""" - self.service_response = service_request.get( - self.base_url + '/skill/id/' + self.skill_id + skill_service_response = service_request.get( + self.config['SELENE_BASE_URL'] + '/skill/id/' + self.skill_id ) - self.response_data = self.service_response.json() - self.response_data['description'] = markdown( - self.response_data['description'], + self._check_for_service_errors(skill_service_response) + self.response_skill = skill_service_response.json() + + def _build_response_data(self): + self.response_skill['description'] = markdown( + self.response_skill['description'], output_format='html5' ) - self.response_data['summary'] = markdown( - self.response_data['summary'], + self.response_skill['summary'] = markdown( + self.response_skill['summary'], output_format='html5' ) \ No newline at end of file diff --git a/market/backend/v1/market-api/market_api/endpoints/skill_install.py b/market/backend/v1/market-api/market_api/endpoints/skill_install.py index 445ef1a8..cc7fbf02 100644 --- a/market/backend/v1/market-api/market_api/endpoints/skill_install.py +++ b/market/backend/v1/market-api/market_api/endpoints/skill_install.py @@ -2,94 +2,67 @@ from logging import getLogger import json -from flask import request, current_app -from flask_restful import Resource import requests -from selene_util.auth import decode_auth_token, AuthorizationError +from selene_util.api import SeleneEndpoint, APIError _log = getLogger(__package__) -class ServiceUrlNotFound(Exception): - pass - - -class ServiceServerError(Exception): - pass - - -class SkillInstallView(Resource): +class SkillInstallEndpoint(SeleneEndpoint): """ Install a skill on user device(s). """ def __init__(self): - self.service_response = None - self.frontend_response = None - self.frontend_response_status_code = HTTPStatus.OK - self.user_uuid: str = None - self.tartarus_token: str = None - self.selene_token: str = None - self.device_uuid = None - self.installer_skill_settings = [] + super(SkillInstallEndpoint, self).__init__() + self.device_uuid: str = None + self.installer_skill_settings: list = [] + self.installer_update_response = None def put(self): try: - self._get_auth_tokens() - self._validate_auth_token() - self._install_skill() - except AuthorizationError as ae: - self._build_unauthorized_response(str(ae)) - except ServiceUrlNotFound as nf: - self._build_server_error_response(str(nf)) - except ServiceServerError as se: - self._build_server_error_response(str(se)) + self._authenticate() + self._get_installer_skill() + self._apply_update() + except APIError: + pass else: - self._build_frontend_response() - - return self.frontend_response - - def _get_auth_tokens(self): - try: - self.selene_token = request.cookies['seleneToken'] - self.tartarus_token = request.cookies['tartarusToken'] - except KeyError: - raise AuthorizationError( - 'no authentication tokens found in request' - ) + self.response = (self.installer_update_response, HTTPStatus.OK) - def _validate_auth_token(self): - self.user_uuid = decode_auth_token( - self.selene_token, - current_app.config['SECRET_KEY'] - ) + return self.response - def _install_skill(self): - self._get_users_installer_skill_settings() - installer_skill = self._find_installer_skill() + def _get_installer_skill(self): + installed_skills = self._get_installed_skills() + installer_skill = self._find_installer_skill(installed_skills) self._find_installer_settings(installer_skill) - self._update_skill_installer_settings() - def _get_users_installer_skill_settings(self): + def _get_installed_skills(self): service_request_headers = { 'Authorization': 'Bearer ' + self.tartarus_token } service_url = ( - current_app.config['TARTARUS_BASE_URL'] + + self.config['TARTARUS_BASE_URL'] + '/user/' + self.user_uuid + '/skill' ) - self.service_response = requests.get( + user_service_response = requests.get( service_url, headers=service_request_headers ) - self.check_for_tartarus_errors(service_url) + if user_service_response.status_code != HTTPStatus.OK: + self._check_for_service_errors(user_service_response) + if user_service_response.status_code == HTTPStatus.UNAUTHORIZED: + # override response built in _build_service_error_response() + # so that user knows there is a authentication issue + self.response = (self.response[0], HTTPStatus.UNAUTHORIZED) + raise APIError() - def _find_installer_skill(self): - service_response_data = json.loads(self.service_response.content) + return json.loads(user_service_response.content) + + def _find_installer_skill(self, installed_skills): installer_skill = None - for skill in service_response_data['skills']: + for skill in installed_skills['skills']: if skill['skill']['name'] == 'Installer': self.device_uuid = skill['deviceUuid'] installer_skill = skill['skill'] @@ -103,23 +76,30 @@ def _find_installer_settings(self, installer_skill): if setting['type'] != 'label': self.installer_skill_settings.append(setting) - def _update_skill_installer_settings(self): - service_url = current_app.config['TARTARUS_BASE_URL'] + '/skill/field' + def _apply_update(self): + service_url = self.config['TARTARUS_BASE_URL'] + '/skill/field' service_request_headers = { - 'Authorization': 'Bearer ' + self.tartarus_token + 'Authorization': 'Bearer ' + self.tartarus_token, + 'Content-Type': 'application/json' } - self.service_response = requests.patch( + service_request_data = json.dumps(self._build_update_request_body()) + skill_service_response = requests.patch( service_url, - data=json.dumps(self._build_install_request_body()), + data=service_request_data, headers=service_request_headers ) - self.check_for_tartarus_errors(service_url) + if skill_service_response.status_code != HTTPStatus.OK: + self._check_for_service_errors(skill_service_response) + + self.installer_update_response = json.loads( + skill_service_response.content + ) - def _build_install_request_body(self): + def _build_update_request_body(self): install_request_body = [] for setting in self.installer_skill_settings: if setting['name'] == 'installer_link': - setting_value = 'foo' + setting_value = self.request.json['skill_url'] elif setting['name'] == 'auto_install': setting_value = True else: @@ -130,53 +110,10 @@ def _build_install_request_body(self): raise ValueError(error_message.format(setting['name'])) install_request_body.append( dict( - fieldUiud=setting['uuid'], - deviceUuid=self.device_uuid, value=setting_value + fieldUuid=setting['uuid'], + deviceUuid=self.device_uuid, + value=setting_value ) ) - return dict(batch=install_request_body) - - def check_for_tartarus_errors(self, service_url): - if self.service_response.status_code == HTTPStatus.UNAUTHORIZED: - error_message = 'invalid Tartarus token' - _log.error(error_message) - raise AuthorizationError(error_message) - elif self.service_response.status_code == HTTPStatus.NOT_FOUND: - error_message = 'service url {} not found'.format(service_url) - _log.error(error_message) - raise ServiceUrlNotFound(error_message) - elif self.service_response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR: - error_message = ( - 'error occurred during GET request to URL ' + service_url - ) - _log.error(error_message) - raise ServiceServerError(error_message) - - def _build_unauthorized_response(self, error_message): - self.frontend_response = ( - dict(errorMessage=error_message), - HTTPStatus.UNAUTHORIZED - ) - def _build_server_error_response(self, error_message): - self.frontend_response = ( - dict(errorMessage=error_message), - HTTPStatus.INTERNAL_SERVER_ERROR - ) - - def _build_frontend_response(self): - if self.service_response.status_code == HTTPStatus.OK: - service_response_data = json.loads(self.service_response.content) - self.frontend_response = ( - dict(name=service_response_data.get('name')), - HTTPStatus.OK - ) - elif self.service_response.status_code == HTTPStatus.NOT_FOUND: - error_message = 'service url {} not found' - _log.error(error_message) - self.frontend_response = ( - dict(error_message=error_message), - HTTPStatus.INTERNAL_SERVER_ERROR - ) - else: - self.frontend_response = ({}, self.service_response.status_code) + return dict(batch=install_request_body) diff --git a/market/backend/v1/market-api/market_api/endpoints/skill_summary.py b/market/backend/v1/market-api/market_api/endpoints/skill_summary.py index b89d7fff..4b60cbd2 100644 --- a/market/backend/v1/market-api/market_api/endpoints/skill_summary.py +++ b/market/backend/v1/market-api/market_api/endpoints/skill_summary.py @@ -1,45 +1,118 @@ """Endpoint to provide skill summary data to the marketplace.""" from collections import defaultdict +from http import HTTPStatus +from logging import getLogger -from flask import request from markdown import markdown import requests as service_request -from selene_util.api import SeleneBaseView, AuthorizationError +from selene_util.api import SeleneEndpoint, APIError -UNDEFINED = 'Undefined' +UNDEFINED = 'Not Categorized' +_log = getLogger(__package__) + + +class SkillSummaryEndpoint(SeleneEndpoint): + authentication_required = False -class SkillSummaryView(SeleneBaseView): def __init__(self): - super(SkillSummaryView, self).__init__() - self.response_data = defaultdict(list) - self.search_term = None + super(SkillSummaryEndpoint, self).__init__() + self.available_skills: list = [] + self.installed_skills: list = [] + self.response_skills = defaultdict(list) def get(self): - """Handle a HTTP GET request.""" try: self._authenticate() - except AuthorizationError: + self._get_skills() + except APIError: pass - self._build_response() + else: + self._build_response_data() + self.response = (self.response_skills, HTTPStatus.OK) return self.response + def _get_skills(self): + self._get_available_skills() + self._get_installed_skills() + + def _get_available_skills(self): + skill_service_response = service_request.get( + self.config['SELENE_BASE_URL'] + '/skill/all' + ) + if skill_service_response.status_code != HTTPStatus.OK: + self._check_for_service_errors(skill_service_response) + self.available_skills = skill_service_response.json() + + # TODO: this is a temporary measure until skill IDs can be assigned + # the list of installed skills returned by Tartarus are keyed by a value + # that is not guaranteed to be the same as the skill title in the skill + # metadata. a skill ID needs to be defined and propagated. + def _get_installed_skills(self): + """Get the skills a user has already installed on their device(s) + + Installed skills will be marked as such in the marketplace so a user + knows it is already installed. + """ + if self.authenticated: + service_request_headers = { + 'Authorization': 'Bearer ' + self.tartarus_token + } + service_url = ( + self.config['TARTARUS_BASE_URL'] + + '/user/' + + self.user_uuid + + '/skill' + ) + user_service_response = service_request.get( + service_url, + headers=service_request_headers + ) + if user_service_response.status_code != HTTPStatus.OK: + self._check_for_service_errors(user_service_response) + + response_skills = user_service_response.json() + for skill in response_skills.get('skills', []): + self.installed_skills.append(skill['skill']['name']) + def _build_response_data(self): """Build the data to include in the response.""" - self.skill_service_response = service_request.get( - self.base_url + '/skill/all' - ) - if request.query_string: - query_string = request.query_string.decode() - self.search_term = query_string.lower().split('=')[1] - self._reformat_skills() + if self.request.query_string: + skills_to_include = self._filter_skills() + else: + skills_to_include = self.available_skills + self._reformat_skills(skills_to_include) self._sort_skills() - def _reformat_skills(self): + def _filter_skills(self) -> list: + skills_to_include = [] + + query_string = self.request.query_string.decode() + search_term = query_string.lower().split('=')[1] + for skill in self.available_skills: + search_term_match = ( + search_term is None or + search_term in skill['title'].lower() or + search_term in skill['description'].lower() or + search_term in skill['summary'].lower() + ) + if skill['categories'] and not search_term_match: + search_term_match = ( + search_term in skill['categories'][0].lower() + ) + for trigger in skill['triggers']: + if search_term in trigger.lower(): + search_term_match = True + if search_term_match: + skills_to_include.append(skill) + + return skills_to_include + + def _reformat_skills(self, skills_to_include: list): """Build the response data from the skill service response""" - for skill in self.skill_service_response.json(): + for skill in skills_to_include: if not skill['icon']: skill['icon'] = dict(icon='comment-alt', color='#6C7A89') skill_summary = dict( @@ -47,28 +120,25 @@ def _reformat_skills(self): icon=skill['icon'], icon_image=skill.get('icon_image'), id=skill['id'], - # TODO remove skill_name when login/install is implemented - skill_name=skill['skill_name'], + installed=skill['title'] in self.installed_skills, + repository_url=skill['repository_url'], summary=markdown(skill['summary'], output_format='html5'), title=skill['title'], triggers=skill['triggers'] ) - search_term_match = ( - self.search_term is None or - self.search_term in skill['title'].lower() - ) - if search_term_match: + if 'system' in skill['tags']: + skill_category = 'System' + elif skill['categories']: # a skill may have many categories. the first one in the # list is considered the "primary" category. This is the # category the marketplace will use to group the skill. - if skill['categories']: - skill_category = skill['categories'][0] - else: - skill_category = UNDEFINED - self.response_data[skill_category].append(skill_summary) + skill_category = skill['categories'][0] + else: + skill_category = UNDEFINED + self.response_skills[skill_category].append(skill_summary) def _sort_skills(self): """Sort the skills in alphabetical order""" - for skill_category, skills in self.response_data.items(): + for skill_category, skills in self.response_skills.items(): sorted_skills = sorted(skills, key=lambda skill: skill['title']) - self.response_data[skill_category] = sorted_skills + self.response_skills[skill_category] = sorted_skills diff --git a/market/backend/v1/market-api/market_api/endpoints/user.py b/market/backend/v1/market-api/market_api/endpoints/user.py index efb02dcc..58e4a1f7 100644 --- a/market/backend/v1/market-api/market_api/endpoints/user.py +++ b/market/backend/v1/market-api/market_api/endpoints/user.py @@ -1,55 +1,45 @@ """API endpoint to return the user's name to the marketplace""" from http import HTTPStatus -import json -from flask import request, current_app -from flask_restful import Resource import requests -from selene_util.auth import decode_auth_token +from selene_util.api import SeleneEndpoint, APIError -class UserView(Resource): - """ - User Login Resource - """ +class UserEndpoint(SeleneEndpoint): + """Retrieve information about the user based on their UUID""" def __init__(self): - self.service_response = None + super(UserEndpoint, self).__init__() + self.user = None self.frontend_response = None def get(self): - self._get_user_from_service() - self._build_frontend_response() + try: + self._authenticate() + self._get_user() + except APIError: + pass + else: + self._build_response() - return self.frontend_response + return self.response - def _get_user_from_service(self): - selene_token = request.cookies.get('seleneToken') - user_uuid = decode_auth_token( - selene_token, - current_app.config['SECRET_KEY'] - ) - tartarus_token = request.cookies.get('tartarusToken') - service_request_headers = {'Authorization': 'Bearer ' + tartarus_token} + def _get_user(self): + service_request_headers = { + 'Authorization': 'Bearer ' + self.tartarus_token + } service_url = ( - current_app.config['TARTARUS_BASE_URL'] + + self.config['TARTARUS_BASE_URL'] + '/user/' + - user_uuid + self.user_uuid ) - self.service_response = requests.get( + user_service_response = requests.get( service_url, headers=service_request_headers ) + self._check_for_service_errors(user_service_response) + self.user = user_service_response.json() - def _build_frontend_response(self): - if self.service_response.status_code == HTTPStatus.OK: - service_response_data = json.loads(self.service_response.content) - frontend_response_data = dict( - name=service_response_data.get('name') - ) - else: - frontend_response_data = {} - self.frontend_response = ( - frontend_response_data, - self.service_response.status_code - ) + def _build_response(self): + response_data = dict(name=self.user['name']) + self.response = (response_data, HTTPStatus.OK) diff --git a/market/frontend/v1/market-ui/package-lock.json b/market/frontend/v1/market-ui/package-lock.json index 43d6d7ff..0c6c5934 100644 --- a/market/frontend/v1/market-ui/package-lock.json +++ b/market/frontend/v1/market-ui/package-lock.json @@ -338,9 +338,9 @@ } }, "@angular/flex-layout": { - "version": "6.0.0-beta.17", - "resolved": "https://registry.npmjs.org/@angular/flex-layout/-/flex-layout-6.0.0-beta.17.tgz", - "integrity": "sha512-WrCWlE7NuvvxbeO8+S6aR5cvzX+1CVzpIy0izP8kMLWjAPZ0xjePHc2kJKJVapWMt7aniYZ1inl+GpsvkllycA==", + "version": "6.0.0-beta.16", + "resolved": "https://registry.npmjs.org/@angular/flex-layout/-/flex-layout-6.0.0-beta.16.tgz", + "integrity": "sha512-0AYtIBGrEJshdFMc6TXGloCkD19YTCRKVJl6xZHX4H5dLnUn+daqXcbh4UsWhayevnLp85HEf2ViHLmTa6jv3g==", "requires": { "tslib": "^1.7.1" } @@ -8334,9 +8334,9 @@ } }, "rxjs": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.1.tgz", - "integrity": "sha512-OwMxHxmnmHTUpgO+V7dZChf3Tixf4ih95cmXjzzadULziVl/FKhHScGLj4goEw9weePVOH2Q0+GcCBUhKCZc/g==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.2.tgz", + "integrity": "sha512-0MI8+mkKAXZUF9vMrEoPnaoHkfzBPP4IGwUYRJhIRJF6/w3uByO1e91bEHn8zd43RdkTMKiooYKmwz7RH6zfOQ==", "requires": { "tslib": "^1.9.0" } diff --git a/market/frontend/v1/market-ui/package.json b/market/frontend/v1/market-ui/package.json index aba64756..083a5139 100644 --- a/market/frontend/v1/market-ui/package.json +++ b/market/frontend/v1/market-ui/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "scripts": { "ng": "ng", - "start": "ng serve --open", + "start": "ng serve --open --proxy-config proxy.config.json", "build-dev": "ng build --configuration=development", "build-test": "ng build --configuration=test", "build-prod": "ng build --prod", @@ -18,7 +18,7 @@ "@angular/common": "^6.0.3", "@angular/compiler": "^6.0.3", "@angular/core": "^6.0.3", - "@angular/flex-layout": "^6.0.0-beta.16", + "@angular/flex-layout": "6.0.0-beta.16", "@angular/forms": "^6.0.3", "@angular/http": "^6.0.3", "@angular/material": "^6.3.2", @@ -32,7 +32,7 @@ "angular-in-memory-web-api": "^0.6.0", "core-js": "^2.5.4", "font-awesome": "^4.7.0", - "rxjs": "^6.0.0", + "rxjs": "6.2.2", "zone.js": "^0.8.26" }, "devDependencies": { diff --git a/market/frontend/v1/market-ui/proxy.config.json b/market/frontend/v1/market-ui/proxy.config.json new file mode 100644 index 00000000..31af6ab8 --- /dev/null +++ b/market/frontend/v1/market-ui/proxy.config.json @@ -0,0 +1,8 @@ +{ + "/api/*": { + "target": "http://localhost:5002", + "secure": false, + "logLevel": "debug", + "changeOrigin": true + } +} diff --git a/market/frontend/v1/market-ui/src/app/app-routing.module.ts b/market/frontend/v1/market-ui/src/app/app-routing.module.ts index 8dc38df8..21a67fef 100644 --- a/market/frontend/v1/market-ui/src/app/app-routing.module.ts +++ b/market/frontend/v1/market-ui/src/app/app-routing.module.ts @@ -1,11 +1,9 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; -import { LoginComponent } from "./header/login/login.component"; import { PageNotFoundComponent } from "./page-not-found/page-not-found.component"; const routes: Routes = [ - { path: 'login', component: LoginComponent}, { path: '', redirectTo: '/skills', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; diff --git a/market/frontend/v1/market-ui/src/app/app.component.scss b/market/frontend/v1/market-ui/src/app/app.component.scss index 532da7b3..8023a7b4 100644 --- a/market/frontend/v1/market-ui/src/app/app.component.scss +++ b/market/frontend/v1/market-ui/src/app/app.component.scss @@ -1,3 +1,7 @@ +@import '../stylesheets/global'; + .app-body { - margin: 50px; + margin-left: 3vw; + margin-right: 3vw; + margin-top: 30px; } diff --git a/market/frontend/v1/market-ui/src/app/header/header.component.html b/market/frontend/v1/market-ui/src/app/header/header.component.html index 86931a93..2f09b1d7 100644 --- a/market/frontend/v1/market-ui/src/app/header/header.component.html +++ b/market/frontend/v1/market-ui/src/app/header/header.component.html @@ -1,26 +1,22 @@ - - + +
MARKETPLACE
-
PREVIEW
- - - - - - - - - - - - - - - - - + + + + +
\ No newline at end of file diff --git a/market/frontend/v1/market-ui/src/app/header/header.component.scss b/market/frontend/v1/market-ui/src/app/header/header.component.scss index f9e386f3..62e0fe70 100644 --- a/market/frontend/v1/market-ui/src/app/header/header.component.scss +++ b/market/frontend/v1/market-ui/src/app/header/header.component.scss @@ -4,16 +4,16 @@ mat-toolbar { background-color: $mycroft-primary; color: $mycroft-white; img { + height: 20px; + margin-top: -7px; + } + .separator { + font-size: 5px; + padding-left: 10px; padding-right: 10px; - height: 40px; } - mat-divider { - border-color: #FFFFFF; - border-top-width: 0; - border-right-width: 1px; - border-right-style: solid; - height: 60%; - margin-right: 10px; + .mat-subheading-1 { + margin-bottom: 0; } fa-icon { padding-right: 5px; diff --git a/market/frontend/v1/market-ui/src/app/header/header.component.ts b/market/frontend/v1/market-ui/src/app/header/header.component.ts index ccb4b9b6..4570428c 100644 --- a/market/frontend/v1/market-ui/src/app/header/header.component.ts +++ b/market/frontend/v1/market-ui/src/app/header/header.component.ts @@ -1,7 +1,12 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { Subscription } from "rxjs/internal/Subscription"; -import { faCaretDown, faSignInAlt, faSignOutAlt } from "@fortawesome/free-solid-svg-icons"; +import { + faCaretDown, + faCircle, + faSignInAlt, + faSignOutAlt +} from "@fortawesome/free-solid-svg-icons"; import { LoginService } from "../shared/login.service"; @@ -13,6 +18,7 @@ import { LoginService } from "../shared/login.service"; export class HeaderComponent implements OnInit, OnDestroy { public isLoggedIn: boolean; private loginStatus: Subscription; + public separatorIcon = faCircle; public signInIcon = faSignInAlt; public signOutIcon = faSignOutAlt; public menuButtonIcon = faCaretDown; @@ -45,14 +51,18 @@ export class HeaderComponent implements OnInit, OnDestroy { } logout() { - let expiration = new Date(); - let domain = document.domain.replace('market.', ''); - document.cookie = 'seleneToken=""' + - '; expires=' + expiration.toUTCString() + - '; domain=' + domain; - document.cookie = 'tartarusToken=""' + - '; expires=' + expiration.toUTCString() + - '; domain=' + domain; - this.loginService.setLoginStatus(); + this.loginService.logout().subscribe( + (response) => { + let expiration = new Date(); + let domain = document.domain.replace('market.', ''); + document.cookie = 'seleneToken=""' + + '; expires=' + expiration.toUTCString() + + '; domain=' + domain; + document.cookie = 'tartarusToken=""' + + '; expires=' + expiration.toUTCString() + + '; domain=' + domain; + this.loginService.setLoginStatus(); + } + ) } } diff --git a/market/frontend/v1/market-ui/src/app/header/header.module.ts b/market/frontend/v1/market-ui/src/app/header/header.module.ts index 585f54c5..b372ed2c 100644 --- a/market/frontend/v1/market-ui/src/app/header/header.module.ts +++ b/market/frontend/v1/market-ui/src/app/header/header.module.ts @@ -6,7 +6,6 @@ import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; import { MaterialModule } from "../shared/material.module"; import { HeaderComponent } from './header.component'; -import { LoginComponent } from "./login/login.component"; @NgModule({ imports: [ @@ -15,7 +14,7 @@ import { LoginComponent } from "./login/login.component"; FontAwesomeModule, MaterialModule ], - declarations: [ HeaderComponent, LoginComponent ], + declarations: [ HeaderComponent], exports: [ HeaderComponent ], }) export class HeaderModule { } diff --git a/market/frontend/v1/market-ui/src/app/header/login/login.component.html b/market/frontend/v1/market-ui/src/app/header/login/login.component.html deleted file mode 100644 index 6c54503f..00000000 --- a/market/frontend/v1/market-ui/src/app/header/login/login.component.html +++ /dev/null @@ -1,3 +0,0 @@ -
- -
\ No newline at end of file diff --git a/market/frontend/v1/market-ui/src/app/header/login/login.component.scss b/market/frontend/v1/market-ui/src/app/header/login/login.component.scss deleted file mode 100644 index e5b1cd1e..00000000 --- a/market/frontend/v1/market-ui/src/app/header/login/login.component.scss +++ /dev/null @@ -1,8 +0,0 @@ -div { - width: 100%; - iframe { - border: none; - height: 1000px; - width: 100%; - } -} \ No newline at end of file diff --git a/market/frontend/v1/market-ui/src/app/header/login/login.component.spec.ts b/market/frontend/v1/market-ui/src/app/header/login/login.component.spec.ts deleted file mode 100644 index d6d85a84..00000000 --- a/market/frontend/v1/market-ui/src/app/header/login/login.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { LoginComponent } from './login.component'; - -describe('LoginComponent', () => { - let component: LoginComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ LoginComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(LoginComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/market/frontend/v1/market-ui/src/app/header/login/login.component.ts b/market/frontend/v1/market-ui/src/app/header/login/login.component.ts deleted file mode 100644 index 8ea00b6c..00000000 --- a/market/frontend/v1/market-ui/src/app/header/login/login.component.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser"; -import { Router } from '@angular/router'; - -import { LoginService } from '../../shared/login.service'; -import { environment } from "../../../environments/environment"; - - -@Component({ - selector: 'mycroft-login', - templateUrl: './login.component.html', - styleUrls: ['./login.component.scss'] -}) -export class LoginComponent implements OnInit { - public loginUrl: SafeResourceUrl; - - constructor( - public loginService: LoginService, - public router: Router, - private sanitizer: DomSanitizer - ) - { - this.loginUrl = sanitizer.bypassSecurityTrustResourceUrl(environment.loginUrl); - } - - ngOnInit() { - window.onmessage = (event) => { - this.redirectAfterLogin(event) - } - } - - redirectAfterLogin(loginEvent) { - if (loginEvent.origin.includes('login.mycroft') && loginEvent.data === 'loggedIn') { - this.loginService.setLoginStatus(); - if (this.loginService.isLoggedIn) { - let redirect = this.loginService.redirectUrl ? this.loginService.redirectUrl : '/'; - this.router.navigate([redirect]); - } - } - } -} \ No newline at end of file diff --git a/market/frontend/v1/market-ui/src/app/header/login/login.service.spec.ts b/market/frontend/v1/market-ui/src/app/shared/login.service.spec.ts similarity index 100% rename from market/frontend/v1/market-ui/src/app/header/login/login.service.spec.ts rename to market/frontend/v1/market-ui/src/app/shared/login.service.spec.ts diff --git a/market/frontend/v1/market-ui/src/app/shared/login.service.ts b/market/frontend/v1/market-ui/src/app/shared/login.service.ts index 79181427..0b7dea94 100644 --- a/market/frontend/v1/market-ui/src/app/shared/login.service.ts +++ b/market/frontend/v1/market-ui/src/app/shared/login.service.ts @@ -4,6 +4,7 @@ import { Router } from "@angular/router"; import { Observable } from "rxjs/internal/Observable"; import { Subject } from "rxjs/internal/Subject"; +import { environment } from "../../environments/environment"; export class User { name: string; @@ -13,19 +14,28 @@ export class User { export class LoginService { public isLoggedIn = new Subject(); public redirectUrl: string; + private logoutUrl = environment.loginUrl + '/api/logout'; private userUrl = '/api/user'; - constructor(private http: HttpClient, private router: Router) { } + constructor(private http: HttpClient, private router: Router) { + } getUser(): Observable { - return this.http.get(this.userUrl) + return this.http.get(this.userUrl); } setLoginStatus() { - this.isLoggedIn.next(document.cookie.includes('seleneToken')); + let cookies = document.cookie, + seleneTokenExists = cookies.includes('seleneToken'), + seleneTokenEmpty = cookies.includes('seleneToken=""'); + this.isLoggedIn.next( seleneTokenExists && !seleneTokenEmpty); } login() { - this.router.navigate(['/login']); + window.location.assign(environment.loginUrl); + } + + logout(): Observable { + return this.http.get(this.logoutUrl); } } \ No newline at end of file diff --git a/market/frontend/v1/market-ui/src/app/skills/skill-detail/skill-detail.component.html b/market/frontend/v1/market-ui/src/app/skills/skill-detail/skill-detail.component.html index c75fb56a..c77f9f54 100644 --- a/market/frontend/v1/market-ui/src/app/skills/skill-detail/skill-detail.component.html +++ b/market/frontend/v1/market-ui/src/app/skills/skill-detail/skill-detail.component.html @@ -1,27 +1,38 @@ -
-
+ +
+ +
- -
-
-

- - - - - {{skill.title}} -

-
+ +
+ + + + +
+

{{skill.title}}

+
-
- +
+ + +
+
+ +
+
- -
- -
-
-
HEY MYCROFT
-
+ + +
+ + +
+
+
hey mycroft
+
+
{{trigger}} - -
-
-
DESCRIPTION
-
-
-
-
CREDITS
-
-
{{credit.name}}
- -
-
-
SUPPORTED DEVICES
-
- - Mark I -
-
- - Mark II -
-
- - Picroft -
+
+
description
+
+
+
+
credits
+
+ {{credit.name}} +
+
+
+ + +
+
+
supported devices
+
+ + Mark I +
+
+ + Mark II
-
-
SUPPORTED LANGUAGES
-
English
+
+ + Picroft
-
-
CATEGORY
-
{{skill.categories[0]}}
+
+ + KDE
+
+
supported languages
+
English
+
+
+
category
+
{{skill.categories[0]}}
+
-
\ No newline at end of file + +
diff --git a/market/frontend/v1/market-ui/src/app/skills/skill-detail/skill-detail.component.scss b/market/frontend/v1/market-ui/src/app/skills/skill-detail/skill-detail.component.scss index d3115c67..3c37d3e7 100644 --- a/market/frontend/v1/market-ui/src/app/skills/skill-detail/skill-detail.component.scss +++ b/market/frontend/v1/market-ui/src/app/skills/skill-detail/skill-detail.component.scss @@ -1,36 +1,60 @@ @import '../../../stylesheets/global'; -.skill-detail { +@mixin skill-detail-size { + margin: 0 auto; max-width: 1000px; +} + +.navigate-back { + @include skill-detail-size; + color: $mycroft-dark-grey; + padding-bottom: 10px; +} + +.skill-detail { + @include skill-detail-size; + box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.12); + border-radius: 10px; .skill-detail-header { - background-color: $mycroft-blue-grey; + background-color: #f7f9fa; border-top-left-radius: 10px; border-top-right-radius: 10px; - height: 150px; - padding: 30px; + padding-bottom: 3vh; + padding-left: 4vw; + padding-right: 4vw; + padding-top: 4vh; .skill-detail-header-left { color: $mycroft-secondary; - width: 70%; + margin-right: 50px; + min-width: 340px; + fa { + font-size: 70px; + margin-right: 20px; + } + img { + margin-right: 20px; + } h1 { font-family: 'Roboto Mono', monospace; - margin-bottom: 15px; + margin-bottom: 10px; margin-top: 0; } - .mat-subheading-1 { - padding-right: 30px; - @include ellipsis-overflow - } } .skill-detail-header-right { - width: 30%; - button { + margin-right: 20px; + .install-button { @include action-button; - margin-top: 5px; - width: 160px; + width: 140px; + } + .install-button:hover { + background-color: $mycroft-tertiary-green; + color: $mycroft-secondary; } + .github-button { - background-color: $mycroft-blue-grey; color: $mycroft-dark-grey; + font-weight: normal; + width: 135px; fa-icon { padding-right: 5px; } @@ -41,27 +65,44 @@ background-color: $mycroft-white; border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; - padding: 30px; + margin-bottom: 50px; + padding-bottom: 3vh; + padding-left: 4vw; + padding-right: 4vw; + padding-top: 3vh; .mat-subheading-1 { - color: $mycroft-dark-grey + color: $mycroft-dark-grey; + font-variant: small-caps; + font-weight: 500; + margin-bottom: 5px; + } + .mat-body-1 { + color: $mycroft-secondary; + } + .kde-icon { + height: 40px; + width: 40px; + } + .skill-detail-section { + margin-bottom: 30px; } .skill-detail-body-left { - width: 70%; - button { - @include skill-trigger-button; + min-width: 340px; + margin-right: 50px; + .skill-trigger { + @include skill-trigger; @include ellipsis-overflow; - max-width: 100%; + margin-right: 10px; + margin-bottom: 10px; + max-width: 340px; } } .skill-detail-body-right { - width: 30%; + margin-right: 20px; + white-space: nowrap; img { - padding-right: 15px; + padding-right: 10px; } } - .skill-detail-section { - padding-bottom: 30px; - padding-right: 30px; - } } } \ No newline at end of file diff --git a/market/frontend/v1/market-ui/src/app/skills/skill-detail/skill-detail.component.ts b/market/frontend/v1/market-ui/src/app/skills/skill-detail/skill-detail.component.ts index 4cadc771..2c8bef96 100644 --- a/market/frontend/v1/market-ui/src/app/skills/skill-detail/skill-detail.component.ts +++ b/market/frontend/v1/market-ui/src/app/skills/skill-detail/skill-detail.component.ts @@ -3,7 +3,7 @@ import { Router, ActivatedRoute, ParamMap } from '@angular/router'; import { Observable } from "rxjs/internal/Observable"; import { switchMap } from "rxjs/operators"; -import { faComment, faCodeBranch} from '@fortawesome/free-solid-svg-icons'; +import { faArrowLeft, faComment, faCodeBranch } from '@fortawesome/free-solid-svg-icons'; import { Skill, SkillsService } from "../skills.service"; @@ -13,6 +13,7 @@ import { Skill, SkillsService } from "../skills.service"; styleUrls: ['./skill-detail.component.scss'] }) export class SkillDetailComponent implements OnInit { + public backArrow = faArrowLeft; public githubIcon = faCodeBranch; public skill$: Observable; public triggerIcon = faComment; diff --git a/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-card-header/skill-card-header.component.scss b/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-card-header/skill-card-header.component.scss index 917a32e5..e9d05c47 100644 --- a/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-card-header/skill-card-header.component.scss +++ b/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-card-header/skill-card-header.component.scss @@ -4,16 +4,18 @@ mat-card-header { justify-content: center; margin-bottom: 15px; .mycroft-icon { - left: 15px; + left: 18px; position: absolute; - top: 15px; + top: 18px; img { height: 20px; width: 20px; } } .skill-icon { - position: relative; + //offset the skill icon by the width of the + // mycroft icon to center it on card + margin-left: -15px; fa-icon { font-size: 28px; } diff --git a/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-summary.component.html b/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-summary.component.html index e804cb3f..0152ffb0 100644 --- a/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-summary.component.html +++ b/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-summary.component.html @@ -2,18 +2,30 @@
- {{skill.title}} - - +
 
- + +
\ No newline at end of file diff --git a/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-summary.component.scss b/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-summary.component.scss index ce038837..c2d21e51 100644 --- a/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-summary.component.scss +++ b/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-summary.component.scss @@ -8,24 +8,24 @@ mat-card { @include card-width; border-radius: 10px; cursor: pointer; - margin: 15px; - padding: 15px; + margin: 10px; + padding: 18px; mat-card-title { @include ellipsis-overflow; color: $mycroft-secondary; font-family: 'Roboto Mono', monospace; font-weight: bold; + padding-bottom: 5px; text-align: center; } mat-card-subtitle { - button { - @include skill-trigger-button; + .skill-trigger { @include ellipsis-overflow; - @include card-width; - margin: 0; + @include skill-trigger; } } mat-card-content { + color: $mycroft-secondary; @include ellipsis-overflow; text-align: center; } @@ -37,9 +37,19 @@ mat-card { @include card-width; margin-bottom: 15px; } + .installed-button { + background-color: $mycroft-tertiary-green; + fa-icon { + padding-right: 10px; + } + } + .install-button:hover { + background-color: $mycroft-tertiary-green; + color: $mycroft-secondary; + } } } mat-card:hover{ - box-shadow: -1px 10px 29px 0px; + box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.2); } diff --git a/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-summary.component.ts b/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-summary.component.ts index 7f54eedf..08d9f72a 100644 --- a/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-summary.component.ts +++ b/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-summary.component.ts @@ -1,7 +1,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { MatSnackBar } from "@angular/material"; -import { faComment } from '@fortawesome/free-solid-svg-icons'; +import { faCheck, faComment } from '@fortawesome/free-solid-svg-icons'; import { SkillsService, Skill } from "../skills.service"; @@ -11,9 +11,10 @@ import { SkillsService, Skill } from "../skills.service"; styleUrls: ['./skill-summary.component.scss'], }) export class SkillSummaryComponent implements OnInit { + public installedIcon = faCheck; @Input() public skills: Skill[]; - private skillToInstall: Skill; public voiceIcon = faComment; + private skillInstalling: Skill; constructor(public loginSnackbar: MatSnackBar, private skillsService: SkillsService) { } @@ -25,7 +26,7 @@ export class SkillSummaryComponent implements OnInit { * @param {Skill} skill */ install_skill(skill: Skill) : void { - this.skillToInstall = skill; + this.skillInstalling = skill; this.skillsService.installSkill(skill).subscribe( (response) => { this.onInstallSuccess(response) @@ -46,7 +47,15 @@ export class SkillSummaryComponent implements OnInit { * @param response */ onInstallSuccess(response) : void { - console.log('success!') + this.loginSnackbar.open( + 'The ' + this.skillInstalling.title + ' skill is ' + + 'installing. Please allow up to two minutes for installation' + + 'to complete before using the skill. Only one skill can be ' + + 'installed at a time so please wait before selecting another' + + 'skill to install', + null, + {panelClass: 'mycroft-snackbar', duration:20000} + ); } /** @@ -59,33 +68,11 @@ export class SkillSummaryComponent implements OnInit { */ onInstallFailure(response) : void { if (response.status === 401) { - let skillNameParts = this.skillToInstall.skill_name.split('-'); - let installName = []; - skillNameParts.forEach( - (part) => { - if (part.toLowerCase() != 'mycroft' && part.toLowerCase() != 'skill') { - installName.push(part); - } - } - ); this.loginSnackbar.open( - 'Skill installation functionality coming soon. ' + - 'In the meantime use your voice to install skills ' + - 'by saying: "Hey Mycroft, install ' + installName.join(' ') + '"', - '', + 'To install a skill, log in to your account.', + 'LOG IN', {panelClass: 'mycroft-snackbar', duration: 5000} - ); - - // This is the snackbar logic for when the login and install - // functionality is in place - // - // this.loginSnackbar.open( - // 'To install a skill, log in to your account.', - // 'LOG IN', - // {panelClass: 'mycroft-snackbar', duration: 5000} - // - // ); } } } diff --git a/market/frontend/v1/market-ui/src/app/skills/skill-toolbar/skill-toolbar.component.html b/market/frontend/v1/market-ui/src/app/skills/skill-toolbar/skill-toolbar.component.html index f7edbaf3..c5ad012d 100644 --- a/market/frontend/v1/market-ui/src/app/skills/skill-toolbar/skill-toolbar.component.html +++ b/market/frontend/v1/market-ui/src/app/skills/skill-toolbar/skill-toolbar.component.html @@ -1,11 +1,11 @@ -
-
- - +
+
+ + + -
@@ -20,3 +20,9 @@
+
+ +
diff --git a/market/frontend/v1/market-ui/src/app/skills/skill-toolbar/skill-toolbar.component.scss b/market/frontend/v1/market-ui/src/app/skills/skill-toolbar/skill-toolbar.component.scss index 7465bbbd..77c73feb 100644 --- a/market/frontend/v1/market-ui/src/app/skills/skill-toolbar/skill-toolbar.component.scss +++ b/market/frontend/v1/market-ui/src/app/skills/skill-toolbar/skill-toolbar.component.scss @@ -1,13 +1,27 @@ @import '../../../stylesheets/global'; +.back-button { + color: $mycroft-dark-grey; + margin-left: 20px; + width: 100px; +} + fa-icon { color: $mycroft-dark-grey; } .skill-toolbar { margin-left: 15px; - margin-right: 15px; .search-field { - width: 80%; + background-color: white; + border-radius: 10px; + color: $mycroft-dark-grey; + min-width: 330px; + padding-left: 20px; + padding-right: 20px; + padding-top: 10px; + mat-form-field { + width: 100%; + } } } diff --git a/market/frontend/v1/market-ui/src/app/skills/skill-toolbar/skill-toolbar.component.ts b/market/frontend/v1/market-ui/src/app/skills/skill-toolbar/skill-toolbar.component.ts index ee8d1c34..e01825c1 100644 --- a/market/frontend/v1/market-ui/src/app/skills/skill-toolbar/skill-toolbar.component.ts +++ b/market/frontend/v1/market-ui/src/app/skills/skill-toolbar/skill-toolbar.component.ts @@ -1,6 +1,7 @@ -import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, OnInit, OnDestroy, Output } from '@angular/core'; -import { faSearch, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { Subscription } from "rxjs/internal/Subscription"; +import { faArrowLeft, faSearch } from '@fortawesome/free-solid-svg-icons'; import { SkillsService } from "../skills.service"; @@ -9,7 +10,8 @@ import { SkillsService } from "../skills.service"; templateUrl: './skill-toolbar.component.html', styleUrls: ['./skill-toolbar.component.scss'] }) -export class SkillToolbarComponent implements OnInit { +export class SkillToolbarComponent implements OnInit, OnDestroy { + public backArrow = faArrowLeft; public languages = [ {value: 'english', display: 'English'} ]; @@ -17,27 +19,36 @@ export class SkillToolbarComponent implements OnInit { @Output() public searchResults = new EventEmitter(); public searchTerm: string; public selectedLanguage = this.languages[0].value; + public skillsAreFiltered: Subscription; + public showBackButton: boolean = false; constructor(private skillsService: SkillsService) { } - ngOnInit() { } - - onClick(): void { - if (this.searchIcon === faSearch) { - this.searchSkills(); - this.searchIcon = faTimes; - } else { - this.searchTerm = ''; - this.searchSkills(); - this.searchIcon = faSearch; - } + ngOnInit() { + this.skillsAreFiltered = this.skillsService.isFiltered.subscribe( + (isFiltered) => { this.onFilteredStateChange(isFiltered) } + ); + } + + ngOnDestroy() { + this.skillsAreFiltered.unsubscribe(); + } + + clearSearch(): void { + this.searchTerm = ''; + this.searchSkills() } searchSkills(): void { this.skillsService.searchSkills(this.searchTerm).subscribe( (skills) => { this.searchResults.emit(skills); + console.log(this.skillsAreFiltered); } ); } + + onFilteredStateChange (isFiltered) { + this.showBackButton = isFiltered + } } diff --git a/market/frontend/v1/market-ui/src/app/skills/skills.component.scss b/market/frontend/v1/market-ui/src/app/skills/skills.component.scss index f783eb90..85c7b781 100644 --- a/market/frontend/v1/market-ui/src/app/skills/skills.component.scss +++ b/market/frontend/v1/market-ui/src/app/skills/skills.component.scss @@ -6,7 +6,8 @@ background-color: $market-background; color: $mycroft-dark-grey; font-size: xx-large; - margin-top: 30px; + margin-top: 20px; + padding-left: 10px; fa-icon { margin-right: 15px; } diff --git a/market/frontend/v1/market-ui/src/app/skills/skills.component.ts b/market/frontend/v1/market-ui/src/app/skills/skills.component.ts index 1ff4ed6a..565a8cc5 100644 --- a/market/frontend/v1/market-ui/src/app/skills/skills.component.ts +++ b/market/frontend/v1/market-ui/src/app/skills/skills.component.ts @@ -27,11 +27,27 @@ export class SkillsComponent implements OnInit { } get_skill_categories(skills): void { + let skillCategories = [], + systemCategoryFound = false; this.skillCategories = []; Object.keys(skills).forEach( - category_name => {this.skillCategories.push(category_name);} + categoryName => {skillCategories.push(categoryName);} ); - this.skillCategories.sort() + skillCategories.sort(); + + // Make the "System" category display last, if it exists + skillCategories.forEach( + categoryName => { + if (categoryName === 'System') { + systemCategoryFound = true; + } else { + this.skillCategories.push(categoryName) + } + } + ); + if (systemCategoryFound) { + this.skillCategories.push('System') + } } showSearchResults(searchResults): void { diff --git a/market/frontend/v1/market-ui/src/app/skills/skills.service.spec.ts b/market/frontend/v1/market-ui/src/app/skills/skills.service.spec.ts index 73045062..e5a3584d 100644 --- a/market/frontend/v1/market-ui/src/app/skills/skills.service.spec.ts +++ b/market/frontend/v1/market-ui/src/app/skills/skills.service.spec.ts @@ -1,15 +1,15 @@ import { TestBed, inject } from '@angular/core/testing'; -import { SkillService } from './skill.service'; +import { SkillsService } from './skills.service'; describe('SkillsService', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [SkillService] + providers: [SkillsService] }); }); - it('should be created', inject([SkillService], (service: SkillService) => { + it('should be created', inject([SkillsService], (service: SkillsService) => { expect(service).toBeTruthy(); })); }); diff --git a/market/frontend/v1/market-ui/src/app/skills/skills.service.ts b/market/frontend/v1/market-ui/src/app/skills/skills.service.ts index c2d1fb8a..ac84b35c 100644 --- a/market/frontend/v1/market-ui/src/app/skills/skills.service.ts +++ b/market/frontend/v1/market-ui/src/app/skills/skills.service.ts @@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; +import { Subject } from "rxjs/internal/Subject"; export class Skill { id: number; @@ -10,7 +11,7 @@ export class Skill { description: string; icon: Object; icon_image: string; - skill_name: string; + installed: boolean; title: string; summary: string; repository_url: string; @@ -19,10 +20,11 @@ export class Skill { @Injectable() export class SkillsService { - private installUrl = '/api/install-skill'; + private installUrl = '/api/install'; private skillUrl = '/api/skill/'; private skillsUrl = '/api/skills'; private searchQuery = '?search='; + public isFiltered = new Subject(); constructor(private http: HttpClient) { } @@ -35,6 +37,7 @@ export class SkillsService { } searchSkills(searchTerm: string): Observable { + this.isFiltered.next(!!searchTerm); return this.http.get(this.skillsUrl + this.searchQuery + searchTerm) } @@ -43,6 +46,5 @@ export class SkillsService { this.installUrl, {skill_url: skill.repository_url} ) - } } diff --git a/market/frontend/v1/market-ui/src/assets/header-logo.svg b/market/frontend/v1/market-ui/src/assets/header-logo.svg new file mode 100644 index 00000000..9b345aac --- /dev/null +++ b/market/frontend/v1/market-ui/src/assets/header-logo.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + diff --git a/market/frontend/v1/market-ui/src/assets/kde.svg b/market/frontend/v1/market-ui/src/assets/kde.svg new file mode 100644 index 00000000..793c6b24 --- /dev/null +++ b/market/frontend/v1/market-ui/src/assets/kde.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + diff --git a/market/frontend/v1/market-ui/src/environments/environment.prod.ts b/market/frontend/v1/market-ui/src/environments/environment.prod.ts index 3e0168e0..60f39a82 100644 --- a/market/frontend/v1/market-ui/src/environments/environment.prod.ts +++ b/market/frontend/v1/market-ui/src/environments/environment.prod.ts @@ -1,4 +1,16 @@ export const environment = { production: true, - loginUrl: 'http://login.mycroft.test' + loginUrl: 'https://login.mycroft.ai' }; + +document.write( + '' +); +document.write( + '' +); \ No newline at end of file diff --git a/market/frontend/v1/market-ui/src/environments/environment.test.ts b/market/frontend/v1/market-ui/src/environments/environment.test.ts index c9736981..74313ac4 100644 --- a/market/frontend/v1/market-ui/src/environments/environment.test.ts +++ b/market/frontend/v1/market-ui/src/environments/environment.test.ts @@ -1,4 +1,4 @@ export const environment = { production: false, - loginUrl: 'http://login.mycroft-test.net' + loginUrl: 'https://login.mycroft-test.net' }; diff --git a/market/frontend/v1/market-ui/src/environments/environment.ts b/market/frontend/v1/market-ui/src/environments/environment.ts index 77986ab7..fa2e4d0c 100644 --- a/market/frontend/v1/market-ui/src/environments/environment.ts +++ b/market/frontend/v1/market-ui/src/environments/environment.ts @@ -4,10 +4,8 @@ export const environment = { production: false, - - // URL of development API - apiUrl: 'http://localhost:5000/', - loginUrl: 'http://login.mycroft.test' + apiUrl: 'http://localhost:5002', + loginUrl: 'http://localhost:4201' }; /* diff --git a/market/frontend/v1/market-ui/src/index.html b/market/frontend/v1/market-ui/src/index.html index 2e346d07..5446d6b0 100644 --- a/market/frontend/v1/market-ui/src/index.html +++ b/market/frontend/v1/market-ui/src/index.html @@ -10,7 +10,7 @@ - + diff --git a/market/frontend/v1/market-ui/src/stylesheets/base/_mycroft-colors.scss b/market/frontend/v1/market-ui/src/stylesheets/base/_mycroft-colors.scss index b0d49fb6..aac8a28f 100644 --- a/market/frontend/v1/market-ui/src/stylesheets/base/_mycroft-colors.scss +++ b/market/frontend/v1/market-ui/src/stylesheets/base/_mycroft-colors.scss @@ -11,4 +11,4 @@ $mycroft-black: #000000; $mycroft-dark-grey: #6c7a89; $mycroft-light-grey: #bdc3c7; $mycroft-blue-grey: #e4f1fe; -$market-background: #f1f1f1; +$market-background: #f1f3f4; diff --git a/market/frontend/v1/market-ui/src/stylesheets/components/_buttons.scss b/market/frontend/v1/market-ui/src/stylesheets/components/_buttons.scss index b230ae50..3de55e0b 100644 --- a/market/frontend/v1/market-ui/src/stylesheets/components/_buttons.scss +++ b/market/frontend/v1/market-ui/src/stylesheets/components/_buttons.scss @@ -5,18 +5,5 @@ $button-border-radius: 4px; border-radius: $button-border-radius; background-color: $mycroft-primary; color: $mycroft-white; - font-weight: normal; -} - -@mixin skill-trigger-button { - background-color: $mycroft-blue-grey; - border-radius: $button-border-radius; - color: $mycroft-secondary; - font-weight: normal; - margin-bottom: 15px; - margin-right: 15px; - fa-icon { - color: $mycroft-secondary; - margin-right: 5px; - } + letter-spacing: 0.5px; } diff --git a/market/frontend/v1/market-ui/src/stylesheets/components/_text.scss b/market/frontend/v1/market-ui/src/stylesheets/components/_text.scss index 7455475e..404727b0 100644 --- a/market/frontend/v1/market-ui/src/stylesheets/components/_text.scss +++ b/market/frontend/v1/market-ui/src/stylesheets/components/_text.scss @@ -1,5 +1,22 @@ +@import "../base/mycroft-colors"; + @mixin ellipsis-overflow { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +@mixin skill-trigger { + background-color: $mycroft-blue-grey; + border-radius: 4px; + color: $mycroft-secondary; + font-weight: normal; + padding-bottom: 7px; + padding-left: 12px; + padding-right: 12px; + padding-top: 7px; + fa-icon { + color: $mycroft-primary; + margin-right: 5px; + } +} diff --git a/shared/selene_util/api.py b/shared/selene_util/api.py index 721d8092..48a02381 100644 --- a/shared/selene_util/api.py +++ b/shared/selene_util/api.py @@ -5,106 +5,89 @@ from flask import request, current_app from flask_restful import Resource -from .auth import decode_auth_token, AuthorizationError +from .auth import decode_auth_token, AuthenticationError - -class ServiceUrlNotFound(Exception): - pass - - -class ServiceServerError(Exception): - pass +# The logger is initialized here but this should be overridden with a +# package-specific logger (e.g. _log = getLogger(__package__) +_log = getLogger() -class MethodNotAllowedError(Exception): +class APIError(Exception): + """Raise this exception whenever a non-successful response is built""" pass -class SeleneBaseView(Resource): +class SeleneEndpoint(Resource): """ - Install a skill on user device(s). + Abstract base class for Selene Flask Restful API calls. + + Subclasses must do the following: + - override the allowed_methods class attribute to a list of all allowed + HTTP methods. Each list member must be a HTTPMethod enum + - override the _build_response_data method """ - # The logger is initialized here but this should be overridden with a - # package-specific logger (e.g. _log = getLogger(__package__) - _log = getLogger() + authentication_required: bool = True def __init__(self): - self.base_url = current_app.config['SELENE_BASE_URL'] + self.config = current_app.config + self.authenticated = False + self.request = request self.response = None - self.response_data = None - self.tartarus_token: str = None self.selene_token: str = None - self.service_response = None + self.tartarus_token: str = None self.user_uuid: str = None def _authenticate(self): - self._get_auth_token() - self._validate_auth_token() + """ + Authenticate the user using tokens passed via cookies. + + :raises: APIError() + """ + try: + self._get_auth_token() + self._validate_auth_token() + except AuthenticationError as ae: + if self.authentication_required: + self.response = (str(ae), HTTPStatus.UNAUTHORIZED) + raise APIError() + else: + self.authenticated = True def _get_auth_token(self): + """Get the Selene JWT (and the tartarus token) from cookies. + + :raises: AuthenticationError + """ try: self.selene_token = request.cookies['seleneToken'] self.tartarus_token = request.cookies['tartarusToken'] except KeyError: - raise AuthorizationError( + raise AuthenticationError( 'no authentication token found in request' ) def _validate_auth_token(self): + """Decode the Selene JWT. + + :raises: AuthenticationError + """ self.user_uuid = decode_auth_token( self.selene_token, - current_app.config['SECRET_KEY'] + self.config['SECRET_KEY'] ) - def check_for_service_errors(self, service, response): - if response.status_code == HTTPStatus.UNAUTHORIZED: - error_message = 'invalid authentication token' - self._log.error(error_message) - raise AuthorizationError(error_message) - elif response.status_code == HTTPStatus.NOT_FOUND: - error_message = '{service} service URL {url} not found'.format( - service=service, - url=response.request.url - ) - self._log.error(error_message) - raise ServiceUrlNotFound(error_message) - elif response.status_code != HTTPStatus.OK: + def _check_for_service_errors(self, service_response): + """Common logic to handle non-successful returns from service calls.""" + if service_response.status_code != HTTPStatus.OK: error_message = ( - '{service} service URL {url} HTTP status {status}'.format( - service=service, - status=response.status_code, - url=response.request.url + 'service URL {url} returned HTTP status {status}'.format( + status=service_response.status_code, + url=service_response.request.url ) ) - self._log.error(error_message) - raise ServiceServerError(error_message) - - def _build_response(self): - try: - self._build_response_data() - except AuthorizationError as ae: - self._build_unauthorized_response(str(ae)) - except ServiceUrlNotFound as nf: - self._build_server_error_response(str(nf)) - except ServiceServerError as se: - self._build_server_error_response(str(se)) - else: - self._build_success_response() - - def _build_response_data(self): - raise NotImplementedError - - def _build_unauthorized_response(self, error_message): - self.response = ( - dict(errorMessage=error_message), - HTTPStatus.UNAUTHORIZED - ) - - def _build_server_error_response(self, error_message): - self.response = ( - dict(errorMessage=error_message), - HTTPStatus.INTERNAL_SERVER_ERROR - ) - - def _build_success_response(self): - self.response = (self.response_data, HTTPStatus.OK) + _log.error(error_message) + if service_response.status_code == HTTPStatus.UNAUTHORIZED: + self.response = (error_message, HTTPStatus.UNAUTHORIZED) + else: + self.response = (error_message, HTTPStatus.INTERNAL_SERVER_ERROR) + raise APIError() diff --git a/shared/selene_util/auth.py b/shared/selene_util/auth.py index 258cdeda..5d343c11 100644 --- a/shared/selene_util/auth.py +++ b/shared/selene_util/auth.py @@ -1,21 +1,44 @@ +from datetime import datetime from logging import getLogger +from time import time import jwt +THIRTY_DAYS = 2592000 + _log = getLogger(__package__) -class AuthorizationError(Exception): +class AuthenticationError(Exception): pass +def encode_auth_token(secret_key, user_uuid): + """ + Generates the Auth Token + :return: string + """ + token_expiration = time() + THIRTY_DAYS + payload = dict(iat=datetime.utcnow(), exp=token_expiration, sub=user_uuid) + selene_token = jwt.encode( + payload, + secret_key, + algorithm='HS256' + ) + + # before returning the token, convert it from bytes to string so that + # it can be included in a JSON response object + return selene_token.decode() + + def decode_auth_token(auth_token: str, secret_key: str) -> tuple: """ Decodes the auth token - :param auth_token: the Selene JSON Web Token extracted from the request cookies. + :param auth_token: the Selene JSON Web Token extracted from cookies. :param secret_key: the key needed to decode the token - :return: two-value tuple containing a boolean value indicating if the token is good and the - user UUID extracted from the token. UUID will be None if token is invalid. + :return: two-value tuple containing a boolean value indicating if the + token is good and the user UUID extracted from the token. UUID will + be None if token is invalid. """ try: payload = jwt.decode(auth_token, secret_key) @@ -23,10 +46,10 @@ def decode_auth_token(auth_token: str, secret_key: str) -> tuple: except jwt.ExpiredSignatureError: error_msg = 'Selene token expired' _log.info(error_msg) - raise AuthorizationError(error_msg) + raise AuthenticationError(error_msg) except jwt.InvalidTokenError: error_msg = 'Invalid Selene token' _log.info(error_msg) - raise AuthorizationError(error_msg) + raise AuthenticationError(error_msg) return user_uuid