diff --git a/README.md b/README.md deleted file mode 100644 index d66455c..0000000 --- a/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Projet Sécurité INFO5A 2022 - -**Auteurs**: -- Baptiste Peupier -- Wilfried Vallée -- Mihary Ranaivoson -- Yûki Vachot - -**Branches**: -- db1: Base de données Utilisateur -- db2: Base de données Log -- backend: Flask Backend -- frontend: Serveur Angular avec un autre serveur Flask Backend communiquant avec la "branche" backend -- development: non mis à jour ne pas cloner - -docker compose up pour chaque branche diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..2987276 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,6 @@ +FROM python:latest +WORKDIR /data/backend +COPY requirements.txt requirements.txt +RUN pip install --upgrade pip +RUN pip install -r requirements.txt +COPY . . \ No newline at end of file diff --git a/backend/__pycache__/app.cpython-310.pyc b/backend/__pycache__/app.cpython-310.pyc new file mode 100644 index 0000000..56671cd Binary files /dev/null and b/backend/__pycache__/app.cpython-310.pyc differ diff --git a/backend/__pycache__/app.cpython-38.pyc b/backend/__pycache__/app.cpython-38.pyc new file mode 100644 index 0000000..951bfbe Binary files /dev/null and b/backend/__pycache__/app.cpython-38.pyc differ diff --git a/backend/__pycache__/config.cpython-310.pyc b/backend/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000..6ca6f0b Binary files /dev/null and b/backend/__pycache__/config.cpython-310.pyc differ diff --git a/backend/__pycache__/config.cpython-38.pyc b/backend/__pycache__/config.cpython-38.pyc new file mode 100644 index 0000000..6e1dae2 Binary files /dev/null and b/backend/__pycache__/config.cpython-38.pyc differ diff --git a/backend/__pycache__/fictive_users.cpython-38.pyc b/backend/__pycache__/fictive_users.cpython-38.pyc new file mode 100644 index 0000000..477bdc3 Binary files /dev/null and b/backend/__pycache__/fictive_users.cpython-38.pyc differ diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..50e795c --- /dev/null +++ b/backend/app.py @@ -0,0 +1,8 @@ +from application import create_app +import os + +app = create_app(os.environ.get('FLASK_ENV', None)) + +if __name__ == "__main__": + PORT = os.environ.get('PORT', 33507) + app.run(host='0.0.0.0', port=PORT, DEBUG=True) diff --git a/backend/application/__init__.py b/backend/application/__init__.py new file mode 100644 index 0000000..b68cd09 --- /dev/null +++ b/backend/application/__init__.py @@ -0,0 +1,35 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_cors import CORS +import sys + +db = SQLAlchemy() + + +def create_app(flask_env='development'): + app = Flask(__name__, instance_relative_config=False) + origin = app.config.get('ALLOW_ORIGIN') + if origin is None: + origin = ['http://127.0.0.1:4200', 'http://localhost:4200'] + CORS(app, supports_credentials=True, origins=origin) + if flask_env == 'production': + app.config.from_object("config.ProductionConfig") + elif flask_env == 'testing': + app.config.from_object("config.TestingConfig") + elif flask_env == 'development': + app.config.from_object("config.DevelopmentConfig") + else: + app.config.from_object("config.Config") + + if app.config['SQLALCHEMY_DATABASE_URI_1'] is None or app.config['SQLALCHEMY_DATABASE_URI_2'] is None: + print('No ENV Variable for DATABASE_URL_USERS or DATABASE_URL_LOGS') + sys.exit(1) + else: + print('ENV Variables passed : ', app.config['SQLALCHEMY_BINDS']) + + db.init_app(app) + with app.app_context(): + from . import routes + app.register_blueprint(routes.bp) + db.create_all() + return app diff --git a/backend/application/__pycache__/__init__.cpython-310.pyc b/backend/application/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..4f97792 Binary files /dev/null and b/backend/application/__pycache__/__init__.cpython-310.pyc differ diff --git a/backend/application/__pycache__/__init__.cpython-38.pyc b/backend/application/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..b057e9d Binary files /dev/null and b/backend/application/__pycache__/__init__.cpython-38.pyc differ diff --git a/backend/application/__pycache__/api_functions.cpython-310.pyc b/backend/application/__pycache__/api_functions.cpython-310.pyc new file mode 100644 index 0000000..037557d Binary files /dev/null and b/backend/application/__pycache__/api_functions.cpython-310.pyc differ diff --git a/backend/application/__pycache__/api_functions.cpython-38.pyc b/backend/application/__pycache__/api_functions.cpython-38.pyc new file mode 100644 index 0000000..9f49edf Binary files /dev/null and b/backend/application/__pycache__/api_functions.cpython-38.pyc differ diff --git a/backend/application/__pycache__/logs_model.cpython-310.pyc b/backend/application/__pycache__/logs_model.cpython-310.pyc new file mode 100644 index 0000000..c317af2 Binary files /dev/null and b/backend/application/__pycache__/logs_model.cpython-310.pyc differ diff --git a/backend/application/__pycache__/logs_model.cpython-38.pyc b/backend/application/__pycache__/logs_model.cpython-38.pyc new file mode 100644 index 0000000..f7aa63a Binary files /dev/null and b/backend/application/__pycache__/logs_model.cpython-38.pyc differ diff --git a/backend/application/__pycache__/responses.cpython-310.pyc b/backend/application/__pycache__/responses.cpython-310.pyc new file mode 100644 index 0000000..05d2496 Binary files /dev/null and b/backend/application/__pycache__/responses.cpython-310.pyc differ diff --git a/backend/application/__pycache__/responses.cpython-38.pyc b/backend/application/__pycache__/responses.cpython-38.pyc new file mode 100644 index 0000000..7975ba1 Binary files /dev/null and b/backend/application/__pycache__/responses.cpython-38.pyc differ diff --git a/backend/application/__pycache__/routes.cpython-310.pyc b/backend/application/__pycache__/routes.cpython-310.pyc new file mode 100644 index 0000000..d5552d7 Binary files /dev/null and b/backend/application/__pycache__/routes.cpython-310.pyc differ diff --git a/backend/application/__pycache__/routes.cpython-38.pyc b/backend/application/__pycache__/routes.cpython-38.pyc new file mode 100644 index 0000000..7a95df1 Binary files /dev/null and b/backend/application/__pycache__/routes.cpython-38.pyc differ diff --git a/backend/application/__pycache__/sessionJWT.cpython-310.pyc b/backend/application/__pycache__/sessionJWT.cpython-310.pyc new file mode 100644 index 0000000..2b9734d Binary files /dev/null and b/backend/application/__pycache__/sessionJWT.cpython-310.pyc differ diff --git a/backend/application/__pycache__/sessionJWT.cpython-38.pyc b/backend/application/__pycache__/sessionJWT.cpython-38.pyc new file mode 100644 index 0000000..2ea8bca Binary files /dev/null and b/backend/application/__pycache__/sessionJWT.cpython-38.pyc differ diff --git a/backend/application/__pycache__/users_model.cpython-310.pyc b/backend/application/__pycache__/users_model.cpython-310.pyc new file mode 100644 index 0000000..e386a6a Binary files /dev/null and b/backend/application/__pycache__/users_model.cpython-310.pyc differ diff --git a/backend/application/__pycache__/users_model.cpython-38.pyc b/backend/application/__pycache__/users_model.cpython-38.pyc new file mode 100644 index 0000000..43995d7 Binary files /dev/null and b/backend/application/__pycache__/users_model.cpython-38.pyc differ diff --git a/backend/application/api_functions.py b/backend/application/api_functions.py new file mode 100644 index 0000000..bb67cbd --- /dev/null +++ b/backend/application/api_functions.py @@ -0,0 +1,364 @@ +import hashlib +import os +from datetime import datetime +from flask_sqlalchemy import inspect +from sqlalchemy import asc, desc, or_ +from .users_model import Users, db +from .logs_model import Logs + + +def db_create_log(ip, action, message, has_succeeded, status_code, table=None, id_user=None): + log = Logs( + date=datetime.now().strftime('%Y-%m-%dT%H:%M:%S'), + ip=ip, + action=action, + message=message, + has_succeeded=has_succeeded, + status_code=status_code, + table=table, + id_user=id_user + ) + db.session.add(log) + db.session.commit() + return log.json() + + +def hash_password(salt, password): + return hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000) + + +def db_login(ip, email, password): + user = Users.query.filter( + Users.email == email + ).scalar() + + # Check User and Hash Pass + if user and user.hash_pass == hash_password(user.salt, password): + message = 'User authenticated.' + db_create_log( + ip=ip, + action='login', + message=message, + has_succeeded=True, + status_code=0, + table='users', + id_user=user.id + ) + return {'status': 0, 'message': message, 'data': user.json()} + else: + message = f'Email or password invalid' + db_create_log( + ip=ip, + action='login', + message=message, + has_succeeded=False, + status_code=1, + table='users', + id_user=None + ) + return {'status': 1, 'message': message} # Email or password invalid + + +def db_register(ip, email, nickname, password, is_admin=False): + user = Users.query.filter( + Users.email == email + ).scalar() + if user: + message = f'{email} already exist.' + db_create_log( + ip=ip, + action='register', + message=message, + has_succeeded=False, + status_code=1, + table='users', + id_user=None + ) + return {'status': 1, 'message': message} # User already exist + + # Salt Hash Pass with SHA256 + salt = os.urandom(32) + hash_pass = hash_password(salt, password) + + user = Users( + email=email, + hash_pass=hash_pass, + nickname=nickname, + salt=salt, + is_admin=is_admin + ) + user_inspect = inspect(user) + db.session.add(user) + check_inspect = user_inspect.pending + db.session.commit() + + # Add to logs + if check_inspect: + id_user = user.json()['id'] + message = 'User registered.' + has_succeeded = True + status_code = 0 + else: + id_user = None + message = 'Internal Error: User not registered.' + has_succeeded = False + status_code = 1 + + db_create_log( + ip=ip, + action='register', + message=message, + has_succeeded=has_succeeded, + status_code=status_code, + table='users', + id_user=id_user + ) + if status_code == 0: + return {'status': 0, 'message': message, 'data': user.json()} + elif status_code == 1: + return {'status': 1, 'message': message} + + +def db_user_update(ip, user_id, nickname, password): + user = Users.query.filter( + Users.id == user_id + ).scalar() + if user: + has_succeeded = False + status_code = 2 + if nickname and password: + # Salt Hash Pass with SHA256 + salt = os.urandom(32) + hash_pass = hash_password(salt, password) + Users.query.filter(Users.id == user_id).update({'nickname': nickname, 'hash_pass': hash_pass, 'salt': salt}) + db.session.commit() + message = 'User nickname and password updated.' + has_succeeded = True + status_code = 0 + elif nickname: + Users.query.filter(Users.id == user_id).update({'nickname': nickname}) + db.session.commit() + message = 'User nickname updated.' + has_succeeded = True + status_code = 0 + elif password: + # Salt Hash Pass with SHA256 + salt = os.urandom(32) + hash_pass = hash_password(salt, password) + Users.query.filter(Users.id == user_id).update({'hash_pass': hash_pass, 'salt': salt}) + db.session.commit() + message = 'User password updated.' + has_succeeded = True + status_code = 0 + else: + message = 'Only nickname and/or password can be changed.' + + db_create_log( + ip=ip, + action='user_update', + message=message, + has_succeeded=has_succeeded, + status_code=status_code, + table='users', + id_user=user_id + ) + return {'status': status_code, 'message': message, 'data': user.json()} + else: + message = 'User do not exist.' + db_create_log( + ip=ip, + action='user_update', + message=message, + has_succeeded=False, + status_code=1, + table='users', + id_user=user_id + ) + return {'status': 1, 'message': message} + + +def db_user_delete(ip, user_id): + user_to_delete = Users.query.filter(Users.id == user_id).scalar() + if user_to_delete: + is_admin = bool(user_to_delete.json()['is_admin']) + if is_admin and (Users.query.filter(Users.is_admin == True).count() <= 1 or user_id == 0): + message = 'Can\'t delete last admin' + db_create_log( + ip=ip, + action='user_delete', + message=message, + has_succeeded=False, + status_code=2, + table='users', + id_user=user_id + ) + return {'status': 2, 'message': message} + else: + test = Users.query.filter(Users.id == user_id).delete() + if test == 1: + db.session.commit() + message = 'User deleted.' + db_create_log( + ip=ip, + action='user_delete', + message=message, + has_succeeded=True, + status_code=0, + table='users', + id_user=user_id + ) + return {'status': 0, 'message': message, 'data': None} + else: + message = 'User do not exist.' + db_create_log( + ip=ip, + action='user_delete', + message=message, + has_succeeded=False, + status_code=1, + table='users', + id_user=user_id + ) + return {'status': 1, 'message': message} + else: + message = 'User do not exist.' + db_create_log( + ip=ip, + action='user_delete', + message=message, + has_succeeded=False, + status_code=1, + table='users', + id_user=user_id + ) + return {'status': 1, 'message': message} + + +def db_admin_update_user(ip, user_id, is_admin, password): + user = Users.query.filter( + Users.id == user_id + ).scalar() + if user: + has_succeeded = False + status_code = 2 + if is_admin is not None and password: + # Salt Hash Pass with SHA256 + salt = os.urandom(32) + hash_pass = hash_password(salt, password) + Users.query.filter(Users.id == user_id).update({'is_admin': is_admin, 'hash_pass': hash_pass, 'salt': salt}) + db.session.commit() + message = 'User is_admin and password updated.' + has_succeeded = True + status_code = 0 + elif is_admin is not None: + Users.query.filter(Users.id == user_id).update({'is_admin': is_admin}) + db.session.commit() + message = 'User is_admin updated.' + has_succeeded = True + status_code = 0 + elif password: + # Salt Hash Pass with SHA256 + salt = os.urandom(32) + hash_pass = hash_password(salt, password) + Users.query.filter(Users.id == user_id).update({'hash_pass': hash_pass, 'salt': salt}) + db.session.commit() + message = 'User password updated.' + has_succeeded = True + status_code = 0 + else: + message = 'Only is_admin and/or password can be changed.' + + db_create_log( + ip=ip, + action='user_update', + message=message, + has_succeeded=has_succeeded, + status_code=status_code, + table='users', + id_user=user_id + ) + return {'status': status_code, 'message': message, 'data': user.json()} + else: + message = 'User do not exist.' + db_create_log( + ip=ip, + action='user_update', + message=message, + has_succeeded=False, + status_code=1, + table='users', + id_user=user_id + ) + return {'status': 1, 'message': message} + + +def db_users(ip, user_id, query, by='email,nickname', id=None, is_admin=None, order_by='email,asc'): + # q= or id = + # if q= then by= (default: email,nickname) or email or nickname + # is_admin = + # order_by = (default: email,asc), (nickname,asc), (id,desc), (is_admin,desc) + + users = Users.query + if query is id and query is not None: + message = 'Query or id field' + db_create_log( + ip=ip, + action='users', + message=message, + has_succeeded=False, + status_code=1, + table='users', + id_user=user_id + ) + return {'status': 1, 'message': message} + + if query and by: + if by == 'email': + users = users.filter(Users.email.like('%' + query + '%')) + elif by == 'nickname': + users = users.filter(Users.nickname.like('%' + query + '%')) + else: + users = users.filter(or_(Users.nickname.like('%' + query + '%'), Users.email.like('%' + query + '%'))) + elif query and not by: + users = users.filter(or_(Users.nickname.like('%' + query + '%'), Users.email.like('%' + query + '%'))) + elif id: + users = users.filter(Users.id == id) + else: + pass + + if is_admin is not None: + users = users.filter(Users.is_admin == is_admin) + + order_by = order_by.split(',') + if order_by[0] == 'nickname': + order = Users.nickname + elif order_by[0] == 'id': + order = Users.id + elif order_by[0] == 'is_admin': + order = Users.is_admin + else: + order = Users.email + + if len(order_by) > 1: + if order_by[1] == 'asc': + users = users.order_by(asc(order)) + elif order_by[1] == 'desc': + users = users.order_by(desc(order)) + else: + users = users.order_by(asc(order)) + else: + users = users.order_by(asc(order)) + + users = users.all() + + message = f'query({query}), by({by}), id({id}), is_admin({is_admin}) and order_by({order_by}): {len(users)} result(s)' + db_create_log( + ip=ip, + action='users', + message=message, + has_succeeded=True, + status_code=0, + table='users', + id_user=user_id + ) + return {'status': 0, 'message': message, 'data': [user.json() for user in users]} diff --git a/backend/application/logs_model.py b/backend/application/logs_model.py new file mode 100644 index 0000000..53ac617 --- /dev/null +++ b/backend/application/logs_model.py @@ -0,0 +1,41 @@ +from . import db + + +class Logs(db.Model): + __bind_key__ = 'db-logs' + + id = db.Column(db.Integer, primary_key=True) + date = db.Column(db.TIMESTAMP(), nullable=False) + id_user = db.Column(db.Integer, nullable=True) + ip = db.Column(db.String(), nullable=False) + table = db.Column(db.String(), nullable=False) + action = db.Column(db.String(), nullable=False) + message = db.Column(db.String(), nullable=False) + has_succeeded = db.Column(db.Boolean, nullable=False) + status_code = db.Column(db.Integer, nullable=False) + + def __init__(self, date, id_user, ip, table, action, message, has_succeeded, status_code): + self.date = date + self.id_user = id_user + self.ip = ip + self.table = table + self.action = action + self.message = message + self.has_succeeded = has_succeeded + self.status_code = status_code + + def __repr__(self): + return { + 'id': self.id, + 'date': self.date, + 'id_user': self.id_user, + 'ip': self.ip, + 'table': self.table, + 'action': self.action, + 'message': self.message, + 'has_succeeded': self.has_succeeded, + 'status_code': self.status_code + } + + def json(self): + return self.__repr__() diff --git a/backend/application/responses.py b/backend/application/responses.py new file mode 100644 index 0000000..03e11a6 --- /dev/null +++ b/backend/application/responses.py @@ -0,0 +1,35 @@ +from flask import current_app as app +import json + + +def send_error(status_code, message, token=None): + data_json = { + 'status': 'error', + 'message': message + } + res = app.response_class( + response=json.dumps(data_json), + status=status_code, + mimetype='application/json' + ) + if token is not None: + res.set_cookie('SESSIONID', token) + return res + + +def send_message(message, data, token=None, token_delete=False): + data_json = { + 'status': 'success', + 'message': message, + 'data': data + } + res = app.response_class( + response=json.dumps(data_json), + status=200, + mimetype='application/json' + ) + if token is not None: + res.set_cookie('SESSIONID', token) + if token_delete: + res.delete_cookie('SESSIONID') + return res diff --git a/backend/application/routes.py b/backend/application/routes.py new file mode 100644 index 0000000..70d5160 --- /dev/null +++ b/backend/application/routes.py @@ -0,0 +1,345 @@ +from flask import request, Blueprint +from werkzeug.exceptions import HTTPException +from .responses import send_message, send_error +from .api_functions import db_login, db_register, db_user_update, db_create_log, db_user_delete, db_admin_update_user, db_users +from .sessionJWT import create_auth_token, check_auth_token + +bp = Blueprint('myapp', __name__) + + +@bp.app_errorhandler(HTTPException) +def handle_exception(e): + return send_error(e.code, e.name) + + +# Login +@bp.route('/api/login', methods=['POST']) +def login(): + post_json = request.json + try: + post_email = str(post_json['email']) + post_password = str(post_json['password']) + if post_email != '' and post_password != '': + ip = request.remote_addr + res = db_login(ip, post_email, post_password) + if res['status'] == 0: + user = res['data'] + token = create_auth_token(user) + return send_message(res['message'], user, token) + elif res['status'] == 1: + user = None + token = create_auth_token(user) + return send_error(400, res['message'], token) + else: + return send_error(400, 'Empty email and/or password fields.') + except KeyError as e: + return send_error(400, 'Need email, password fields.') + + +# Register +@bp.route('/api/register', methods=['POST']) +def register(): + post_json = request.json + try: + post_email = str(post_json['email']) + post_nickname = str(post_json['nickname']) + post_password = str(post_json['password']) + if post_email != '' and post_password != '' and post_nickname != '': + ip = request.remote_addr + res = db_register(ip, post_email, post_nickname, post_password) + if res['status'] == 1: + return send_error(500, res['message']) + elif res['status'] == 0: + return send_message(res['message'], res['data']) + else: + return send_error(400, 'Empty email and/or password and/or nickname fields.') + except KeyError as e: + return send_error(400, 'Need ' + str(e) + 'field.') + + +# Logout +@bp.route('/api/logout', methods=['DELETE']) +def logout(): + token = check_auth_token(request) + if token['success']: + ip = request.remote_addr + message = 'User disconnected.' + db_create_log( + ip=ip, + action='logout', + message=message, + has_succeeded=True, + status_code=0, + table='users', + id_user=token['payload']['id'] + ) + return send_message(message, None, token_delete=True) + else: + return send_error(500, token['message']) + + +# Update User (Nickname, Password) +@bp.route('/api/user/update', methods=['PUT']) +def user_update(): + token = check_auth_token(request) + if token['success']: + post_json = request.json + post_nickname = None + post_password = None + fields = '' + if 'nickname' in post_json: + post_nickname = str(post_json['nickname']) + else: + fields += 'nickname ' + + if 'password' in post_json: + post_password = str(post_json['password']) + else: + fields += 'password ' + + if post_nickname is not None or post_password is not None: + if post_nickname != '' and post_password != '': + ip = request.remote_addr + user_id = token['payload']['id'] + res = db_user_update(ip, user_id, post_nickname, post_password) + if res['status'] == 1: + return send_error(500, res['message']) + elif res['status'] == 0: + return send_message(res['message'], res['data']) + else: + return send_error(400, 'Empty nickname and/or password fields.') + else: + return send_error(400, 'Need ' + fields + 'field.') + else: + return send_error(500, token['message']) + + +# Delete User +@bp.route('/api/user/delete', methods=['DELETE']) +def user_delete(): + token = check_auth_token(request) + if token['success']: + ip = request.remote_addr + user_id = token['payload']['id'] + res = db_user_delete(ip, user_id) + if res['status'] != 0: + return send_error(500, res['message']) + else: + db_create_log( + ip=ip, + action='delete', + message='User deleted.', + has_succeeded=True, + status_code=0, + table='users', + id_user=token['payload']['id'] + ) + return send_message(res['message'], None, token_delete=True) + else: + return send_error(500, token['message']) + + +# Admin : Create User +@bp.route('/api/admin/create/user', methods=['POST']) +def admin_create_user(): + token = check_auth_token(request) + if token['success']: + ip = request.remote_addr + user_id = token['payload']['id'] + is_admin = token['payload']['is_admin'] + if is_admin: + post_json = request.json + post_email = None + post_nickname = None + post_password = None + post_is_admin = None + fields = '' + if 'email' in post_json: + post_email = str(post_json['email']) + else: + fields += 'email ' + + if 'nickname' in post_json: + post_nickname = str(post_json['nickname']) + else: + fields += 'nickname ' + + if 'password' in post_json: + post_password = str(post_json['password']) + else: + fields += 'password ' + + if 'is_admin' in post_json: + post_is_admin = bool(post_json['is_admin']) + else: + fields += 'is_admin ' + + if post_email is not None or post_nickname is not None or post_password is not None or post_is_admin is not None: + if post_email != '' and post_nickname != '' and post_password != '' and str(post_is_admin) != '': + res = db_register(ip, post_email, post_nickname, post_password, is_admin=post_is_admin) + if res['status'] == 1: + db_create_log( + ip=ip, + action='admin/create/user', + message=res['message'], + has_succeeded=False, + status_code=res['status'], + table='users', + id_user=user_id + ) + return send_error(500, res['message']) + elif res['status'] == 0: + db_create_log( + ip=ip, + action='admin/create/user', + message=res['message'], + has_succeeded=True, + status_code=res['status'], + table='users', + id_user=user_id + ) + return send_message(res['message'], res['data']) + else: + return send_error(400, 'Empty email and/or nickname and/or password and/or is_admin fields.') + else: + return send_error(400, 'Need ' + fields + 'field.') + else: + return send_error(500, 'User does not have permission.') + else: + return send_error(500, token['message']) + + +# Admin : Change User password and/or role +@bp.route('/api/admin/update/user', methods=['PUT']) +def admin_update_user(): + token = check_auth_token(request) + if token['success']: + user_id = token['payload']['id'] + is_admin = token['payload']['is_admin'] + if is_admin: + post_json = request.json + post_is_admin = None + post_password = None + post_user_id_delete = None + fields = '' + if 'id' in post_json: + post_user_id_delete = int(post_json['id']) + else: + fields += 'id ' + + if 'is_admin' in post_json: + post_is_admin = bool(post_json['is_admin']) + else: + fields += 'is_admin ' + + if 'password' in post_json: + post_password = str(post_json['password']) + else: + fields += 'password ' + + if post_user_id_delete is not None and (post_is_admin is not None or post_password is not None): + if str(post_is_admin) != '' and post_password != '' and str(post_user_id_delete) != '': + ip = request.remote_addr + res = db_admin_update_user(ip, post_user_id_delete, post_is_admin, post_password) + if res['status'] == 1: + db_create_log( + ip=ip, + action='admin/update/user', + message=res['message'], + has_succeeded=False, + status_code=res['status'], + table='users', + id_user=user_id + ) + return send_error(500, res['message']) + elif res['status'] == 0: + db_create_log( + ip=ip, + action='admin/update/user', + message=res['message'], + has_succeeded=True, + status_code=res['status'], + table='users', + id_user=user_id + ) + return send_message(res['message'], res['data']) + else: + return send_error(400, 'Empty is_admin and/or password fields.') + else: + return send_error(400, 'Need ' + fields + 'field.') + else: + return send_error(500, 'User does not have permission.') + else: + return send_error(500, token['message']) + + +# Admin : Delete User +@bp.route('/api/admin/delete/user/', methods=['DELETE']) +def admin_delete_user(id): + token = check_auth_token(request) + if token['success']: + ip = request.remote_addr + user_id = token['payload']['id'] + is_admin = token['payload']['is_admin'] + if is_admin: + post_json = {'id': id} + post_user_id_delete = None + fields = '' + if 'id' in post_json: + post_user_id_delete = int(post_json['id']) + else: + fields += 'id' + if post_user_id_delete is not None: + if str(post_user_id_delete) != '': + res = db_user_delete(ip, int(post_user_id_delete)) + if res['status'] == 1: + db_create_log( + ip=ip, + action='admin/delete/user', + message=res['message'], + has_succeeded=False, + status_code=res['status'], + table='users', + id_user=user_id + ) + return send_error(500, res['message']) + else: + db_create_log( + ip=ip, + action='admin/delete/user', + message=res['message'], + has_succeeded=True, + status_code=res['status'], + table='users', + id_user=user_id + ) + return send_message(res['message'], None) + else: + return send_error(400, 'Empty id field.') + else: + return send_error(400, 'Need ' + fields + 'field.') + else: + return send_error(500, 'User does not have permission.') + else: + return send_error(500, token['message']) + + +# List of User (must be authenticated) & Search +@bp.route('/api/users', methods=['GET']) +def users(): + token = check_auth_token(request) + if token['success']: + ip = request.remote_addr + user_id = token['payload']['id'] + get_query = request.args.get('q') + get_by = request.args.get('by') + get_id = request.args.get('id') + get_is_admin = request.args.get('is_admin') + get_order_by = request.args.get('order_by') + res = db_users(ip, user_id, get_query, get_by, get_id, get_is_admin, get_order_by) + if res['status'] == 1: + return send_error(500, res['message']) + else: + return send_message(res['message'], res['data']) + else: + return send_error(500, token['message']) diff --git a/backend/application/sessionJWT.py b/backend/application/sessionJWT.py new file mode 100644 index 0000000..ef228fb --- /dev/null +++ b/backend/application/sessionJWT.py @@ -0,0 +1,39 @@ +from datetime import datetime, timedelta +from flask import current_app as app +import jwt + + +def create_auth_token(user, time_second=1800): + try: + time = datetime.now() + payload = { + 'exp': time + timedelta(days=0, seconds=time_second), + 'iat': time, + 'user': user + } + return jwt.encode( + payload, + app.config.get('SECRET_KEY'), + algorithm='HS256' + ) + except Exception as e: + return e + + +def decode_auth_token(auth_token): + try: + payload = jwt.decode( + auth_token, + app.config.get('SECRET_KEY'), + algorithms='HS256' + ) + return {'success': True, 'payload': payload['user']} + except jwt.ExpiredSignatureError: + return {'success': False, 'message': 'Signature expired . Please log in again.'} + except jwt.InvalidTokenError as e: + return {'success': False, 'message': 'User not authenticated.'} + + +def check_auth_token(request): + token = request.cookies.get('SESSIONID') + return decode_auth_token(token) diff --git a/backend/application/users_model.py b/backend/application/users_model.py new file mode 100644 index 0000000..b932328 --- /dev/null +++ b/backend/application/users_model.py @@ -0,0 +1,37 @@ +from . import db + + +class Users(db.Model): + __bind_key__ = 'db-users' + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + email = db.Column(db.String(), nullable=False, unique=True) + nickname = db.Column(db.String(), nullable=False) + hash_pass = db.Column(db.LargeBinary(), nullable=False) + salt = db.Column(db.LargeBinary(), nullable=False) + is_admin = db.Column(db.Boolean, default=False, nullable=False) + + def __init__(self, email, nickname, hash_pass, salt, is_admin): + self.email = email + self.hash_pass = hash_pass + self.nickname = nickname + self.salt = salt + self.is_admin = is_admin + + def __repr__(self): + return { + 'id': self.id, + 'email': self.email, + 'nickname': self.nickname, + 'hash_pass': self.hash_pass, + 'salt': self.salt, + 'is_admin': self.is_admin + } + + def json(self): + return { + 'id': self.id, + 'email': self.email, + 'nickname': self.nickname, + 'is_admin': self.is_admin + } diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..ae0e613 --- /dev/null +++ b/backend/config.py @@ -0,0 +1,43 @@ +import os + +basedir = os.path.abspath(os.path.dirname(__file__)) + + +class Config(object): + DEBUG = False + TESTING = False + CSRF_ENABLED = True + + FLASK_APP = os.environ.get('FLASK_APP', None) + FLASK_ENV = os.environ.get('FLASK_ENV', None) + + SQLALCHEMY_ECHO = False + SQLALCHEMY_TRACK_MODIFICATIONS = False + SQLALCHEMY_DATABASE_URI_1 = os.environ.get('DATABASE_URL_USERS', None) + SQLALCHEMY_DATABASE_URI_2 = os.environ.get('DATABASE_URL_LOGS', None) + SQLALCHEMY_BINDS = { + 'db-users': SQLALCHEMY_DATABASE_URI_1, + 'db-logs': SQLALCHEMY_DATABASE_URI_2 + } + + SECRET_KEY = os.environ.get('SECRET_KEY', 'default_secret_key') + ALLOW_ORIGIN = os.environ.get('ALLOW_ORIGIN', None) + + +class ProductionConfig(Config): + DEBUG = False + SQLALCHEMY_ECHO = False + SQLALCHEMY_TRACK_MODIFICATIONS = False + + +class TestingConfig(Config): + TESTING = True + SQLALCHEMY_ECHO = False + SQLALCHEMY_TRACK_MODIFICATIONS = False + + +class DevelopmentConfig(Config): + DEVELOPMENT = True + DEBUG = True + SQLALCHEMY_ECHO = True + SQLALCHEMY_TRACK_MODIFICATIONS = True diff --git a/backend/env.sh b/backend/env.sh new file mode 100755 index 0000000..da2fa7f --- /dev/null +++ b/backend/env.sh @@ -0,0 +1,6 @@ +export FLASK_APP=app.py +export FLASK_ENV=development +export FLASK_DEBUG=1 +export PYTHONUNBUFFERED=1 +export DATABASE_URL_USERS=postgresql://flaskaled1:aled1@localhost:5433/flaskaledDb1 +export DATABASE_URL_LOGS=postgresql://flaskaled2:aled2@localhost:5434/flaskaledDb2 diff --git a/backend/fictive_users.py b/backend/fictive_users.py new file mode 100644 index 0000000..6f97529 --- /dev/null +++ b/backend/fictive_users.py @@ -0,0 +1,47 @@ +import os +from application.users_model import Users +from application.api_functions import hash_password + +TAB_USER_WITH_PASSWORD = [ + { + "id": 1, + "email": "riri@gmail.com", + "nickname": "Riri", + "password": "ririPass", + "is_admin": False + }, + { + "id": 2, + "email": "fifi@gmail.com", + "nickname": "Fifi", + "password": "fifiPass", + "is_admin": False + }, + { + "id": 3, + "email": "donald@gmail.com", + "nickname": "Donald", + "password": "donaldPass", + "is_admin": False + }, + { + "id": 4, + "email": "daisy@gmail.com", + "nickname": "Daisy", + "password": "daisyPass", + "is_admin": True + }, +] + + +# Convert user with passord (uwp) to user +def uwp_to_user(uwp): + salt0 = os.urandom(32) + hash_pass0 = hash_password(salt0, uwp["password"]) + return Users( + email=uwp["email"], + nickname=uwp["nickname"], + hash_pass=hash_pass0, + salt=salt0, + is_admin=uwp["is_admin"] + ) diff --git a/backend/init-db1.sql b/backend/init-db1.sql new file mode 100755 index 0000000..bbec183 --- /dev/null +++ b/backend/init-db1.sql @@ -0,0 +1,13 @@ +-- Table: users + +CREATE TABLE IF NOT EXISTS users +( + id serial PRIMARY KEY, + email character varying(320) NOT NULL, + nickname character varying(50) NOT NULL, + hash_pass bytea NOT NULL, + salt bytea NOT NULL, + is_admin boolean NOT NULL DEFAULT FALSE +); + +INSERT INTO users VALUES(0,'admin@admin.admin','Admin',decode('e5ed79b503704ed20a1d250770db68182118de7fe0236db9bbfb0dd9684087d6', 'hex'),decode('7012f69f1ac7c23c5dca498c30fa94527b507cc9e40fab9bae284d1465a37724', 'hex'),TRUE); \ No newline at end of file diff --git a/backend/init-db2.sql b/backend/init-db2.sql new file mode 100644 index 0000000..02ebdc9 --- /dev/null +++ b/backend/init-db2.sql @@ -0,0 +1,14 @@ +-- Table: logs + +CREATE TABLE IF NOT EXISTS logs +( + id serial PRIMARY KEY, + date timestamp NOT NULL, + id_user integer, + ip character varying(15) NOT NULL, + "table" character varying(25) NOT NULL, + action character varying(50) NOT NULL, + message character varying(512) NOT NULL, + has_succeeded boolean NOT NULL, + status_code smallint NOT NULL +); \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..6317cbe --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,13 @@ +alembic==1.7.5 +Flask==2.0.2 +Flask-Migrate==3.1.0 +Flask-Script==2.0.6 +Flask-Testing==0.8.1 +Flask-SQLAlchemy==2.5.1 +Flask-WTF==0.15.1 +pipreqs==0.4.10 +PyJWT==2.3.0 +pytest==6.2.5 +SQLAlchemy==1.4.27 +psycopg2==2.9.2 +Flask-Cors==3.0.10 \ No newline at end of file diff --git a/backend/test.py b/backend/test.py new file mode 100644 index 0000000..2aa595f --- /dev/null +++ b/backend/test.py @@ -0,0 +1,542 @@ +import unittest +from flask_testing import TestCase +from fictive_users import TAB_USER_WITH_PASSWORD, uwp_to_user +from application import db, create_app + + +class BaseTestCase(TestCase): + + def create_app(self): + app = create_app('testing') + return app + + def setUp(self): + db.drop_all() + db.create_all() + for uwp in TAB_USER_WITH_PASSWORD: + db.session.add(uwp_to_user(uwp)) + db.session.commit() + + def tearDown(self): + db.session.remove() + db.drop_all() + + +class FlaskTestCase(BaseTestCase): + + # -- UTILS --- + + def login(self, email, password): + data0 = { + "email": email, + "password": password + } + response = self.client.post('/api/login', json=data0) + return response + + # --- LOGIN --- + + def test_login_NoFields_statusCode(self): + response = self.client.post('/api/login', json={}) + self.assertEqual(response.status_code, 400) + + def test_login_NoFields_message(self): + response = self.client.post('/api/login', json={}) + self.assertEqual(response.json['message'], 'Need email, password fields.') + + def test_login_emptyFields_statusCode(self): + data0 = { + "email": "", + "password": "blabla" + } + response = self.client.post('/api/login', json=data0) + self.assertEqual(response.status_code, 400) + + def test_login_emptyFields_message(self): + data0 = { + "email": "", + "password": "blabla" + } + response = self.client.post('/api/login', json=data0) + self.assertEqual(response.json['message'], 'Empty email and/or password fields.') + + def test_login_wrongFields_statusCode(self): + data0 = { + "email": "nimp@gmail.com", + "password": "nimp" + } + response = self.client.post('/api/login', json=data0) + self.assertEqual(response.status_code, 400) + + def test_login_wrongFields_message(self): + data0 = { + "email": "nimp@gmail.com", + "password": "nimp" + } + response = self.client.post('/api/login', json=data0) + self.assertEqual(response.json['message'], 'Email or password invalid') + + def test_login_success_statusCode(self): + data0 = { + "email": "riri@gmail.com", + "password": "ririPass" + } + response = self.client.post('/api/login', json=data0) + self.assertEqual(response.status_code, 200) + + def test_login_success_message(self): + data0 = { + "email": "riri@gmail.com", + "password": "ririPass" + } + response = self.client.post('/api/login', json=data0) + self.assertEqual(response.json['message'], 'User authenticated.') + + # --- REGISTER --- + + def test_register_noFields_statusCode(self): + response = self.client.post('/api/register', json={}) + self.assertEqual(response.status_code, 400) + + def test_register_noFields_message(self): + response = self.client.post('/api/register', json={}) + self.assertIn('Need', response.json['message']) + + def test_register_emptyFields_statusCode(self): + data0 = { + "email": "", + "password": "blabla", + "nickname": "blabla" + } + response = self.client.post('/api/register', json=data0) + self.assertEqual(response.status_code, 400) + + def test_register_emptyFields_message(self): + data0 = { + "email": "", + "password": "blabla", + "nickname": "blabla" + } + response = self.client.post('/api/register', json=data0) + self.assertEqual(response.json['message'], 'Empty email and/or password and/or nickname fields.') + + def test_register_alreadyExist_statusCode(self): + data0 = { + "email": "riri@gmail.com", + "password": "blabla", + "nickname": "blabla" + } + response = self.client.post('/api/register', json=data0) + self.assertEqual(response.status_code, 500) + + def test_register_alreadyExist_statusCode(self): + data0 = { + "email": "riri@gmail.com", + "password": "blabla", + "nickname": "blabla" + } + response = self.client.post('/api/register', json=data0) + self.assertIn('already exist', response.json['message']) + + def test_register_success_statusCode(self): + data0 = { + "email": "loulou@gmail.com", + "password": "loulouPass", + "nickname": "Loulou" + } + response = self.client.post('/api/register', json=data0) + self.assertEqual(response.status_code, 200) + + def test_register_success_message(self): + data0 = { + "email": "loulou@gmail.com", + "password": "loulouPass", + "nickname": "Loulou" + } + response = self.client.post('/api/register', json=data0) + self.assertEqual(response.json['message'], 'User registered.') + + # --- LOGOUT --- + + def test_logout_fail_(self): + response = self.client.delete('/api/logout') + self.assertEqual(response.status_code, 500) + + def test_logout_success(self): + response = self.login("riri@gmail.com", "ririPass") + self.assertEqual(response.status_code, 200) + response = self.client.delete('/api/logout') + self.assertEqual(response.status_code, 200) + + # --- USER/UPDATE --- + + def test_userUpdate_notConnected_statusCode(self): + response = self.client.put('/api/user/update', json={}) + self.assertEqual(response.status_code, 500) + + def test_userUpdate_notConnected_message(self): + response = self.client.put('/api/user/update', json={}) + self.assertEqual(response.json['message'], 'User not authenticated.') + + def test_userUpdate_noFields_statusCode(self): + response = self.login("riri@gmail.com", "ririPass") + self.assertEqual(response.status_code, 200) + response = self.client.put('/api/user/update', json={}) + self.assertEqual(response.status_code, 400) + + def test_userUpdate_noFields_message(self): + response = self.login("riri@gmail.com", "ririPass") + self.assertEqual(response.status_code, 200) + response = self.client.put('/api/user/update', json={}) + self.assertIn('Need', response.json['message']) + + def test_userUpdate_emptyFields_statusCode(self): + response = self.login("riri@gmail.com", "ririPass") + self.assertEqual(response.status_code, 200) + data0 = { + "nickname": "", + "password": "blabla" + } + response = self.client.put('/api/user/update', json=data0) + self.assertEqual(response.status_code, 400) + + def test_userUpdate_emptyFields_message(self): + response = self.login("riri@gmail.com", "ririPass") + self.assertEqual(response.status_code, 200) + data0 = { + "nickname": "", + "password": "blabla" + } + response = self.client.put('/api/user/update', json=data0) + self.assertEqual(response.json['message'], 'Empty nickname and/or password fields.') + + def test_self_update_success_statusCode(self): + response = self.login("riri@gmail.com", "ririPass") + self.assertEqual(response.status_code, 200) + data0 = { + "nickname": "Ririri", + "password": "ririPass" + } + response = self.client.put('/api/user/update', json=data0) + self.assertEqual(response.status_code, 200) + + # --- USER/DELETE --- + + def test_userDelete_notConnected_statusCode(self): + response = self.client.delete('/api/user/delete') + self.assertEqual(response.status_code, 500) + + def test_userDelete_notConnected_message(self): + response = self.client.delete('/api/user/delete') + self.assertEqual(response.json['message'], 'User not authenticated.') + + def test_userDelete_success_statusCode(self): + response = self.login('riri@gmail.com', 'ririPass') + self.assertEqual(response.status_code, 200) + response = self.client.delete('/api/user/delete') + self.assertEqual(response.status_code, 200) + + def test_userDelete_success_message(self): + response = self.login('riri@gmail.com', 'ririPass') + self.assertEqual(response.status_code, 200) + response = self.client.delete('/api/user/delete') + self.assertEqual(response.json['message'], 'User deleted.') + + def test_userDelete_lastAdmin_statusCode(self): + response = self.login('donald@gmail.com', 'donaldPass') + self.assertEqual(response.status_code, 200) + response = self.client.delete('/api/user/delete') + self.assertEqual(response.status_code, 200) + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + response = self.client.delete('/api/user/delete') + self.assertEqual(response.status_code, 500) + + def test_userDelete_lastAdmin_message(self): + response = self.login('donald@gmail.com', 'donaldPass') + self.assertEqual(response.status_code, 200) + response = self.client.delete('/api/user/delete') + self.assertEqual(response.status_code, 200) + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + response = self.client.delete('/api/user/delete') + self.assertEqual(response.json['message'], 'Can\'t delete last admin') + + # --- ADMIN/CREATE/USER --- + + def test_adminCreate_notConnected_statusCode(self): + response = self.client.post('/api/admin/create/user', json={}) + self.assertEqual(response.status_code, 500) + + def test_adminCreate_notConnected_message(self): + response = self.client.post('/api/admin/create/user', json={}) + self.assertEqual(response.json['message'], 'User not authenticated.') + + def test_adminCreate_noPermission_statusCode(self): + response = self.login('riri@gmail.com', 'ririPass') + self.assertEqual(response.status_code, 200) + response = self.client.post('/api/admin/create/user', json={}) + self.assertEqual(response.status_code, 500) + + def test_adminCreate_noPermission_message(self): + response = self.login('riri@gmail.com', 'ririPass') + self.assertEqual(response.status_code, 200) + response = self.client.post('/api/admin/create/user', json={}) + self.assertEqual(response.json['message'], 'User does not have permission.') + + def test_adminCreate_noFields_statusCode(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + response = self.client.post('/api/admin/create/user', json={}) + self.assertEqual(response.status_code, 400) + + def test_adminCreate_noFields_message(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + response = self.client.post('/api/admin/create/user', json={}) + self.assertIn('Need', response.json['message']) + + def test_adminCreate_emptyFields_statusCode(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + data0 = { + "email": "", + "nickname": "Mickey", + "password": "mickeyPass", + "is_admin": True, + } + response = self.client.post('/api/admin/create/user', json=data0) + self.assertEqual(response.status_code, 400) + + def test_adminCreate_emptyFields_message(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + data0 = { + "email": "", + "nickname": "Mickey", + "password": "mickeyPass", + "is_admin": True, + } + response = self.client.post('/api/admin/create/user', json=data0) + self.assertEqual(response.json['message'], + 'Empty email and/or nickname and/or password and/or is_admin fields.') + + def test_adminCreate_alreadyExist_statusCode(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + data0 = { + "email": "riri@gmail.com", + "passord": "blabla", + "nickname": "blabla", + } + response = self.client.post('/api/admin/create/user', json=data0) + self.assertEqual(response.status_code, 500) + + def test_adminCreate_alreadyExist_message(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + data0 = { + "email": "riri@gmail.com", + "passord": "blabla", + "nickname": "blabla", + } + response = self.client.post('/api/admin/create/user', json=data0) + self.assertIn('already exist', response.json['message']) + + def test_adminCreate_success_statusCode(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + data0 = { + "email": "mickey@gmail.com", + "nickname": "Mickey", + "password": "mickeyPass", + "is_admin": True, + } + response = self.client.post('/api/admin/create/user', json=data0) + self.assertEqual(response.status_code, 200) + + def test_adminCreate_success_message(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + data0 = { + "email": "mickey@gmail.com", + "nickname": "Mickey", + "password": "mickeyPass", + "is_admin": True, + } + response = self.client.post('/api/admin/create/user', json=data0) + self.assertEqual(response.json['message'], 'User registered.') + + # --- ADMIN/UPDATE/USER --- + + def test_adminUpdate_notConnected_statusCode(self): + response = self.client.put('/api/admin/update/user', json={}) + self.assertEqual(response.status_code, 500) + + def test_adminUpdate_notConnected_message(self): + response = self.client.put('/api/admin/update/user', json={}) + self.assertEqual(response.json['message'], 'User not authenticated.') + + def test_adminUpdate_noPermission_statusCode(self): + response = self.login('riri@gmail.com', 'ririPass') + self.assertEqual(response.status_code, 200) + response = self.client.put('/api/admin/update/user', json={}) + self.assertEqual(response.status_code, 500) + + def test_adminUpdate_noPermission_message(self): + response = self.login('riri@gmail.com', 'ririPass') + self.assertEqual(response.status_code, 200) + response = self.client.put('/api/admin/update/user', json={}) + self.assertEqual(response.json['message'], 'User does not have permission.') + + def test_adminUpdate_noFields_statusCode(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + response = self.client.put('/api/admin/update/user', json={}) + self.assertEqual(response.status_code, 400) + + def test_adminUpdate_noFields_message(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + response = self.client.put('/api/admin/update/user', json={}) + self.assertIn('Need', response.json['message']) + + def test_adminUpdate_emptyFields_statusCode(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + data0 = { + "id": 1, + "password": "", + "is_admin": False, + } + response = self.client.put('/api/admin/update/user', json=data0) + self.assertEqual(response.status_code, 400) + + def test_adminUpdate_emptyFields_message(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + data0 = { + "id": 1, + "password": "", + "is_admin": False, + } + response = self.client.put('/api/admin/update/user', json=data0) + self.assertEqual(response.json['message'], 'Empty is_admin and/or password fields.') + + def test_adminUpdate_notExists_statusCode(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + data0 = { + "id": 99, + "password": "blabla", + "is_admin": False + } + response = self.client.put('/api/admin/update/user', json=data0) + self.assertEqual(response.status_code, 500) + + def test_adminUpdate_notExists_message(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + data0 = { + "id": 99, + "password": "blabla", + "is_admin": False + } + response = self.client.put('/api/admin/update/user', json=data0) + self.assertEqual(response.json['message'], 'User do not exist.') + + def test_adminUpdate_success_message(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + data0 = { + "id": 1, + "password": "roroPass", + "is_admin": False, + } + response = self.client.put('/api/admin/update/user', json=data0) + self.assertEqual(response.status_code, 200) + + def test_adminUpdate_success_statusCode(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + data0 = { + "id": 1, + "password": "roroPass", + "is_admin": False, + } + response = self.client.put('/api/admin/update/user', json=data0) + self.assertIn("updated", response.json['message']) + + # --- ADMIN/DELETE/USER --- + + def test_adminDelete_notConnected_statusCode(self): + response = self.client.delete('/api/admin/delete/user/1') + self.assertEqual(response.status_code, 500) + + def test_adminDelete_notConnected_message(self): + response = self.client.delete('/api/admin/delete/user/1') + self.assertEqual(response.json['message'], 'User not authenticated.') + + def test_adminDelete_noPermission_statusCode(self): + response = self.login('riri@gmail.com', 'ririPass') + self.assertEqual(response.status_code, 200) + response = self.client.delete('/api/admin/delete/user/1') + self.assertEqual(response.status_code, 500) + + def test_adminDelete_noPermission_message(self): + response = self.login('riri@gmail.com', 'ririPass') + self.assertEqual(response.status_code, 200) + response = self.client.delete('/api/admin/delete/user/1') + self.assertEqual(response.json['message'], 'User does not have permission.') + + def test_adminDelete_noFields_statusCode(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + response = self.client.delete('/api/admin/delete/user/') + self.assertEqual(response.status_code, 404) + + def test_adminDelete_no_fields(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + response = self.client.delete('/api/admin/delete/user') + self.assertEqual('Not Found', response.json['message']) + + def test_adminDelete_notExists_statusCode(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + response = self.client.delete('/api/admin/delete/user/99') + self.assertEqual(response.status_code, 500) + + def test_adminDelete_notExists_message(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + response = self.client.delete('/api/admin/delete/user/99') + self.assertEqual(response.json['message'], 'User do not exist.') + + def test_adminDelete_success_statusCode(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + response = self.client.delete('/api/admin/delete/user/2') + self.assertEqual(response.status_code, 200) + + def test_adminDelete_success_message(self): + response = self.login('daisy@gmail.com', 'daisyPass') + self.assertEqual(response.status_code, 200) + response = self.client.delete('/api/admin/delete/user/2') + self.assertEqual(response.json['message'], 'User deleted.') + + # --- LIST OF USER --- + + def test_listOfUsers_fail(self): + response = self.client.get('/api/users') + self.assertEqual(response.status_code, 500) + + def test_listOfUsers_success(self): + response = self.login('riri@gmail.com', 'ririPass') + self.assertEqual(response.status_code, 200) + response = self.client.get('/api/users?order_by=nickname') + self.assertEqual(response.status_code, 200) + + +if __name__ == '__main__': + unittest.main() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f5519be --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,93 @@ +version: '3.8' + +services: + flaskaled-srv1: + image: postgres:latest + container_name: flaskaled-srv1 + ports: + - "5433:5432" + volumes: + - ./backend/init-db1.sql:/docker-entrypoint-initdb.d/init-db1.sql + environment: + - POSTGRES_HOST=flaskaled-srv1 + - POSTGRES_PORT=5432 + - POSTGRES_DB=flaskaledDb1 + - POSTGRES_USER=flaskaled1 + - POSTGRES_PASSWORD=aled1 + restart: unless-stopped + + flaskaled-srv2: + image: postgres:latest + container_name: flaskaled-srv2 + ports: + - "5434:5432" + volumes: + - ./backend/init-db2.sql:/docker-entrypoint-initdb.d/init-db2.sql + environment: + - POSTGRES_HOST=flaskaled-srv2 + - POSTGRES_PORT=5432 + - POSTGRES_DB=flaskaledDb2 + - POSTGRES_USER=flaskaled2 + - POSTGRES_PASSWORD=aled2 + restart: unless-stopped + + backend: + container_name: backend + build: ./backend + command: python -m flask run --host=0.0.0.0 + ports: + - "5000:5000" + volumes: + - ./backend:/data/backend + depends_on: + - flaskaled-srv1 + - flaskaled-srv2 + links: + - flaskaled-srv1 + - flaskaled-srv2 + environment: + - FLASK_APP=app.py + - FLASK_ENV=development + - FLASK_DEBUG=1 + - PYTHONUNBUFFERED=1 + - DATABASE_URL_USERS=postgresql://flaskaled1:aled1@flaskaled-srv1/flaskaledDb1 + - DATABASE_URL_LOGS=postgresql://flaskaled2:aled2@flaskaled-srv2/flaskaledDb2 + - ALLOW_ORIGIN=frontend + - SECRET_KEY=default_secret_key + + frontend: + container_name: frontend + build: ./frontend + command: npm start + ports: + - "4200:4200" + volumes: + - ./frontend:/data/frontend + - ./frontend/node_modules:/data/frontend/node_modules + depends_on: + - backend + links: + - backend + environment: + - NODE_ENV=development + + test: + container_name: test + build: ./backend + command: python test.py + volumes: + - ./backend:/data/backend + depends_on: + - flaskaled-srv1 + - flaskaled-srv2 + links: + - flaskaled-srv1 + - flaskaled-srv2 + environment: + - FLASK_APP=app.py + - FLASK_ENV=test + - FLASK_DEBUG=0 + - PYTHONUNBUFFERED=1 + - DATABASE_URL_USERS=postgresql://flaskaled1:aled1@flaskaled-srv1/flaskaledDb1 + - DATABASE_URL_LOGS=postgresql://flaskaled2:aled2@flaskaled-srv2/flaskaledDb2 + - SECRET_KEY=default_secret_key diff --git a/frontend/.browserslistrc b/frontend/.browserslistrc new file mode 100644 index 0000000..427441d --- /dev/null +++ b/frontend/.browserslistrc @@ -0,0 +1,17 @@ +# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries + +# For the full list of supported browsers by the Angular framework, please see: +# https://angular.io/guide/browser-support + +# You can see what browsers were selected by your queries by running: +# npx browserslist + +last 1 Chrome version +last 1 Firefox version +last 2 Edge major versions +last 2 Safari major versions +last 2 iOS major versions +Firefox ESR +not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. diff --git a/frontend/.editorconfig b/frontend/.editorconfig new file mode 100644 index 0000000..59d9a3a --- /dev/null +++ b/frontend/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..bac05b4 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,49 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp +/out-tsc +# Only exists if Bazel was run +/bazel-out + +# dependencies +/node_modules + +# profiling files +chrome-profiler-events*.json + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* +.idea/* + +# misc +/.angular/cache +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db + +package-lock.json diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..007013d --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,7 @@ +FROM node:current-slim +WORKDIR /data/frontend +COPY ["package.json", "package-lock.json*", "./"] +RUN npm install -g npm +RUN npm install --NODE_ENV +RUN npm install -g @angular/cli +COPY . . diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..bdce2f4 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,27 @@ +# Frontend + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 12.2.10. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/frontend/angular.json b/frontend/angular.json new file mode 100644 index 0000000..99c397a --- /dev/null +++ b/frontend/angular.json @@ -0,0 +1,113 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "frontend": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + }, + "@schematics/angular:application": { + "strict": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/frontend", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", + "./node_modules/bootstrap/scss/bootstrap.scss", + "src/styles.scss" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "frontend:build:production" + }, + "development": { + "browserTarget": "frontend:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "frontend:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.spec.json", + "karmaConfig": "karma.conf.js", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.scss" + ], + "scripts": [] + } + } + } + } + }, + "defaultProject": "frontend" +} diff --git a/frontend/karma.conf.js b/frontend/karma.conf.js new file mode 100644 index 0000000..3635639 --- /dev/null +++ b/frontend/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage/frontend'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..11c99ee --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,44 @@ +{ + "name": "frontend", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve --host 0.0.0.0", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/animations": "~13.1.1", + "@angular/cdk": "^13.0.2", + "@angular/common": "~13.1.1", + "@angular/compiler": "~13.1.1", + "@angular/core": "~13.1.1", + "@angular/forms": "~13.1.1", + "@angular/material": "^13.0.2", + "@angular/platform-browser": "~13.1.1", + "@angular/platform-browser-dynamic": "~13.1.1", + "@angular/router": "~13.1.1", + "bootstrap": "^5.1.3", + "jquery": "^3.6.0", + "popper": "^1.0.1", + "rxjs": "~6.6.0", + "tslib": "^2.3.0", + "zone.js": "~0.11.4" + }, + "devDependencies": { + "@angular-devkit/build-angular": "~13.1.2", + "@angular/cli": "~13.1.2", + "@angular/compiler-cli": "~13.1.1", + "@types/jasmine": "~3.8.0", + "@types/node": "^12.11.1", + "jasmine-core": "~3.8.0", + "karma": "~6.3.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.0.3", + "karma-jasmine": "~4.0.0", + "karma-jasmine-html-reporter": "~1.7.0", + "typescript": "~4.5.4" + } +} diff --git a/frontend/src/app/admin/myProfil/commentary.txt b/frontend/src/app/admin/myProfil/commentary.txt new file mode 100644 index 0000000..ef51b49 --- /dev/null +++ b/frontend/src/app/admin/myProfil/commentary.txt @@ -0,0 +1,7 @@ +La page "admin/myProfil" contient: + - les informations de l'admin (composant "page-profil") + - un bouton "modifier profil" pour modifier les informations de l'admin (composant "popup-update-profil") + +Cette page est la même que la page de la partie utilisateur. + +Ainsi, on a rangé cette page dans le dossier "common/components/profil". diff --git a/frontend/src/app/admin/userList/page-user-list/page-user-list.component.html b/frontend/src/app/admin/userList/page-user-list/page-user-list.component.html new file mode 100644 index 0000000..483b6db --- /dev/null +++ b/frontend/src/app/admin/userList/page-user-list/page-user-list.component.html @@ -0,0 +1,67 @@ +
+ + + + +
+ +
+ + +
+ + Filter + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Pseudo{{person.nickname}}Email{{person.email}}Rôle{{person.role}}Actions + + +
+ + + + + +
+

diff --git a/frontend/src/app/admin/userList/page-user-list/page-user-list.component.scss b/frontend/src/app/admin/userList/page-user-list/page-user-list.component.scss new file mode 100644 index 0000000..78b21a9 --- /dev/null +++ b/frontend/src/app/admin/userList/page-user-list/page-user-list.component.scss @@ -0,0 +1,18 @@ +mat-paginator, table { + width: 80%; + margin: auto 10%; +} + +.btnContainer { + margin: 50px 10% 20px 0px; + text-align: right; +} + +.btnAjouter { + border: solid 1px black; +} + +.filtre { + text-align: center; + width: 33%; +} diff --git a/frontend/src/app/admin/userList/page-user-list/page-user-list.component.spec.ts b/frontend/src/app/admin/userList/page-user-list/page-user-list.component.spec.ts new file mode 100644 index 0000000..6bad939 --- /dev/null +++ b/frontend/src/app/admin/userList/page-user-list/page-user-list.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PageUserListComponent } from './page-user-list.component'; + +describe('PageAdminComponent', () => { + let component: PageUserListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PageUserListComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PageUserListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/admin/userList/page-user-list/page-user-list.component.ts b/frontend/src/app/admin/userList/page-user-list/page-user-list.component.ts new file mode 100644 index 0000000..6496d7e --- /dev/null +++ b/frontend/src/app/admin/userList/page-user-list/page-user-list.component.ts @@ -0,0 +1,152 @@ +import {AfterViewInit, Component, ViewChild} from '@angular/core'; +import {MatTableDataSource} from "@angular/material/table"; +import {MatSort} from "@angular/material/sort"; +import {MatPaginator} from "@angular/material/paginator"; +import {MatDialog} from "@angular/material/dialog"; +import {PopupCreatePersonComponent} from "../popup-create-person/popup-create-person.component"; +import {MatSnackBar} from "@angular/material/snack-bar"; +import {PopupUpdatePersonAdminComponent} from "../popup-update-person-admin/popup-update-person-admin.component"; +import {PopupDeleteProfilComponent} from "../../../common/components/popup-delete-profil/popup-delete-profil.component"; +import {MessageService} from "../../../common/services/message/message.service"; + + + +@Component({ + selector: 'app-page-user-list', + templateUrl: './page-user-list.component.html', + styleUrls: ['./page-user-list.component.scss'] +}) +export class PageUserListComponent implements AfterViewInit +{ + displayedColumns: string[] = [ "nickname", "email", "role", "actions" ]; + dataSource: MatTableDataSource = new MatTableDataSource(); + @ViewChild(MatSort) sort: MatSort; + @ViewChild(MatPaginator) paginator: MatPaginator; + configSnackBar = { duration: 2000, panelClass: "custom-class" }; + + + constructor( private messageService: MessageService, + public dialog: MatDialog, + private snackBar: MatSnackBar) { } + + + ngAfterViewInit(): void + { + this.messageService + .get('users?order_by=nickname') + .subscribe(retour => this.ngAfterViewInitCallback(retour), err => this.ngAfterViewInitCallback(err)); + } + + + ngAfterViewInitCallback(retour: any): void + { + if(retour.status !== "success") { + console.log(retour); + } + else { + let tabPerson: { id: number, email: string, nickname: string, is_admin: boolean }[] = retour.data; + tabPerson = tabPerson.map( person => { + if(!person.is_admin) return Object.assign(person, {role: "utilisateur"}); + else return Object.assign(person, {role: "admin"}); + }); + this.dataSource = new MatTableDataSource(tabPerson); + this.dataSource.sort = this.sort; + this.dataSource.paginator = this.paginator; + } + } + + + applyFilter(event: Event) + { + const filterValue = (event.target as HTMLInputElement).value; + this.dataSource.filter = filterValue.trim().toLowerCase(); + } + + + // Appuie sur le bouton "create" + onCreate(): void + { + const config = { width: '50%' }; + this.dialog + .open(PopupCreatePersonComponent, config) + .afterClosed() + .subscribe( retour => { + + if((retour === null) || (retour === undefined)) + { + this.snackBar.open( "Opération annulée", "", this.configSnackBar); + } + else { + if(retour.data.is_admin) retour.data.role = "admin" ; + else retour.data.role = "utilisateur" ; + this.dataSource.data.push(retour.data); + this.dataSource.data = this.dataSource.data; + this.dataSource = this.dataSource; + this.snackBar.open( "L'utilisateur a bien été créé ✔", "", this.configSnackBar); + } + }); + } + + + // Appuie sur le bouton "edit" + onUpdate(personToUpdate: any): void + { + const config = { + width: '50%', + data: { person: personToUpdate } + }; + this.dialog + .open(PopupUpdatePersonAdminComponent, config) + .afterClosed() + .subscribe( is_admin => { + + if((is_admin === null) || (is_admin === undefined)) + { + this.snackBar.open("Opération annulée", "", this.configSnackBar); + } + else { + const index = this.dataSource.data.findIndex(elt => (elt.id === personToUpdate.id)); + this.dataSource.data[index].is_admin = is_admin; + if(is_admin) this.dataSource.data[index].role = "admin"; + else this.dataSource.data[index].role = "utilisateur"; + this.snackBar.open("L'utilisateur a bien été modifié ✔", "", this.configSnackBar); + } + + }); + } + + + // Appuie sur le bouton "delete" + onDelete(personToDelete: any): void + { + const config = { + data: { + id: personToDelete.id, + email: personToDelete.email, + me: false, + } + }; + this.dialog + .open(PopupDeleteProfilComponent, config) + .afterClosed() + .subscribe( retour => { + + if((retour === null) || (retour === undefined)) + { + this.snackBar.open("Opération annulée", "", this.configSnackBar); + } + else if(retour.status === "error") + { + this.snackBar.open(retour.error.message, "", this.configSnackBar); + } + else { + const index = this.dataSource.data.findIndex( elt => (elt.id === personToDelete.id)); + this.dataSource.data.splice(index, 1); + this.dataSource.data = this.dataSource.data; + this.dataSource = this.dataSource; + this.snackBar.open("L'utilisateur a bien été supprimé ✔", "", this.configSnackBar); + } + }); + } + +} diff --git a/frontend/src/app/admin/userList/popup-create-person/popup-create-person.component.html b/frontend/src/app/admin/userList/popup-create-person/popup-create-person.component.html new file mode 100644 index 0000000..15a27f5 --- /dev/null +++ b/frontend/src/app/admin/userList/popup-create-person/popup-create-person.component.html @@ -0,0 +1,47 @@ +
+ +

Ajouter un utilisateur

+ + + + + + Pseudo + +
+ + + + Email + +
+ + + + Mot de passe + +
+ + + + Confirmation mot de passe + +
+ + + + Utilisateur   + Admin +

+ + + + +
+ {{errorMessage}} +
+ + + + +
diff --git a/frontend/src/app/admin/userList/popup-create-person/popup-create-person.component.scss b/frontend/src/app/admin/userList/popup-create-person/popup-create-person.component.scss new file mode 100644 index 0000000..a95b14d --- /dev/null +++ b/frontend/src/app/admin/userList/popup-create-person/popup-create-person.component.scss @@ -0,0 +1,13 @@ +.myContainer { + text-align: center; +} + +::ng-deep .mat-radio-inner-circle { + color: black !important; + background-color: black !important; +} + +::ng-deep .mat-radio-outer-circle{ + color: black !important; + border: solid 1px gray !important; +} diff --git a/frontend/src/app/admin/userList/popup-create-person/popup-create-person.component.spec.ts b/frontend/src/app/admin/userList/popup-create-person/popup-create-person.component.spec.ts new file mode 100644 index 0000000..29a8b97 --- /dev/null +++ b/frontend/src/app/admin/userList/popup-create-person/popup-create-person.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PopupCreatePersonComponent } from './popup-create-person.component'; + +describe('PopupCreerUtilisateurComponent', () => { + let component: PopupCreatePersonComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PopupCreatePersonComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PopupCreatePersonComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/admin/userList/popup-create-person/popup-create-person.component.ts b/frontend/src/app/admin/userList/popup-create-person/popup-create-person.component.ts new file mode 100644 index 0000000..11f0b1c --- /dev/null +++ b/frontend/src/app/admin/userList/popup-create-person/popup-create-person.component.ts @@ -0,0 +1,95 @@ +import {Component, Inject} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; +import {CheckEmailService} from "../../../common/services/checkEmail/check-email.service"; +import {MessageService} from "../../../common/services/message/message.service"; + + + +@Component({ + selector: 'app-popup-create-person', + templateUrl: './popup-create-person.component.html', + styleUrls: ['./popup-create-person.component.scss'] +}) +export class PopupCreatePersonComponent +{ + nickname: string = ""; + email: string = ""; + is_admin: boolean = false; + password: string = ""; + + confirmPassword: string = "" ; + hasError: boolean = false; + errorMessage: string = "" ; + + + constructor( public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any, + private checkEmailService: CheckEmailService, + private messageService: MessageService ) { } + + + // Appuie sur le bouton "valider" + onValider(): void + { + this.checkField(); + if(!this.hasError) + { + const data = { + email: this.email, + nickname: this.nickname, + password: this.password, + is_admin: this.is_admin + }; + this.messageService + .post("admin/create/user", data) + .subscribe(ret => this.onValiderCallback(ret), err => this.onValiderCallback(err)); + } + } + + + // Callback de 'onValider' + onValiderCallback(retour: any) + { + if(retour.status !== 'success') + { + console.log(retour); + this.errorMessage = retour.error.message; + this.hasError = true; + } + else + { + this.dialogRef.close(retour); + } + } + + + // Check les champs saisis par l'utilisateur + checkField(): void + { + if(this.nickname.length === 0) { + this.errorMessage = "Veuillez remplir le champ 'pseudo'."; + this.hasError = true; + } + else if(this.email.length === 0) { + this.errorMessage = "Veuillez remplir le champ 'email'."; + this.hasError = true; + } + else if(!this.checkEmailService.isValidEmail(this.email)) { + this.errorMessage = "Email invalide."; + this.hasError = true; + } + else if(this.password.length === 0) { + this.errorMessage = "Veuillez remplir le champ 'mot de passe'."; + this.hasError = true; + } + else if(this.password !== this.confirmPassword) { + this.errorMessage = "Le mot de passe est différent de sa confirmation."; + this.hasError = true; + } + else { + this.errorMessage = "" ; + this.hasError = false; + } + } + +} diff --git a/frontend/src/app/admin/userList/popup-update-person-admin/popup-update-person-admin.component.html b/frontend/src/app/admin/userList/popup-update-person-admin/popup-update-person-admin.component.html new file mode 100644 index 0000000..2c2e58c --- /dev/null +++ b/frontend/src/app/admin/userList/popup-update-person-admin/popup-update-person-admin.component.html @@ -0,0 +1,57 @@ +
+
+ +

Modifier

+ + +
+ + + + Utilisateur   + Admin +

+ + +
+ + +
+ Modifier mot de passe: + +
+ + +
+ + + Nouveau mot de passe + + +
+ + + Confirmation nouveau mot de passe + + +
+

+ + +
+ + +
+ {{errorMessage}} +
+ + +
+ + +
+ +
+
diff --git a/frontend/src/app/admin/userList/popup-update-person-admin/popup-update-person-admin.component.scss b/frontend/src/app/admin/userList/popup-update-person-admin/popup-update-person-admin.component.scss new file mode 100644 index 0000000..1317c7a --- /dev/null +++ b/frontend/src/app/admin/userList/popup-update-person-admin/popup-update-person-admin.component.scss @@ -0,0 +1,37 @@ +.boite { + font-size: small; +} + +h3 { + text-align: center; +} + +button { + font-size: small; +} + +img { + margin: 0px 0px 10px 0px; + width: 5vw; + height: 5vw; + border: solid 2px black; + border-radius: 50%; + font-size: xxx-large; +} + +// ------------------------------------------------------------------------- + +// aura +::ng-deep .mat-checkbox-ripple .mat-ripple-element { + background-color: grey !important; +} + +// contenu coche +::ng-deep .mat-checkbox-checked.mat-accent .mat-checkbox-background { + background-color: black !important; +} + +// indeterminate +::ng-deep .mat-checkbox .mat-checkbox-frame { + background-color: white !important; +} diff --git a/frontend/src/app/admin/userList/popup-update-person-admin/popup-update-person-admin.component.spec.ts b/frontend/src/app/admin/userList/popup-update-person-admin/popup-update-person-admin.component.spec.ts new file mode 100644 index 0000000..431857c --- /dev/null +++ b/frontend/src/app/admin/userList/popup-update-person-admin/popup-update-person-admin.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PopupUpdatePersonAdminComponent } from './popup-update-person-admin.component'; + +describe('PopupUpdatePersonAdminComponent', () => { + let component: PopupUpdatePersonAdminComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PopupUpdatePersonAdminComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PopupUpdatePersonAdminComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/admin/userList/popup-update-person-admin/popup-update-person-admin.component.ts b/frontend/src/app/admin/userList/popup-update-person-admin/popup-update-person-admin.component.ts new file mode 100644 index 0000000..1bf55e4 --- /dev/null +++ b/frontend/src/app/admin/userList/popup-update-person-admin/popup-update-person-admin.component.ts @@ -0,0 +1,87 @@ +import {Component, Inject, OnInit} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; +import {CheckEmailService} from "../../../common/services/checkEmail/check-email.service"; +import {MessageService} from "../../../common/services/message/message.service"; + + + +@Component({ + selector: 'app-popup-update-person-admin', + templateUrl: './popup-update-person-admin.component.html', + styleUrls: ['./popup-update-person-admin.component.scss'] +}) +export class PopupUpdatePersonAdminComponent implements OnInit +{ + id: number = 0; + is_admin: boolean = false; + newPassword: string = ""; + + confirmNewPassword: string = "" ; + changePassword: boolean = false ; + hasError: boolean = false; + errorMessage: string = "" ; + + + constructor( public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any, + private checkEmailService: CheckEmailService, + private messageService: MessageService ) { } + + + ngOnInit(): void + { + this.id = this.data.person.id; + this.is_admin = this.data.person.is_admin; + } + + + // Appuie sur le bouton "valider" + onValider(): void + { + this.checkField(); + if(!this.hasError) + { + let data = {}; + if(this.changePassword) data = { id: this.id, is_admin: this.is_admin, password: this.newPassword } + else data = { id: this.id, is_admin: this.is_admin }; + this.messageService + .put("admin/update/user", data) + .subscribe(ret => this.onValiderCallback(ret), err => this.onValiderCallback(err)); + } + } + + + // Callback de 'onValider' + onValiderCallback(retour: any) + { + if(retour.status !== 'success') + { + console.log(retour); + this.errorMessage = retour.error.message; + this.hasError = true; + } + else + { + this.dialogRef.close(this.is_admin); + } + } + + + // Check les champs saisis par l'utilisateur + checkField(): void + { + if((this.changePassword) && (this.newPassword.length === 0)) { + this.errorMessage = "Veuillez remplir le champ 'mot de passe'"; + this.hasError = true; + } + else if((this.changePassword) && (this.newPassword !== this.confirmNewPassword)) { + this.errorMessage = "Le mot de passe est différent de sa confirmation"; + this.hasError = true; + } + else { + this.errorMessage = "" ; + this.hasError = false; + } + } + +} diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts new file mode 100644 index 0000000..6fd371d --- /dev/null +++ b/frontend/src/app/app-routing.module.ts @@ -0,0 +1,31 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import {PageLoginComponent} from "./login/page-login/page-login.component"; +import {PageRegisterComponent} from "./register/page-register/page-register.component"; +import {PageProfilComponent} from "./common/components/page-profil/page-profil.component"; +import {PageUserListComponent} from "./admin/userList/page-user-list/page-user-list.component"; +import {PageRegistryComponent} from "./user/page-registry/page-registry.component"; +import {UserGuard} from "./common/guards/user/user.guard"; +import {AdminGuard} from "./common/guards/admin/admin.guard"; + +const routes: Routes = [ + + { path: "", component: PageLoginComponent }, + { path: "login", component: PageLoginComponent }, + + { path: "register", component: PageRegisterComponent }, + + { path: "user", component: PageRegistryComponent, canActivate: [UserGuard] }, + { path: "user/registry", component: PageRegistryComponent, canActivate: [UserGuard] }, + { path: "user/myProfil", component: PageProfilComponent, canActivate: [UserGuard] }, + + { path: "admin", component: PageUserListComponent, canActivate: [AdminGuard] }, + { path: "admin/userList", component: PageUserListComponent, canActivate: [AdminGuard] }, + { path: "admin/myProfil", component: PageProfilComponent, canActivate: [AdminGuard] }, +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule] +}) +export class AppRoutingModule { } diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html new file mode 100644 index 0000000..d5d92f3 --- /dev/null +++ b/frontend/src/app/app.component.html @@ -0,0 +1,2 @@ + + diff --git a/frontend/src/app/app.component.scss b/frontend/src/app/app.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/app.component.spec.ts b/frontend/src/app/app.component.spec.ts new file mode 100644 index 0000000..74b5b3e --- /dev/null +++ b/frontend/src/app/app.component.spec.ts @@ -0,0 +1,35 @@ +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + RouterTestingModule + ], + declarations: [ + AppComponent + ], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have as title 'frontend'`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('frontend'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.content span')?.textContent).toContain('frontend app is running!'); + }); +}); diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts new file mode 100644 index 0000000..0c5a793 --- /dev/null +++ b/frontend/src/app/app.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent { + title = 'frontend'; +} diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts new file mode 100644 index 0000000..ebfbd59 --- /dev/null +++ b/frontend/src/app/app.module.ts @@ -0,0 +1,72 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; + +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; +import { PageRegisterComponent } from './register/page-register/page-register.component'; +import {FormsModule} from "@angular/forms"; +import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; +import {MatFormFieldModule} from "@angular/material/form-field"; +import {MatInputModule} from "@angular/material/input"; +import {PageLoginComponent} from "./login/page-login/page-login.component"; +import { NavbarComponent } from './common/components/navbar/navbar.component'; +import {MatButtonModule} from "@angular/material/button"; +import { PageProfilComponent } from './common/components/page-profil/page-profil.component'; +import { PopupUpdateProfilComponent } from './common/components/popup-update-profil/popup-update-profil.component'; +import {MatDividerModule} from "@angular/material/divider"; +import {MatCheckboxModule} from "@angular/material/checkbox"; +import {MatDialogModule} from "@angular/material/dialog"; +import {MatSnackBarModule} from "@angular/material/snack-bar"; +import { PageUserListComponent } from './admin/userList/page-user-list/page-user-list.component'; +import { PopupCreatePersonComponent } from './admin/userList/popup-create-person/popup-create-person.component'; +import {MatTableModule} from "@angular/material/table"; +import {MatPaginatorModule} from "@angular/material/paginator"; +import { PopupConfirmRegisterComponent } from './register/popup-confirm-register/popup-confirm-register.component'; +import {MatIconModule} from "@angular/material/icon"; +import {MatRadioModule} from "@angular/material/radio"; +import { PageRegistryComponent } from './user/page-registry/page-registry.component'; +import { PopupDeleteProfilComponent } from './common/components/popup-delete-profil/popup-delete-profil.component'; +import {MatSortModule} from "@angular/material/sort"; +import { PopupUpdatePersonAdminComponent } from './admin/userList/popup-update-person-admin/popup-update-person-admin.component'; +import {HttpClientModule} from "@angular/common/http"; + + + +@NgModule({ + declarations: [ + AppComponent, + PageLoginComponent, + PageRegisterComponent, + NavbarComponent, + PageProfilComponent, + PopupUpdateProfilComponent, + PageUserListComponent, + PopupCreatePersonComponent, + PopupConfirmRegisterComponent, + PageRegistryComponent, + PopupDeleteProfilComponent, + PopupUpdatePersonAdminComponent + ], + imports: [ + BrowserModule, + AppRoutingModule, + FormsModule, + BrowserAnimationsModule, + HttpClientModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + MatDividerModule, + MatCheckboxModule, + MatDialogModule, + MatSnackBarModule, + MatTableModule, + MatPaginatorModule, + MatSortModule, + MatIconModule, + MatRadioModule + ], + providers: [], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/frontend/src/app/common/components/navbar/navbar.component.html b/frontend/src/app/common/components/navbar/navbar.component.html new file mode 100644 index 0000000..2d09333 --- /dev/null +++ b/frontend/src/app/common/components/navbar/navbar.component.html @@ -0,0 +1,108 @@ + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
diff --git a/frontend/src/app/common/components/navbar/navbar.component.scss b/frontend/src/app/common/components/navbar/navbar.component.scss new file mode 100644 index 0000000..a64cc59 --- /dev/null +++ b/frontend/src/app/common/components/navbar/navbar.component.scss @@ -0,0 +1,59 @@ +.navbar { + background-color: black; + height: 60px; + font-size: medium; + color: white; +} + + +.navbar-expand-lg { + border-bottom: solid; + border-color: white; + border-bottom-width: 2px; +} + + +// PolyNotFound +.navbar-brand { + font-family: cursive; + font-weight: bold; + font-size: x-large; + margin-left: 15px; + color: white; +} + + +// Recherche, Mes Playlists, Historique +.nav-link { + color: white; +} +.nav-link:hover { + color: grey; +} + + +// Bonton deconnexion +.btnDeconnexion { + font-size: medium; + margin: 0px 10px 0px 10px +} +.btnDeconnexion:hover { + color: grey; +} + + +.monLi { + margin: 0px 10px 0px 10px; +} + + +img { + border: solid 2px white; + border-radius: 50px; + margin: 0px 10px 0px 15px; + width: 40px; + height: 40px; +} +img:hover { + cursor: pointer; +} diff --git a/frontend/src/app/common/components/navbar/navbar.component.spec.ts b/frontend/src/app/common/components/navbar/navbar.component.spec.ts new file mode 100644 index 0000000..f8ccd6f --- /dev/null +++ b/frontend/src/app/common/components/navbar/navbar.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NavbarComponent } from './navbar.component'; + +describe('NavbarComponent', () => { + let component: NavbarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ NavbarComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NavbarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/common/components/navbar/navbar.component.ts b/frontend/src/app/common/components/navbar/navbar.component.ts new file mode 100644 index 0000000..19d0c04 --- /dev/null +++ b/frontend/src/app/common/components/navbar/navbar.component.ts @@ -0,0 +1,32 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {ProfilService} from "../../services/profil/profil.service"; +import {MessageService} from "../../services/message/message.service"; + +@Component({ + selector: 'app-navbar', + templateUrl: './navbar.component.html', + styleUrls: ['./navbar.component.scss'] +}) +export class NavbarComponent implements OnInit +{ + @Input() pour = "login"; + + constructor(private profilService: ProfilService, private messageService: MessageService) { } + + ngOnInit(): void {} + + onDeconnexion(): void + { + this.messageService + .delete('logout') + .subscribe(retour => this.onDeconnexionCallback(retour), err => this.onDeconnexionCallback(err)); + this.profilService.setId(-1); + this.profilService.setIsAdmin(false); + } + + onDeconnexionCallback(retour: any): void + { + if(retour.status !== "success") console.log(retour); + } + +} diff --git a/frontend/src/app/common/components/page-profil/page-profil.component.html b/frontend/src/app/common/components/page-profil/page-profil.component.html new file mode 100644 index 0000000..ad7d8a3 --- /dev/null +++ b/frontend/src/app/common/components/page-profil/page-profil.component.html @@ -0,0 +1,53 @@ +
+ + + + + + +
+ + +
+
Pseudo:
+
{{person.nickname}}
+
+ + +
+
Mail:
+
{{person.email}}
+
+ + +
+
Rôle:
+
+ utilisateur + admin +
+ + +
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+ + +
diff --git a/frontend/src/app/common/components/page-profil/page-profil.component.scss b/frontend/src/app/common/components/page-profil/page-profil.component.scss new file mode 100644 index 0000000..fa50561 --- /dev/null +++ b/frontend/src/app/common/components/page-profil/page-profil.component.scss @@ -0,0 +1,45 @@ +.myContainer { + max-width: 100vw; + height: 100vh; + overflow-x: hidden; +} + + +.boite { + margin-left: auto; + margin-right: auto; + width: 50%; + margin-top: 10vh; + border: solid 3px; + border-radius: 10px; + padding: 20px 40px 20px 40px; + background-color: #ffffff; + text-align: center; + box-shadow: 10px 5px 5px black; +} + + +.myRow { + margin: 15px 0px 15px 0px; +} +.myLabel { + text-align: right; + padding: 0px 5px 0px 0px; + margin: 0px; + font-weight: bold; +} +.myValue { + text-align: left; + padding: 0px 0px 0px 5px; + margin: 0px; +} + + +.btnContainer { + text-align: center; + margin-top: 40px; +} +.myBtn { + border: solid 1px black; + background-color: white; +} diff --git a/frontend/src/app/common/components/page-profil/page-profil.component.spec.ts b/frontend/src/app/common/components/page-profil/page-profil.component.spec.ts new file mode 100644 index 0000000..958b80e --- /dev/null +++ b/frontend/src/app/common/components/page-profil/page-profil.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PageProfilComponent } from './page-profil.component'; + +describe('PageUtilisateurComponent', () => { + let component: PageProfilComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PageProfilComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PageProfilComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/common/components/page-profil/page-profil.component.ts b/frontend/src/app/common/components/page-profil/page-profil.component.ts new file mode 100644 index 0000000..23e3d43 --- /dev/null +++ b/frontend/src/app/common/components/page-profil/page-profil.component.ts @@ -0,0 +1,110 @@ +import { Component, OnInit } from '@angular/core'; +import {MatDialog} from "@angular/material/dialog"; +import {MatSnackBar} from "@angular/material/snack-bar"; +import {PopupUpdateProfilComponent} from "../popup-update-profil/popup-update-profil.component"; +import {Router} from "@angular/router"; +import {PopupDeleteProfilComponent} from "../popup-delete-profil/popup-delete-profil.component"; +import {MessageService} from "../../services/message/message.service"; +import {HttpParams} from "@angular/common/http"; +import {ProfilService} from "../../services/profil/profil.service"; + + + +@Component({ + selector: 'app-page-profil', + templateUrl: './page-profil.component.html', + styleUrls: ['./page-profil.component.scss'] +}) +export class PageProfilComponent implements OnInit +{ + person = { + id: "", + nickname: "", + email: "", + is_admin: false, + }; + from: string = "" ; + configSnackbar = { duration: 3000, panelClass: "custom-class" }; + + + constructor( private messageService: MessageService, + private profilService: ProfilService, + public dialog: MatDialog, + private snackBar: MatSnackBar, + private router: Router ) { } + + + ngOnInit(): void + { + if(this.router.url.startsWith("/user")) this.from = "user" ; + else if(this.router.url.startsWith("/admin")) this.from = "admin" ; + + let params = new HttpParams() + params = params.set("order_by", "nickname"); + params = params.set("id", this.profilService.getId()); + this.messageService + .get("users", params) + .subscribe(ret => this.ngOnInitCallback(ret), err => this.ngOnInitCallback(err)); + } + + + // Callback de ngOnInit + ngOnInitCallback(retour: any): void + { + if(retour.status !== "success") { + console.log(retour); + } + else { + this.person = retour.data[0]; + } + } + + + // Appuie sur le bouton modifier + onModifier(): void + { + const config = { + width: '25%', + data: { person: this.person } + }; + this.dialog + .open(PopupUpdateProfilComponent, config) + .afterClosed() + .subscribe(retour => this.onModifierCallback(retour)); + } + + + // Callback de onModifier + onModifierCallback(retour: any): void + { + if((retour === null) || (retour === undefined)) this.snackBar.open( "Opération annulé", "", this.configSnackbar); + else if(retour.status === "success") this.person = retour.data; + } + + + // Appuie sur le bouton supprimer + onSupprimer(): void + { + const config = { + data: { + id: this.person.id, + email: this.person.email, + me: true, + } + }; + this.dialog + .open(PopupDeleteProfilComponent, config) + .afterClosed() + .subscribe(retour => this.onSupprimerCallback(retour)); + } + + + // Callback de onSupprimer + onSupprimerCallback(retour: any): void + { + if((retour === null) || (retour === undefined)) this.snackBar.open( "Opération annulé", "", this.configSnackbar); + else if(retour.status === "error") this.snackBar.open(retour.error.message, "", this.configSnackbar); + else if(retour.status === "success") this.router.navigateByUrl("/login"); + } + +} diff --git a/frontend/src/app/common/components/popup-delete-profil/popup-delete-profil.component.html b/frontend/src/app/common/components/popup-delete-profil/popup-delete-profil.component.html new file mode 100644 index 0000000..9118e3b --- /dev/null +++ b/frontend/src/app/common/components/popup-delete-profil/popup-delete-profil.component.html @@ -0,0 +1,14 @@ + + Êtes-vous sûr de vouloir supprimer votre compte ? + + + + + Êtes-vous sûr de vouloir supprimer {{email}} ? + + + + + + + diff --git a/frontend/src/app/common/components/popup-delete-profil/popup-delete-profil.component.scss b/frontend/src/app/common/components/popup-delete-profil/popup-delete-profil.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/common/components/popup-delete-profil/popup-delete-profil.component.spec.ts b/frontend/src/app/common/components/popup-delete-profil/popup-delete-profil.component.spec.ts new file mode 100644 index 0000000..8743fad --- /dev/null +++ b/frontend/src/app/common/components/popup-delete-profil/popup-delete-profil.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PopupDeleteProfilComponent } from './popup-delete-profil.component'; + +describe('PopupDeleteProfilComponent', () => { + let component: PopupDeleteProfilComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PopupDeleteProfilComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PopupDeleteProfilComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/common/components/popup-delete-profil/popup-delete-profil.component.ts b/frontend/src/app/common/components/popup-delete-profil/popup-delete-profil.component.ts new file mode 100644 index 0000000..49d6b5b --- /dev/null +++ b/frontend/src/app/common/components/popup-delete-profil/popup-delete-profil.component.ts @@ -0,0 +1,68 @@ +import {Component, Inject, OnInit} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; +import {MessageService} from "../../services/message/message.service"; +import {HttpParams} from "@angular/common/http"; + + + +@Component({ + selector: 'app-popup-delete-profil', + templateUrl: './popup-delete-profil.component.html', + styleUrls: ['./popup-delete-profil.component.scss'] +}) +export class PopupDeleteProfilComponent implements OnInit +{ + id: number; + me: boolean = false; // on se supprime soi-même + email: string = ""; + + + constructor( private messageService: MessageService, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any ) { } + + + ngOnInit(): void { + this.id = this.data.id; + this.me = this.data.me; + this.email = this.data.email; + } + + + // Appuie sur 'valider' + onValider(): void + { + if(this.me) + { + this.messageService + .delete("user/delete") + .subscribe(ret => this.onValiderCallback(ret), err => this.onValiderCallback(err)); + } + else { + //let params = (new HttpParams()).set("id", this.id); + this.messageService + .delete("admin/delete/user/"+this.id) + .subscribe(ret => this.onValiderCallback(ret), err => this.onValiderCallback(err)); + } + } + + + // Callback de onValider + onValiderCallback(retour: any): void + { + if(retour.status === "success") + { + this.dialogRef.close(retour); + } + else if(retour.status === "error") + { + console.log(retour); + this.dialogRef.close(retour); + } + else { + console.log(retour); + this.dialogRef.close(null); + } + } + +} diff --git a/frontend/src/app/common/components/popup-update-profil/popup-update-profil.component.html b/frontend/src/app/common/components/popup-update-profil/popup-update-profil.component.html new file mode 100644 index 0000000..c28fe07 --- /dev/null +++ b/frontend/src/app/common/components/popup-update-profil/popup-update-profil.component.html @@ -0,0 +1,55 @@ +
+
+ +

Modifier

+ + +

+ + + + Pseudo + +
+ + +
+ + +
+ Modifier mot de passe: + +
+ + +
+ + + Nouveau mot de passe + + +
+ + + Confirmation nouveau mot de passe + + +
+

+ + +
+ + +
+ {{errorMessage}} +
+ + +
+ + +
+ +
+
diff --git a/frontend/src/app/common/components/popup-update-profil/popup-update-profil.component.scss b/frontend/src/app/common/components/popup-update-profil/popup-update-profil.component.scss new file mode 100644 index 0000000..1317c7a --- /dev/null +++ b/frontend/src/app/common/components/popup-update-profil/popup-update-profil.component.scss @@ -0,0 +1,37 @@ +.boite { + font-size: small; +} + +h3 { + text-align: center; +} + +button { + font-size: small; +} + +img { + margin: 0px 0px 10px 0px; + width: 5vw; + height: 5vw; + border: solid 2px black; + border-radius: 50%; + font-size: xxx-large; +} + +// ------------------------------------------------------------------------- + +// aura +::ng-deep .mat-checkbox-ripple .mat-ripple-element { + background-color: grey !important; +} + +// contenu coche +::ng-deep .mat-checkbox-checked.mat-accent .mat-checkbox-background { + background-color: black !important; +} + +// indeterminate +::ng-deep .mat-checkbox .mat-checkbox-frame { + background-color: white !important; +} diff --git a/frontend/src/app/common/components/popup-update-profil/popup-update-profil.component.spec.ts b/frontend/src/app/common/components/popup-update-profil/popup-update-profil.component.spec.ts new file mode 100644 index 0000000..6d2a26b --- /dev/null +++ b/frontend/src/app/common/components/popup-update-profil/popup-update-profil.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PopupUpdateProfilComponent } from './popup-update-profil.component'; + +describe('PopupModifierUtilisateurComponent', () => { + let component: PopupUpdateProfilComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PopupUpdateProfilComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PopupUpdateProfilComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/common/components/popup-update-profil/popup-update-profil.component.ts b/frontend/src/app/common/components/popup-update-profil/popup-update-profil.component.ts new file mode 100644 index 0000000..4d92bc2 --- /dev/null +++ b/frontend/src/app/common/components/popup-update-profil/popup-update-profil.component.ts @@ -0,0 +1,108 @@ +import {Component, Inject, OnInit} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; +import {CheckEmailService} from "../../services/checkEmail/check-email.service"; +import {MessageService} from "../../services/message/message.service"; + + + +@Component({ + selector: 'app-popup-update-profil', + templateUrl: './popup-update-profil.component.html', + styleUrls: ['./popup-update-profil.component.scss'] +}) +export class PopupUpdateProfilComponent implements OnInit +{ + personCopy: any; + newPassword: string = ""; + confirmNewPassword: string = "" ; + changePassword: boolean = false ; + hasError: boolean = false; + errorMessage: string = "" ; + + + constructor( private checkEmailService: CheckEmailService, + private messageService: MessageService, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any ) { } + + + ngOnInit(): void + { + const person = this.data.person; + this.personCopy = { + id: person.id, + nickname: person.nickname, + email: person.email, + is_admin: person.is_admin + }; + } + + + // Appuie sur le bouton "valider" + onValider(): void + { + this.checkField(); + if(!this.hasError) + { + let data: any = {nickname: this.personCopy.nickname}; + if(this.changePassword) data = { + nickname: this.personCopy.nickname, + password: this.newPassword + }; + this.messageService + .put("user/update", data) + .subscribe(ret => this.onValiderCallback(ret), err => this.onValiderCallback(err)); + } + } + + + // Callback de 'onValider' + onValiderCallback(retour: any) + { + if(retour.status === "success") + { + this.dialogRef.close(retour); + } + else if(retour.status === "error") + { + console.log(retour); + this.errorMessage = retour.message; + this.hasError = true; + } + else { + console.log(retour); + this.dialogRef.close(null); + } + } + + + // Check les champs saisis par l'utilisateur + checkField(): void + { + if(this.personCopy.nickname.length === 0) { + this.errorMessage = "Veuillez remplir le champ 'pseudo'" ; + this.hasError = true; + } + else if(this.personCopy.email.length === 0) { + this.errorMessage = "Veuillez remplir le champ 'email'" ; + this.hasError = true; + } + else if(!this.checkEmailService.isValidEmail(this.personCopy.email)) { + this.errorMessage = "Email invalide" ; + this.hasError = true; + } + else if((this.changePassword) && (this.newPassword.length === 0)) { + this.errorMessage = "Veuillez remplir le champ 'mot de passe'"; + this.hasError = true; + } + else if((this.changePassword) && (this.newPassword !== this.confirmNewPassword)) { + this.errorMessage = "Le mot de passe est différent de sa confirmation"; + this.hasError = true; + } + else { + this.errorMessage = "" ; + this.hasError = false; + } + } + +} diff --git a/frontend/src/app/common/guards/admin/admin.guard.spec.ts b/frontend/src/app/common/guards/admin/admin.guard.spec.ts new file mode 100644 index 0000000..e982f62 --- /dev/null +++ b/frontend/src/app/common/guards/admin/admin.guard.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AdminGuard } from './admin.guard'; + +describe('AdminGuard', () => { + let guard: AdminGuard; + + beforeEach(() => { + TestBed.configureTestingModule({}); + guard = TestBed.inject(AdminGuard); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/common/guards/admin/admin.guard.ts b/frontend/src/app/common/guards/admin/admin.guard.ts new file mode 100644 index 0000000..701b288 --- /dev/null +++ b/frontend/src/app/common/guards/admin/admin.guard.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree} from '@angular/router'; +import { Observable } from 'rxjs'; +import {ProfilService} from "../../services/profil/profil.service"; + +@Injectable({ + providedIn: 'root' +}) +export class AdminGuard implements CanActivate +{ + + constructor(private profilService: ProfilService, private router: Router) {} + + + canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable | Promise | boolean | UrlTree + { + if(this.profilService.getId() === -1) // si non connecté + { + this.router.navigateByUrl("/login"); + return false; + } + else { + if(this.profilService.isAdmin()) return true; + else { + this.router.navigateByUrl("/login"); + return false; + } + } + } + +} diff --git a/frontend/src/app/common/guards/user/user.guard.spec.ts b/frontend/src/app/common/guards/user/user.guard.spec.ts new file mode 100644 index 0000000..a657320 --- /dev/null +++ b/frontend/src/app/common/guards/user/user.guard.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserGuard } from './user.guard'; + +describe('UserGuard', () => { + let guard: UserGuard; + + beforeEach(() => { + TestBed.configureTestingModule({}); + guard = TestBed.inject(UserGuard); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/common/guards/user/user.guard.ts b/frontend/src/app/common/guards/user/user.guard.ts new file mode 100644 index 0000000..d357d34 --- /dev/null +++ b/frontend/src/app/common/guards/user/user.guard.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree} from '@angular/router'; +import { Observable } from 'rxjs'; +import {ProfilService} from "../../services/profil/profil.service"; + +@Injectable({ + providedIn: 'root' +}) +export class UserGuard implements CanActivate +{ + + constructor(private profilService: ProfilService, private router: Router) {} + + + canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable | Promise | boolean | UrlTree + { + if(this.profilService.getId() === -1) // si non connecté + { + this.router.navigateByUrl("/login"); + return false; + } + else { + if(!this.profilService.isAdmin()) return true; + else { + this.router.navigateByUrl("/login"); + return false; + } + } + } + +} diff --git a/frontend/src/app/common/services/checkEmail/check-email.service.spec.ts b/frontend/src/app/common/services/checkEmail/check-email.service.spec.ts new file mode 100644 index 0000000..49250c3 --- /dev/null +++ b/frontend/src/app/common/services/checkEmail/check-email.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CheckEmailService } from './check-email.service'; + +describe('CheckEmailService', () => { + let service: CheckEmailService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CheckEmailService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/common/services/checkEmail/check-email.service.ts b/frontend/src/app/common/services/checkEmail/check-email.service.ts new file mode 100644 index 0000000..0ed4cb8 --- /dev/null +++ b/frontend/src/app/common/services/checkEmail/check-email.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class CheckEmailService +{ + + isValidEmail(email: string): boolean + { + let re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + return re.test(email); + } + +} diff --git a/frontend/src/app/common/services/fictitiousDatas/fictitious-datas.service.spec.ts b/frontend/src/app/common/services/fictitiousDatas/fictitious-datas.service.spec.ts new file mode 100644 index 0000000..17d518a --- /dev/null +++ b/frontend/src/app/common/services/fictitiousDatas/fictitious-datas.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { FictitiousDatasService } from './fictitious-datas.service'; + +describe('FictitiousDatasService', () => { + let service: FictitiousDatasService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(FictitiousDatasService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/common/services/fictitiousDatas/fictitious-datas.service.ts b/frontend/src/app/common/services/fictitiousDatas/fictitious-datas.service.ts new file mode 100644 index 0000000..e4eb760 --- /dev/null +++ b/frontend/src/app/common/services/fictitiousDatas/fictitious-datas.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@angular/core'; + + +@Injectable({ + providedIn: 'root' +}) +export class FictitiousDatasService +{ + + getUser() + { + const id = (Math.floor(Math.random()*100000)).toString() + return { + id: id, + nickname: "Riri"+id, + email: "riri"+id+"@gmail.com", + hash_pass: "blablabla", + is_admin: false, + } + } + + getAdmin() + { + const id = (Math.floor(Math.random()*100000)).toString() + return { + id: id, + nickname: "Fifi"+id, + email: "fifi"+id+"@gmail.com", + hash_pass: "blablabla", + is_admin: true, + } + } + + getTabPerson(n: number) + { + let tab = []; + + for(let i=0 ; i { + let service: HashageService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(HashageService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/common/services/hashage/hashage.service.ts b/frontend/src/app/common/services/hashage/hashage.service.ts new file mode 100644 index 0000000..f76562e --- /dev/null +++ b/frontend/src/app/common/services/hashage/hashage.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class HashageService +{ + // Fonction de hashage (faible) + run(input: string): string + { + let hash = 0; + for (let i = 0; i < input.length; i++) { + let ch = input.charCodeAt(i); + hash = ((hash << 5) - hash) + ch; + hash = hash & hash; + } + return hash.toString(); + } +} diff --git a/frontend/src/app/common/services/message/message.service.spec.ts b/frontend/src/app/common/services/message/message.service.spec.ts new file mode 100644 index 0000000..1db761b --- /dev/null +++ b/frontend/src/app/common/services/message/message.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { MessageService } from './message.service'; + +describe('MessageService', () => { + let service: MessageService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(MessageService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/common/services/message/message.service.ts b/frontend/src/app/common/services/message/message.service.ts new file mode 100644 index 0000000..4c4ec1c --- /dev/null +++ b/frontend/src/app/common/services/message/message.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@angular/core'; +import {HttpClient, HttpParams} from "@angular/common/http"; +import {Observable} from "rxjs"; +import {environment} from "../../../../environments/environment"; + +@Injectable({ + providedIn: 'root' +}) +export class MessageService +{ + + constructor( private http: HttpClient ) { } + + post(url: string, data: any): Observable + { + const urlComplete = environment.debutUrl + url ; + return this.http.post(urlComplete, data, {withCredentials: true}); + } + + get(url: string, params:HttpParams = new HttpParams()): Observable + { + const urlComplete = environment.debutUrl + url ; + return this.http.get(urlComplete,{ withCredentials: true, params: params }); + } + + put(url: string, data: any): Observable + { + const urlComplete = environment.debutUrl + url ; + return this.http.put(urlComplete, data, {withCredentials: true}); + } + + delete(url: string, params:HttpParams = new HttpParams()): Observable + { + const urlComplete = environment.debutUrl + url ; + return this.http.delete(urlComplete,{withCredentials: true}); + } + +} diff --git a/frontend/src/app/common/services/profil/profil.service.spec.ts b/frontend/src/app/common/services/profil/profil.service.spec.ts new file mode 100644 index 0000000..5cee000 --- /dev/null +++ b/frontend/src/app/common/services/profil/profil.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ProfilService } from './profil.service'; + +describe('ProfilService', () => { + let service: ProfilService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ProfilService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/common/services/profil/profil.service.ts b/frontend/src/app/common/services/profil/profil.service.ts new file mode 100644 index 0000000..71fac4a --- /dev/null +++ b/frontend/src/app/common/services/profil/profil.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class ProfilService +{ + + constructor() + { + if(localStorage.getItem('id') === null) this.setId(-1); + if(localStorage.getItem('isAdmin') === null) this.setIsAdmin(false); + } + + getId(): number + { + let idString = localStorage.getItem('id'); + if(idString === null) return -1; + else return parseInt(idString); + } + + setId(id: number): void + { + localStorage.setItem('id', id.toString()); + } + + isAdmin(): boolean + { + let isAdminString = localStorage.getItem('isAdmin'); + if(isAdminString === "T") return true; + else return false; + } + + setIsAdmin(isAdmin: boolean): void + { + let isAdminString = "" ; + if(isAdmin) isAdminString = "T"; + else isAdminString = "F"; + localStorage.setItem('isAdmin', isAdminString); + } + +} diff --git a/frontend/src/app/login/page-login/page-login.component.html b/frontend/src/app/login/page-login/page-login.component.html new file mode 100644 index 0000000..493e7b7 --- /dev/null +++ b/frontend/src/app/login/page-login/page-login.component.html @@ -0,0 +1,40 @@ +
+
+ + + + + + + +
+
+ + +
+

FlaskAled

+ User Icon +
+ + +
+ + + +
+ {{errorMessage}} +
+ +
+ + + + +
+
+ + +
+
diff --git a/frontend/src/app/login/page-login/page-login.component.scss b/frontend/src/app/login/page-login/page-login.component.scss new file mode 100644 index 0000000..8924202 --- /dev/null +++ b/frontend/src/app/login/page-login/page-login.component.scss @@ -0,0 +1,271 @@ +html { + background-color: #56baed; +} + +body { + font-family: "Poppins", sans-serif; + height: 100vh; +} + +a { + color: #5E89FF; + display:inline-block; + text-decoration: none; + font-weight: 400; +} + +h2 { + text-align: center; + font-size: 16px; + font-weight: 600; + text-transform: uppercase; + display:inline-block; + margin: 40px 8px 10px 8px; + color: #cccccc; +} + + + +/* STRUCTURE */ + +.wrapper { + display: flex; + align-items: center; + flex-direction: column; + justify-content: center; + width: 100%; + min-height: 80%; + padding: 20px; +} + +#formContent { + -webkit-border-radius: 10px 10px 10px 10px; + border-radius: 10px 10px 10px 10px; + background: #fff; + padding: 30px; + width: 90%; + max-width: 450px; + position: relative; + padding: 0px; + -webkit-box-shadow: 0 30px 60px 0 rgba(0,0,0,0.3); + box-shadow: 0 30px 60px 0 rgba(0,0,0,0.3); + text-align: center; +} + +#formFooter { + background-color: #f6f6f6; + border-top: 1px solid #dce8f1; + padding: 25px; + text-align: center; + -webkit-border-radius: 0 0 10px 10px; + border-radius: 0 0 10px 10px; +} + + + +/* TABS */ + +h2.inactive { + color: #cccccc; +} + +h2.active { + color: #0d0d0d; + border-bottom: 2px solid #5fbae9; +} + + + +/* FORM TYPOGRAPHY*/ + +input[type=button], input[type=submit], input[type=reset] { + background-color: #5E89FF; + border: none; + color: white; + padding: 15px 80px; + text-align: center; + text-decoration: none; + display: inline-block; + text-transform: uppercase; + font-size: 13px; + -webkit-box-shadow: 0 10px 30px 0 rgba(95,186,233,0.4); + box-shadow: 0 10px 30px 0 rgba(95,186,233,0.4); + -webkit-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; + margin: 5px 20px 40px 20px; + -webkit-transition: all 0.3s ease-in-out; + -moz-transition: all 0.3s ease-in-out; + -ms-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; +} + +input[type=button]:hover, input[type=submit]:hover, input[type=reset]:hover { + background-color: #39ace7; +} + +input[type=button]:active, input[type=submit]:active, input[type=reset]:active { + -moz-transform: scale(0.95); + -webkit-transform: scale(0.95); + -o-transform: scale(0.95); + -ms-transform: scale(0.95); + transform: scale(0.95); +} + +input[type=text], input[type=password] { + background-color: #f6f6f6; + border: none; + color: #0d0d0d; + padding: 15px 32px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 5px; + width: 85%; + border: 2px solid #f6f6f6; + -webkit-transition: all 0.5s ease-in-out; + -moz-transition: all 0.5s ease-in-out; + -ms-transition: all 0.5s ease-in-out; + -o-transition: all 0.5s ease-in-out; + transition: all 0.5s ease-in-out; + -webkit-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; +} + + + +input[type=text]:focus, input[type=password]:focus { + background-color: #fff; + border-bottom: 2px solid #5fbae9; +} + +input[type=text]::placeholder, input[type=password]::placeholder { + color: #cccccc; +} + +.bg{ + margin: 0; + padding: 0; + height: 100vh; + width: 100vw; + overflow-y: hidden; + overflow-x: hidden; +} + +/* ANIMATIONS */ + +/* Simple CSS3 Fade-in-down Animation */ +.fadeInDown { + -webkit-animation-name: fadeInDown; + animation-name: fadeInDown; + -webkit-animation-duration: 1s; + animation-duration: 1s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; +} + +@-webkit-keyframes fadeInDown { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInDown { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +/* Simple CSS3 Fade-in Animation */ +@-webkit-keyframes fadeIn { from { opacity:0; } to { opacity:1; } } +@-moz-keyframes fadeIn { from { opacity:0; } to { opacity:1; } } +@keyframes fadeIn { from { opacity:0; } to { opacity:1; } } + +.fadeIn { + opacity:0; + -webkit-animation:fadeIn ease-in 1; + -moz-animation:fadeIn ease-in 1; + animation:fadeIn ease-in 1; + + -webkit-animation-fill-mode:forwards; + -moz-animation-fill-mode:forwards; + animation-fill-mode:forwards; + + -webkit-animation-duration:1s; + -moz-animation-duration:1s; + animation-duration:1s; +} + +.fadeIn.first { + -webkit-animation-delay: 0.4s; + -moz-animation-delay: 0.4s; + animation-delay: 0.4s; +} + +.fadeIn.second { + -webkit-animation-delay: 0.6s; + -moz-animation-delay: 0.6s; + animation-delay: 0.6s; +} + +.fadeIn.third { + -webkit-animation-delay: 0.8s; + -moz-animation-delay: 0.8s; + animation-delay: 0.8s; +} + +.fadeIn.fourth { + -webkit-animation-delay: 1s; + -moz-animation-delay: 1s; + animation-delay: 1s; +} + +/* Simple CSS3 Fade-in Animation */ +.underlineHover:after { + display: block; + left: 0; + bottom: -10px; + width: 0; + height: 2px; + //background-color: #5E89FF; + background-color: #5E89FF; + content: ""; + transition: width 0.2s; +} + +.underlineHover:hover { + color: #0d0d0d; +} + +.underlineHover:hover:after{ + width: 100%; +} + +h1{ + color: black; +} + +/* OTHERS */ + +*:focus { + outline: none; +} + +#icon { + width:30%; +} diff --git a/frontend/src/app/login/page-login/page-login.component.spec.ts b/frontend/src/app/login/page-login/page-login.component.spec.ts new file mode 100644 index 0000000..28d80c7 --- /dev/null +++ b/frontend/src/app/login/page-login/page-login.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PageConnexionComponent } from './page-connexion.component'; + +describe('PageConnexionComponent', () => { + let component: PageConnexionComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PageConnexionComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PageConnexionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/login/page-login/page-login.component.ts b/frontend/src/app/login/page-login/page-login.component.ts new file mode 100644 index 0000000..54ce267 --- /dev/null +++ b/frontend/src/app/login/page-login/page-login.component.ts @@ -0,0 +1,78 @@ +import {Component} from '@angular/core'; +import {Router} from "@angular/router"; +import {MessageService} from "../../common/services/message/message.service"; +import {ProfilService} from "../../common/services/profil/profil.service"; + + + +@Component({ + selector: 'app-page-nickname', + templateUrl: './page-login.component.html', + styleUrls: ['./page-login.component.scss'] +}) +export class PageLoginComponent +{ + email: string = "" ; + password: string = "" ; + hasError: boolean = false; + errorMessage: string = ""; + + + constructor( private messageService: MessageService, + private router: Router, + private profilService: ProfilService ) { } + + + // Appuie sur le bouton "seConnecter" + onSeConnecter(): void + { + this.checkField(); + if(!this.hasError) + { + const data = { + email: this.email, + password: this.password + }; + this.messageService + .post('login', data) + .subscribe( retour => this.onSeConnecterCallback(retour), err => this.onSeConnecterCallback(err)); + } + } + + + // Callback de "onSeConnecter" + onSeConnecterCallback(retour: any): void + { + if(retour.status !== "success") + { + console.log(retour); + this.errorMessage = retour.error.message; + this.hasError = true; + } + else { + this.profilService.setId(retour.data.id); + this.profilService.setIsAdmin(retour.data.is_admin) + if(retour.data.is_admin) this.router.navigateByUrl('admin/userList'); + else this.router.navigateByUrl('user/registry'); + } + } + + + // Check les champs saisis par l'utilisateur + checkField(): void + { + if(this.email === "") { + this.errorMessage = "Veuillez remplir le champ email" ; + this.hasError = true; + } + else if(this.password === "") { + this.errorMessage = "Veuillez remplir le champ mot de passe" ; + this.hasError = true; + } + else { + this.errorMessage = "" ; + this.hasError = false; + } + } + +} diff --git a/frontend/src/app/register/page-register/page-register.component.html b/frontend/src/app/register/page-register/page-register.component.html new file mode 100644 index 0000000..afa10a8 --- /dev/null +++ b/frontend/src/app/register/page-register/page-register.component.html @@ -0,0 +1,49 @@ +
+ + + + + + + +
+ + + + Pseudo + +
+ + + + Email + +
+ + + + Mot de passe + +
+ + + + Confirmation mot de passe + +
+ + +
+ {{errorMessage}} +
+ + + + +
+ + + +
diff --git a/frontend/src/app/register/page-register/page-register.component.scss b/frontend/src/app/register/page-register/page-register.component.scss new file mode 100644 index 0000000..293c5a1 --- /dev/null +++ b/frontend/src/app/register/page-register/page-register.component.scss @@ -0,0 +1,21 @@ +.contenuContainer { + padding: 20px 20px 20px 20px; + width: 30%; + margin: auto auto; + margin-top: 50px; + border: solid 1px black; + text-align: center; +} + +.boite { + margin-left: auto; + margin-right: auto; + width: 50%; + margin-top: 10vh; + border: solid 3px; + border-radius: 10px; + padding: 20px 40px 20px 40px; + background-color: #ffffff; + text-align: center; + box-shadow: 10px 5px 5px black; +} diff --git a/frontend/src/app/register/page-register/page-register.component.spec.ts b/frontend/src/app/register/page-register/page-register.component.spec.ts new file mode 100644 index 0000000..847bd4f --- /dev/null +++ b/frontend/src/app/register/page-register/page-register.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PageRegisterComponent } from './page-register.component'; + +describe('PageInscriptionComponent', () => { + let component: PageRegisterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PageRegisterComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PageRegisterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/register/page-register/page-register.component.ts b/frontend/src/app/register/page-register/page-register.component.ts new file mode 100644 index 0000000..ebd1e62 --- /dev/null +++ b/frontend/src/app/register/page-register/page-register.component.ts @@ -0,0 +1,98 @@ +import { Component } from '@angular/core'; +import {HashageService} from "../../common/services/hashage/hashage.service"; +import {Router} from "@angular/router"; +import {CheckEmailService} from "../../common/services/checkEmail/check-email.service"; +import {MatDialog} from "@angular/material/dialog"; +import {PopupConfirmRegisterComponent} from "../popup-confirm-register/popup-confirm-register.component"; +import {MessageService} from "../../common/services/message/message.service"; + + + +@Component({ + selector: 'app-page-register', + templateUrl: './page-register.component.html', + styleUrls: ['./page-register.component.scss'] +}) +export class PageRegisterComponent +{ + email: string = ""; + nickname: string = ""; + password: string = ""; + confirmPassword: string = ""; + hasError: boolean = false; + errorMessage: string = ""; + + + constructor( private checkEmailService: CheckEmailService, + private messageService: MessageService, + private router: Router, + public dialog: MatDialog ) { } + + + // Envoie de l'utilisateur au backend + onValider(): void + { + this.checkField(); + if(!this.hasError) + { + const data = { + email: this.email, + nickname: this.nickname, + password: this.password, + is_admin: false + }; + this.messageService + .post('register', data) + .subscribe( retour => this.onValiderCallback(retour), err => this.onValiderCallback(err)); + } + } + + + // Callback de "onValider" + onValiderCallback(retour: any): void + { + if(retour.status !== "success") + { + console.log(retour); + this.errorMessage = retour.error.message; + this.hasError = true; + } + else { + this.dialog + .open(PopupConfirmRegisterComponent, {}) + .afterClosed() + .subscribe(retour => this.router.navigateByUrl("/login")); + } + } + + + // Check les champs saisis par l'utilisateur + checkField(): void + { + if(this.nickname.length === 0) { + this.errorMessage = "Veuillez remplir le champ 'pseudo'."; + this.hasError = true; + } + else if(this.email.length === 0) { + this.errorMessage = "Veuillez remplir le champ 'email'."; + this.hasError = true; + } + else if(!this.checkEmailService.isValidEmail(this.email)) { + this.errorMessage = "Email invalide."; + this.hasError = true; + } + else if(this.password.length === 0) { + this.errorMessage = "Veuillez remplir le champ 'mot de passe'."; + this.hasError = true; + } + else if(this.password !== this.confirmPassword) { + this.errorMessage = "Le mot de passe est différent de sa confirmation."; + this.hasError = true; + } + else { + this.errorMessage = "" ; + this.hasError = false; + } + } + +} diff --git a/frontend/src/app/register/popup-confirm-register/popup-confirm-register.component.html b/frontend/src/app/register/popup-confirm-register/popup-confirm-register.component.html new file mode 100644 index 0000000..6ecf288 --- /dev/null +++ b/frontend/src/app/register/popup-confirm-register/popup-confirm-register.component.html @@ -0,0 +1,2 @@ +

Votre inscription a bien été pris en compte

+ diff --git a/frontend/src/app/register/popup-confirm-register/popup-confirm-register.component.scss b/frontend/src/app/register/popup-confirm-register/popup-confirm-register.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/register/popup-confirm-register/popup-confirm-register.component.spec.ts b/frontend/src/app/register/popup-confirm-register/popup-confirm-register.component.spec.ts new file mode 100644 index 0000000..37dd0bc --- /dev/null +++ b/frontend/src/app/register/popup-confirm-register/popup-confirm-register.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PopupConfirmRegisterComponent } from './popup-confirm-register.component'; + +describe('PopupConfirmRegisterComponent', () => { + let component: PopupConfirmRegisterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PopupConfirmRegisterComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PopupConfirmRegisterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/register/popup-confirm-register/popup-confirm-register.component.ts b/frontend/src/app/register/popup-confirm-register/popup-confirm-register.component.ts new file mode 100644 index 0000000..f40b648 --- /dev/null +++ b/frontend/src/app/register/popup-confirm-register/popup-confirm-register.component.ts @@ -0,0 +1,13 @@ +import {Component} from '@angular/core'; + + + +@Component({ + selector: 'app-popup-confirm-register', + templateUrl: './popup-confirm-register.component.html', + styleUrls: ['./popup-confirm-register.component.scss'] +}) +export class PopupConfirmRegisterComponent +{ + +} diff --git a/frontend/src/app/user/myProfil/commentary.txt b/frontend/src/app/user/myProfil/commentary.txt new file mode 100644 index 0000000..902847e --- /dev/null +++ b/frontend/src/app/user/myProfil/commentary.txt @@ -0,0 +1,7 @@ +La page "admin/myProfil" contient: + - les informations de l'utilisateur (composant "page-profil") + - un bouton "modifier profil" pour modifier les informations de l'utilisateur (composant "popup-update-profil") + +Cette page est la même que la page de la partie admin. + +Ainsi, on a rangé cette page dans le dossier "common/components/profil". diff --git a/frontend/src/app/user/page-registry/page-registry.component.html b/frontend/src/app/user/page-registry/page-registry.component.html new file mode 100644 index 0000000..6c7f471 --- /dev/null +++ b/frontend/src/app/user/page-registry/page-registry.component.html @@ -0,0 +1,45 @@ +
+ +

+ + +
+ + Filter + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Pseudo{{person.nickname}}Email{{person.email}}Rôle{{person.role}}
+ + + + +
+

diff --git a/frontend/src/app/user/page-registry/page-registry.component.scss b/frontend/src/app/user/page-registry/page-registry.component.scss new file mode 100644 index 0000000..7440dcb --- /dev/null +++ b/frontend/src/app/user/page-registry/page-registry.component.scss @@ -0,0 +1,9 @@ +mat-paginator, table { + width: 80%; + margin: auto 10%; +} + +.filtre { + text-align: center; + width: 33%; +} diff --git a/frontend/src/app/user/page-registry/page-registry.component.spec.ts b/frontend/src/app/user/page-registry/page-registry.component.spec.ts new file mode 100644 index 0000000..1851010 --- /dev/null +++ b/frontend/src/app/user/page-registry/page-registry.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PageRegistryComponent } from './page-registry.component'; + +describe('RegistryComponent', () => { + let component: PageRegistryComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PageRegistryComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PageRegistryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/user/page-registry/page-registry.component.ts b/frontend/src/app/user/page-registry/page-registry.component.ts new file mode 100644 index 0000000..6fca3ed --- /dev/null +++ b/frontend/src/app/user/page-registry/page-registry.component.ts @@ -0,0 +1,57 @@ +import {AfterViewInit, Component, ViewChild} from '@angular/core'; +import {MatTableDataSource} from "@angular/material/table"; +import {MatSort} from "@angular/material/sort"; +import {MatPaginator} from "@angular/material/paginator"; +import {MessageService} from "../../common/services/message/message.service"; + + + +@Component({ + selector: 'app-page-registry', + templateUrl: './page-registry.component.html', + styleUrls: ['./page-registry.component.scss'] +}) +export class PageRegistryComponent implements AfterViewInit +{ + displayedColumns: string[] = [ "nickname", "email", "role" ]; + dataSource: MatTableDataSource = new MatTableDataSource(); + @ViewChild(MatSort) sort: MatSort; + @ViewChild(MatPaginator) paginator: MatPaginator; + + + constructor( private messageService: MessageService ) { } + + + ngAfterViewInit(): void + { + this.messageService + .get('users?order_by=nickname') + .subscribe(retour => this.ngAfterViewInitCallback(retour), err => this.ngAfterViewInitCallback(err)); + } + + + ngAfterViewInitCallback(retour: any): void + { + if(retour.status !== "success") { + console.log(retour); + } + else { + let tabPerson: { id: number, email: string, nickname: string, is_admin: boolean }[] = retour.data; + tabPerson = tabPerson.map( person => { + if(!person.is_admin) return Object.assign(person, {role: "utilisateur"}); + else return Object.assign(person, {role: "admin"}); + }); + this.dataSource = new MatTableDataSource(tabPerson); + this.dataSource.sort = this.sort; + this.dataSource.paginator = this.paginator; + } + } + + + applyFilter(event: Event): void + { + const filterValue = (event.target as HTMLInputElement).value; + this.dataSource.filter = filterValue.trim().toLowerCase(); + } + +} diff --git a/frontend/src/assets/.gitkeep b/frontend/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png new file mode 100644 index 0000000..a229003 Binary files /dev/null and b/frontend/src/assets/logo.png differ diff --git a/frontend/src/assets/logo1.png b/frontend/src/assets/logo1.png new file mode 100644 index 0000000..93b9375 Binary files /dev/null and b/frontend/src/assets/logo1.png differ diff --git a/frontend/src/environments/environment.prod.ts b/frontend/src/environments/environment.prod.ts new file mode 100644 index 0000000..3612073 --- /dev/null +++ b/frontend/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts new file mode 100644 index 0000000..0dfd866 --- /dev/null +++ b/frontend/src/environments/environment.ts @@ -0,0 +1,18 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + + +export const environment = { + production: false, + debutUrl: 'http://127.0.0.1:5000/api/' +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/frontend/src/favicon.ico b/frontend/src/favicon.ico new file mode 100644 index 0000000..997406a Binary files /dev/null and b/frontend/src/favicon.ico differ diff --git a/frontend/src/index.html b/frontend/src/index.html new file mode 100644 index 0000000..3af61ec --- /dev/null +++ b/frontend/src/index.html @@ -0,0 +1,13 @@ + + + + + Frontend + + + + + + + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..c7b673c --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,12 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/frontend/src/polyfills.ts b/frontend/src/polyfills.ts new file mode 100644 index 0000000..dcd18ea --- /dev/null +++ b/frontend/src/polyfills.ts @@ -0,0 +1,53 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js'; // Included with Angular CLI. + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss new file mode 100644 index 0000000..29ad8c8 --- /dev/null +++ b/frontend/src/styles.scss @@ -0,0 +1,3 @@ +/* You can add global styles to this file, and also import other style files */ +html, body { height: 100%; } +body { margin: 0; } diff --git a/frontend/src/test.ts b/frontend/src/test.ts new file mode 100644 index 0000000..b4dd603 --- /dev/null +++ b/frontend/src/test.ts @@ -0,0 +1,27 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context(path: string, deep?: boolean, filter?: RegExp): { + keys(): string[]; + (id: string): T; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), + { teardown: { destroyAfterEach: true }}, +); + +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..82d91dc --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..6815c14 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,33 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "es2017", + "module": "es2020", + "lib": [ + "es2018", + "dom" + ], + + "strictPropertyInitialization": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictPropertyInitialization": false + //"strictInjectionParameters": true, + //"strictInputAccessModifiers": true, + //"strictTemplates": true + } +} diff --git a/frontend/tsconfig.spec.json b/frontend/tsconfig.spec.json new file mode 100644 index 0000000..092345b --- /dev/null +++ b/frontend/tsconfig.spec.json @@ -0,0 +1,18 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "src/test.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +}