Merge pull request #6 from MycroftAI/login-and-install
Add login and install functionalitypull/7/head
commit
6c3ed20f3f
|
@ -16,6 +16,7 @@
|
|||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
__pycache__/
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
|
@ -37,3 +38,5 @@ testem.log
|
|||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# The selene-shared parent image contains all the common Docker configs for
|
||||
# all Selene apps and services see the "shared" directory in this repository.
|
||||
FROM selene-shared:latest
|
||||
FROM docker.mycroft.ai/selene-shared:latest
|
||||
LABEL description="Run the API for the Mycroft login screen"
|
||||
|
||||
# Use pipenv to install the package's dependencies in the container
|
||||
|
|
|
@ -9,9 +9,10 @@ requests = "*"
|
|||
pyjwt = "*"
|
||||
flask-restful = "*"
|
||||
certifi = "*"
|
||||
gunicorn = "*"
|
||||
uwsgi = "*"
|
||||
|
||||
[dev-packages]
|
||||
selene-util = {path = "./../../../../shared"}
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "624569e9bc0207f58dca3a683d5868e374e78a5be00a34b442aae4a656e4eac2"
|
||||
"sha256": "7cf1dde24d5a966645f3e49d93dde93dad42c6b6fa62f9b254b79d6b58e93e06"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
@ -40,10 +40,11 @@
|
|||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
|
||||
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"version": "==6.7"
|
||||
"markers": "python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.1.*'",
|
||||
"version": "==7.0"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
|
@ -61,14 +62,6 @@
|
|||
"index": "pypi",
|
||||
"version": "==0.3.6"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
"sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471",
|
||||
"sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==19.9.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
|
||||
|
@ -133,6 +126,13 @@
|
|||
"markers": "python_version >= '2.6' and python_version != '3.1.*' and python_version != '3.3.*' and python_version < '4' and python_version != '3.0.*' and python_version != '3.2.*'",
|
||||
"version": "==1.23"
|
||||
},
|
||||
"uwsgi": {
|
||||
"hashes": [
|
||||
"sha256:d2318235c74665a60021a4fc7770e9c2756f9fc07de7b8c22805efe85b5ab277"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0.17.1"
|
||||
},
|
||||
"werkzeug": {
|
||||
"hashes": [
|
||||
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
|
||||
|
@ -141,5 +141,9 @@
|
|||
"version": "==0.14.1"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
"develop": {
|
||||
"selene-util": {
|
||||
"path": "./../../../../shared"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,41 @@
|
|||
from flask import Flask
|
||||
from flask import Flask, request
|
||||
from flask_restful import Api
|
||||
|
||||
from .authorize import AuthorizeAntisocialView
|
||||
from .endpoints import (
|
||||
AuthenticateAntisocialEndpoint,
|
||||
SocialLoginTokensEndpoint,
|
||||
AuthorizeFacebookEndpoint,
|
||||
AuthorizeGithubEndpoint,
|
||||
AuthorizeGoogleEndpoint,
|
||||
LogoutEndpoint
|
||||
)
|
||||
from .config import get_config_location
|
||||
from .logout import LogoutView
|
||||
|
||||
BASE_URL = '/api/auth/'
|
||||
# Initialize the Flask application and the Flask Restful API
|
||||
login = Flask(__name__)
|
||||
login.config.from_object(get_config_location())
|
||||
login_api = Api(login, catch_all_404s=True)
|
||||
|
||||
antisocial_view_url = BASE_URL + 'antisocial'
|
||||
login_api.add_resource(AuthorizeAntisocialView, antisocial_view_url)
|
||||
# Define the endpoints
|
||||
login_api.add_resource(AuthenticateAntisocialEndpoint, '/api/antisocial')
|
||||
login_api.add_resource(AuthorizeFacebookEndpoint, '/api/social/facebook')
|
||||
login_api.add_resource(AuthorizeGithubEndpoint, '/api/social/github')
|
||||
login_api.add_resource(AuthorizeGoogleEndpoint, '/api/social/google')
|
||||
login_api.add_resource(SocialLoginTokensEndpoint, '/api/social/tokens')
|
||||
login_api.add_resource(LogoutEndpoint, '/api/logout')
|
||||
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -8,20 +8,30 @@ class LoginConfigException(Exception):
|
|||
class BaseConfig:
|
||||
"""Base configuration."""
|
||||
DEBUG = False
|
||||
LOGIN_BASE_URL = os.environ['LOGIN_BASE_URL']
|
||||
SECRET_KEY = os.environ['JWT_SECRET']
|
||||
SELENE_BASE_URL = os.environ['SELENE_BASE_URL']
|
||||
TARTARUS_BASE_URL = os.environ['TARTARUS_BASE_URL']
|
||||
|
||||
|
||||
class DevelopmentConfig(BaseConfig):
|
||||
"""Development configuration."""
|
||||
DEBUG = True
|
||||
TARTARUS_BASE_URL = 'https://api-test.mycroft.ai/v1'
|
||||
|
||||
|
||||
class TestConfig(BaseConfig):
|
||||
pass
|
||||
|
||||
|
||||
class ProdConfig(BaseConfig):
|
||||
pass
|
||||
|
||||
|
||||
def get_config_location():
|
||||
environment_configs = dict(
|
||||
dev='login_api.config.DevelopmentConfig',
|
||||
# test=TestConfig,
|
||||
# prod=ProdConfig
|
||||
test=TestConfig,
|
||||
prod=ProdConfig
|
||||
)
|
||||
|
||||
try:
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
||||
)
|
|
@ -3,7 +3,7 @@
|
|||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"frontend": {
|
||||
"mycroft-login": {
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"@angular/common": "^6.0.3",
|
||||
"@angular/compiler": "^6.0.3",
|
||||
"@angular/core": "^6.0.3",
|
||||
"@angular/flex-layout": "^6.0.0-beta.16",
|
||||
"@angular/flex-layout": "6.0.0-beta.16",
|
||||
"@angular/forms": "^6.0.3",
|
||||
"@angular/http": "^6.0.9",
|
||||
"@angular/material": "^6.4.1",
|
||||
|
@ -29,7 +29,7 @@
|
|||
"@fortawesome/free-regular-svg-icons": "^5.2.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.2.0",
|
||||
"core-js": "^2.5.4",
|
||||
"rxjs": "^6.0.0",
|
||||
"rxjs": "6.2.2",
|
||||
"zone.js": "^0.8.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -1,3 +1,31 @@
|
|||
<div class="split top"></div>
|
||||
<div class="split bottom"></div>
|
||||
<login-authenticate></login-authenticate>
|
||||
<!--
|
||||
Don't display the login page if the URL contains the data returned from a
|
||||
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>
|
||||
|
|
|
@ -2,24 +2,42 @@
|
|||
|
||||
/* Split the screen in half */
|
||||
.split {
|
||||
height: 50%;
|
||||
left: 0;
|
||||
overflow-x: hidden;
|
||||
padding-top: 20px;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: -1;
|
||||
height: 50%;
|
||||
left: 0;
|
||||
overflow-x: hidden;
|
||||
padding-top: 20px;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Control the top side */
|
||||
/* Top Half */
|
||||
.top {
|
||||
top: 0;
|
||||
background-color: $mycroft-primary;
|
||||
top: 0;
|
||||
background-color: $mycroft-primary;
|
||||
}
|
||||
|
||||
/* Control the bottom side */
|
||||
/* Bottom Half */
|
||||
.bottom {
|
||||
bottom: 0;
|
||||
background-color: #e5e5e5;
|
||||
bottom: 0;
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
|
||||
|
||||
mat-tab-group {
|
||||
height: 485px;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.login-options {
|
||||
background-color: $mycroft-white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-bottom: 50px;
|
||||
margin-top: 50px;
|
||||
width: 600px;
|
||||
}
|
|
@ -1,10 +1,26 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent {
|
||||
export class AppComponent implements OnInit {
|
||||
title = 'Mycroft Login';
|
||||
public socialLoginDataFound: boolean = false;
|
||||
|
||||
constructor () {
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
let uriParams = decodeURIComponent(window.location.search);
|
||||
|
||||
if (uriParams) {
|
||||
this.socialLoginDataFound = true;
|
||||
window.opener.postMessage(uriParams, window.location.origin);
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import { AuthModule } from "./auth/auth.module";
|
|||
BrowserModule,
|
||||
AuthModule,
|
||||
BrowserAnimationsModule,
|
||||
FlexModule,
|
||||
FlexModule
|
||||
],
|
||||
providers: [ ],
|
||||
bootstrap: [ AppComponent ]
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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() { }
|
||||
}
|
|
@ -1,19 +1,49 @@
|
|||
<div fxLayout="column" fxLayoutAlign="center center">
|
||||
<div align="center">
|
||||
<img src="../../assets/mycroft-ai-no-logo.svg"/>
|
||||
</div>
|
||||
<div class="login-options">
|
||||
<mat-tab-group>
|
||||
<mat-tab label="LOG IN">
|
||||
<login-auth-social></login-auth-social>
|
||||
<div class="mat-subheading-2">OR</div>
|
||||
<login-auth-antisocial></login-auth-antisocial>
|
||||
</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 class="social">
|
||||
<button mat-button class="google-button" (click)="authenticateGoogle()">
|
||||
<img src="../../../assets/google-logo.svg">
|
||||
Log in with Google
|
||||
</button>
|
||||
<button mat-button class="facebook-button" (click)="authenticateFacebook()">
|
||||
<fa-icon [icon]="facebookIcon"></fa-icon>
|
||||
Continue with Facebook
|
||||
</button>
|
||||
<button mat-button class="github-button" (click)="authenticateGithub()">
|
||||
<fa-icon [icon]="githubIcon"></fa-icon>
|
||||
Log in with GitHub
|
||||
</button>
|
||||
</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>
|
||||
|
|
|
@ -1,13 +1,70 @@
|
|||
@import '../../stylesheets/global';
|
||||
|
||||
mat-tab-group {
|
||||
height: 485px;
|
||||
width: 320px;
|
||||
button {
|
||||
@include login-button;
|
||||
}
|
||||
|
||||
.login-options {
|
||||
border-radius: 10px;
|
||||
.social {
|
||||
padding: 20px;
|
||||
button {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
fa-icon {
|
||||
margin-right: 15px;
|
||||
font-size: 28px;
|
||||
}
|
||||
.facebook-button {
|
||||
background-color: #3b5998;
|
||||
padding-left: 5px;
|
||||
}
|
||||
.github-button {
|
||||
background-color: #333333;
|
||||
margin-right: 12px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
.google-button {
|
||||
background-color: #4285F4;
|
||||
padding-left: 1px;
|
||||
img {
|
||||
margin-right: 10px;
|
||||
width: 14%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
@include login-button;
|
||||
}
|
||||
|
||||
form {
|
||||
background-color: $mycroft-white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
fa-icon {
|
||||
color: $mycroft-dark-grey;
|
||||
margin-right: 15px;
|
||||
}
|
||||
mat-checkbox {
|
||||
color: $mycroft-dark-grey;
|
||||
}
|
||||
.forgot-password {
|
||||
margin-left: 30px;
|
||||
}
|
||||
button {
|
||||
background-color: $mycroft-primary;
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
button:hover {
|
||||
background-color: $mycroft-tertiary-green;
|
||||
color: $mycroft-secondary;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.mat-body-2 {
|
||||
color: $mycroft-tertiary-red;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.mat-subheading-2 {
|
||||
|
@ -16,9 +73,3 @@ mat-tab-group {
|
|||
margin-top: -15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-bottom: 50px;
|
||||
margin-top: 50px;
|
||||
width: 600px;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,55 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { faFacebook, faGithub } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faLock, faUser } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { AuthResponse, AuthService } from "./auth.service";
|
||||
|
||||
@Component({
|
||||
selector: 'login-authenticate',
|
||||
templateUrl: './auth.component.html',
|
||||
styleUrls: ['./auth.component.scss']
|
||||
})
|
||||
export class AuthComponent implements OnInit {
|
||||
public facebookIcon = faFacebook;
|
||||
public githubIcon = faGithub;
|
||||
public authFailed: boolean;
|
||||
public password: string;
|
||||
public passwordIcon = faLock;
|
||||
public username: string;
|
||||
public usernameIcon = faUser;
|
||||
|
||||
constructor() { }
|
||||
constructor(private authService: AuthService) { }
|
||||
|
||||
ngOnInit() { }
|
||||
|
||||
authenticateFacebook(): void {
|
||||
this.authService.authenticateWithFacebook()
|
||||
}
|
||||
|
||||
authenticateGithub(): void {
|
||||
this.authService.authenticateWithGithub();
|
||||
}
|
||||
|
||||
authenticateGoogle(): void {
|
||||
this.authService.authenticateWithGoogle();
|
||||
}
|
||||
authorizeUser(): void {
|
||||
this.authService.authorizeAntisocial(this.username, this.password).subscribe(
|
||||
(response) => {this.onAuthSuccess(response)},
|
||||
(response) => {this.onAuthFailure(response)}
|
||||
);
|
||||
}
|
||||
|
||||
onAuthSuccess(authResponse: AuthResponse) {
|
||||
this.authFailed = false;
|
||||
this.authService.generateTokenCookies(authResponse);
|
||||
window.history.back();
|
||||
}
|
||||
|
||||
onAuthFailure(authorizeUserResponse) {
|
||||
if (authorizeUserResponse.status === 401) {
|
||||
this.authFailed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,18 +9,16 @@ import {
|
|||
MatDividerModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatTabsModule
|
||||
MatSnackBarModule
|
||||
} from "@angular/material";
|
||||
|
||||
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
|
||||
|
||||
import { AuthComponent } from './auth.component';
|
||||
import { AuthService } from "./auth.service";
|
||||
import { AuthSocialComponent } from './auth-social/auth-social.component';
|
||||
import { AuthAntisocialComponent } from './auth-antisocial/auth-antisocial.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ AuthComponent, AuthSocialComponent, AuthAntisocialComponent ],
|
||||
declarations: [ AuthComponent ],
|
||||
exports: [ AuthComponent ],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -33,7 +31,7 @@ import { AuthAntisocialComponent } from './auth-antisocial/auth-antisocial.compo
|
|||
MatDividerModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatTabsModule
|
||||
MatSnackBarModule
|
||||
],
|
||||
providers: [ AuthService ]
|
||||
})
|
||||
|
|
|
@ -2,19 +2,33 @@ import { Injectable } from '@angular/core';
|
|||
import { HttpClient, HttpHeaders} from "@angular/common/http";
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { isArray } from "util";
|
||||
|
||||
export class AuthResponse {
|
||||
import { MatSnackBar } from "@angular/material";
|
||||
|
||||
export interface AuthResponse {
|
||||
expiration: number;
|
||||
seleneToken: string;
|
||||
tartarusToken: string;
|
||||
}
|
||||
|
||||
export interface SocialLoginData {
|
||||
uuid: string;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiration: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private antisocialAuthUrl = '/api/auth/antisocial';
|
||||
private facebookAuthUrl = '/api/auth/facebook';
|
||||
private antisocialAuthUrl = '/api/antisocial';
|
||||
private facebookAuthUrl = '/api/social/facebook';
|
||||
private githubAuthUrl = '/api/social/github';
|
||||
private googleAuthUrl = '/api/social/google';
|
||||
private generateTokensUrl = 'api/social/tokens';
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
constructor(private http: HttpClient, public loginSnackbar: MatSnackBar) {
|
||||
}
|
||||
|
||||
authorizeAntisocial (username, password): Observable<AuthResponse> {
|
||||
let rawCredentials = `${username}:${password}`;
|
||||
|
@ -25,8 +39,71 @@ export class AuthService {
|
|||
return this.http.get<AuthResponse>(this.antisocialAuthUrl, {headers: httpHeaders})
|
||||
}
|
||||
|
||||
authorizeFacebook(userData: any) {
|
||||
const httpHeaders = new HttpHeaders({'token': userData.token});
|
||||
return this.http.get<AuthResponse>(this.facebookAuthUrl, {headers: httpHeaders})
|
||||
authenticateWithFacebook() {
|
||||
window.open(this.facebookAuthUrl);
|
||||
window.onmessage = (event) => {this.generateSocialLoginTokens(event)};
|
||||
}
|
||||
|
||||
authenticateWithGithub() {
|
||||
window.open(this.githubAuthUrl);
|
||||
window.onmessage = (event) => {this.generateSocialLoginTokens(event)};
|
||||
}
|
||||
|
||||
authenticateWithGoogle() {
|
||||
window.open(this.googleAuthUrl);
|
||||
window.onmessage = (event) => {this.generateSocialLoginTokens(event)};
|
||||
}
|
||||
|
||||
generateSocialLoginTokens(event: any) {
|
||||
let socialLoginData = this.parseUriParams(event.data);
|
||||
if (socialLoginData) {
|
||||
this.http.post<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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# The selene-shared parent image contains all the common Docker configs for
|
||||
# all Selene apps and services see the "shared" directory in this repository.
|
||||
FROM selene-shared:latest
|
||||
FROM docker.mycroft.ai/selene-shared:latest
|
||||
LABEL description="Run the API for the Mycroft marketplace"
|
||||
|
||||
# Use pipenv to install the package's dependencies in the container
|
||||
|
|
|
@ -40,10 +40,11 @@
|
|||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
|
||||
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"version": "==6.7"
|
||||
"markers": "python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.1.*'",
|
||||
"version": "==7.0"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
|
@ -82,11 +83,11 @@
|
|||
},
|
||||
"markdown": {
|
||||
"hashes": [
|
||||
"sha256:9ba587db9daee7ec761cfc656272be6aabe2ed300fece21208e4aab2e457bc8f",
|
||||
"sha256:a856869c7ff079ad84a3e19cd87a64998350c2b94e9e08e44270faef33400f81"
|
||||
"sha256:c00429bd503a47ec88d5e30a751e147dcb4c6889663cd3e2ba0afe858e009baa",
|
||||
"sha256:d02e0f9b04c500cde6637c11ad7c72671f359b87b9fe924b2383649d8841db7c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.6.11"
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
|
|
|
@ -3,10 +3,10 @@ from flask_restful import Api
|
|||
|
||||
from .config import get_config_location
|
||||
from market_api.endpoints import (
|
||||
SkillSummaryView,
|
||||
SkillDetailView,
|
||||
SkillInstallView,
|
||||
UserView
|
||||
SkillSummaryEndpoint,
|
||||
SkillDetailEndpoint,
|
||||
SkillInstallEndpoint,
|
||||
UserEndpoint
|
||||
)
|
||||
|
||||
|
||||
|
@ -14,7 +14,7 @@ marketplace = Flask(__name__)
|
|||
marketplace.config.from_object(get_config_location())
|
||||
|
||||
marketplace_api = Api(marketplace)
|
||||
marketplace_api.add_resource(SkillSummaryView, '/api/skills')
|
||||
marketplace_api.add_resource(SkillDetailView, '/api/skill/<skill_id>')
|
||||
marketplace_api.add_resource(SkillInstallView, '/api/install-skill')
|
||||
marketplace_api.add_resource(UserView, '/api/user')
|
||||
marketplace_api.add_resource(SkillSummaryEndpoint, '/api/skills')
|
||||
marketplace_api.add_resource(SkillDetailEndpoint, '/api/skill/<skill_id>')
|
||||
marketplace_api.add_resource(SkillInstallEndpoint, '/api/install')
|
||||
marketplace_api.add_resource(UserEndpoint, '/api/user')
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from .skill_detail import SkillDetailView
|
||||
from .skill_install import SkillInstallView
|
||||
from .skill_summary import SkillSummaryView
|
||||
from .user import UserView
|
||||
from .skill_detail import SkillDetailEndpoint
|
||||
from .skill_install import SkillInstallEndpoint
|
||||
from .skill_summary import SkillSummaryEndpoint
|
||||
from .user import UserEndpoint
|
||||
|
|
|
@ -1,37 +1,47 @@
|
|||
"""View to return detailed information about a skill"""
|
||||
from http import HTTPStatus
|
||||
|
||||
from markdown import markdown
|
||||
import requests as service_request
|
||||
|
||||
from selene_util.api import SeleneBaseView, AuthorizationError
|
||||
from selene_util.api import SeleneEndpoint, APIError
|
||||
|
||||
|
||||
class SkillDetailView(SeleneBaseView):
|
||||
class SkillDetailEndpoint(SeleneEndpoint):
|
||||
authentication_required = False
|
||||
|
||||
def __init__(self):
|
||||
super(SkillDetailView, self).__init__()
|
||||
super(SkillDetailEndpoint, self).__init__()
|
||||
self.skill_id = None
|
||||
self.response_skill = None
|
||||
|
||||
def get(self, skill_id):
|
||||
"""Handle and HTTP GET request."""
|
||||
self.skill_id = skill_id
|
||||
try:
|
||||
self._authenticate()
|
||||
except AuthorizationError:
|
||||
self._get_skill_details()
|
||||
except APIError:
|
||||
pass
|
||||
self._build_response()
|
||||
else:
|
||||
self._build_response_data()
|
||||
self.response = (self.response_skill, HTTPStatus.OK)
|
||||
|
||||
return self.response
|
||||
|
||||
def _build_response_data(self):
|
||||
def _get_skill_details(self):
|
||||
"""Build the data to include in the response."""
|
||||
self.service_response = service_request.get(
|
||||
self.base_url + '/skill/id/' + self.skill_id
|
||||
skill_service_response = service_request.get(
|
||||
self.config['SELENE_BASE_URL'] + '/skill/id/' + self.skill_id
|
||||
)
|
||||
self.response_data = self.service_response.json()
|
||||
self.response_data['description'] = markdown(
|
||||
self.response_data['description'],
|
||||
self._check_for_service_errors(skill_service_response)
|
||||
self.response_skill = skill_service_response.json()
|
||||
|
||||
def _build_response_data(self):
|
||||
self.response_skill['description'] = markdown(
|
||||
self.response_skill['description'],
|
||||
output_format='html5'
|
||||
)
|
||||
self.response_data['summary'] = markdown(
|
||||
self.response_data['summary'],
|
||||
self.response_skill['summary'] = markdown(
|
||||
self.response_skill['summary'],
|
||||
output_format='html5'
|
||||
)
|
|
@ -2,94 +2,67 @@ 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
|
||||
from selene_util.api import SeleneEndpoint, APIError
|
||||
|
||||
_log = getLogger(__package__)
|
||||
|
||||
|
||||
class ServiceUrlNotFound(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ServiceServerError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SkillInstallView(Resource):
|
||||
class SkillInstallEndpoint(SeleneEndpoint):
|
||||
"""
|
||||
Install a skill on user device(s).
|
||||
"""
|
||||
def __init__(self):
|
||||
self.service_response = None
|
||||
self.frontend_response = None
|
||||
self.frontend_response_status_code = HTTPStatus.OK
|
||||
self.user_uuid: str = None
|
||||
self.tartarus_token: str = None
|
||||
self.selene_token: str = None
|
||||
self.device_uuid = None
|
||||
self.installer_skill_settings = []
|
||||
super(SkillInstallEndpoint, self).__init__()
|
||||
self.device_uuid: str = None
|
||||
self.installer_skill_settings: list = []
|
||||
self.installer_update_response = None
|
||||
|
||||
def put(self):
|
||||
try:
|
||||
self._get_auth_tokens()
|
||||
self._validate_auth_token()
|
||||
self._install_skill()
|
||||
except AuthorizationError as ae:
|
||||
self._build_unauthorized_response(str(ae))
|
||||
except ServiceUrlNotFound as nf:
|
||||
self._build_server_error_response(str(nf))
|
||||
except ServiceServerError as se:
|
||||
self._build_server_error_response(str(se))
|
||||
self._authenticate()
|
||||
self._get_installer_skill()
|
||||
self._apply_update()
|
||||
except APIError:
|
||||
pass
|
||||
else:
|
||||
self._build_frontend_response()
|
||||
self.response = (self.installer_update_response, HTTPStatus.OK)
|
||||
|
||||
return self.frontend_response
|
||||
return self.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 _install_skill(self):
|
||||
self._get_users_installer_skill_settings()
|
||||
installer_skill = self._find_installer_skill()
|
||||
def _get_installer_skill(self):
|
||||
installed_skills = self._get_installed_skills()
|
||||
installer_skill = self._find_installer_skill(installed_skills)
|
||||
self._find_installer_settings(installer_skill)
|
||||
self._update_skill_installer_settings()
|
||||
|
||||
def _get_users_installer_skill_settings(self):
|
||||
def _get_installed_skills(self):
|
||||
service_request_headers = {
|
||||
'Authorization': 'Bearer ' + self.tartarus_token
|
||||
}
|
||||
service_url = (
|
||||
current_app.config['TARTARUS_BASE_URL'] +
|
||||
self.config['TARTARUS_BASE_URL'] +
|
||||
'/user/' +
|
||||
self.user_uuid +
|
||||
'/skill'
|
||||
)
|
||||
self.service_response = requests.get(
|
||||
user_service_response = requests.get(
|
||||
service_url,
|
||||
headers=service_request_headers
|
||||
)
|
||||
self.check_for_tartarus_errors(service_url)
|
||||
if user_service_response.status_code != HTTPStatus.OK:
|
||||
self._check_for_service_errors(user_service_response)
|
||||
if user_service_response.status_code == HTTPStatus.UNAUTHORIZED:
|
||||
# override response built in _build_service_error_response()
|
||||
# so that user knows there is a authentication issue
|
||||
self.response = (self.response[0], HTTPStatus.UNAUTHORIZED)
|
||||
raise APIError()
|
||||
|
||||
def _find_installer_skill(self):
|
||||
service_response_data = json.loads(self.service_response.content)
|
||||
return json.loads(user_service_response.content)
|
||||
|
||||
def _find_installer_skill(self, installed_skills):
|
||||
installer_skill = None
|
||||
for skill in service_response_data['skills']:
|
||||
for skill in installed_skills['skills']:
|
||||
if skill['skill']['name'] == 'Installer':
|
||||
self.device_uuid = skill['deviceUuid']
|
||||
installer_skill = skill['skill']
|
||||
|
@ -103,23 +76,30 @@ class SkillInstallView(Resource):
|
|||
if setting['type'] != 'label':
|
||||
self.installer_skill_settings.append(setting)
|
||||
|
||||
def _update_skill_installer_settings(self):
|
||||
service_url = current_app.config['TARTARUS_BASE_URL'] + '/skill/field'
|
||||
def _apply_update(self):
|
||||
service_url = self.config['TARTARUS_BASE_URL'] + '/skill/field'
|
||||
service_request_headers = {
|
||||
'Authorization': 'Bearer ' + self.tartarus_token
|
||||
'Authorization': 'Bearer ' + self.tartarus_token,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
self.service_response = requests.patch(
|
||||
service_request_data = json.dumps(self._build_update_request_body())
|
||||
skill_service_response = requests.patch(
|
||||
service_url,
|
||||
data=json.dumps(self._build_install_request_body()),
|
||||
data=service_request_data,
|
||||
headers=service_request_headers
|
||||
)
|
||||
self.check_for_tartarus_errors(service_url)
|
||||
if skill_service_response.status_code != HTTPStatus.OK:
|
||||
self._check_for_service_errors(skill_service_response)
|
||||
|
||||
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 = []
|
||||
for setting in self.installer_skill_settings:
|
||||
if setting['name'] == 'installer_link':
|
||||
setting_value = 'foo'
|
||||
setting_value = self.request.json['skill_url']
|
||||
elif setting['name'] == 'auto_install':
|
||||
setting_value = True
|
||||
else:
|
||||
|
@ -130,53 +110,10 @@ class SkillInstallView(Resource):
|
|||
raise ValueError(error_message.format(setting['name']))
|
||||
install_request_body.append(
|
||||
dict(
|
||||
fieldUiud=setting['uuid'],
|
||||
deviceUuid=self.device_uuid, value=setting_value
|
||||
fieldUuid=setting['uuid'],
|
||||
deviceUuid=self.device_uuid,
|
||||
value=setting_value
|
||||
)
|
||||
)
|
||||
|
||||
return dict(batch=install_request_body)
|
||||
|
||||
def check_for_tartarus_errors(self, service_url):
|
||||
if self.service_response.status_code == HTTPStatus.UNAUTHORIZED:
|
||||
error_message = 'invalid Tartarus token'
|
||||
_log.error(error_message)
|
||||
raise AuthorizationError(error_message)
|
||||
elif self.service_response.status_code == HTTPStatus.NOT_FOUND:
|
||||
error_message = 'service url {} not found'.format(service_url)
|
||||
_log.error(error_message)
|
||||
raise ServiceUrlNotFound(error_message)
|
||||
elif self.service_response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR:
|
||||
error_message = (
|
||||
'error occurred during GET request to URL ' + service_url
|
||||
)
|
||||
_log.error(error_message)
|
||||
raise ServiceServerError(error_message)
|
||||
|
||||
def _build_unauthorized_response(self, error_message):
|
||||
self.frontend_response = (
|
||||
dict(errorMessage=error_message),
|
||||
HTTPStatus.UNAUTHORIZED
|
||||
)
|
||||
|
||||
def _build_server_error_response(self, error_message):
|
||||
self.frontend_response = (
|
||||
dict(errorMessage=error_message),
|
||||
HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
def _build_frontend_response(self):
|
||||
if self.service_response.status_code == HTTPStatus.OK:
|
||||
service_response_data = json.loads(self.service_response.content)
|
||||
self.frontend_response = (
|
||||
dict(name=service_response_data.get('name')),
|
||||
HTTPStatus.OK
|
||||
)
|
||||
elif self.service_response.status_code == HTTPStatus.NOT_FOUND:
|
||||
error_message = 'service url {} not found'
|
||||
_log.error(error_message)
|
||||
self.frontend_response = (
|
||||
dict(error_message=error_message),
|
||||
HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
)
|
||||
else:
|
||||
self.frontend_response = ({}, self.service_response.status_code)
|
||||
|
|
|
@ -1,45 +1,118 @@
|
|||
"""Endpoint to provide skill summary data to the marketplace."""
|
||||
from collections import defaultdict
|
||||
from http import HTTPStatus
|
||||
from logging import getLogger
|
||||
|
||||
from flask import request
|
||||
from markdown import markdown
|
||||
import requests as service_request
|
||||
|
||||
from selene_util.api import SeleneBaseView, AuthorizationError
|
||||
from selene_util.api import SeleneEndpoint, APIError
|
||||
|
||||
UNDEFINED = 'Undefined'
|
||||
UNDEFINED = 'Not Categorized'
|
||||
|
||||
_log = getLogger(__package__)
|
||||
|
||||
|
||||
class SkillSummaryView(SeleneBaseView):
|
||||
class SkillSummaryEndpoint(SeleneEndpoint):
|
||||
authentication_required = False
|
||||
|
||||
def __init__(self):
|
||||
super(SkillSummaryView, self).__init__()
|
||||
self.response_data = defaultdict(list)
|
||||
self.search_term = None
|
||||
super(SkillSummaryEndpoint, self).__init__()
|
||||
self.available_skills: list = []
|
||||
self.installed_skills: list = []
|
||||
self.response_skills = defaultdict(list)
|
||||
|
||||
def get(self):
|
||||
"""Handle a HTTP GET request."""
|
||||
try:
|
||||
self._authenticate()
|
||||
except AuthorizationError:
|
||||
self._get_skills()
|
||||
except APIError:
|
||||
pass
|
||||
self._build_response()
|
||||
else:
|
||||
self._build_response_data()
|
||||
self.response = (self.response_skills, HTTPStatus.OK)
|
||||
|
||||
return self.response
|
||||
|
||||
def _get_skills(self):
|
||||
self._get_available_skills()
|
||||
self._get_installed_skills()
|
||||
|
||||
def _get_available_skills(self):
|
||||
skill_service_response = service_request.get(
|
||||
self.config['SELENE_BASE_URL'] + '/skill/all'
|
||||
)
|
||||
if skill_service_response.status_code != HTTPStatus.OK:
|
||||
self._check_for_service_errors(skill_service_response)
|
||||
self.available_skills = skill_service_response.json()
|
||||
|
||||
# TODO: this is a temporary measure until skill IDs can be assigned
|
||||
# the list of installed skills returned by Tartarus are keyed by a value
|
||||
# that is not guaranteed to be the same as the skill title in the skill
|
||||
# metadata. a skill ID needs to be defined and propagated.
|
||||
def _get_installed_skills(self):
|
||||
"""Get the skills a user has already installed on their device(s)
|
||||
|
||||
Installed skills will be marked as such in the marketplace so a user
|
||||
knows it is already installed.
|
||||
"""
|
||||
if self.authenticated:
|
||||
service_request_headers = {
|
||||
'Authorization': 'Bearer ' + self.tartarus_token
|
||||
}
|
||||
service_url = (
|
||||
self.config['TARTARUS_BASE_URL'] +
|
||||
'/user/' +
|
||||
self.user_uuid +
|
||||
'/skill'
|
||||
)
|
||||
user_service_response = service_request.get(
|
||||
service_url,
|
||||
headers=service_request_headers
|
||||
)
|
||||
if user_service_response.status_code != HTTPStatus.OK:
|
||||
self._check_for_service_errors(user_service_response)
|
||||
|
||||
response_skills = user_service_response.json()
|
||||
for skill in response_skills.get('skills', []):
|
||||
self.installed_skills.append(skill['skill']['name'])
|
||||
|
||||
def _build_response_data(self):
|
||||
"""Build the data to include in the response."""
|
||||
self.skill_service_response = service_request.get(
|
||||
self.base_url + '/skill/all'
|
||||
)
|
||||
if request.query_string:
|
||||
query_string = request.query_string.decode()
|
||||
self.search_term = query_string.lower().split('=')[1]
|
||||
self._reformat_skills()
|
||||
if self.request.query_string:
|
||||
skills_to_include = self._filter_skills()
|
||||
else:
|
||||
skills_to_include = self.available_skills
|
||||
self._reformat_skills(skills_to_include)
|
||||
self._sort_skills()
|
||||
|
||||
def _reformat_skills(self):
|
||||
def _filter_skills(self) -> list:
|
||||
skills_to_include = []
|
||||
|
||||
query_string = self.request.query_string.decode()
|
||||
search_term = query_string.lower().split('=')[1]
|
||||
for skill in self.available_skills:
|
||||
search_term_match = (
|
||||
search_term is None or
|
||||
search_term in skill['title'].lower() or
|
||||
search_term in skill['description'].lower() or
|
||||
search_term in skill['summary'].lower()
|
||||
)
|
||||
if skill['categories'] and not search_term_match:
|
||||
search_term_match = (
|
||||
search_term in skill['categories'][0].lower()
|
||||
)
|
||||
for trigger in skill['triggers']:
|
||||
if search_term in trigger.lower():
|
||||
search_term_match = True
|
||||
if search_term_match:
|
||||
skills_to_include.append(skill)
|
||||
|
||||
return skills_to_include
|
||||
|
||||
def _reformat_skills(self, skills_to_include: list):
|
||||
"""Build the response data from the skill service response"""
|
||||
for skill in self.skill_service_response.json():
|
||||
for skill in skills_to_include:
|
||||
if not skill['icon']:
|
||||
skill['icon'] = dict(icon='comment-alt', color='#6C7A89')
|
||||
skill_summary = dict(
|
||||
|
@ -47,28 +120,25 @@ class SkillSummaryView(SeleneBaseView):
|
|||
icon=skill['icon'],
|
||||
icon_image=skill.get('icon_image'),
|
||||
id=skill['id'],
|
||||
# TODO remove skill_name when login/install is implemented
|
||||
skill_name=skill['skill_name'],
|
||||
installed=skill['title'] in self.installed_skills,
|
||||
repository_url=skill['repository_url'],
|
||||
summary=markdown(skill['summary'], output_format='html5'),
|
||||
title=skill['title'],
|
||||
triggers=skill['triggers']
|
||||
)
|
||||
search_term_match = (
|
||||
self.search_term is None or
|
||||
self.search_term in skill['title'].lower()
|
||||
)
|
||||
if search_term_match:
|
||||
if 'system' in skill['tags']:
|
||||
skill_category = 'System'
|
||||
elif skill['categories']:
|
||||
# a skill may have many categories. the first one in the
|
||||
# list is considered the "primary" category. This is the
|
||||
# category the marketplace will use to group the skill.
|
||||
if skill['categories']:
|
||||
skill_category = skill['categories'][0]
|
||||
else:
|
||||
skill_category = UNDEFINED
|
||||
self.response_data[skill_category].append(skill_summary)
|
||||
skill_category = skill['categories'][0]
|
||||
else:
|
||||
skill_category = UNDEFINED
|
||||
self.response_skills[skill_category].append(skill_summary)
|
||||
|
||||
def _sort_skills(self):
|
||||
"""Sort the skills in alphabetical order"""
|
||||
for skill_category, skills in self.response_data.items():
|
||||
for skill_category, skills in self.response_skills.items():
|
||||
sorted_skills = sorted(skills, key=lambda skill: skill['title'])
|
||||
self.response_data[skill_category] = sorted_skills
|
||||
self.response_skills[skill_category] = sorted_skills
|
||||
|
|
|
@ -1,55 +1,45 @@
|
|||
"""API endpoint to return the user's name to the marketplace"""
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
|
||||
from flask import request, current_app
|
||||
from flask_restful import Resource
|
||||
import requests
|
||||
|
||||
from selene_util.auth import decode_auth_token
|
||||
from selene_util.api import SeleneEndpoint, APIError
|
||||
|
||||
|
||||
class UserView(Resource):
|
||||
"""
|
||||
User Login Resource
|
||||
"""
|
||||
class UserEndpoint(SeleneEndpoint):
|
||||
"""Retrieve information about the user based on their UUID"""
|
||||
def __init__(self):
|
||||
self.service_response = None
|
||||
super(UserEndpoint, self).__init__()
|
||||
self.user = None
|
||||
self.frontend_response = None
|
||||
|
||||
def get(self):
|
||||
self._get_user_from_service()
|
||||
self._build_frontend_response()
|
||||
try:
|
||||
self._authenticate()
|
||||
self._get_user()
|
||||
except APIError:
|
||||
pass
|
||||
else:
|
||||
self._build_response()
|
||||
|
||||
return self.frontend_response
|
||||
return self.response
|
||||
|
||||
def _get_user_from_service(self):
|
||||
selene_token = request.cookies.get('seleneToken')
|
||||
user_uuid = decode_auth_token(
|
||||
selene_token,
|
||||
current_app.config['SECRET_KEY']
|
||||
)
|
||||
tartarus_token = request.cookies.get('tartarusToken')
|
||||
service_request_headers = {'Authorization': 'Bearer ' + tartarus_token}
|
||||
def _get_user(self):
|
||||
service_request_headers = {
|
||||
'Authorization': 'Bearer ' + self.tartarus_token
|
||||
}
|
||||
service_url = (
|
||||
current_app.config['TARTARUS_BASE_URL'] +
|
||||
self.config['TARTARUS_BASE_URL'] +
|
||||
'/user/' +
|
||||
user_uuid
|
||||
self.user_uuid
|
||||
)
|
||||
self.service_response = requests.get(
|
||||
user_service_response = requests.get(
|
||||
service_url,
|
||||
headers=service_request_headers
|
||||
)
|
||||
self._check_for_service_errors(user_service_response)
|
||||
self.user = user_service_response.json()
|
||||
|
||||
def _build_frontend_response(self):
|
||||
if self.service_response.status_code == HTTPStatus.OK:
|
||||
service_response_data = json.loads(self.service_response.content)
|
||||
frontend_response_data = dict(
|
||||
name=service_response_data.get('name')
|
||||
)
|
||||
else:
|
||||
frontend_response_data = {}
|
||||
self.frontend_response = (
|
||||
frontend_response_data,
|
||||
self.service_response.status_code
|
||||
)
|
||||
def _build_response(self):
|
||||
response_data = dict(name=self.user['name'])
|
||||
self.response = (response_data, HTTPStatus.OK)
|
||||
|
|
|
@ -338,9 +338,9 @@
|
|||
}
|
||||
},
|
||||
"@angular/flex-layout": {
|
||||
"version": "6.0.0-beta.17",
|
||||
"resolved": "https://registry.npmjs.org/@angular/flex-layout/-/flex-layout-6.0.0-beta.17.tgz",
|
||||
"integrity": "sha512-WrCWlE7NuvvxbeO8+S6aR5cvzX+1CVzpIy0izP8kMLWjAPZ0xjePHc2kJKJVapWMt7aniYZ1inl+GpsvkllycA==",
|
||||
"version": "6.0.0-beta.16",
|
||||
"resolved": "https://registry.npmjs.org/@angular/flex-layout/-/flex-layout-6.0.0-beta.16.tgz",
|
||||
"integrity": "sha512-0AYtIBGrEJshdFMc6TXGloCkD19YTCRKVJl6xZHX4H5dLnUn+daqXcbh4UsWhayevnLp85HEf2ViHLmTa6jv3g==",
|
||||
"requires": {
|
||||
"tslib": "^1.7.1"
|
||||
}
|
||||
|
@ -8334,9 +8334,9 @@
|
|||
}
|
||||
},
|
||||
"rxjs": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.1.tgz",
|
||||
"integrity": "sha512-OwMxHxmnmHTUpgO+V7dZChf3Tixf4ih95cmXjzzadULziVl/FKhHScGLj4goEw9weePVOH2Q0+GcCBUhKCZc/g==",
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.2.tgz",
|
||||
"integrity": "sha512-0MI8+mkKAXZUF9vMrEoPnaoHkfzBPP4IGwUYRJhIRJF6/w3uByO1e91bEHn8zd43RdkTMKiooYKmwz7RH6zfOQ==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.0"
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --open",
|
||||
"start": "ng serve --open --proxy-config proxy.config.json",
|
||||
"build-dev": "ng build --configuration=development",
|
||||
"build-test": "ng build --configuration=test",
|
||||
"build-prod": "ng build --prod",
|
||||
|
@ -18,7 +18,7 @@
|
|||
"@angular/common": "^6.0.3",
|
||||
"@angular/compiler": "^6.0.3",
|
||||
"@angular/core": "^6.0.3",
|
||||
"@angular/flex-layout": "^6.0.0-beta.16",
|
||||
"@angular/flex-layout": "6.0.0-beta.16",
|
||||
"@angular/forms": "^6.0.3",
|
||||
"@angular/http": "^6.0.3",
|
||||
"@angular/material": "^6.3.2",
|
||||
|
@ -32,7 +32,7 @@
|
|||
"angular-in-memory-web-api": "^0.6.0",
|
||||
"core-js": "^2.5.4",
|
||||
"font-awesome": "^4.7.0",
|
||||
"rxjs": "^6.0.0",
|
||||
"rxjs": "6.2.2",
|
||||
"zone.js": "^0.8.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"/api/*": {
|
||||
"target": "http://localhost:5002",
|
||||
"secure": false,
|
||||
"logLevel": "debug",
|
||||
"changeOrigin": true
|
||||
}
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { LoginComponent } from "./header/login/login.component";
|
||||
import { PageNotFoundComponent } from "./page-not-found/page-not-found.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: 'login', component: LoginComponent},
|
||||
{ path: '', redirectTo: '/skills', pathMatch: 'full' },
|
||||
{ path: '**', component: PageNotFoundComponent }
|
||||
];
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
@import '../stylesheets/global';
|
||||
|
||||
.app-body {
|
||||
margin: 50px;
|
||||
margin-left: 3vw;
|
||||
margin-right: 3vw;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
|
|
@ -1,26 +1,22 @@
|
|||
<mat-toolbar>
|
||||
<img src="../../assets/header-logo.png">
|
||||
<mat-divider [vertical]="true"></mat-divider>
|
||||
<img src="../../assets/header-logo.svg">
|
||||
<fa-icon class='separator' [icon]="separatorIcon"></fa-icon>
|
||||
<div class="mat-subheading-1" style="margin-bottom: 0">MARKETPLACE</div>
|
||||
|
||||
<div fxFlex fxLayout="row" fxLayoutAlign="end center">
|
||||
<div class="mat-subheading-1" style="margin-bottom: 0">PREVIEW</div>
|
||||
|
||||
<!--commenting out the below code temporarily until the login API is ready-->
|
||||
|
||||
<!--<button mat-button (click)="login()" *ngIf="!isLoggedIn">-->
|
||||
<!--<fa-icon [icon]="signInIcon"></fa-icon>-->
|
||||
<!--LOG IN-->
|
||||
<!--</button>-->
|
||||
<!--<button mat-button class="menu-button" [matMenuTriggerFor]="menu" *ngIf="isLoggedIn">-->
|
||||
<!--{{userMenuButtonText}}-->
|
||||
<!--<fa-icon [icon]="menuButtonIcon"></fa-icon>-->
|
||||
<!--</button>-->
|
||||
<!--<mat-menu [overlapTrigger]="false" #menu="matMenu">-->
|
||||
<!--<button mat-menu-item (click)="logout()">-->
|
||||
<!--<fa-icon [icon]="signOutIcon"></fa-icon>-->
|
||||
<!--Logout-->
|
||||
<!--</button>-->
|
||||
<!--</mat-menu>-->
|
||||
<button mat-button (click)="login()" *ngIf="!isLoggedIn">
|
||||
<fa-icon [icon]="signInIcon"></fa-icon>
|
||||
LOG IN
|
||||
</button>
|
||||
<button mat-button class="menu-button" [matMenuTriggerFor]="menu" *ngIf="isLoggedIn">
|
||||
{{userMenuButtonText}}
|
||||
<fa-icon [icon]="menuButtonIcon"></fa-icon>
|
||||
</button>
|
||||
<mat-menu [overlapTrigger]="false" #menu="matMenu">
|
||||
<button mat-menu-item (click)="logout()">
|
||||
<fa-icon [icon]="signOutIcon"></fa-icon>
|
||||
Logout
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</mat-toolbar>
|
|
@ -4,16 +4,16 @@ mat-toolbar {
|
|||
background-color: $mycroft-primary;
|
||||
color: $mycroft-white;
|
||||
img {
|
||||
padding-right: 10px;
|
||||
height: 40px;
|
||||
height: 20px;
|
||||
margin-top: -7px;
|
||||
}
|
||||
mat-divider {
|
||||
border-color: #FFFFFF;
|
||||
border-top-width: 0;
|
||||
border-right-width: 1px;
|
||||
border-right-style: solid;
|
||||
height: 60%;
|
||||
margin-right: 10px;
|
||||
.separator {
|
||||
font-size: 5px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.mat-subheading-1 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
fa-icon {
|
||||
padding-right: 5px;
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Subscription } from "rxjs/internal/Subscription";
|
||||
|
||||
import { faCaretDown, faSignInAlt, faSignOutAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faCaretDown,
|
||||
faCircle,
|
||||
faSignInAlt,
|
||||
faSignOutAlt
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { LoginService } from "../shared/login.service";
|
||||
|
||||
|
@ -13,6 +18,7 @@ import { LoginService } from "../shared/login.service";
|
|||
export class HeaderComponent implements OnInit, OnDestroy {
|
||||
public isLoggedIn: boolean;
|
||||
private loginStatus: Subscription;
|
||||
public separatorIcon = faCircle;
|
||||
public signInIcon = faSignInAlt;
|
||||
public signOutIcon = faSignOutAlt;
|
||||
public menuButtonIcon = faCaretDown;
|
||||
|
@ -45,14 +51,18 @@ export class HeaderComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
logout() {
|
||||
let expiration = new Date();
|
||||
let domain = document.domain.replace('market.', '');
|
||||
document.cookie = 'seleneToken=""' +
|
||||
'; expires=' + expiration.toUTCString() +
|
||||
'; domain=' + domain;
|
||||
document.cookie = 'tartarusToken=""' +
|
||||
'; expires=' + expiration.toUTCString() +
|
||||
'; domain=' + domain;
|
||||
this.loginService.setLoginStatus();
|
||||
this.loginService.logout().subscribe(
|
||||
(response) => {
|
||||
let expiration = new Date();
|
||||
let domain = document.domain.replace('market.', '');
|
||||
document.cookie = 'seleneToken=""' +
|
||||
'; expires=' + expiration.toUTCString() +
|
||||
'; domain=' + domain;
|
||||
document.cookie = 'tartarusToken=""' +
|
||||
'; expires=' + expiration.toUTCString() +
|
||||
'; domain=' + domain;
|
||||
this.loginService.setLoginStatus();
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
|
|||
|
||||
import { MaterialModule } from "../shared/material.module";
|
||||
import { HeaderComponent } from './header.component';
|
||||
import { LoginComponent } from "./login/login.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -15,7 +14,7 @@ import { LoginComponent } from "./login/login.component";
|
|||
FontAwesomeModule,
|
||||
MaterialModule
|
||||
],
|
||||
declarations: [ HeaderComponent, LoginComponent ],
|
||||
declarations: [ HeaderComponent],
|
||||
exports: [ HeaderComponent ],
|
||||
})
|
||||
export class HeaderModule { }
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<iframe [src]='loginUrl' name="login"></iframe>
|
||||
</div>
|
|
@ -1,8 +0,0 @@
|
|||
div {
|
||||
width: 100%;
|
||||
iframe {
|
||||
border: none;
|
||||
height: 1000px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import { Router } from "@angular/router";
|
|||
|
||||
import { Observable } from "rxjs/internal/Observable";
|
||||
import { Subject } from "rxjs/internal/Subject";
|
||||
import { environment } from "../../environments/environment";
|
||||
|
||||
export class User {
|
||||
name: string;
|
||||
|
@ -13,19 +14,28 @@ export class User {
|
|||
export class LoginService {
|
||||
public isLoggedIn = new Subject<boolean>();
|
||||
public redirectUrl: string;
|
||||
private logoutUrl = environment.loginUrl + '/api/logout';
|
||||
private userUrl = '/api/user';
|
||||
|
||||
constructor(private http: HttpClient, private router: Router) { }
|
||||
constructor(private http: HttpClient, private router: Router) {
|
||||
}
|
||||
|
||||
getUser(): Observable<User> {
|
||||
return this.http.get<User>(this.userUrl)
|
||||
return this.http.get<User>(this.userUrl);
|
||||
}
|
||||
|
||||
setLoginStatus() {
|
||||
this.isLoggedIn.next(document.cookie.includes('seleneToken'));
|
||||
let cookies = document.cookie,
|
||||
seleneTokenExists = cookies.includes('seleneToken'),
|
||||
seleneTokenEmpty = cookies.includes('seleneToken=""');
|
||||
this.isLoggedIn.next( seleneTokenExists && !seleneTokenEmpty);
|
||||
}
|
||||
|
||||
login() {
|
||||
this.router.navigate(['/login']);
|
||||
window.location.assign(environment.loginUrl);
|
||||
}
|
||||
|
||||
logout(): Observable<any> {
|
||||
return this.http.get(this.logoutUrl);
|
||||
}
|
||||
}
|
|
@ -1,27 +1,38 @@
|
|||
<div fxLayout="row" fxLayoutAlign="center">
|
||||
<div class="skill-detail" *ngIf="skill$ | async as skill">
|
||||
<div class="navigate-back">
|
||||
<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 -->
|
||||
<div class="skill-detail-header" fxLayout="row">
|
||||
<div class="skill-detail-header-left">
|
||||
<h1>
|
||||
<!-- there cannot be an icon and an icon_image. show the
|
||||
image if it exists otherwise show the icon -->
|
||||
<img *ngIf="skill.icon_image" src={{skill.icon_image}} height="30" width="30">
|
||||
<fa
|
||||
*ngIf="!skill.icon_image"
|
||||
[ngStyle]="{'color': skill.icon.color}"
|
||||
name={{skill.icon.icon}}
|
||||
>
|
||||
</fa>
|
||||
{{skill.title}}
|
||||
</h1>
|
||||
<div class="mat-subheading-1" [innerHTML]="skill.summary"></div>
|
||||
<!-- Left Side -->
|
||||
<div class="skill-detail-header-left" fxFlex>
|
||||
<!-- there cannot be an icon and an icon_image. show the
|
||||
image if it exists otherwise show the icon -->
|
||||
<img *ngIf="skill.icon_image" src={{skill.icon_image}} height="70" width="70">
|
||||
<fa
|
||||
*ngIf="!skill.icon_image"
|
||||
[ngStyle]="{'color': skill.icon.color}"
|
||||
name={{skill.icon.icon}}
|
||||
>
|
||||
</fa>
|
||||
<div fxFlex>
|
||||
<h1>{{skill.title}}</h1>
|
||||
<div class="mat-body-1" [innerHTML]="skill.summary"></div>
|
||||
</div>
|
||||
<div class="skill-detail-header-right">
|
||||
<button mat-button>INSTALL</button>
|
||||
</div>
|
||||
|
||||
<!-- 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
|
||||
mat-flat-button
|
||||
mat-icon-button
|
||||
class="github-button"
|
||||
(click)="navigateToGithubRepo(skill.repository_url)"
|
||||
>
|
||||
|
@ -31,55 +42,65 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail block -->
|
||||
<div class="skill-detail-body" fxLayout="row">
|
||||
<!-- Left Side -->
|
||||
<div class="skill-detail-body-left">
|
||||
<div class="skill-detail-section">
|
||||
<div class="mat-subheading-1">HEY MYCROFT</div>
|
||||
<button mat-flat-button [disabled]="true" *ngFor="let trigger of skill.triggers">
|
||||
</div>
|
||||
|
||||
<!-- Detail block -->
|
||||
<div class="skill-detail-body" fxLayout="row wrap">
|
||||
|
||||
<!-- Left Side -->
|
||||
<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>
|
||||
{{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>
|
||||
<!-- Right Side -->
|
||||
<div class="skill-detail-body-right">
|
||||
<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>
|
||||
<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 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 class="mat-body-1" *ngFor="let credit of skill.credits">
|
||||
{{credit.name}}
|
||||
</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>
|
||||
|
|
|
@ -1,36 +1,60 @@
|
|||
@import '../../../stylesheets/global';
|
||||
|
||||
.skill-detail {
|
||||
@mixin skill-detail-size {
|
||||
margin: 0 auto;
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
.navigate-back {
|
||||
@include skill-detail-size;
|
||||
color: $mycroft-dark-grey;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.skill-detail {
|
||||
@include skill-detail-size;
|
||||
box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
border-radius: 10px;
|
||||
.skill-detail-header {
|
||||
background-color: $mycroft-blue-grey;
|
||||
background-color: #f7f9fa;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
height: 150px;
|
||||
padding: 30px;
|
||||
padding-bottom: 3vh;
|
||||
padding-left: 4vw;
|
||||
padding-right: 4vw;
|
||||
padding-top: 4vh;
|
||||
.skill-detail-header-left {
|
||||
color: $mycroft-secondary;
|
||||
width: 70%;
|
||||
margin-right: 50px;
|
||||
min-width: 340px;
|
||||
fa {
|
||||
font-size: 70px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
img {
|
||||
margin-right: 20px;
|
||||
}
|
||||
h1 {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 0;
|
||||
}
|
||||
.mat-subheading-1 {
|
||||
padding-right: 30px;
|
||||
@include ellipsis-overflow
|
||||
}
|
||||
}
|
||||
.skill-detail-header-right {
|
||||
width: 30%;
|
||||
button {
|
||||
margin-right: 20px;
|
||||
.install-button {
|
||||
@include action-button;
|
||||
margin-top: 5px;
|
||||
width: 160px;
|
||||
width: 140px;
|
||||
}
|
||||
.install-button:hover {
|
||||
background-color: $mycroft-tertiary-green;
|
||||
color: $mycroft-secondary;
|
||||
}
|
||||
|
||||
.github-button {
|
||||
background-color: $mycroft-blue-grey;
|
||||
color: $mycroft-dark-grey;
|
||||
font-weight: normal;
|
||||
width: 135px;
|
||||
fa-icon {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
@ -41,27 +65,44 @@
|
|||
background-color: $mycroft-white;
|
||||
border-bottom-left-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
padding: 30px;
|
||||
margin-bottom: 50px;
|
||||
padding-bottom: 3vh;
|
||||
padding-left: 4vw;
|
||||
padding-right: 4vw;
|
||||
padding-top: 3vh;
|
||||
.mat-subheading-1 {
|
||||
color: $mycroft-dark-grey
|
||||
color: $mycroft-dark-grey;
|
||||
font-variant: small-caps;
|
||||
font-weight: 500;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.mat-body-1 {
|
||||
color: $mycroft-secondary;
|
||||
}
|
||||
.kde-icon {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
.skill-detail-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.skill-detail-body-left {
|
||||
width: 70%;
|
||||
button {
|
||||
@include skill-trigger-button;
|
||||
min-width: 340px;
|
||||
margin-right: 50px;
|
||||
.skill-trigger {
|
||||
@include skill-trigger;
|
||||
@include ellipsis-overflow;
|
||||
max-width: 100%;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
max-width: 340px;
|
||||
}
|
||||
}
|
||||
.skill-detail-body-right {
|
||||
width: 30%;
|
||||
margin-right: 20px;
|
||||
white-space: nowrap;
|
||||
img {
|
||||
padding-right: 15px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
.skill-detail-section {
|
||||
padding-bottom: 30px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ import { Router, ActivatedRoute, ParamMap } from '@angular/router';
|
|||
import { Observable } from "rxjs/internal/Observable";
|
||||
import { switchMap } from "rxjs/operators";
|
||||
|
||||
import { faComment, faCodeBranch} from '@fortawesome/free-solid-svg-icons';
|
||||
import { faArrowLeft, faComment, faCodeBranch } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { Skill, SkillsService } from "../skills.service";
|
||||
|
||||
|
@ -13,6 +13,7 @@ import { Skill, SkillsService } from "../skills.service";
|
|||
styleUrls: ['./skill-detail.component.scss']
|
||||
})
|
||||
export class SkillDetailComponent implements OnInit {
|
||||
public backArrow = faArrowLeft;
|
||||
public githubIcon = faCodeBranch;
|
||||
public skill$: Observable<Skill>;
|
||||
public triggerIcon = faComment;
|
||||
|
|
|
@ -4,16 +4,18 @@ mat-card-header {
|
|||
justify-content: center;
|
||||
margin-bottom: 15px;
|
||||
.mycroft-icon {
|
||||
left: 15px;
|
||||
left: 18px;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
top: 18px;
|
||||
img {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
.skill-icon {
|
||||
position: relative;
|
||||
//offset the skill icon by the width of the
|
||||
// mycroft icon to center it on card
|
||||
margin-left: -15px;
|
||||
fa-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
|
|
@ -2,18 +2,30 @@
|
|||
<mat-card *ngFor="let skill of skills; let i = index">
|
||||
<div [routerLink]="['/skill', skill.id]">
|
||||
<market-skill-card-header [skill]="skills[i]"></market-skill-card-header>
|
||||
<mat-card-title align="center">{{skill.title}}</mat-card-title>
|
||||
<mat-card-subtitle>
|
||||
<button mat-flat-button [disabled]="true">
|
||||
<mat-card-title *ngIf="skill.title" align="center">{{skill.title}}</mat-card-title>
|
||||
<mat-card-title *ngIf="!skill.title" align="center"> </mat-card-title>
|
||||
<mat-card-subtitle fxLayoutAlign="center">
|
||||
<div class="skill-trigger">
|
||||
<fa-icon [icon]="voiceIcon"></fa-icon>
|
||||
{{skill.triggers[0]}}
|
||||
</button>
|
||||
</div>
|
||||
</mat-card-subtitle>
|
||||
<mat-card-content *ngIf="skill.summary" [innerHTML]="skill.summary"></mat-card-content>
|
||||
<mat-card-content *ngIf="!skill.summary"> </mat-card-content>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
|
@ -8,24 +8,24 @@ mat-card {
|
|||
@include card-width;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
margin: 15px;
|
||||
padding: 15px;
|
||||
margin: 10px;
|
||||
padding: 18px;
|
||||
mat-card-title {
|
||||
@include ellipsis-overflow;
|
||||
color: $mycroft-secondary;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-weight: bold;
|
||||
padding-bottom: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
mat-card-subtitle {
|
||||
button {
|
||||
@include skill-trigger-button;
|
||||
.skill-trigger {
|
||||
@include ellipsis-overflow;
|
||||
@include card-width;
|
||||
margin: 0;
|
||||
@include skill-trigger;
|
||||
}
|
||||
}
|
||||
mat-card-content {
|
||||
color: $mycroft-secondary;
|
||||
@include ellipsis-overflow;
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -37,9 +37,19 @@ mat-card {
|
|||
@include card-width;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.installed-button {
|
||||
background-color: $mycroft-tertiary-green;
|
||||
fa-icon {
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
.install-button:hover {
|
||||
background-color: $mycroft-tertiary-green;
|
||||
color: $mycroft-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mat-card:hover{
|
||||
box-shadow: -1px 10px 29px 0px;
|
||||
box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { MatSnackBar } from "@angular/material";
|
||||
|
||||
import { faComment } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCheck, faComment } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { SkillsService, Skill } from "../skills.service";
|
||||
|
||||
|
@ -11,9 +11,10 @@ import { SkillsService, Skill } from "../skills.service";
|
|||
styleUrls: ['./skill-summary.component.scss'],
|
||||
})
|
||||
export class SkillSummaryComponent implements OnInit {
|
||||
public installedIcon = faCheck;
|
||||
@Input() public skills: Skill[];
|
||||
private skillToInstall: Skill;
|
||||
public voiceIcon = faComment;
|
||||
private skillInstalling: Skill;
|
||||
|
||||
constructor(public loginSnackbar: MatSnackBar, private skillsService: SkillsService) { }
|
||||
|
||||
|
@ -25,7 +26,7 @@ export class SkillSummaryComponent implements OnInit {
|
|||
* @param {Skill} skill
|
||||
*/
|
||||
install_skill(skill: Skill) : void {
|
||||
this.skillToInstall = skill;
|
||||
this.skillInstalling = skill;
|
||||
this.skillsService.installSkill(skill).subscribe(
|
||||
(response) => {
|
||||
this.onInstallSuccess(response)
|
||||
|
@ -46,7 +47,15 @@ export class SkillSummaryComponent implements OnInit {
|
|||
* @param response
|
||||
*/
|
||||
onInstallSuccess(response) : void {
|
||||
console.log('success!')
|
||||
this.loginSnackbar.open(
|
||||
'The ' + this.skillInstalling.title + ' skill is ' +
|
||||
'installing. Please allow up to two minutes for installation' +
|
||||
'to complete before using the skill. Only one skill can be ' +
|
||||
'installed at a time so please wait before selecting another' +
|
||||
'skill to install',
|
||||
null,
|
||||
{panelClass: 'mycroft-snackbar', duration:20000}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,33 +68,11 @@ export class SkillSummaryComponent implements OnInit {
|
|||
*/
|
||||
onInstallFailure(response) : void {
|
||||
if (response.status === 401) {
|
||||
let skillNameParts = this.skillToInstall.skill_name.split('-');
|
||||
let installName = [];
|
||||
skillNameParts.forEach(
|
||||
(part) => {
|
||||
if (part.toLowerCase() != 'mycroft' && part.toLowerCase() != 'skill') {
|
||||
installName.push(part);
|
||||
}
|
||||
}
|
||||
);
|
||||
this.loginSnackbar.open(
|
||||
'Skill installation functionality coming soon. ' +
|
||||
'In the meantime use your voice to install skills ' +
|
||||
'by saying: "Hey Mycroft, install ' + installName.join(' ') + '"',
|
||||
'',
|
||||
'To install a skill, log in to your account.',
|
||||
'LOG IN',
|
||||
{panelClass: 'mycroft-snackbar', duration: 5000}
|
||||
|
||||
);
|
||||
|
||||
// This is the snackbar logic for when the login and install
|
||||
// functionality is in place
|
||||
//
|
||||
// this.loginSnackbar.open(
|
||||
// 'To install a skill, log in to your account.',
|
||||
// 'LOG IN',
|
||||
// {panelClass: 'mycroft-snackbar', duration: 5000}
|
||||
//
|
||||
// );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<div class="skill-toolbar" fxLayout="row">
|
||||
<div fxFlex="80" (keydown.enter)="onClick()">
|
||||
<mat-form-field class="search-field">
|
||||
<input matInput placeholder="Search" type="text" [(ngModel)]="searchTerm">
|
||||
<div fxLayout="row" fxLayoutAlign="center" class="skill-toolbar">
|
||||
<div fxFlex="60" (keydown.enter)="searchSkills()" class="search-field">
|
||||
<mat-form-field>
|
||||
<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>
|
||||
<button mat-icon-button>
|
||||
<fa-icon [icon]="searchIcon" (click)="onClick()"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- commenting out the language picker until such time as there are -->
|
||||
|
@ -20,3 +20,9 @@
|
|||
<!--</mat-form-field>-->
|
||||
|
||||
</div>
|
||||
<div *ngIf="showBackButton">
|
||||
<button mat-icon-button class="back-button" (click)="clearSearch()">
|
||||
<fa-icon [icon]="backArrow"></fa-icon>
|
||||
All Skills
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -1,13 +1,27 @@
|
|||
@import '../../../stylesheets/global';
|
||||
|
||||
.back-button {
|
||||
color: $mycroft-dark-grey;
|
||||
margin-left: 20px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
fa-icon {
|
||||
color: $mycroft-dark-grey;
|
||||
}
|
||||
|
||||
.skill-toolbar {
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
.search-field {
|
||||
width: 80%;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
color: $mycroft-dark-grey;
|
||||
min-width: 330px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-top: 10px;
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, OnInit, OnDestroy, Output } from '@angular/core';
|
||||
|
||||
import { faSearch, faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
import { Subscription } from "rxjs/internal/Subscription";
|
||||
import { faArrowLeft, faSearch } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { SkillsService } from "../skills.service";
|
||||
|
||||
|
@ -9,7 +10,8 @@ import { SkillsService } from "../skills.service";
|
|||
templateUrl: './skill-toolbar.component.html',
|
||||
styleUrls: ['./skill-toolbar.component.scss']
|
||||
})
|
||||
export class SkillToolbarComponent implements OnInit {
|
||||
export class SkillToolbarComponent implements OnInit, OnDestroy {
|
||||
public backArrow = faArrowLeft;
|
||||
public languages = [
|
||||
{value: 'english', display: 'English'}
|
||||
];
|
||||
|
@ -17,27 +19,36 @@ export class SkillToolbarComponent implements OnInit {
|
|||
@Output() public searchResults = new EventEmitter();
|
||||
public searchTerm: string;
|
||||
public selectedLanguage = this.languages[0].value;
|
||||
public skillsAreFiltered: Subscription;
|
||||
public showBackButton: boolean = false;
|
||||
|
||||
constructor(private skillsService: SkillsService) { }
|
||||
|
||||
ngOnInit() { }
|
||||
ngOnInit() {
|
||||
this.skillsAreFiltered = this.skillsService.isFiltered.subscribe(
|
||||
(isFiltered) => { this.onFilteredStateChange(isFiltered) }
|
||||
);
|
||||
}
|
||||
|
||||
onClick(): void {
|
||||
if (this.searchIcon === faSearch) {
|
||||
this.searchSkills();
|
||||
this.searchIcon = faTimes;
|
||||
} else {
|
||||
this.searchTerm = '';
|
||||
this.searchSkills();
|
||||
this.searchIcon = faSearch;
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.skillsAreFiltered.unsubscribe();
|
||||
}
|
||||
|
||||
clearSearch(): void {
|
||||
this.searchTerm = '';
|
||||
this.searchSkills()
|
||||
}
|
||||
|
||||
searchSkills(): void {
|
||||
this.skillsService.searchSkills(this.searchTerm).subscribe(
|
||||
(skills) => {
|
||||
this.searchResults.emit(skills);
|
||||
console.log(this.skillsAreFiltered);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onFilteredStateChange (isFiltered) {
|
||||
this.showBackButton = isFiltered
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
background-color: $market-background;
|
||||
color: $mycroft-dark-grey;
|
||||
font-size: xx-large;
|
||||
margin-top: 30px;
|
||||
margin-top: 20px;
|
||||
padding-left: 10px;
|
||||
fa-icon {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
|
|
@ -27,11 +27,27 @@ export class SkillsComponent implements OnInit {
|
|||
}
|
||||
|
||||
get_skill_categories(skills): void {
|
||||
let skillCategories = [],
|
||||
systemCategoryFound = false;
|
||||
this.skillCategories = [];
|
||||
Object.keys(skills).forEach(
|
||||
category_name => {this.skillCategories.push(category_name);}
|
||||
categoryName => {skillCategories.push(categoryName);}
|
||||
);
|
||||
this.skillCategories.sort()
|
||||
skillCategories.sort();
|
||||
|
||||
// Make the "System" category display last, if it exists
|
||||
skillCategories.forEach(
|
||||
categoryName => {
|
||||
if (categoryName === 'System') {
|
||||
systemCategoryFound = true;
|
||||
} else {
|
||||
this.skillCategories.push(categoryName)
|
||||
}
|
||||
}
|
||||
);
|
||||
if (systemCategoryFound) {
|
||||
this.skillCategories.push('System')
|
||||
}
|
||||
}
|
||||
|
||||
showSearchResults(searchResults): void {
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { SkillService } from './skill.service';
|
||||
import { SkillsService } from './skills.service';
|
||||
|
||||
describe('SkillsService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [SkillService]
|
||||
providers: [SkillsService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([SkillService], (service: SkillService) => {
|
||||
it('should be created', inject([SkillsService], (service: SkillsService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http';
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { Subject } from "rxjs/internal/Subject";
|
||||
|
||||
export class Skill {
|
||||
id: number;
|
||||
|
@ -10,7 +11,7 @@ export class Skill {
|
|||
description: string;
|
||||
icon: Object;
|
||||
icon_image: string;
|
||||
skill_name: string;
|
||||
installed: boolean;
|
||||
title: string;
|
||||
summary: string;
|
||||
repository_url: string;
|
||||
|
@ -19,10 +20,11 @@ export class Skill {
|
|||
|
||||
@Injectable()
|
||||
export class SkillsService {
|
||||
private installUrl = '/api/install-skill';
|
||||
private installUrl = '/api/install';
|
||||
private skillUrl = '/api/skill/';
|
||||
private skillsUrl = '/api/skills';
|
||||
private searchQuery = '?search=';
|
||||
public isFiltered = new Subject<boolean>();
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
|
@ -35,6 +37,7 @@ export class SkillsService {
|
|||
}
|
||||
|
||||
searchSkills(searchTerm: string): Observable<Skill[]> {
|
||||
this.isFiltered.next(!!searchTerm);
|
||||
return this.http.get<Skill[]>(this.skillsUrl + this.searchQuery + searchTerm)
|
||||
}
|
||||
|
||||
|
@ -43,6 +46,5 @@ export class SkillsService {
|
|||
this.installUrl,
|
||||
{skill_url: skill.repository_url}
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -1,4 +1,4 @@
|
|||
export const environment = {
|
||||
production: false,
|
||||
loginUrl: 'http://login.mycroft-test.net'
|
||||
loginUrl: 'https://login.mycroft-test.net'
|
||||
};
|
||||
|
|
|
@ -4,10 +4,8 @@
|
|||
|
||||
export const environment = {
|
||||
production: false,
|
||||
|
||||
// URL of development API
|
||||
apiUrl: 'http://localhost:5000/',
|
||||
loginUrl: 'http://login.mycroft.test'
|
||||
apiUrl: 'http://localhost:5002',
|
||||
loginUrl: 'http://localhost:4201'
|
||||
};
|
||||
|
||||
/*
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<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">
|
||||
</head>
|
||||
<body>
|
||||
<body style="background-color: #f1f3f4; margin: 0">
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -11,4 +11,4 @@ $mycroft-black: #000000;
|
|||
$mycroft-dark-grey: #6c7a89;
|
||||
$mycroft-light-grey: #bdc3c7;
|
||||
$mycroft-blue-grey: #e4f1fe;
|
||||
$market-background: #f1f1f1;
|
||||
$market-background: #f1f3f4;
|
||||
|
|
|
@ -5,18 +5,5 @@ $button-border-radius: 4px;
|
|||
border-radius: $button-border-radius;
|
||||
background-color: $mycroft-primary;
|
||||
color: $mycroft-white;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@mixin skill-trigger-button {
|
||||
background-color: $mycroft-blue-grey;
|
||||
border-radius: $button-border-radius;
|
||||
color: $mycroft-secondary;
|
||||
font-weight: normal;
|
||||
margin-bottom: 15px;
|
||||
margin-right: 15px;
|
||||
fa-icon {
|
||||
color: $mycroft-secondary;
|
||||
margin-right: 5px;
|
||||
}
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,22 @@
|
|||
@import "../base/mycroft-colors";
|
||||
|
||||
@mixin ellipsis-overflow {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@mixin skill-trigger {
|
||||
background-color: $mycroft-blue-grey;
|
||||
border-radius: 4px;
|
||||
color: $mycroft-secondary;
|
||||
font-weight: normal;
|
||||
padding-bottom: 7px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
padding-top: 7px;
|
||||
fa-icon {
|
||||
color: $mycroft-primary;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,106 +5,89 @@ from logging import getLogger
|
|||
from flask import request, current_app
|
||||
from flask_restful import Resource
|
||||
|
||||
from .auth import decode_auth_token, AuthorizationError
|
||||
from .auth import decode_auth_token, AuthenticationError
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
class ServiceServerError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MethodNotAllowedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SeleneBaseView(Resource):
|
||||
class SeleneEndpoint(Resource):
|
||||
"""
|
||||
Install a skill on user device(s).
|
||||
Abstract base class for Selene Flask Restful API calls.
|
||||
|
||||
Subclasses must do the following:
|
||||
- override the allowed_methods class attribute to a list of all allowed
|
||||
HTTP methods. Each list member must be a HTTPMethod enum
|
||||
- override the _build_response_data method
|
||||
"""
|
||||
# The logger is initialized here but this should be overridden with a
|
||||
# package-specific logger (e.g. _log = getLogger(__package__)
|
||||
_log = getLogger()
|
||||
authentication_required: bool = True
|
||||
|
||||
def __init__(self):
|
||||
self.base_url = current_app.config['SELENE_BASE_URL']
|
||||
self.config = current_app.config
|
||||
self.authenticated = False
|
||||
self.request = request
|
||||
self.response = None
|
||||
self.response_data = None
|
||||
self.tartarus_token: str = None
|
||||
self.selene_token: str = None
|
||||
self.service_response = None
|
||||
self.tartarus_token: str = None
|
||||
self.user_uuid: str = None
|
||||
|
||||
def _authenticate(self):
|
||||
self._get_auth_token()
|
||||
self._validate_auth_token()
|
||||
"""
|
||||
Authenticate the user using tokens passed via cookies.
|
||||
|
||||
:raises: APIError()
|
||||
"""
|
||||
try:
|
||||
self._get_auth_token()
|
||||
self._validate_auth_token()
|
||||
except AuthenticationError as ae:
|
||||
if self.authentication_required:
|
||||
self.response = (str(ae), HTTPStatus.UNAUTHORIZED)
|
||||
raise APIError()
|
||||
else:
|
||||
self.authenticated = True
|
||||
|
||||
def _get_auth_token(self):
|
||||
"""Get the Selene JWT (and the tartarus token) from cookies.
|
||||
|
||||
:raises: AuthenticationError
|
||||
"""
|
||||
try:
|
||||
self.selene_token = request.cookies['seleneToken']
|
||||
self.tartarus_token = request.cookies['tartarusToken']
|
||||
except KeyError:
|
||||
raise AuthorizationError(
|
||||
raise AuthenticationError(
|
||||
'no authentication token found in request'
|
||||
)
|
||||
|
||||
def _validate_auth_token(self):
|
||||
"""Decode the Selene JWT.
|
||||
|
||||
:raises: AuthenticationError
|
||||
"""
|
||||
self.user_uuid = decode_auth_token(
|
||||
self.selene_token,
|
||||
current_app.config['SECRET_KEY']
|
||||
self.config['SECRET_KEY']
|
||||
)
|
||||
|
||||
def check_for_service_errors(self, service, response):
|
||||
if response.status_code == HTTPStatus.UNAUTHORIZED:
|
||||
error_message = 'invalid authentication token'
|
||||
self._log.error(error_message)
|
||||
raise AuthorizationError(error_message)
|
||||
elif response.status_code == HTTPStatus.NOT_FOUND:
|
||||
error_message = '{service} service URL {url} not found'.format(
|
||||
service=service,
|
||||
url=response.request.url
|
||||
)
|
||||
self._log.error(error_message)
|
||||
raise ServiceUrlNotFound(error_message)
|
||||
elif response.status_code != HTTPStatus.OK:
|
||||
def _check_for_service_errors(self, service_response):
|
||||
"""Common logic to handle non-successful returns from service calls."""
|
||||
if service_response.status_code != HTTPStatus.OK:
|
||||
error_message = (
|
||||
'{service} service URL {url} HTTP status {status}'.format(
|
||||
service=service,
|
||||
status=response.status_code,
|
||||
url=response.request.url
|
||||
'service URL {url} returned HTTP status {status}'.format(
|
||||
status=service_response.status_code,
|
||||
url=service_response.request.url
|
||||
)
|
||||
)
|
||||
self._log.error(error_message)
|
||||
raise ServiceServerError(error_message)
|
||||
|
||||
def _build_response(self):
|
||||
try:
|
||||
self._build_response_data()
|
||||
except AuthorizationError as ae:
|
||||
self._build_unauthorized_response(str(ae))
|
||||
except ServiceUrlNotFound as nf:
|
||||
self._build_server_error_response(str(nf))
|
||||
except ServiceServerError as se:
|
||||
self._build_server_error_response(str(se))
|
||||
else:
|
||||
self._build_success_response()
|
||||
|
||||
def _build_response_data(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _build_unauthorized_response(self, error_message):
|
||||
self.response = (
|
||||
dict(errorMessage=error_message),
|
||||
HTTPStatus.UNAUTHORIZED
|
||||
)
|
||||
|
||||
def _build_server_error_response(self, error_message):
|
||||
self.response = (
|
||||
dict(errorMessage=error_message),
|
||||
HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
def _build_success_response(self):
|
||||
self.response = (self.response_data, HTTPStatus.OK)
|
||||
_log.error(error_message)
|
||||
if service_response.status_code == HTTPStatus.UNAUTHORIZED:
|
||||
self.response = (error_message, HTTPStatus.UNAUTHORIZED)
|
||||
else:
|
||||
self.response = (error_message, HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
raise APIError()
|
||||
|
|
|
@ -1,21 +1,44 @@
|
|||
from datetime import datetime
|
||||
from logging import getLogger
|
||||
from time import time
|
||||
|
||||
import jwt
|
||||
|
||||
THIRTY_DAYS = 2592000
|
||||
|
||||
_log = getLogger(__package__)
|
||||
|
||||
|
||||
class AuthorizationError(Exception):
|
||||
class AuthenticationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def encode_auth_token(secret_key, user_uuid):
|
||||
"""
|
||||
Generates the Auth Token
|
||||
:return: string
|
||||
"""
|
||||
token_expiration = time() + THIRTY_DAYS
|
||||
payload = dict(iat=datetime.utcnow(), exp=token_expiration, sub=user_uuid)
|
||||
selene_token = jwt.encode(
|
||||
payload,
|
||||
secret_key,
|
||||
algorithm='HS256'
|
||||
)
|
||||
|
||||
# before returning the token, convert it from bytes to string so that
|
||||
# it can be included in a JSON response object
|
||||
return selene_token.decode()
|
||||
|
||||
|
||||
def decode_auth_token(auth_token: str, secret_key: str) -> tuple:
|
||||
"""
|
||||
Decodes the auth token
|
||||
:param auth_token: the Selene JSON Web Token extracted from the request cookies.
|
||||
:param auth_token: the Selene JSON Web Token extracted from cookies.
|
||||
:param secret_key: the key needed to decode the token
|
||||
:return: two-value tuple containing a boolean value indicating if the token is good and the
|
||||
user UUID extracted from the token. UUID will be None if token is invalid.
|
||||
:return: two-value tuple containing a boolean value indicating if the
|
||||
token is good and the user UUID extracted from the token. UUID will
|
||||
be None if token is invalid.
|
||||
"""
|
||||
try:
|
||||
payload = jwt.decode(auth_token, secret_key)
|
||||
|
@ -23,10 +46,10 @@ def decode_auth_token(auth_token: str, secret_key: str) -> tuple:
|
|||
except jwt.ExpiredSignatureError:
|
||||
error_msg = 'Selene token expired'
|
||||
_log.info(error_msg)
|
||||
raise AuthorizationError(error_msg)
|
||||
raise AuthenticationError(error_msg)
|
||||
except jwt.InvalidTokenError:
|
||||
error_msg = 'Invalid Selene token'
|
||||
_log.info(error_msg)
|
||||
raise AuthorizationError(error_msg)
|
||||
raise AuthenticationError(error_msg)
|
||||
|
||||
return user_uuid
|
||||
|
|
Loading…
Reference in New Issue