Merge pull request #7 from MycroftAI/dev

Merging 2018.2 changes into master
pull/8/head 2018.2
Chris Veilleux 2018-10-09 17:06:29 -05:00 committed by GitHub
commit 42d1d18d8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
87 changed files with 1405 additions and 1199 deletions

3
.gitignore vendored
View File

@ -16,6 +16,7 @@
*.launch *.launch
.settings/ .settings/
*.sublime-workspace *.sublime-workspace
__pycache__/
# IDE - VSCode # IDE - VSCode
.vscode/* .vscode/*
@ -37,3 +38,5 @@ testem.log
# System Files # System Files
.DS_Store .DS_Store
Thumbs.db Thumbs.db

View File

@ -2,7 +2,7 @@
# The selene-shared parent image contains all the common Docker configs for # The selene-shared parent image contains all the common Docker configs for
# all Selene apps and services see the "shared" directory in this repository. # 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" LABEL description="Run the API for the Mycroft login screen"
# Use pipenv to install the package's dependencies in the container # Use pipenv to install the package's dependencies in the container

View File

@ -9,9 +9,10 @@ requests = "*"
pyjwt = "*" pyjwt = "*"
flask-restful = "*" flask-restful = "*"
certifi = "*" certifi = "*"
gunicorn = "*" uwsgi = "*"
[dev-packages] [dev-packages]
selene-util = {path = "./../../../../shared"}
[requires] [requires]
python_version = "3.7" python_version = "3.7"

View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "624569e9bc0207f58dca3a683d5868e374e78a5be00a34b442aae4a656e4eac2" "sha256": "7cf1dde24d5a966645f3e49d93dde93dad42c6b6fa62f9b254b79d6b58e93e06"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -40,10 +40,11 @@
}, },
"click": { "click": {
"hashes": [ "hashes": [
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" "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": { "flask": {
"hashes": [ "hashes": [
@ -61,14 +62,6 @@
"index": "pypi", "index": "pypi",
"version": "==0.3.6" "version": "==0.3.6"
}, },
"gunicorn": {
"hashes": [
"sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471",
"sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3"
],
"index": "pypi",
"version": "==19.9.0"
},
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", "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.*'", "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" "version": "==1.23"
}, },
"uwsgi": {
"hashes": [
"sha256:d2318235c74665a60021a4fc7770e9c2756f9fc07de7b8c22805efe85b5ab277"
],
"index": "pypi",
"version": "==2.0.17.1"
},
"werkzeug": { "werkzeug": {
"hashes": [ "hashes": [
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
@ -141,5 +141,9 @@
"version": "==0.14.1" "version": "==0.14.1"
} }
}, },
"develop": {} "develop": {
"selene-util": {
"path": "./../../../../shared"
}
}
} }

View File

@ -1,17 +1,41 @@
from flask import Flask from flask import Flask, request
from flask_restful import Api 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 .config import get_config_location
from .logout import LogoutView # Initialize the Flask application and the Flask Restful API
BASE_URL = '/api/auth/'
login = Flask(__name__) login = Flask(__name__)
login.config.from_object(get_config_location()) login.config.from_object(get_config_location())
login_api = Api(login, catch_all_404s=True) login_api = Api(login, catch_all_404s=True)
antisocial_view_url = BASE_URL + 'antisocial' # Define the endpoints
login_api.add_resource(AuthorizeAntisocialView, antisocial_view_url) 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')
logout_view_url = BASE_URL + 'logout'
login_api.add_resource(LogoutView, logout_view_url) 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
login.after_request(add_cors_headers)

View File

@ -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
)

View File

@ -8,20 +8,30 @@ class LoginConfigException(Exception):
class BaseConfig: class BaseConfig:
"""Base configuration.""" """Base configuration."""
DEBUG = False DEBUG = False
LOGIN_BASE_URL = os.environ['LOGIN_BASE_URL']
SECRET_KEY = os.environ['JWT_SECRET'] 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): class DevelopmentConfig(BaseConfig):
"""Development configuration.""" """Development configuration."""
DEBUG = True DEBUG = True
TARTARUS_BASE_URL = 'https://api-test.mycroft.ai/v1'
class TestConfig(BaseConfig):
pass
class ProdConfig(BaseConfig):
pass
def get_config_location(): def get_config_location():
environment_configs = dict( environment_configs = dict(
dev='login_api.config.DevelopmentConfig', dev='login_api.config.DevelopmentConfig',
# test=TestConfig, test=TestConfig,
# prod=ProdConfig prod=ProdConfig
) )
try: try:

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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
)

View File

@ -6,7 +6,8 @@ WORKDIR /usr/src/app
COPY package*.json ./ COPY package*.json ./
RUN npm install RUN npm install
COPY . . 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. # STAGE TWO: build the web server and copy the compiled angular app to it.
FROM nginx:latest FROM nginx:latest

View File

@ -3,7 +3,7 @@
"version": 1, "version": 1,
"newProjectRoot": "projects", "newProjectRoot": "projects",
"projects": { "projects": {
"frontend": { "mycroft-login": {
"root": "", "root": "",
"sourceRoot": "src", "sourceRoot": "src",
"projectType": "application", "projectType": "application",

View File

@ -4,7 +4,9 @@
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve --open --proxy-config proxy.config.json", "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", "test": "ng test",
"lint": "ng lint", "lint": "ng lint",
"e2e": "ng e2e" "e2e": "ng e2e"
@ -16,7 +18,7 @@
"@angular/common": "^6.0.3", "@angular/common": "^6.0.3",
"@angular/compiler": "^6.0.3", "@angular/compiler": "^6.0.3",
"@angular/core": "^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/forms": "^6.0.3",
"@angular/http": "^6.0.9", "@angular/http": "^6.0.9",
"@angular/material": "^6.4.1", "@angular/material": "^6.4.1",
@ -29,7 +31,7 @@
"@fortawesome/free-regular-svg-icons": "^5.2.0", "@fortawesome/free-regular-svg-icons": "^5.2.0",
"@fortawesome/free-solid-svg-icons": "^5.2.0", "@fortawesome/free-solid-svg-icons": "^5.2.0",
"core-js": "^2.5.4", "core-js": "^2.5.4",
"rxjs": "^6.0.0", "rxjs": "6.2.2",
"zone.js": "^0.8.26" "zone.js": "^0.8.26"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,3 +1,31 @@
<div class="split top"></div> <!--
<div class="split bottom"></div> Don't display the login page if the URL contains the data returned from a
<login-authenticate></login-authenticate> call to the Tartarus social login endpoint. When this happens, a view call
will be made to get the tokens and the user will be returned to the previous
window.
This condition is temporary it should be removed when the social login logic
is refactored and moved into Selene.
-->
<div *ngIf="!socialLoginDataFound">
<div class="split top"></div>
<div class="split bottom"></div>
<div fxLayout="column" fxLayoutAlign="center center">
<div align="center">
<img src="../../assets/mycroft-ai-no-logo.svg"/>
</div>
<div class="login-options">
<login-authenticate></login-authenticate>
<!--<mat-tab-group>-->
<!--<mat-tab label="LOG IN">-->
<!--<login-authenticate></login-authenticate>-->
<!--</mat-tab>-->
<!--<mat-tab label="SIGN UP">-->
<!--<login-auth-social></login-auth-social>-->
<!--<div class="mat-subheading-2">OR</div>-->
<!--<login-auth-antisocial></login-auth-antisocial>-->
<!--</mat-tab>-->
<!--</mat-tab-group>-->
</div>
</div>
</div>

View File

@ -2,24 +2,42 @@
/* Split the screen in half */ /* Split the screen in half */
.split { .split {
height: 50%; height: 50%;
left: 0; left: 0;
overflow-x: hidden; overflow-x: hidden;
padding-top: 20px; padding-top: 20px;
position: fixed; position: fixed;
width: 100%; width: 100%;
z-index: -1; z-index: -1;
} }
/* Control the top side */ /* Top Half */
.top { .top {
top: 0; top: 0;
background-color: $mycroft-primary; background-color: $mycroft-primary;
} }
/* Control the bottom side */ /* Bottom Half */
.bottom { .bottom {
bottom: 0; bottom: 0;
background-color: #e5e5e5; 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;
}

View File

@ -1,10 +1,26 @@
import { Component } from '@angular/core'; import { Component, OnInit } from '@angular/core';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'] styleUrls: ['./app.component.scss']
}) })
export class AppComponent { export class AppComponent implements OnInit {
title = 'Mycroft Login'; 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();
}
}
} }

View File

@ -1,6 +1,6 @@
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; 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 { NgModule } from '@angular/core';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
@ -12,7 +12,7 @@ import { AuthModule } from "./auth/auth.module";
BrowserModule, BrowserModule,
AuthModule, AuthModule,
BrowserAnimationsModule, BrowserAnimationsModule,
FlexModule, FlexLayoutModule
], ],
providers: [ ], providers: [ ],
bootstrap: [ AppComponent ] bootstrap: [ AppComponent ]

View File

@ -1,29 +0,0 @@
<form #loginForm="ngForm" (ngSubmit)="authorizeUser()" (keydown.enter)="authorizeUser()">
<fa-icon [icon]="usernameIcon"></fa-icon>
<mat-form-field>
<input
id="username"
matInput
name="username"
placeholder="Email or Username"
required
type="text"
[(ngModel)]="username"
>
</mat-form-field>
<fa-icon [icon]="passwordIcon"></fa-icon>
<mat-form-field>
<input
id="password"
matInput
name="password"
placeholder="Password"
required
type="password"
[(ngModel)]="password"
>
<mat-hint>Forgot password?</mat-hint>
</mat-form-field>
<button mat-button type="submit" class="login-button">LOG IN</button>
</form>
<div class="mat-body-2" *ngIf="authFailed">Invalid username/password combination; try again</div>

View File

@ -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;
}

View File

@ -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<AuthAntisocialComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AuthAntisocialComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AuthAntisocialComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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;
}
}
}

View File

@ -1,14 +0,0 @@
<div class="social">
<button mat-button class="google-button">
<img src="../../../assets/google-logo.svg">
Log in with Google
</button>
<button mat-button class="facebook-button">
<fa-icon [icon]="facebookIcon"></fa-icon>
Continue with Facebook
</button>
<button mat-button class="github-button" id="githubButton">
<fa-icon [icon]="githubIcon"></fa-icon>
Log in with GitHub
</button>
</div>

View File

@ -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%;
}
}
}

View File

@ -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<AuthSocialComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AuthSocialComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AuthSocialComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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() { }
}

View File

@ -1,19 +1,49 @@
<div fxLayout="column" fxLayoutAlign="center center"> <div class="social">
<div align="center"> <button mat-button class="google-button" (click)="authenticateGoogle()">
<img src="../../assets/mycroft-ai-no-logo.svg"/> <img src="../../../assets/google-logo.svg">
</div> Log in with Google
<div class="login-options"> </button>
<mat-tab-group> <button mat-button class="facebook-button" (click)="authenticateFacebook()">
<mat-tab label="LOG IN"> <fa-icon [icon]="facebookIcon"></fa-icon>
<login-auth-social></login-auth-social> Continue with Facebook
<div class="mat-subheading-2">OR</div> </button>
<login-auth-antisocial></login-auth-antisocial> <button mat-button class="github-button" (click)="authenticateGithub()">
</mat-tab> <fa-icon [icon]="githubIcon"></fa-icon>
<mat-tab label="SIGN UP"> Log in with GitHub
<login-auth-social></login-auth-social> </button>
<div class="mat-subheading-2">OR</div>
<login-auth-antisocial></login-auth-antisocial>
</mat-tab>
</mat-tab-group>
</div>
</div> </div>
<div class="mat-subheading-2">OR</div>
<form
fxLayout="column"
#loginForm="ngForm"
(ngSubmit)="authorizeUser()"
(keydown.enter)="authorizeUser()"
>
<mat-form-field>
<fa-icon [icon]="usernameIcon" matPrefix></fa-icon>
<input
id="username"
matInput
name="username"
placeholder="Email or Username"
required
type="text"
[(ngModel)]="username"
>
</mat-form-field>
<mat-form-field>
<fa-icon [icon]="passwordIcon" matPrefix></fa-icon>
<input
id="password"
matInput
name="password"
placeholder="Password"
required
type="password"
[(ngModel)]="password"
>
<mat-hint>Forgot password?</mat-hint>
</mat-form-field>
<button mat-button type="submit" class="login-button">LOG IN</button>
</form>
<div class="mat-body-2" *ngIf="authFailed">Invalid username/password combination; try again</div>

View File

@ -1,13 +1,70 @@
@import '../../stylesheets/global'; @import '../../stylesheets/global';
mat-tab-group { button {
height: 485px; @include login-button;
width: 320px;
} }
.login-options { .social {
border-radius: 10px; 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; 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 { .mat-subheading-2 {
@ -16,9 +73,3 @@ mat-tab-group {
margin-top: -15px; margin-top: -15px;
text-align: center; text-align: center;
} }
img {
margin-bottom: 50px;
margin-top: 50px;
width: 600px;
}

View File

@ -1,13 +1,55 @@
import { Component, OnInit } from '@angular/core'; 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({ @Component({
selector: 'login-authenticate', selector: 'login-authenticate',
templateUrl: './auth.component.html', templateUrl: './auth.component.html',
styleUrls: ['./auth.component.scss'] styleUrls: ['./auth.component.scss']
}) })
export class AuthComponent implements OnInit { 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() { } 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;
}
}
} }

View File

@ -1,7 +1,7 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from "@angular/forms"; import { FormsModule } from "@angular/forms";
import { FlexModule } from "@angular/flex-layout"; import { FlexLayoutModule } from "@angular/flex-layout";
import { HttpClientModule } from "@angular/common/http"; import { HttpClientModule } from "@angular/common/http";
import { import {
MatButtonModule, MatButtonModule,
@ -9,22 +9,20 @@ import {
MatDividerModule, MatDividerModule,
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
MatTabsModule MatSnackBarModule
} from "@angular/material"; } from "@angular/material";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { AuthComponent } from './auth.component'; import { AuthComponent } from './auth.component';
import { AuthService } from "./auth.service"; import { AuthService } from "./auth.service";
import { AuthSocialComponent } from './auth-social/auth-social.component';
import { AuthAntisocialComponent } from './auth-antisocial/auth-antisocial.component';
@NgModule({ @NgModule({
declarations: [ AuthComponent, AuthSocialComponent, AuthAntisocialComponent ], declarations: [ AuthComponent ],
exports: [ AuthComponent ], exports: [ AuthComponent ],
imports: [ imports: [
CommonModule, CommonModule,
FlexModule, FlexLayoutModule,
FontAwesomeModule, FontAwesomeModule,
FormsModule, FormsModule,
HttpClientModule, HttpClientModule,
@ -33,7 +31,7 @@ import { AuthAntisocialComponent } from './auth-antisocial/auth-antisocial.compo
MatDividerModule, MatDividerModule,
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
MatTabsModule MatSnackBarModule
], ],
providers: [ AuthService ] providers: [ AuthService ]
}) })

View File

@ -2,19 +2,33 @@ import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders} from "@angular/common/http"; import { HttpClient, HttpHeaders} from "@angular/common/http";
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { isArray } from "util";
export class AuthResponse { import { MatSnackBar } from "@angular/material";
export interface AuthResponse {
expiration: number; expiration: number;
seleneToken: string; seleneToken: string;
tartarusToken: string; tartarusToken: string;
} }
export interface SocialLoginData {
uuid: string;
accessToken: string;
refreshToken: string;
expiration: string;
}
@Injectable() @Injectable()
export class AuthService { export class AuthService {
private antisocialAuthUrl = '/api/auth/antisocial'; private antisocialAuthUrl = '/api/antisocial';
private facebookAuthUrl = '/api/auth/facebook'; 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<AuthResponse> { authorizeAntisocial (username, password): Observable<AuthResponse> {
let rawCredentials = `${username}:${password}`; let rawCredentials = `${username}:${password}`;
@ -25,8 +39,71 @@ export class AuthService {
return this.http.get<AuthResponse>(this.antisocialAuthUrl, {headers: httpHeaders}) return this.http.get<AuthResponse>(this.antisocialAuthUrl, {headers: httpHeaders})
} }
authorizeFacebook(userData: any) { authenticateWithFacebook() {
const httpHeaders = new HttpHeaders({'token': userData.token}); window.open(this.facebookAuthUrl);
return this.http.get<AuthResponse>(this.facebookAuthUrl, {headers: httpHeaders}) 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<AuthResponse>(
this.generateTokensUrl,
socialLoginData
).subscribe(
(response) => {this.generateTokenCookies(response)}
);
}
return this.http.post<AuthResponse>(
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 = <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;
}
} }

View File

@ -1,3 +1,15 @@
export const environment = { export const environment = {
production: true production: true
}; };
document.write(
'<script async src="https://www.googletagmanager.com/gtag/js?id=UA-101772425-11"></script>'
);
document.write(
'<script>' +
'window.dataLayer = window.dataLayer || []; ' +
'function gtag(){dataLayer.push(arguments);} ' +
'gtag("js", new Date()); ' +
'gtag("config", "UA-101772425-11"); ' +
'</script>'
);

View File

@ -13,28 +13,6 @@
<link href="https://fonts.googleapis.com/css?family=Roboto+Mono:300,400,500" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Roboto+Mono:300,400,500" rel="stylesheet">
</head> </head>
<body> <body>
<!-- Facebook SDK -->
<script>
window.fbAsyncInit = function() {
FB.init({
appId : '2266714353557295',
cookie : true,
xfbml : true,
version : 'v3.1'
});
FB.AppEvents.logPageView();
};
(function(d, s, id) {
let js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) {
return;
}
js = d.createElement(s); js.id = id;
js.src = "https://connect.facebook.net/en_US/sdk.js";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
</script>
<app-root></app-root> <app-root></app-root>
</body> </body>
</html> </html>

View File

@ -2,7 +2,7 @@
# The selene-shared parent image contains all the common Docker configs for # The selene-shared parent image contains all the common Docker configs for
# all Selene apps and services see the "shared" directory in this repository. # 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" LABEL description="Run the API for the Mycroft marketplace"
# Use pipenv to install the package's dependencies in the container # Use pipenv to install the package's dependencies in the container

View File

@ -40,10 +40,11 @@
}, },
"click": { "click": {
"hashes": [ "hashes": [
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" "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": { "flask": {
"hashes": [ "hashes": [
@ -82,11 +83,11 @@
}, },
"markdown": { "markdown": {
"hashes": [ "hashes": [
"sha256:9ba587db9daee7ec761cfc656272be6aabe2ed300fece21208e4aab2e457bc8f", "sha256:c00429bd503a47ec88d5e30a751e147dcb4c6889663cd3e2ba0afe858e009baa",
"sha256:a856869c7ff079ad84a3e19cd87a64998350c2b94e9e08e44270faef33400f81" "sha256:d02e0f9b04c500cde6637c11ad7c72671f359b87b9fe924b2383649d8841db7c"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.6.11" "version": "==3.0.1"
}, },
"markupsafe": { "markupsafe": {
"hashes": [ "hashes": [

View File

@ -3,10 +3,10 @@ from flask_restful import Api
from .config import get_config_location from .config import get_config_location
from market_api.endpoints import ( from market_api.endpoints import (
SkillSummaryView, SkillSummaryEndpoint,
SkillDetailView, SkillDetailEndpoint,
SkillInstallView, SkillInstallEndpoint,
UserView UserEndpoint
) )
@ -14,7 +14,7 @@ marketplace = Flask(__name__)
marketplace.config.from_object(get_config_location()) marketplace.config.from_object(get_config_location())
marketplace_api = Api(marketplace) marketplace_api = Api(marketplace)
marketplace_api.add_resource(SkillSummaryView, '/api/skills') marketplace_api.add_resource(SkillSummaryEndpoint, '/api/skills')
marketplace_api.add_resource(SkillDetailView, '/api/skill/<skill_id>') marketplace_api.add_resource(SkillDetailEndpoint, '/api/skill/<skill_id>')
marketplace_api.add_resource(SkillInstallView, '/api/install-skill') marketplace_api.add_resource(SkillInstallEndpoint, '/api/install')
marketplace_api.add_resource(UserView, '/api/user') marketplace_api.add_resource(UserEndpoint, '/api/user')

View File

@ -1,4 +1,4 @@
from .skill_detail import SkillDetailView from .skill_detail import SkillDetailEndpoint
from .skill_install import SkillInstallView from .skill_install import SkillInstallEndpoint
from .skill_summary import SkillSummaryView from .skill_summary import SkillSummaryEndpoint
from .user import UserView from .user import UserEndpoint

View File

@ -1,37 +1,47 @@
"""View to return detailed information about a skill""" """View to return detailed information about a skill"""
from http import HTTPStatus
from markdown import markdown from markdown import markdown
import requests as service_request import requests as service_request
from selene_util.api import SeleneBaseView, AuthorizationError from selene_util.api import SeleneEndpoint, APIError
class SkillDetailView(SeleneBaseView): class SkillDetailEndpoint(SeleneEndpoint):
authentication_required = False
def __init__(self): def __init__(self):
super(SkillDetailView, self).__init__() super(SkillDetailEndpoint, self).__init__()
self.skill_id = None self.skill_id = None
self.response_skill = None
def get(self, skill_id): def get(self, skill_id):
"""Handle and HTTP GET request."""
self.skill_id = skill_id self.skill_id = skill_id
try: try:
self._authenticate() self._authenticate()
except AuthorizationError: self._get_skill_details()
except APIError:
pass pass
self._build_response() else:
self._build_response_data()
self.response = (self.response_skill, HTTPStatus.OK)
return self.response return self.response
def _build_response_data(self): def _get_skill_details(self):
"""Build the data to include in the response.""" """Build the data to include in the response."""
self.service_response = service_request.get( skill_service_response = service_request.get(
self.base_url + '/skill/id/' + self.skill_id self.config['SELENE_BASE_URL'] + '/skill/id/' + self.skill_id
) )
self.response_data = self.service_response.json() self._check_for_service_errors(skill_service_response)
self.response_data['description'] = markdown( self.response_skill = skill_service_response.json()
self.response_data['description'],
def _build_response_data(self):
self.response_skill['description'] = markdown(
self.response_skill['description'],
output_format='html5' output_format='html5'
) )
self.response_data['summary'] = markdown( self.response_skill['summary'] = markdown(
self.response_data['summary'], self.response_skill['summary'],
output_format='html5' output_format='html5'
) )

View File

@ -2,94 +2,67 @@ from http import HTTPStatus
from logging import getLogger from logging import getLogger
import json import json
from flask import request, current_app
from flask_restful import Resource
import requests import requests
from selene_util.auth import decode_auth_token, AuthorizationError from selene_util.api import SeleneEndpoint, APIError
_log = getLogger(__package__) _log = getLogger(__package__)
class ServiceUrlNotFound(Exception): class SkillInstallEndpoint(SeleneEndpoint):
pass
class ServiceServerError(Exception):
pass
class SkillInstallView(Resource):
""" """
Install a skill on user device(s). Install a skill on user device(s).
""" """
def __init__(self): def __init__(self):
self.service_response = None super(SkillInstallEndpoint, self).__init__()
self.frontend_response = None self.device_uuid: str = None
self.frontend_response_status_code = HTTPStatus.OK self.installer_skill_settings: list = []
self.user_uuid: str = None self.installer_update_response = None
self.tartarus_token: str = None
self.selene_token: str = None
self.device_uuid = None
self.installer_skill_settings = []
def put(self): def put(self):
try: try:
self._get_auth_tokens() self._authenticate()
self._validate_auth_token() self._get_installer_skill()
self._install_skill() self._apply_update()
except AuthorizationError as ae: except APIError:
self._build_unauthorized_response(str(ae)) pass
except ServiceUrlNotFound as nf:
self._build_server_error_response(str(nf))
except ServiceServerError as se:
self._build_server_error_response(str(se))
else: else:
self._build_frontend_response() self.response = (self.installer_update_response, HTTPStatus.OK)
return self.frontend_response return self.response
def _get_auth_tokens(self): def _get_installer_skill(self):
try: installed_skills = self._get_installed_skills()
self.selene_token = request.cookies['seleneToken'] installer_skill = self._find_installer_skill(installed_skills)
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 _install_skill(self):
self._get_users_installer_skill_settings()
installer_skill = self._find_installer_skill()
self._find_installer_settings(installer_skill) 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 = { service_request_headers = {
'Authorization': 'Bearer ' + self.tartarus_token 'Authorization': 'Bearer ' + self.tartarus_token
} }
service_url = ( service_url = (
current_app.config['TARTARUS_BASE_URL'] + self.config['TARTARUS_BASE_URL'] +
'/user/' + '/user/' +
self.user_uuid + self.user_uuid +
'/skill' '/skill'
) )
self.service_response = requests.get( user_service_response = requests.get(
service_url, service_url,
headers=service_request_headers 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): return json.loads(user_service_response.content)
service_response_data = json.loads(self.service_response.content)
def _find_installer_skill(self, installed_skills):
installer_skill = None installer_skill = None
for skill in service_response_data['skills']: for skill in installed_skills['skills']:
if skill['skill']['name'] == 'Installer': if skill['skill']['name'] == 'Installer':
self.device_uuid = skill['deviceUuid'] self.device_uuid = skill['deviceUuid']
installer_skill = skill['skill'] installer_skill = skill['skill']
@ -103,23 +76,30 @@ class SkillInstallView(Resource):
if setting['type'] != 'label': if setting['type'] != 'label':
self.installer_skill_settings.append(setting) self.installer_skill_settings.append(setting)
def _update_skill_installer_settings(self): def _apply_update(self):
service_url = current_app.config['TARTARUS_BASE_URL'] + '/skill/field' service_url = self.config['TARTARUS_BASE_URL'] + '/skill/field'
service_request_headers = { 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, service_url,
data=json.dumps(self._build_install_request_body()), data=service_request_data,
headers=service_request_headers 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)
def _build_install_request_body(self): self.installer_update_response = json.loads(
skill_service_response.content
)
def _build_update_request_body(self):
install_request_body = [] install_request_body = []
for setting in self.installer_skill_settings: for setting in self.installer_skill_settings:
if setting['name'] == 'installer_link': if setting['name'] == 'installer_link':
setting_value = 'foo' setting_value = self.request.json['skill_url']
elif setting['name'] == 'auto_install': elif setting['name'] == 'auto_install':
setting_value = True setting_value = True
else: else:
@ -130,53 +110,10 @@ class SkillInstallView(Resource):
raise ValueError(error_message.format(setting['name'])) raise ValueError(error_message.format(setting['name']))
install_request_body.append( install_request_body.append(
dict( dict(
fieldUiud=setting['uuid'], fieldUuid=setting['uuid'],
deviceUuid=self.device_uuid, value=setting_value deviceUuid=self.device_uuid,
value=setting_value
) )
) )
return dict(batch=install_request_body) 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)

View File

@ -1,45 +1,118 @@
"""Endpoint to provide skill summary data to the marketplace.""" """Endpoint to provide skill summary data to the marketplace."""
from collections import defaultdict from collections import defaultdict
from http import HTTPStatus
from logging import getLogger
from flask import request
from markdown import markdown from markdown import markdown
import requests as service_request 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 SkillSummaryView(SeleneBaseView): class SkillSummaryEndpoint(SeleneEndpoint):
authentication_required = False
def __init__(self): def __init__(self):
super(SkillSummaryView, self).__init__() super(SkillSummaryEndpoint, self).__init__()
self.response_data = defaultdict(list) self.available_skills: list = []
self.search_term = None self.installed_skills: list = []
self.response_skills = defaultdict(list)
def get(self): def get(self):
"""Handle a HTTP GET request."""
try: try:
self._authenticate() self._authenticate()
except AuthorizationError: self._get_skills()
except APIError:
pass pass
self._build_response() else:
self._build_response_data()
self.response = (self.response_skills, HTTPStatus.OK)
return self.response 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): def _build_response_data(self):
"""Build the data to include in the response.""" """Build the data to include in the response."""
self.skill_service_response = service_request.get( if self.request.query_string:
self.base_url + '/skill/all' skills_to_include = self._filter_skills()
) else:
if request.query_string: skills_to_include = self.available_skills
query_string = request.query_string.decode() self._reformat_skills(skills_to_include)
self.search_term = query_string.lower().split('=')[1]
self._reformat_skills()
self._sort_skills() 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""" """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']: if not skill['icon']:
skill['icon'] = dict(icon='comment-alt', color='#6C7A89') skill['icon'] = dict(icon='comment-alt', color='#6C7A89')
skill_summary = dict( skill_summary = dict(
@ -47,28 +120,25 @@ class SkillSummaryView(SeleneBaseView):
icon=skill['icon'], icon=skill['icon'],
icon_image=skill.get('icon_image'), icon_image=skill.get('icon_image'),
id=skill['id'], id=skill['id'],
# TODO remove skill_name when login/install is implemented installed=skill['title'] in self.installed_skills,
skill_name=skill['skill_name'], repository_url=skill['repository_url'],
summary=markdown(skill['summary'], output_format='html5'), summary=markdown(skill['summary'], output_format='html5'),
title=skill['title'], title=skill['title'],
triggers=skill['triggers'] triggers=skill['triggers']
) )
search_term_match = ( if 'system' in skill['tags']:
self.search_term is None or skill_category = 'System'
self.search_term in skill['title'].lower() elif skill['categories']:
)
if search_term_match:
# a skill may have many categories. the first one in the # a skill may have many categories. the first one in the
# list is considered the "primary" category. This is the # list is considered the "primary" category. This is the
# category the marketplace will use to group the skill. # category the marketplace will use to group the skill.
if skill['categories']: skill_category = skill['categories'][0]
skill_category = skill['categories'][0] else:
else: skill_category = UNDEFINED
skill_category = UNDEFINED self.response_skills[skill_category].append(skill_summary)
self.response_data[skill_category].append(skill_summary)
def _sort_skills(self): def _sort_skills(self):
"""Sort the skills in alphabetical order""" """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']) sorted_skills = sorted(skills, key=lambda skill: skill['title'])
self.response_data[skill_category] = sorted_skills self.response_skills[skill_category] = sorted_skills

View File

@ -1,55 +1,45 @@
"""API endpoint to return the user's name to the marketplace""" """API endpoint to return the user's name to the marketplace"""
from http import HTTPStatus from http import HTTPStatus
import json
from flask import request, current_app
from flask_restful import Resource
import requests import requests
from selene_util.auth import decode_auth_token from selene_util.api import SeleneEndpoint, APIError
class UserView(Resource): class UserEndpoint(SeleneEndpoint):
""" """Retrieve information about the user based on their UUID"""
User Login Resource
"""
def __init__(self): def __init__(self):
self.service_response = None super(UserEndpoint, self).__init__()
self.user = None
self.frontend_response = None self.frontend_response = None
def get(self): def get(self):
self._get_user_from_service() try:
self._build_frontend_response() 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): def _get_user(self):
selene_token = request.cookies.get('seleneToken') service_request_headers = {
user_uuid = decode_auth_token( 'Authorization': 'Bearer ' + self.tartarus_token
selene_token, }
current_app.config['SECRET_KEY']
)
tartarus_token = request.cookies.get('tartarusToken')
service_request_headers = {'Authorization': 'Bearer ' + tartarus_token}
service_url = ( service_url = (
current_app.config['TARTARUS_BASE_URL'] + self.config['TARTARUS_BASE_URL'] +
'/user/' + '/user/' +
user_uuid self.user_uuid
) )
self.service_response = requests.get( user_service_response = requests.get(
service_url, service_url,
headers=service_request_headers headers=service_request_headers
) )
self._check_for_service_errors(user_service_response)
self.user = user_service_response.json()
def _build_frontend_response(self): def _build_response(self):
if self.service_response.status_code == HTTPStatus.OK: response_data = dict(name=self.user['name'])
service_response_data = json.loads(self.service_response.content) self.response = (response_data, HTTPStatus.OK)
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
)

View File

@ -338,9 +338,9 @@
} }
}, },
"@angular/flex-layout": { "@angular/flex-layout": {
"version": "6.0.0-beta.17", "version": "6.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@angular/flex-layout/-/flex-layout-6.0.0-beta.17.tgz", "resolved": "https://registry.npmjs.org/@angular/flex-layout/-/flex-layout-6.0.0-beta.16.tgz",
"integrity": "sha512-WrCWlE7NuvvxbeO8+S6aR5cvzX+1CVzpIy0izP8kMLWjAPZ0xjePHc2kJKJVapWMt7aniYZ1inl+GpsvkllycA==", "integrity": "sha512-0AYtIBGrEJshdFMc6TXGloCkD19YTCRKVJl6xZHX4H5dLnUn+daqXcbh4UsWhayevnLp85HEf2ViHLmTa6jv3g==",
"requires": { "requires": {
"tslib": "^1.7.1" "tslib": "^1.7.1"
} }
@ -8334,9 +8334,9 @@
} }
}, },
"rxjs": { "rxjs": {
"version": "6.2.1", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.1.tgz", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.2.tgz",
"integrity": "sha512-OwMxHxmnmHTUpgO+V7dZChf3Tixf4ih95cmXjzzadULziVl/FKhHScGLj4goEw9weePVOH2Q0+GcCBUhKCZc/g==", "integrity": "sha512-0MI8+mkKAXZUF9vMrEoPnaoHkfzBPP4IGwUYRJhIRJF6/w3uByO1e91bEHn8zd43RdkTMKiooYKmwz7RH6zfOQ==",
"requires": { "requires": {
"tslib": "^1.9.0" "tslib": "^1.9.0"
} }

View File

@ -3,7 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve --open", "start": "ng serve --open --proxy-config proxy.config.json",
"build-dev": "ng build --configuration=development", "build-dev": "ng build --configuration=development",
"build-test": "ng build --configuration=test", "build-test": "ng build --configuration=test",
"build-prod": "ng build --prod", "build-prod": "ng build --prod",
@ -18,7 +18,7 @@
"@angular/common": "^6.0.3", "@angular/common": "^6.0.3",
"@angular/compiler": "^6.0.3", "@angular/compiler": "^6.0.3",
"@angular/core": "^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/forms": "^6.0.3",
"@angular/http": "^6.0.3", "@angular/http": "^6.0.3",
"@angular/material": "^6.3.2", "@angular/material": "^6.3.2",
@ -32,7 +32,7 @@
"angular-in-memory-web-api": "^0.6.0", "angular-in-memory-web-api": "^0.6.0",
"core-js": "^2.5.4", "core-js": "^2.5.4",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"rxjs": "^6.0.0", "rxjs": "6.2.2",
"zone.js": "^0.8.26" "zone.js": "^0.8.26"
}, },
"devDependencies": { "devDependencies": {

View File

@ -0,0 +1,8 @@
{
"/api/*": {
"target": "http://localhost:5002",
"secure": false,
"logLevel": "debug",
"changeOrigin": true
}
}

View File

@ -1,11 +1,9 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { LoginComponent } from "./header/login/login.component";
import { PageNotFoundComponent } from "./page-not-found/page-not-found.component"; import { PageNotFoundComponent } from "./page-not-found/page-not-found.component";
const routes: Routes = [ const routes: Routes = [
{ path: 'login', component: LoginComponent},
{ path: '', redirectTo: '/skills', pathMatch: 'full' }, { path: '', redirectTo: '/skills', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent } { path: '**', component: PageNotFoundComponent }
]; ];

View File

@ -1,3 +1,7 @@
@import '../stylesheets/global';
.app-body { .app-body {
margin: 50px; margin-left: 3vw;
margin-right: 3vw;
margin-top: 30px;
} }

View File

@ -1,26 +1,22 @@
<mat-toolbar> <mat-toolbar>
<img src="../../assets/header-logo.png"> <img src="../../assets/header-logo.svg">
<mat-divider [vertical]="true"></mat-divider> <fa-icon class='separator' [icon]="separatorIcon"></fa-icon>
<div class="mat-subheading-1" style="margin-bottom: 0">MARKETPLACE</div> <div class="mat-subheading-1" style="margin-bottom: 0">MARKETPLACE</div>
<div fxFlex fxLayout="row" fxLayoutAlign="end center"> <div fxFlex fxLayout="row" fxLayoutAlign="end center">
<div class="mat-subheading-1" style="margin-bottom: 0">PREVIEW</div> <button mat-button (click)="login()" *ngIf="!isLoggedIn">
<fa-icon [icon]="signInIcon"></fa-icon>
<!--commenting out the below code temporarily until the login API is ready--> LOG IN
</button>
<!--<button mat-button (click)="login()" *ngIf="!isLoggedIn">--> <button mat-button class="menu-button" [matMenuTriggerFor]="menu" *ngIf="isLoggedIn">
<!--<fa-icon [icon]="signInIcon"></fa-icon>--> {{userMenuButtonText}}
<!--LOG IN--> <fa-icon [icon]="menuButtonIcon"></fa-icon>
<!--</button>--> </button>
<!--<button mat-button class="menu-button" [matMenuTriggerFor]="menu" *ngIf="isLoggedIn">--> <mat-menu [overlapTrigger]="false" #menu="matMenu">
<!--{{userMenuButtonText}}--> <button mat-menu-item (click)="logout()">
<!--<fa-icon [icon]="menuButtonIcon"></fa-icon>--> <fa-icon [icon]="signOutIcon"></fa-icon>
<!--</button>--> Logout
<!--<mat-menu [overlapTrigger]="false" #menu="matMenu">--> </button>
<!--<button mat-menu-item (click)="logout()">--> </mat-menu>
<!--<fa-icon [icon]="signOutIcon"></fa-icon>-->
<!--Logout-->
<!--</button>-->
<!--</mat-menu>-->
</div> </div>
</mat-toolbar> </mat-toolbar>

View File

@ -4,16 +4,16 @@ mat-toolbar {
background-color: $mycroft-primary; background-color: $mycroft-primary;
color: $mycroft-white; color: $mycroft-white;
img { img {
padding-right: 10px; height: 20px;
height: 40px; margin-top: -7px;
} }
mat-divider { .separator {
border-color: #FFFFFF; font-size: 5px;
border-top-width: 0; padding-left: 10px;
border-right-width: 1px; padding-right: 10px;
border-right-style: solid; }
height: 60%; .mat-subheading-1 {
margin-right: 10px; margin-bottom: 0;
} }
fa-icon { fa-icon {
padding-right: 5px; padding-right: 5px;

View File

@ -1,7 +1,12 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from "rxjs/internal/Subscription"; 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"; import { LoginService } from "../shared/login.service";
@ -13,6 +18,7 @@ import { LoginService } from "../shared/login.service";
export class HeaderComponent implements OnInit, OnDestroy { export class HeaderComponent implements OnInit, OnDestroy {
public isLoggedIn: boolean; public isLoggedIn: boolean;
private loginStatus: Subscription; private loginStatus: Subscription;
public separatorIcon = faCircle;
public signInIcon = faSignInAlt; public signInIcon = faSignInAlt;
public signOutIcon = faSignOutAlt; public signOutIcon = faSignOutAlt;
public menuButtonIcon = faCaretDown; public menuButtonIcon = faCaretDown;
@ -45,14 +51,18 @@ export class HeaderComponent implements OnInit, OnDestroy {
} }
logout() { logout() {
let expiration = new Date(); this.loginService.logout().subscribe(
let domain = document.domain.replace('market.', ''); (response) => {
document.cookie = 'seleneToken=""' + let expiration = new Date();
'; expires=' + expiration.toUTCString() + let domain = document.domain.replace('market.', '');
'; domain=' + domain; document.cookie = 'seleneToken=""' +
document.cookie = 'tartarusToken=""' + '; expires=' + expiration.toUTCString() +
'; expires=' + expiration.toUTCString() + '; domain=' + domain;
'; domain=' + domain; document.cookie = 'tartarusToken=""' +
this.loginService.setLoginStatus(); '; expires=' + expiration.toUTCString() +
'; domain=' + domain;
this.loginService.setLoginStatus();
}
)
} }
} }

View File

@ -6,7 +6,6 @@ import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { MaterialModule } from "../shared/material.module"; import { MaterialModule } from "../shared/material.module";
import { HeaderComponent } from './header.component'; import { HeaderComponent } from './header.component';
import { LoginComponent } from "./login/login.component";
@NgModule({ @NgModule({
imports: [ imports: [
@ -15,7 +14,7 @@ import { LoginComponent } from "./login/login.component";
FontAwesomeModule, FontAwesomeModule,
MaterialModule MaterialModule
], ],
declarations: [ HeaderComponent, LoginComponent ], declarations: [ HeaderComponent],
exports: [ HeaderComponent ], exports: [ HeaderComponent ],
}) })
export class HeaderModule { } export class HeaderModule { }

View File

@ -1,3 +0,0 @@
<div>
<iframe [src]='loginUrl' name="login"></iframe>
</div>

View File

@ -1,8 +0,0 @@
div {
width: 100%;
iframe {
border: none;
height: 1000px;
width: 100%;
}
}

View File

@ -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<LoginComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoginComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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]);
}
}
}
}

View File

@ -4,6 +4,7 @@ import { Router } from "@angular/router";
import { Observable } from "rxjs/internal/Observable"; import { Observable } from "rxjs/internal/Observable";
import { Subject } from "rxjs/internal/Subject"; import { Subject } from "rxjs/internal/Subject";
import { environment } from "../../environments/environment";
export class User { export class User {
name: string; name: string;
@ -13,19 +14,28 @@ export class User {
export class LoginService { export class LoginService {
public isLoggedIn = new Subject<boolean>(); public isLoggedIn = new Subject<boolean>();
public redirectUrl: string; public redirectUrl: string;
private logoutUrl = environment.loginUrl + '/api/logout';
private userUrl = '/api/user'; private userUrl = '/api/user';
constructor(private http: HttpClient, private router: Router) { } constructor(private http: HttpClient, private router: Router) {
}
getUser(): Observable<User> { getUser(): Observable<User> {
return this.http.get<User>(this.userUrl) return this.http.get<User>(this.userUrl);
} }
setLoginStatus() { 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() { login() {
this.router.navigate(['/login']); window.location.assign(environment.loginUrl);
}
logout(): Observable<any> {
return this.http.get(this.logoutUrl);
} }
} }

View File

@ -1,27 +1,38 @@
<div fxLayout="row" fxLayoutAlign="center"> <div class="navigate-back">
<div class="skill-detail" *ngIf="skill$ | async as skill"> <button mat-icon-button [routerLink]="['/skills']">
<fa-icon [icon]="backArrow"></fa-icon>
Back to Skill Listing
</button>
</div>
<div class="skill-detail" *ngIf="skill$ | async as skill">
<!-- Header block -->
<div class="skill-detail-header" fxLayout="row wrap">
<!-- Header block --> <!-- Left Side -->
<div class="skill-detail-header" fxLayout="row"> <div class="skill-detail-header-left" fxFlex>
<div class="skill-detail-header-left"> <!-- there cannot be an icon and an icon_image. show the
<h1> image if it exists otherwise show the icon -->
<!-- there cannot be an icon and an icon_image. show the <img *ngIf="skill.icon_image" src={{skill.icon_image}} height="70" width="70">
image if it exists otherwise show the icon --> <fa
<img *ngIf="skill.icon_image" src={{skill.icon_image}} height="30" width="30"> *ngIf="!skill.icon_image"
<fa [ngStyle]="{'color': skill.icon.color}"
*ngIf="!skill.icon_image" name={{skill.icon.icon}}
[ngStyle]="{'color': skill.icon.color}" >
name={{skill.icon.icon}} </fa>
> <div fxFlex>
</fa> <h1>{{skill.title}}</h1>
{{skill.title}} <div class="mat-body-1" [innerHTML]="skill.summary"></div>
</h1>
<div class="mat-subheading-1" [innerHTML]="skill.summary"></div>
</div> </div>
<div class="skill-detail-header-right"> </div>
<button mat-button>INSTALL</button>
<!-- Right Side -->
<div class="skill-detail-header-right" fxFlex="20">
<div class="install-button">
<button mat-flat-button class="install-button">INSTALL</button>
</div>
<div>
<button <button
mat-flat-button mat-icon-button
class="github-button" class="github-button"
(click)="navigateToGithubRepo(skill.repository_url)" (click)="navigateToGithubRepo(skill.repository_url)"
> >
@ -31,55 +42,65 @@
</div> </div>
</div> </div>
<!-- Detail block --> </div>
<div class="skill-detail-body" fxLayout="row">
<!-- Left Side --> <!-- Detail block -->
<div class="skill-detail-body-left"> <div class="skill-detail-body" fxLayout="row wrap">
<div class="skill-detail-section">
<div class="mat-subheading-1">HEY MYCROFT</div> <!-- Left Side -->
<button mat-flat-button [disabled]="true" *ngFor="let trigger of skill.triggers"> <div class="skill-detail-body-left" fxFlex>
<div class="skill-detail-section">
<div class="mat-subheading-1">hey mycroft</div>
<div fxLayout="row wrap">
<div class="mat-body-1 skill-trigger" *ngFor="let trigger of skill.triggers">
<fa-icon [icon]="triggerIcon"></fa-icon> <fa-icon [icon]="triggerIcon"></fa-icon>
{{trigger}} {{trigger}}
</button>
</div>
<div class="skill-detail-section">
<div class="mat-subheading-1">DESCRIPTION</div>
<div class="mat-body-1" [innerHTML]="skill.description"></div>
</div>
<div class="skill-detail-section">
<div class="mat-subheading-1">CREDITS</div>
<div *ngFor="let credit of skill.credits">
<div class="mat-body-1">{{credit.name}}</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Right Side --> <div class="skill-detail-section">
<div class="skill-detail-body-right"> <div class="mat-subheading-1">description</div>
<div class="skill-detail-section"> <div class="mat-body-1" [innerHTML]="skill.description"></div>
<div class="mat-subheading-1">SUPPORTED DEVICES</div> </div>
<div class="mat-body-1" fxLayoutAlign="none center"> <div class="skill-detail-section">
<img src="../../../assets/mark-1-icon.svg"> <div class="mat-subheading-1">credits</div>
Mark I <div class="mat-body-1" *ngFor="let credit of skill.credits">
</div> {{credit.name}}
<div class="mat-body-1" fxLayoutAlign="none center">
<img src="../../../assets/mark-2-icon.svg">
Mark II
</div>
<div class="mat-body-1" fxLayoutAlign="none center">
<img src="../../../assets/picroft-icon.svg">
Picroft
</div>
</div>
<div class="skill-detail-section">
<div class="mat-subheading-1">SUPPORTED LANGUAGES</div>
<div class="mat-body-1">English</div>
</div>
<div class="skill-detail-section">
<div class="mat-subheading-1">CATEGORY</div>
<div class="mat-body-1">{{skill.categories[0]}}</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Right Side -->
<div class="skill-detail-body-right" fxFlex="20">
<div class="skill-detail-section">
<div class="mat-subheading-1">supported devices</div>
<div class="mat-body-1" fxLayoutAlign="none center">
<img src="../../../assets/mark-1-icon.svg">
Mark I
</div>
<div class="mat-body-1" fxLayoutAlign="none center">
<img src="../../../assets/mark-2-icon.svg">
Mark II
</div>
<div class="mat-body-1" fxLayoutAlign="none center">
<img src="../../../assets/picroft-icon.svg">
Picroft
</div>
<div class="mat-body-1" fxLayoutAlign="none center">
<img src="../../../assets/kde.svg" class="kde-icon">
KDE
</div>
</div>
<div class="skill-detail-section">
<div class="mat-subheading-1">supported languages</div>
<div class="mat-body-1">English</div>
</div>
<div class="skill-detail-section">
<div class="mat-subheading-1">category</div>
<div class="mat-body-1">{{skill.categories[0]}}</div>
</div>
</div>
</div> </div>
</div>
</div>

View File

@ -1,36 +1,60 @@
@import '../../../stylesheets/global'; @import '../../../stylesheets/global';
.skill-detail { @mixin skill-detail-size {
margin: 0 auto;
max-width: 1000px; 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 { .skill-detail-header {
background-color: $mycroft-blue-grey; background-color: #f7f9fa;
border-top-left-radius: 10px; border-top-left-radius: 10px;
border-top-right-radius: 10px; border-top-right-radius: 10px;
height: 150px; padding-bottom: 3vh;
padding: 30px; padding-left: 4vw;
padding-right: 4vw;
padding-top: 4vh;
.skill-detail-header-left { .skill-detail-header-left {
color: $mycroft-secondary; color: $mycroft-secondary;
width: 70%; margin-right: 50px;
min-width: 340px;
fa {
font-size: 70px;
margin-right: 20px;
}
img {
margin-right: 20px;
}
h1 { h1 {
font-family: 'Roboto Mono', monospace; font-family: 'Roboto Mono', monospace;
margin-bottom: 15px; margin-bottom: 10px;
margin-top: 0; margin-top: 0;
} }
.mat-subheading-1 {
padding-right: 30px;
@include ellipsis-overflow
}
} }
.skill-detail-header-right { .skill-detail-header-right {
width: 30%; margin-right: 20px;
button { .install-button {
@include action-button; @include action-button;
margin-top: 5px; width: 140px;
width: 160px;
} }
.install-button:hover {
background-color: $mycroft-tertiary-green;
color: $mycroft-secondary;
}
.github-button { .github-button {
background-color: $mycroft-blue-grey;
color: $mycroft-dark-grey; color: $mycroft-dark-grey;
font-weight: normal;
width: 135px;
fa-icon { fa-icon {
padding-right: 5px; padding-right: 5px;
} }
@ -41,27 +65,44 @@
background-color: $mycroft-white; background-color: $mycroft-white;
border-bottom-left-radius: 10px; border-bottom-left-radius: 10px;
border-bottom-right-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 { .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 { .skill-detail-body-left {
width: 70%; min-width: 340px;
button { margin-right: 50px;
@include skill-trigger-button; .skill-trigger {
@include skill-trigger;
@include ellipsis-overflow; @include ellipsis-overflow;
max-width: 100%; margin-right: 10px;
margin-bottom: 10px;
max-width: 340px;
} }
} }
.skill-detail-body-right { .skill-detail-body-right {
width: 30%; margin-right: 20px;
white-space: nowrap;
img { img {
padding-right: 15px; padding-right: 10px;
} }
} }
.skill-detail-section {
padding-bottom: 30px;
padding-right: 30px;
}
} }
} }

View File

@ -3,7 +3,7 @@ import { Router, ActivatedRoute, ParamMap } from '@angular/router';
import { Observable } from "rxjs/internal/Observable"; import { Observable } from "rxjs/internal/Observable";
import { switchMap } from "rxjs/operators"; 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"; import { Skill, SkillsService } from "../skills.service";
@ -13,6 +13,7 @@ import { Skill, SkillsService } from "../skills.service";
styleUrls: ['./skill-detail.component.scss'] styleUrls: ['./skill-detail.component.scss']
}) })
export class SkillDetailComponent implements OnInit { export class SkillDetailComponent implements OnInit {
public backArrow = faArrowLeft;
public githubIcon = faCodeBranch; public githubIcon = faCodeBranch;
public skill$: Observable<Skill>; public skill$: Observable<Skill>;
public triggerIcon = faComment; public triggerIcon = faComment;

View File

@ -4,16 +4,18 @@ mat-card-header {
justify-content: center; justify-content: center;
margin-bottom: 15px; margin-bottom: 15px;
.mycroft-icon { .mycroft-icon {
left: 15px; left: 18px;
position: absolute; position: absolute;
top: 15px; top: 18px;
img { img {
height: 20px; height: 20px;
width: 20px; width: 20px;
} }
} }
.skill-icon { .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 { fa-icon {
font-size: 28px; font-size: 28px;
} }

View File

@ -2,18 +2,30 @@
<mat-card *ngFor="let skill of skills; let i = index"> <mat-card *ngFor="let skill of skills; let i = index">
<div [routerLink]="['/skill', skill.id]"> <div [routerLink]="['/skill', skill.id]">
<market-skill-card-header [skill]="skills[i]"></market-skill-card-header> <market-skill-card-header [skill]="skills[i]"></market-skill-card-header>
<mat-card-title align="center">{{skill.title}}</mat-card-title> <mat-card-title *ngIf="skill.title" align="center">{{skill.title}}</mat-card-title>
<mat-card-subtitle> <mat-card-title *ngIf="!skill.title" align="center">&nbsp;</mat-card-title>
<button mat-flat-button [disabled]="true"> <mat-card-subtitle fxLayoutAlign="center">
<div class="skill-trigger">
<fa-icon [icon]="voiceIcon"></fa-icon> <fa-icon [icon]="voiceIcon"></fa-icon>
{{skill.triggers[0]}} {{skill.triggers[0]}}
</button> </div>
</mat-card-subtitle> </mat-card-subtitle>
<mat-card-content *ngIf="skill.summary" [innerHTML]="skill.summary"></mat-card-content> <mat-card-content *ngIf="skill.summary" [innerHTML]="skill.summary"></mat-card-content>
<mat-card-content *ngIf="!skill.summary">&nbsp;</mat-card-content> <mat-card-content *ngIf="!skill.summary">&nbsp;</mat-card-content>
</div> </div>
<mat-card-actions> <mat-card-actions>
<button mat-button (click)="install_skill(skill)">INSTALL</button> <button
class="install-button"
*ngIf="!skill.installed"
mat-flat-button
(click)="install_skill(skill)"
>
INSTALL
</button>
<button *ngIf="skill.installed" mat-button class="installed-button">
<fa-icon [icon]="installedIcon"></fa-icon>
INSTALLED
</button>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
</div> </div>

View File

@ -8,24 +8,24 @@ mat-card {
@include card-width; @include card-width;
border-radius: 10px; border-radius: 10px;
cursor: pointer; cursor: pointer;
margin: 15px; margin: 10px;
padding: 15px; padding: 18px;
mat-card-title { mat-card-title {
@include ellipsis-overflow; @include ellipsis-overflow;
color: $mycroft-secondary; color: $mycroft-secondary;
font-family: 'Roboto Mono', monospace; font-family: 'Roboto Mono', monospace;
font-weight: bold; font-weight: bold;
padding-bottom: 5px;
text-align: center; text-align: center;
} }
mat-card-subtitle { mat-card-subtitle {
button { .skill-trigger {
@include skill-trigger-button;
@include ellipsis-overflow; @include ellipsis-overflow;
@include card-width; @include skill-trigger;
margin: 0;
} }
} }
mat-card-content { mat-card-content {
color: $mycroft-secondary;
@include ellipsis-overflow; @include ellipsis-overflow;
text-align: center; text-align: center;
} }
@ -37,9 +37,19 @@ mat-card {
@include card-width; @include card-width;
margin-bottom: 15px; 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{ mat-card:hover{
box-shadow: -1px 10px 29px 0px; box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.2);
} }

View File

@ -1,7 +1,7 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { MatSnackBar } from "@angular/material"; 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"; import { SkillsService, Skill } from "../skills.service";
@ -11,9 +11,10 @@ import { SkillsService, Skill } from "../skills.service";
styleUrls: ['./skill-summary.component.scss'], styleUrls: ['./skill-summary.component.scss'],
}) })
export class SkillSummaryComponent implements OnInit { export class SkillSummaryComponent implements OnInit {
public installedIcon = faCheck;
@Input() public skills: Skill[]; @Input() public skills: Skill[];
private skillToInstall: Skill;
public voiceIcon = faComment; public voiceIcon = faComment;
private skillInstalling: Skill;
constructor(public loginSnackbar: MatSnackBar, private skillsService: SkillsService) { } constructor(public loginSnackbar: MatSnackBar, private skillsService: SkillsService) { }
@ -25,7 +26,7 @@ export class SkillSummaryComponent implements OnInit {
* @param {Skill} skill * @param {Skill} skill
*/ */
install_skill(skill: Skill) : void { install_skill(skill: Skill) : void {
this.skillToInstall = skill; this.skillInstalling = skill;
this.skillsService.installSkill(skill).subscribe( this.skillsService.installSkill(skill).subscribe(
(response) => { (response) => {
this.onInstallSuccess(response) this.onInstallSuccess(response)
@ -46,7 +47,15 @@ export class SkillSummaryComponent implements OnInit {
* @param response * @param response
*/ */
onInstallSuccess(response) : void { 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 { onInstallFailure(response) : void {
if (response.status === 401) { 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( this.loginSnackbar.open(
'Skill installation functionality coming soon. ' + 'To install a skill, log in to your account.',
'In the meantime use your voice to install skills ' + 'LOG IN',
'by saying: "Hey Mycroft, install ' + installName.join(' ') + '"',
'',
{panelClass: 'mycroft-snackbar', duration: 5000} {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}
//
// );
} }
} }
} }

View File

@ -1,11 +1,11 @@
<div class="skill-toolbar" fxLayout="row"> <div fxLayout="row" fxLayoutAlign="center" class="skill-toolbar">
<div fxFlex="80" (keydown.enter)="onClick()"> <div fxFlex="60" (keydown.enter)="searchSkills()" class="search-field">
<mat-form-field class="search-field"> <mat-form-field>
<input matInput placeholder="Search" type="text" [(ngModel)]="searchTerm"> <input matInput placeholder="Search Skills" type="text" [(ngModel)]="searchTerm">
<button mat-icon-button matSuffix="">
<fa-icon [icon]="searchIcon" (click)="searchSkills()"></fa-icon>
</button>
</mat-form-field> </mat-form-field>
<button mat-icon-button>
<fa-icon [icon]="searchIcon" (click)="onClick()"></fa-icon>
</button>
</div> </div>
<!-- commenting out the language picker until such time as there are --> <!-- commenting out the language picker until such time as there are -->
@ -20,3 +20,9 @@
<!--</mat-form-field>--> <!--</mat-form-field>-->
</div> </div>
<div *ngIf="showBackButton">
<button mat-icon-button class="back-button" (click)="clearSearch()">
<fa-icon [icon]="backArrow"></fa-icon>
All Skills
</button>
</div>

View File

@ -1,13 +1,27 @@
@import '../../../stylesheets/global'; @import '../../../stylesheets/global';
.back-button {
color: $mycroft-dark-grey;
margin-left: 20px;
width: 100px;
}
fa-icon { fa-icon {
color: $mycroft-dark-grey; color: $mycroft-dark-grey;
} }
.skill-toolbar { .skill-toolbar {
margin-left: 15px; margin-left: 15px;
margin-right: 15px;
.search-field { .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%;
}
} }
} }

View File

@ -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"; import { SkillsService } from "../skills.service";
@ -9,7 +10,8 @@ import { SkillsService } from "../skills.service";
templateUrl: './skill-toolbar.component.html', templateUrl: './skill-toolbar.component.html',
styleUrls: ['./skill-toolbar.component.scss'] styleUrls: ['./skill-toolbar.component.scss']
}) })
export class SkillToolbarComponent implements OnInit { export class SkillToolbarComponent implements OnInit, OnDestroy {
public backArrow = faArrowLeft;
public languages = [ public languages = [
{value: 'english', display: 'English'} {value: 'english', display: 'English'}
]; ];
@ -17,27 +19,36 @@ export class SkillToolbarComponent implements OnInit {
@Output() public searchResults = new EventEmitter(); @Output() public searchResults = new EventEmitter();
public searchTerm: string; public searchTerm: string;
public selectedLanguage = this.languages[0].value; public selectedLanguage = this.languages[0].value;
public skillsAreFiltered: Subscription;
public showBackButton: boolean = false;
constructor(private skillsService: SkillsService) { } constructor(private skillsService: SkillsService) { }
ngOnInit() { } ngOnInit() {
this.skillsAreFiltered = this.skillsService.isFiltered.subscribe(
(isFiltered) => { this.onFilteredStateChange(isFiltered) }
);
}
onClick(): void { ngOnDestroy() {
if (this.searchIcon === faSearch) { this.skillsAreFiltered.unsubscribe();
this.searchSkills(); }
this.searchIcon = faTimes;
} else { clearSearch(): void {
this.searchTerm = ''; this.searchTerm = '';
this.searchSkills(); this.searchSkills()
this.searchIcon = faSearch;
}
} }
searchSkills(): void { searchSkills(): void {
this.skillsService.searchSkills(this.searchTerm).subscribe( this.skillsService.searchSkills(this.searchTerm).subscribe(
(skills) => { (skills) => {
this.searchResults.emit(skills); this.searchResults.emit(skills);
console.log(this.skillsAreFiltered);
} }
); );
} }
onFilteredStateChange (isFiltered) {
this.showBackButton = isFiltered
}
} }

View File

@ -6,7 +6,8 @@
background-color: $market-background; background-color: $market-background;
color: $mycroft-dark-grey; color: $mycroft-dark-grey;
font-size: xx-large; font-size: xx-large;
margin-top: 30px; margin-top: 20px;
padding-left: 10px;
fa-icon { fa-icon {
margin-right: 15px; margin-right: 15px;
} }

View File

@ -27,11 +27,27 @@ export class SkillsComponent implements OnInit {
} }
get_skill_categories(skills): void { get_skill_categories(skills): void {
let skillCategories = [],
systemCategoryFound = false;
this.skillCategories = []; this.skillCategories = [];
Object.keys(skills).forEach( 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 { showSearchResults(searchResults): void {

View File

@ -1,15 +1,15 @@
import { TestBed, inject } from '@angular/core/testing'; import { TestBed, inject } from '@angular/core/testing';
import { SkillService } from './skill.service'; import { SkillsService } from './skills.service';
describe('SkillsService', () => { describe('SkillsService', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ 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(); expect(service).toBeTruthy();
})); }));
}); });

View File

@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Subject } from "rxjs/internal/Subject";
export class Skill { export class Skill {
id: number; id: number;
@ -10,7 +11,7 @@ export class Skill {
description: string; description: string;
icon: Object; icon: Object;
icon_image: string; icon_image: string;
skill_name: string; installed: boolean;
title: string; title: string;
summary: string; summary: string;
repository_url: string; repository_url: string;
@ -19,10 +20,11 @@ export class Skill {
@Injectable() @Injectable()
export class SkillsService { export class SkillsService {
private installUrl = '/api/install-skill'; private installUrl = '/api/install';
private skillUrl = '/api/skill/'; private skillUrl = '/api/skill/';
private skillsUrl = '/api/skills'; private skillsUrl = '/api/skills';
private searchQuery = '?search='; private searchQuery = '?search=';
public isFiltered = new Subject<boolean>();
constructor(private http: HttpClient) { } constructor(private http: HttpClient) { }
@ -35,6 +37,7 @@ export class SkillsService {
} }
searchSkills(searchTerm: string): Observable<Skill[]> { searchSkills(searchTerm: string): Observable<Skill[]> {
this.isFiltered.next(!!searchTerm);
return this.http.get<Skill[]>(this.skillsUrl + this.searchQuery + searchTerm) return this.http.get<Skill[]>(this.skillsUrl + this.searchQuery + searchTerm)
} }
@ -43,6 +46,5 @@ export class SkillsService {
this.installUrl, this.installUrl,
{skill_url: skill.repository_url} {skill_url: skill.repository_url}
) )
} }
} }

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 560 85" enable-background="new 0 0 560 85" xml:space="preserve">
<g>
<path fill="#FFFFFF" d="M8.8,27.3l20.6,45.7L50,27.3c0.7-1.7,2.4-2.7,4.2-2.7h0.1c2.6,0,4.6,2.1,4.6,4.6v49c0,1.6-1.3,2.9-2.9,2.9
h0c-1.6,0-2.9-1.3-2.9-2.9V58l0.5-26.3L32.5,79.1c-0.5,1.2-1.7,2-3.1,2h0c-1.3,0-2.5-0.8-3.1-2L5.2,32l0.5,26v20.3
c0,1.6-1.3,2.9-2.9,2.9h0c-1.6,0-2.9-1.3-2.9-2.9l0-49c0-2.6,2.1-4.6,4.6-4.6h0C6.4,24.6,8.1,25.7,8.8,27.3z"/>
<path fill="#FFFFFF" d="M99.6,55.4L116.7,26c0.5-0.9,1.4-1.4,2.4-1.4h0c2.2,0,3.6,2.4,2.4,4.3l-19.2,31.7v17.6
c0,1.6-1.3,2.9-2.9,2.9h0c-1.6,0-2.9-1.3-2.9-2.9V60.3l-19-31.5c-1.1-1.9,0.2-4.3,2.4-4.3h0c1,0,1.9,0.5,2.4,1.4L99.6,55.4z"/>
<path fill="#FFFFFF" d="M180.2,63.3c1.8,0,3.3,1.7,2.8,3.5c-1.1,4.3-3.2,7.7-6.3,10.3c-3.9,3.2-9.1,4.8-15.6,4.8
c-6.9,0-12.4-2.3-16.7-6.9c-4.3-4.6-6.4-11-6.4-19.1v-6.2c0-5.1,1-9.6,2.9-13.6c1.9-3.9,4.6-7,8.2-9.1c3.5-2.1,7.6-3.2,12.3-3.2
c6.5,0,11.6,1.6,15.5,4.9c3.1,2.6,5.1,6,6.2,10.4c0.4,1.8-1,3.5-2.8,3.5h0c-1.3,0-2.5-0.9-2.8-2.2c-0.8-3.3-2.2-6-4.4-7.9
c-2.6-2.4-6.5-3.6-11.6-3.6c-5.4,0-9.7,1.8-12.9,5.5c-3.1,3.7-4.7,8.8-4.7,15.4v6.4c0,6.4,1.5,11.5,4.6,15.2
c3.1,3.7,7.3,5.6,12.6,5.6c5.2,0,9.1-1.1,11.8-3.4c2.2-1.9,3.7-4.6,4.5-8.1C177.7,64.2,178.9,63.3,180.2,63.3L180.2,63.3z"/>
<path fill="#FFFFFF" d="M226,58.4h-17.2v19.8c0,1.6-1.3,2.9-2.9,2.9h0c-1.6,0-2.9-1.3-2.9-2.9V24.6h20c6.5,0,11.6,1.5,15.3,4.6
c3.7,3,5.5,7.2,5.5,12.5c0,3.7-1.1,7-3.4,9.7c-2.3,2.8-5.3,4.7-9,5.7L244,76.8c1.1,1.7,0,4-2.1,4.2l0,0c-1,0.1-2-0.4-2.5-1.3
L226,58.4z M208.9,53.5h15.3c4.2,0,7.6-1.1,10.1-3.2c2.5-2.2,3.8-5,3.8-8.4c0-3.8-1.3-6.7-3.9-8.9c-2.6-2.2-6.2-3.3-10.9-3.3h-14.4
V53.5z"/>
<path fill="#FFFFFF" d="M377.5,55.3H352v23c0,1.6-1.3,2.9-2.9,2.9l0,0c-1.6,0-2.9-1.3-2.9-2.9V24.6h35.4c1.4,0,2.5,1.1,2.5,2.5v0.1
c0,1.4-1.1,2.5-2.5,2.5H352v20.7h25.5c1.4,0,2.5,1.1,2.5,2.5v0C380,54.1,378.9,55.3,377.5,55.3z"/>
<path fill="#FFFFFF" d="M440.7,29.6h-18.1v48.6c0,1.6-1.3,2.9-2.9,2.9l0,0c-1.6,0-2.9-1.3-2.9-2.9V29.6h-18.1
c-1.4,0-2.5-1.1-2.5-2.5v0c0-1.4,1.1-2.5,2.5-2.5h42c1.4,0,2.5,1.1,2.5,2.5v0C443.2,28.5,442.1,29.6,440.7,29.6z"/>
<path fill="#FFFFFF" d="M525,66h-27.4l-5.3,13.3c-0.4,1-1.4,1.7-2.6,1.7h0c-2,0-3.3-2-2.6-3.8l20.4-50.1c0.6-1.6,2.2-2.6,3.8-2.6
l0,0c1.7,0,3.2,1,3.8,2.6l20.2,50.1c0.7,1.8-0.6,3.8-2.6,3.8h0c-1.1,0-2.1-0.7-2.6-1.7L525,66z M499.5,61.1H523l-11.7-29.5
L499.5,61.1z"/>
<path fill="#FFFFFF" d="M557.1,81.1L557.1,81.1c-1.6,0-2.9-1.3-2.9-2.9V27.5c0-1.6,1.3-2.9,2.9-2.9l0,0c1.6,0,2.9,1.3,2.9,2.9v50.7
C560,79.8,558.7,81.1,557.1,81.1z"/>
<g>
<path fill="#FFFFFF" d="M294.4,19.9c-18,0-32.6,14.6-32.6,32.6S276.5,85,294.4,85S327,70.5,327,52.5S312.4,19.9,294.4,19.9z
M294.4,80c-15.2,0-27.5-12.4-27.5-27.5c0-15.2,12.4-27.5,27.5-27.5c15.2,0,27.5,12.4,27.5,27.5C322,67.7,309.6,80,294.4,80z"/>
<path fill="#FFFFFF" d="M271.8,52.5c0,12.5,10.1,22.6,22.6,22.6V29.8C281.9,29.8,271.8,40,271.8,52.5z"/>
<circle fill="#FFFFFF" cx="294.4" cy="5.8" r="5.8"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg821" inkscape:version="0.91 r13725" sodipodi:docname="KDElogoBoxBlue.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 40 40"
style="enable-background:new 0 0 40 40;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#6C7A89;}
</style>
<sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:bbox-nodes="true" inkscape:current-layer="layer1" inkscape:cx="62.936714" inkscape:cy="68.6291" inkscape:document-units="mm" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:snap-bbox="true" inkscape:window-height="2045" inkscape:window-maximized="1" inkscape:window-width="3840" inkscape:window-x="0" inkscape:window-y="0" inkscape:zoom="5.6" pagecolor="#ffffff" showgrid="true" units="px" width="128px">
<inkscape:grid id="grid1391" type="xygrid"></inkscape:grid>
</sodipodi:namedview>
<g>
<rect id="rect4157" x="5.6" y="5.6" class="st0" width="28.8" height="28.8"/>
<path id="path5692_2_-3" inkscape:connector-curvature="0" class="st1" d="M21.6,9.3l-3.7,0.4v15.1l3.6-0.5v-6.4l4.9,7.1l3.8-1.2
l-5-6.9l5-6.5l-3.9-0.9l-4.8,6.5L21.6,9.3z M13.3,13c0,0-0.1,0-0.1,0.1l-1.4,1.4c-0.1,0.1-0.1,0.2,0,0.2l1.7,2.8
c-0.3,0.5-0.5,1-0.7,1.6l-3.1,0.6c-0.1,0-0.1,0.1-0.1,0.2v2c0,0.1,0.1,0.2,0.1,0.2l3,0.7c0.2,0.7,0.4,1.3,0.7,1.9l-1.7,2.6
c0,0.1,0,0.2,0,0.2l1.4,1.4c0.1,0.1,0.2,0.1,0.2,0l2.7-1.7c0.5,0.3,1.1,0.6,1.7,0.7l0.6,3c0,0.1,0.1,0.1,0.2,0.1h2
c0.1,0,0.2-0.1,0.2-0.1l0.7-3.1c0.6-0.2,1.2-0.4,1.8-0.7l2.7,1.8c0.1,0,0.2,0,0.2,0l1.4-1.4c0.1-0.1,0.1-0.2,0-0.2l-1-1.6l-0.3,0.1
c0,0-0.1,0-0.1,0c0,0-0.6-0.9-1.4-2.1c-1,1.9-2.9,3.2-5.2,3.2c-3.2,0-5.8-2.6-5.8-5.8c0-2.4,1.4-4.4,3.4-5.3v-1.5
c-0.4,0.1-0.7,0.3-1.1,0.5c0,0,0,0,0,0L13.4,13C13.4,13,13.3,13,13.3,13L13.3,13z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,4 +1,16 @@
export const environment = { export const environment = {
production: true, production: true,
loginUrl: 'http://login.mycroft.test' loginUrl: 'https://login.mycroft.ai'
}; };
document.write(
'<script async src="https://www.googletagmanager.com/gtag/js?id=UA-101772425-10"></script>'
);
document.write(
'<script>' +
'window.dataLayer = window.dataLayer || []; ' +
'function gtag(){dataLayer.push(arguments);} ' +
'gtag("js", new Date());' +
'gtag("config", "UA-101772425-10"); ' +
'</script>'
);

View File

@ -1,4 +1,4 @@
export const environment = { export const environment = {
production: false, production: false,
loginUrl: 'http://login.mycroft-test.net' loginUrl: 'https://login.mycroft-test.net'
}; };

View File

@ -4,10 +4,8 @@
export const environment = { export const environment = {
production: false, production: false,
apiUrl: 'http://localhost:5002',
// URL of development API loginUrl: 'http://localhost:4201'
apiUrl: 'http://localhost:5000/',
loginUrl: 'http://login.mycroft.test'
}; };
/* /*

View File

@ -10,7 +10,7 @@
<link href="https://fonts.googleapis.com/css?family=Roboto+Mono:300,400,500" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Roboto+Mono:300,400,500" rel="stylesheet">
<link href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous" rel="stylesheet"> <link href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous" rel="stylesheet">
</head> </head>
<body> <body style="background-color: #f1f3f4; margin: 0">
<app-root></app-root> <app-root></app-root>
</body> </body>
</html> </html>

View File

@ -11,4 +11,4 @@ $mycroft-black: #000000;
$mycroft-dark-grey: #6c7a89; $mycroft-dark-grey: #6c7a89;
$mycroft-light-grey: #bdc3c7; $mycroft-light-grey: #bdc3c7;
$mycroft-blue-grey: #e4f1fe; $mycroft-blue-grey: #e4f1fe;
$market-background: #f1f1f1; $market-background: #f1f3f4;

View File

@ -5,18 +5,5 @@ $button-border-radius: 4px;
border-radius: $button-border-radius; border-radius: $button-border-radius;
background-color: $mycroft-primary; background-color: $mycroft-primary;
color: $mycroft-white; color: $mycroft-white;
font-weight: normal; letter-spacing: 0.5px;
}
@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;
}
} }

View File

@ -1,5 +1,22 @@
@import "../base/mycroft-colors";
@mixin ellipsis-overflow { @mixin ellipsis-overflow {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; 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;
}
}

View File

@ -5,106 +5,89 @@ from logging import getLogger
from flask import request, current_app from flask import request, current_app
from flask_restful import Resource from flask_restful import Resource
from .auth import decode_auth_token, AuthorizationError from .auth import decode_auth_token, AuthenticationError
# The logger is initialized here but this should be overridden with a
# package-specific logger (e.g. _log = getLogger(__package__)
_log = getLogger()
class ServiceUrlNotFound(Exception): class APIError(Exception):
"""Raise this exception whenever a non-successful response is built"""
pass pass
class ServiceServerError(Exception): class SeleneEndpoint(Resource):
pass
class MethodNotAllowedError(Exception):
pass
class SeleneBaseView(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 authentication_required: bool = True
# package-specific logger (e.g. _log = getLogger(__package__)
_log = getLogger()
def __init__(self): 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 = None
self.response_data = None
self.tartarus_token: str = None
self.selene_token: str = None self.selene_token: str = None
self.service_response = None self.tartarus_token: str = None
self.user_uuid: str = None self.user_uuid: str = None
def _authenticate(self): 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): def _get_auth_token(self):
"""Get the Selene JWT (and the tartarus token) from cookies.
:raises: AuthenticationError
"""
try: try:
self.selene_token = request.cookies['seleneToken'] self.selene_token = request.cookies['seleneToken']
self.tartarus_token = request.cookies['tartarusToken'] self.tartarus_token = request.cookies['tartarusToken']
except KeyError: except KeyError:
raise AuthorizationError( raise AuthenticationError(
'no authentication token found in request' 'no authentication token found in request'
) )
def _validate_auth_token(self): def _validate_auth_token(self):
"""Decode the Selene JWT.
:raises: AuthenticationError
"""
self.user_uuid = decode_auth_token( self.user_uuid = decode_auth_token(
self.selene_token, self.selene_token,
current_app.config['SECRET_KEY'] self.config['SECRET_KEY']
) )
def check_for_service_errors(self, service, response): def _check_for_service_errors(self, service_response):
if response.status_code == HTTPStatus.UNAUTHORIZED: """Common logic to handle non-successful returns from service calls."""
error_message = 'invalid authentication token' if service_response.status_code != HTTPStatus.OK:
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:
error_message = ( error_message = (
'{service} service URL {url} HTTP status {status}'.format( 'service URL {url} returned HTTP status {status}'.format(
service=service, status=service_response.status_code,
status=response.status_code, url=service_response.request.url
url=response.request.url
) )
) )
self._log.error(error_message) _log.error(error_message)
raise ServiceServerError(error_message) if service_response.status_code == HTTPStatus.UNAUTHORIZED:
self.response = (error_message, HTTPStatus.UNAUTHORIZED)
def _build_response(self): else:
try: self.response = (error_message, HTTPStatus.INTERNAL_SERVER_ERROR)
self._build_response_data() raise APIError()
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)

View File

@ -1,21 +1,44 @@
from datetime import datetime
from logging import getLogger from logging import getLogger
from time import time
import jwt import jwt
THIRTY_DAYS = 2592000
_log = getLogger(__package__) _log = getLogger(__package__)
class AuthorizationError(Exception): class AuthenticationError(Exception):
pass 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: def decode_auth_token(auth_token: str, secret_key: str) -> tuple:
""" """
Decodes the auth token 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 :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 :return: two-value tuple containing a boolean value indicating if the
user UUID extracted from the token. UUID will be None if token is invalid. token is good and the user UUID extracted from the token. UUID will
be None if token is invalid.
""" """
try: try:
payload = jwt.decode(auth_token, secret_key) 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: except jwt.ExpiredSignatureError:
error_msg = 'Selene token expired' error_msg = 'Selene token expired'
_log.info(error_msg) _log.info(error_msg)
raise AuthorizationError(error_msg) raise AuthenticationError(error_msg)
except jwt.InvalidTokenError: except jwt.InvalidTokenError:
error_msg = 'Invalid Selene token' error_msg = 'Invalid Selene token'
_log.info(error_msg) _log.info(error_msg)
raise AuthorizationError(error_msg) raise AuthenticationError(error_msg)
return user_uuid return user_uuid