diff --git a/.gitignore b/.gitignore index 02655632..59050da6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ *.launch .settings/ *.sublime-workspace +__pycache__/ # IDE - VSCode .vscode/* @@ -37,3 +38,5 @@ testem.log # System Files .DS_Store Thumbs.db + + diff --git a/login/backend/v1/login-api/Dockerfile b/login/backend/v1/login-api/Dockerfile index a70a90d3..f5f84987 100644 --- a/login/backend/v1/login-api/Dockerfile +++ b/login/backend/v1/login-api/Dockerfile @@ -2,7 +2,7 @@ # The selene-shared parent image contains all the common Docker configs for # all Selene apps and services see the "shared" directory in this repository. -FROM selene-shared:latest +FROM docker.mycroft.ai/selene-shared:latest LABEL description="Run the API for the Mycroft login screen" # Use pipenv to install the package's dependencies in the container diff --git a/login/backend/v1/login-api/Pipfile b/login/backend/v1/login-api/Pipfile index 6c6495b8..62766ff9 100644 --- a/login/backend/v1/login-api/Pipfile +++ b/login/backend/v1/login-api/Pipfile @@ -9,9 +9,10 @@ requests = "*" pyjwt = "*" flask-restful = "*" certifi = "*" -gunicorn = "*" +uwsgi = "*" [dev-packages] +selene-util = {path = "./../../../../shared"} [requires] python_version = "3.7" diff --git a/login/backend/v1/login-api/Pipfile.lock b/login/backend/v1/login-api/Pipfile.lock index c48cd4e8..0806d099 100644 --- a/login/backend/v1/login-api/Pipfile.lock +++ b/login/backend/v1/login-api/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "624569e9bc0207f58dca3a683d5868e374e78a5be00a34b442aae4a656e4eac2" + "sha256": "7cf1dde24d5a966645f3e49d93dde93dad42c6b6fa62f9b254b79d6b58e93e06" }, "pipfile-spec": 6, "requires": { @@ -40,10 +40,11 @@ }, "click": { "hashes": [ - "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", - "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" ], - "version": "==6.7" + "markers": "python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.1.*'", + "version": "==7.0" }, "flask": { "hashes": [ @@ -61,14 +62,6 @@ "index": "pypi", "version": "==0.3.6" }, - "gunicorn": { - "hashes": [ - "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471", - "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3" - ], - "index": "pypi", - "version": "==19.9.0" - }, "idna": { "hashes": [ "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", @@ -133,6 +126,13 @@ "markers": "python_version >= '2.6' and python_version != '3.1.*' and python_version != '3.3.*' and python_version < '4' and python_version != '3.0.*' and python_version != '3.2.*'", "version": "==1.23" }, + "uwsgi": { + "hashes": [ + "sha256:d2318235c74665a60021a4fc7770e9c2756f9fc07de7b8c22805efe85b5ab277" + ], + "index": "pypi", + "version": "==2.0.17.1" + }, "werkzeug": { "hashes": [ "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", @@ -141,5 +141,9 @@ "version": "==0.14.1" } }, - "develop": {} + "develop": { + "selene-util": { + "path": "./../../../../shared" + } + } } diff --git a/login/backend/v1/login-api/login_api/api.py b/login/backend/v1/login-api/login_api/api.py index 929fd37c..12bd1db0 100644 --- a/login/backend/v1/login-api/login_api/api.py +++ b/login/backend/v1/login-api/login_api/api.py @@ -1,17 +1,41 @@ -from flask import Flask +from flask import Flask, request from flask_restful import Api -from .authorize import AuthorizeAntisocialView +from .endpoints import ( + AuthenticateAntisocialEndpoint, + SocialLoginTokensEndpoint, + AuthorizeFacebookEndpoint, + AuthorizeGithubEndpoint, + AuthorizeGoogleEndpoint, + LogoutEndpoint +) from .config import get_config_location -from .logout import LogoutView - -BASE_URL = '/api/auth/' +# Initialize the Flask application and the Flask Restful API login = Flask(__name__) login.config.from_object(get_config_location()) login_api = Api(login, catch_all_404s=True) -antisocial_view_url = BASE_URL + 'antisocial' -login_api.add_resource(AuthorizeAntisocialView, antisocial_view_url) +# Define the endpoints +login_api.add_resource(AuthenticateAntisocialEndpoint, '/api/antisocial') +login_api.add_resource(AuthorizeFacebookEndpoint, '/api/social/facebook') +login_api.add_resource(AuthorizeGithubEndpoint, '/api/social/github') +login_api.add_resource(AuthorizeGoogleEndpoint, '/api/social/google') +login_api.add_resource(SocialLoginTokensEndpoint, '/api/social/tokens') +login_api.add_resource(LogoutEndpoint, '/api/logout') + + +def add_cors_headers(response): + """Allow any application to logout""" + # if 'logout' in request.url: + response.headers['Access-Control-Allow-Origin'] = '*' + if request.method == 'OPTIONS': + response.headers['Access-Control-Allow-Methods'] = ( + 'DELETE, GET, POST, PUT' + ) + headers = request.headers.get('Access-Control-Request-Headers') + if headers: + response.headers['Access-Control-Allow-Headers'] = headers + return response + -logout_view_url = BASE_URL + 'logout' -login_api.add_resource(LogoutView, logout_view_url) +login.after_request(add_cors_headers) diff --git a/login/backend/v1/login-api/login_api/authorize.py b/login/backend/v1/login-api/login_api/authorize.py deleted file mode 100644 index 88e0f64e..00000000 --- a/login/backend/v1/login-api/login_api/authorize.py +++ /dev/null @@ -1,76 +0,0 @@ -from datetime import datetime -from http import HTTPStatus -import json -from time import time - -from flask import current_app, request as frontend_request -from flask_restful import Resource -import jwt -import requests as service_request - -THIRTY_DAYS = 2592000 - - -def encode_selene_token(user_uuid): - """ - Generates the Auth Token - :return: string - """ - token_expiration = time() + THIRTY_DAYS - payload = dict(iat=datetime.utcnow(), exp=token_expiration, sub=user_uuid) - selene_token = jwt.encode( - payload, - current_app.config['SECRET_KEY'], - algorithm='HS256' - ) - - # before returning the token, convert it from bytes to string so that - # it can be included in a JSON response object - return selene_token.decode() - - -class AuthorizeAntisocialView(Resource): - """ - User Login Resource - """ - def __init__(self): - self.frontend_response = None - self.response_status_code = HTTPStatus.OK - self.tartarus_token = None - self.users_uuid = None - - def get(self): - self._authorize() - self._build_frontend_response() - - return self.frontend_response - - def _authorize(self): - basic_credentials = frontend_request.headers['authorization'] - service_request_headers = {'Authorization': basic_credentials} - auth_service_response = service_request.get( - current_app.config['TARTARUS_BASE_URL'] + '/auth/login', - headers=service_request_headers - ) - if auth_service_response.status_code == HTTPStatus.OK: - auth_service_response_content = json.loads( - auth_service_response.content - ) - self.users_uuid = auth_service_response_content['uuid'] - self.tartarus_token = auth_service_response_content['accessToken'] - else: - self.response_status_code = auth_service_response.status_code - - def _build_frontend_response(self): - if self.response_status_code == HTTPStatus.OK: - frontend_response_data = dict( - expiration=time() + THIRTY_DAYS, - seleneToken=encode_selene_token(self.users_uuid), - tartarusToken=self.tartarus_token, - ) - else: - frontend_response_data = {} - self.frontend_response = ( - frontend_response_data, - self.response_status_code - ) diff --git a/login/backend/v1/login-api/login_api/config.py b/login/backend/v1/login-api/login_api/config.py index 711a346b..1da52994 100644 --- a/login/backend/v1/login-api/login_api/config.py +++ b/login/backend/v1/login-api/login_api/config.py @@ -8,20 +8,30 @@ class LoginConfigException(Exception): class BaseConfig: """Base configuration.""" DEBUG = False + LOGIN_BASE_URL = os.environ['LOGIN_BASE_URL'] SECRET_KEY = os.environ['JWT_SECRET'] + SELENE_BASE_URL = os.environ['SELENE_BASE_URL'] + TARTARUS_BASE_URL = os.environ['TARTARUS_BASE_URL'] class DevelopmentConfig(BaseConfig): """Development configuration.""" DEBUG = True - TARTARUS_BASE_URL = 'https://api-test.mycroft.ai/v1' + + +class TestConfig(BaseConfig): + pass + + +class ProdConfig(BaseConfig): + pass def get_config_location(): environment_configs = dict( dev='login_api.config.DevelopmentConfig', - # test=TestConfig, - # prod=ProdConfig + test=TestConfig, + prod=ProdConfig ) try: diff --git a/login/backend/v1/login-api/login_api/endpoints/__init__.py b/login/backend/v1/login-api/login_api/endpoints/__init__.py new file mode 100644 index 00000000..894858af --- /dev/null +++ b/login/backend/v1/login-api/login_api/endpoints/__init__.py @@ -0,0 +1,6 @@ +from .authenticate_antisocial import AuthenticateAntisocialEndpoint +from .social_login_tokens import SocialLoginTokensEndpoint +from .facebook import AuthorizeFacebookEndpoint +from .github import AuthorizeGithubEndpoint +from .google import AuthorizeGoogleEndpoint +from .logout import LogoutEndpoint diff --git a/login/backend/v1/login-api/login_api/endpoints/authenticate_antisocial.py b/login/backend/v1/login-api/login_api/endpoints/authenticate_antisocial.py new file mode 100644 index 00000000..c6db90f8 --- /dev/null +++ b/login/backend/v1/login-api/login_api/endpoints/authenticate_antisocial.py @@ -0,0 +1,54 @@ +from http import HTTPStatus +import json +from time import time + +import requests as service_request + +from selene_util.api import SeleneEndpoint, APIError +from selene_util.auth import encode_auth_token, THIRTY_DAYS + + +class AuthenticateAntisocialEndpoint(SeleneEndpoint): + """ + User Login Resource + """ + def __init__(self): + super(AuthenticateAntisocialEndpoint, self).__init__() + self.response_status_code = HTTPStatus.OK + self.tartarus_token = None + self.users_uuid = None + + def get(self): + try: + self._authenticate_credentials() + except APIError: + pass + else: + self._build_response() + + return self.response + + def _authenticate_credentials(self): + basic_credentials = self.request.headers['authorization'] + service_request_headers = {'Authorization': basic_credentials} + auth_service_response = service_request.get( + self.config['TARTARUS_BASE_URL'] + '/auth/login', + headers=service_request_headers + ) + self._check_for_service_errors(auth_service_response) + auth_service_response_content = json.loads( + auth_service_response.content + ) + self.users_uuid = auth_service_response_content['uuid'] + self.tartarus_token = auth_service_response_content['accessToken'] + + def _build_response(self): + self.selene_token = encode_auth_token( + self.config['SECRET_KEY'], self.users_uuid + ) + response_data = dict( + expiration=time() + THIRTY_DAYS, + seleneToken=self.selene_token, + tartarusToken=self.tartarus_token, + ) + self.response = (response_data, HTTPStatus.OK) \ No newline at end of file diff --git a/login/backend/v1/login-api/login_api/endpoints/authenticate_social.py b/login/backend/v1/login-api/login_api/endpoints/authenticate_social.py new file mode 100644 index 00000000..89074695 --- /dev/null +++ b/login/backend/v1/login-api/login_api/endpoints/authenticate_social.py @@ -0,0 +1,37 @@ +from http import HTTPStatus + +from selene_util.api import SeleneEndpoint +from selene_util.auth import encode_auth_token, THIRTY_DAYS +from time import time +import json + +class AuthenticateSocialEndpoint(SeleneEndpoint): + def __init__(self): + super(AuthenticateSocialEndpoint, self).__init__() + self.response_status_code = HTTPStatus.OK + self.tartarus_token = None + self.users_uuid = None + + def get(self): + self._get_tartarus_token() + self._build_front_end_response() + return self.response + + def _get_tartarus_token(self): + args = self.request.args + if "data" in args: + self.tartarus_token = args['data'] + token_json = json.loads(self.tartarus_token) + self.users_uuid = token_json["uuid"] + + def _build_front_end_response(self): + self.selene_token = encode_auth_token( + self.config['SECRET_KEY'], self.users_uuid + ) + + response_data = dict( + expiration=time() + THIRTY_DAYS, + seleneToken=self.selene_token, + tartarusToken=self.tartarus_token, + ) + self.response = (response_data, HTTPStatus.OK) \ No newline at end of file diff --git a/login/backend/v1/login-api/login_api/endpoints/facebook.py b/login/backend/v1/login-api/login_api/endpoints/facebook.py new file mode 100644 index 00000000..d064e354 --- /dev/null +++ b/login/backend/v1/login-api/login_api/endpoints/facebook.py @@ -0,0 +1,17 @@ +"""Endpoint for single sign on through Facebook""" +from flask import redirect + +from selene_util.api import SeleneEndpoint + + +class AuthorizeFacebookEndpoint(SeleneEndpoint): + def get(self): + """Call a Tartarus endpoint that will redirect to Facebook login.""" + tartarus_auth_endpoint = ( + '{tartarus_url}/social/auth/facebook' + '?clientUri={login_url}&path=/social/login'.format( + tartarus_url=self.config['TARTARUS_BASE_URL'], + login_url=self.config['LOGIN_BASE_URL'] + ) + ) + return redirect(tartarus_auth_endpoint) diff --git a/login/backend/v1/login-api/login_api/endpoints/github.py b/login/backend/v1/login-api/login_api/endpoints/github.py new file mode 100644 index 00000000..27417297 --- /dev/null +++ b/login/backend/v1/login-api/login_api/endpoints/github.py @@ -0,0 +1,18 @@ +"""Endpoint for single sign on through Github""" +from flask import redirect + +from selene_util.api import SeleneEndpoint + + +class AuthorizeGithubEndpoint(SeleneEndpoint): + + def get(self): + """Call a Tartarus endpoint that will redirect to Github login.""" + tartarus_auth_endpoint = ( + '{tartarus_url}/social/auth/github' + '?clientUri={login_url}&path=/social/login'.format( + tartarus_url=self.config['TARTARUS_BASE_URL'], + login_url=self.config['LOGIN_BASE_URL'] + ) + ) + return redirect(tartarus_auth_endpoint) \ No newline at end of file diff --git a/login/backend/v1/login-api/login_api/endpoints/google.py b/login/backend/v1/login-api/login_api/endpoints/google.py new file mode 100644 index 00000000..62b501af --- /dev/null +++ b/login/backend/v1/login-api/login_api/endpoints/google.py @@ -0,0 +1,18 @@ +"""Endpoint for single sign on through Google""" +from flask import redirect + +from selene_util.api import SeleneEndpoint + + +class AuthorizeGoogleEndpoint(SeleneEndpoint): + + def get(self): + """Call a Tartarus endpoint that will redirect to Google login.""" + tartarus_auth_endpoint = ( + '{tartarus_url}/social/auth/google' + '?clientUri={login_url}&path=/social/login'.format( + login_url=self.config['LOGIN_BASE_URL'], + tartarus_url=self.config['TARTARUS_BASE_URL'] + ) + ) + return redirect(tartarus_auth_endpoint) diff --git a/login/backend/v1/login-api/login_api/endpoints/logout.py b/login/backend/v1/login-api/login_api/endpoints/logout.py new file mode 100644 index 00000000..52407c3a --- /dev/null +++ b/login/backend/v1/login-api/login_api/endpoints/logout.py @@ -0,0 +1,37 @@ +"""Log a user out of Mycroft web sites""" + +from http import HTTPStatus +from logging import getLogger + +import requests + +from selene_util.api import SeleneEndpoint, APIError + +_log = getLogger(__package__) + + +class LogoutEndpoint(SeleneEndpoint): + def __init__(self): + super(LogoutEndpoint, self).__init__() + + def put(self): + try: + self._authenticate() + self._logout() + except APIError: + pass + + return self.response + + def _logout(self): + service_request_headers = { + 'Authorization': 'Bearer ' + self.tartarus_token + } + service_url = self.config['TARTARUS_BASE_URL'] + '/auth/logout' + auth_service_response = requests.get( + service_url, + headers=service_request_headers + ) + self._check_for_service_errors(auth_service_response) + logout_response = auth_service_response.json() + self.response = (logout_response, HTTPStatus.OK) diff --git a/login/backend/v1/login-api/login_api/endpoints/social_login_tokens.py b/login/backend/v1/login-api/login_api/endpoints/social_login_tokens.py new file mode 100644 index 00000000..896e81d2 --- /dev/null +++ b/login/backend/v1/login-api/login_api/endpoints/social_login_tokens.py @@ -0,0 +1,32 @@ +from http import HTTPStatus +import json +from time import time + +from selene_util.api import SeleneEndpoint +from selene_util.auth import encode_auth_token, THIRTY_DAYS + + +class SocialLoginTokensEndpoint(SeleneEndpoint): + def post(self): + self._get_tartarus_token() + self._build_selene_token() + self._build_response() + return self.response + + def _get_tartarus_token(self): + request_data = json.loads(self.request.data) + self.tartarus_token = request_data['accessToken'] + self.user_uuid = request_data["uuid"] + + def _build_selene_token(self): + self.selene_token = encode_auth_token( + self.config['SECRET_KEY'], self.user_uuid + ) + + def _build_response(self): + response_data = dict( + expiration=time() + THIRTY_DAYS, + seleneToken=self.selene_token, + tartarusToken=self.tartarus_token, + ) + self.response = (response_data, HTTPStatus.OK) diff --git a/login/backend/v1/login-api/login_api/facebook.py b/login/backend/v1/login-api/login_api/facebook.py deleted file mode 100644 index 238b9126..00000000 --- a/login/backend/v1/login-api/login_api/facebook.py +++ /dev/null @@ -1,52 +0,0 @@ -# from http import HTTPStatus -# import json -# from time import time -# -# from flask import current_app, request as frontend_request -# from flask_restful import Resource -# import requests as service_request -# -# FACEBOOK_API_URL = 'https://graph.facebook.com/v3.1/me/?fields=name,email' -# THIRTY_DAYS = 2592000 -# -# -# class AuthorizeFacebookView(Resource): -# """ -# Check the authenticity Facebook token obtained by the frontend -# """ -# def __init__(self): -# self.frontend_response = None -# self.response_status_code = HTTPStatus.OK -# self.users_email = None -# self.users_name = None -# -# def get(self): -# self._validate_token() -# self._build_frontend_response() -# -# return self.frontend_response -# -# def _validate_token(self): -# facebook_token = frontend_request.headers['token'] -# service_request_headers = {'Authorization': 'Bearer ' + facebook_token} -# fb_service_response = service_request.get( -# FACEBOOK_API_URL, -# headers=service_request_headers -# ) -# if fb_service_response.status_code == HTTPStatus.OK: -# fb_service_response_content = json.loads(fb_service_response.content) -# self.users_name = fb_service_response_content['name'] -# self.users_email = fb_service_response_content['email'] -# else: -# self.response_status_code = fb_service_response.status_code -# -# def _build_frontend_response(self): -# if self.response_status_code == HTTPStatus.OK: -# frontend_response_data = dict( -# expiration=time() + THIRTY_DAYS, -# seleneToken=encode_selene_token(self.users_uuid), -# tartarusToken=self.tartarus_token, -# ) -# else: -# frontend_response_data = {} -# self.frontend_response = (frontend_response_data, self.response_status_code) diff --git a/login/backend/v1/login-api/login_api/logout.py b/login/backend/v1/login-api/login_api/logout.py deleted file mode 100644 index 9f00d424..00000000 --- a/login/backend/v1/login-api/login_api/logout.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Defines a view that will install a skill on a device running Mycroft core"""\ - -from http import HTTPStatus -from logging import getLogger -import json - -from flask import request, current_app -from flask_restful import Resource -import requests - -from selene_util.auth import decode_auth_token, AuthorizationError - -_log = getLogger(__package__) - - -class ServiceUrlNotFound(Exception): - """Exception to call when a HTTP 404 status is returned.""" - pass - - -class ServiceServerError(Exception): - """Catch all exception for errors not previously identified""" - pass - - -class LogoutView(Resource): - """Log a user out of the Mycroft web presence""" - def __init__(self): - self.service_response = None - self.frontend_response = None - self.user_uuid: str = None - self.tartarus_token: str = None - self.selene_token: str = None - self.device_uuid = None - self.installer_skill_settings = [] - - def put(self): - try: - self._get_auth_tokens() - self._validate_auth_token() - self._logout() - except AuthorizationError as ae: - self._build_unauthorized_response(str(ae)) - except ServiceUrlNotFound as nf: - self._build_server_error_response(str(nf)) - except ServiceServerError as se: - self._build_server_error_response(str(se)) - else: - self._build_success_response() - - return self.frontend_response - - def _get_auth_tokens(self): - try: - self.selene_token = request.cookies['seleneToken'] - self.tartarus_token = request.cookies['tartarusToken'] - except KeyError: - raise AuthorizationError( - 'no authentication tokens found in request' - ) - - def _validate_auth_token(self): - self.user_uuid = decode_auth_token( - self.selene_token, - current_app.config['SECRET_KEY'] - ) - - def _logout(self): - service_request_headers = { - 'Authorization': 'Bearer ' + self.tartarus_token - } - service_url = current_app.config['TARTARUS_BASE_URL'] + '/auth/logout' - service_response = requests.get( - service_url, - headers=service_request_headers - ) - self.check_for_tartarus_errors(service_response, service_url) - self.service_response_data = json.loads(service_response.content) - - def check_for_tartarus_errors(self, service_response, service_url): - if service_response.status_code == HTTPStatus.UNAUTHORIZED: - error_message = 'invalid Tartarus token' - _log.error(error_message) - raise AuthorizationError(error_message) - elif service_response.status_code == HTTPStatus.NOT_FOUND: - error_message = 'service url {} not found'.format(service_url) - _log.error(error_message) - raise ServiceUrlNotFound(error_message) - elif service_response.status_code != HTTPStatus.OK: - error_message = ( - 'error occurred during request to {service} URL {url}' - ) - _log.error(error_message.format( - service='tartarus', - url=service_url) - ) - raise ServiceServerError(error_message) - - def _build_unauthorized_response(self, error_message): - self.frontend_response = ( - dict(errorMessage=error_message), - HTTPStatus.UNAUTHORIZED - ) - - def _build_server_error_response(self, error_message): - self.frontend_response = ( - dict(errorMessage=error_message), - HTTPStatus.INTERNAL_SERVER_ERROR - ) - - def _build_success_response(self): - service_response_data = json.loads(self.service_response.content) - self.frontend_response = ( - dict(name=service_response_data.get('name')), - HTTPStatus.OK - ) diff --git a/login/frontend/v1/login-ui/Dockerfile b/login/frontend/v1/login-ui/Dockerfile index 3f359d95..19de0d16 100644 --- a/login/frontend/v1/login-ui/Dockerfile +++ b/login/frontend/v1/login-ui/Dockerfile @@ -6,7 +6,8 @@ WORKDIR /usr/src/app COPY package*.json ./ RUN npm install COPY . . -RUN npm run build +ARG selene_env +RUN npm run build-${selene_env} # STAGE TWO: build the web server and copy the compiled angular app to it. FROM nginx:latest diff --git a/login/frontend/v1/login-ui/angular.json b/login/frontend/v1/login-ui/angular.json index d115d62a..3cb15ca8 100644 --- a/login/frontend/v1/login-ui/angular.json +++ b/login/frontend/v1/login-ui/angular.json @@ -3,7 +3,7 @@ "version": 1, "newProjectRoot": "projects", "projects": { - "frontend": { + "mycroft-login": { "root": "", "sourceRoot": "src", "projectType": "application", diff --git a/login/frontend/v1/login-ui/package.json b/login/frontend/v1/login-ui/package.json index a6999974..32ff9d47 100644 --- a/login/frontend/v1/login-ui/package.json +++ b/login/frontend/v1/login-ui/package.json @@ -4,7 +4,9 @@ "scripts": { "ng": "ng", "start": "ng serve --open --proxy-config proxy.config.json", - "build": "ng build", + "build-dev": "ng build", + "build-test": "ng build", + "build-prod": "ng build --prod", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" @@ -16,7 +18,7 @@ "@angular/common": "^6.0.3", "@angular/compiler": "^6.0.3", "@angular/core": "^6.0.3", - "@angular/flex-layout": "^6.0.0-beta.16", + "@angular/flex-layout": "6.0.0-beta.16", "@angular/forms": "^6.0.3", "@angular/http": "^6.0.9", "@angular/material": "^6.4.1", @@ -29,7 +31,7 @@ "@fortawesome/free-regular-svg-icons": "^5.2.0", "@fortawesome/free-solid-svg-icons": "^5.2.0", "core-js": "^2.5.4", - "rxjs": "^6.0.0", + "rxjs": "6.2.2", "zone.js": "^0.8.26" }, "devDependencies": { diff --git a/login/frontend/v1/login-ui/src/app/app.component.html b/login/frontend/v1/login-ui/src/app/app.component.html index 167e6937..642d4ed7 100644 --- a/login/frontend/v1/login-ui/src/app/app.component.html +++ b/login/frontend/v1/login-ui/src/app/app.component.html @@ -1,3 +1,31 @@ -
- -