From f97a75404a5df592e5e82be6203cb64258e50be7 Mon Sep 17 00:00:00 2001 From: "Sergey A." Date: Mon, 4 Dec 2023 22:41:17 +0500 Subject: [PATCH 01/36] google auth/login WIP --- backend/.dockerignore | 1 + backend/requirements.txt | 1 + backend/src/application/app_core.py | 4 +- backend/src/application/config.py | 1 + backend/src/application/oauth.py | 89 ++++++++++++++++++++++++++ backend/src/database/wrapped_models.py | 7 +- 6 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 backend/src/application/oauth.py diff --git a/backend/.dockerignore b/backend/.dockerignore index d7261565..c4031c83 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -7,3 +7,4 @@ __pycache__ data venv logs +client_secret_apps.json \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 56b3e036..75c6955e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -20,3 +20,4 @@ pyjwt == 2.6.0 gspread == 5.7.2 sqlalchemy-utils == 0.39.0 werkzeug == 2.3.7 +flask_oauthlib==0.9.6 \ No newline at end of file diff --git a/backend/src/application/app_core.py b/backend/src/application/app_core.py index 63686284..747604cf 100644 --- a/backend/src/application/app_core.py +++ b/backend/src/application/app_core.py @@ -4,7 +4,7 @@ import flask_migrate import jwt -from flask import Flask, current_app, g, jsonify, request +from flask import Flask, current_app, g, request, jsonify from flask_cors import CORS # Добавление автоматически сгенерированных моделей. @@ -22,6 +22,7 @@ from . import config, exceptions from .urls import api_blueprint, public_blueprint +from .oauth import oauth_blueprint, oauth class FlaskLogged(Flask): @@ -92,6 +93,7 @@ def before_request(): with app.app_context(): app.register_blueprint(api_blueprint) app.register_blueprint(public_blueprint) + app.register_blueprint(oauth_blueprint, url_prefix='/oauth', oauth=oauth) try: with app.config["AIEYE_CREDENTIALS_PATH"].open() as file: diff --git a/backend/src/application/config.py b/backend/src/application/config.py index bb2320b8..0b8ad406 100644 --- a/backend/src/application/config.py +++ b/backend/src/application/config.py @@ -21,6 +21,7 @@ ROOT_PATH = Path(__file__).parent.parent.parent.resolve() AIEYE_CREDENTIALS_PATH = (ROOT_PATH / "aieye_credentials.json").resolve() +GOOGLE_CLIENT_SECRET_PATH = (ROOT_PATH / "client_secret_apps.json").resolve() def init_config(): diff --git a/backend/src/application/oauth.py b/backend/src/application/oauth.py new file mode 100644 index 00000000..0ebbe0c6 --- /dev/null +++ b/backend/src/application/oauth.py @@ -0,0 +1,89 @@ +import json +import secrets +import string + +from flask import Blueprint, redirect, url_for, session, jsonify +from flask_oauthlib.client import OAuth + +from backend.src.application.config import GOOGLE_CLIENT_SECRET_PATH +from backend.src.database.db import db as core_db +from backend.src.database.wrapped_models import User, Managers + + +oauth_blueprint = Blueprint('oauth', import_name=__name__, url_prefix="/oauth/",) + +with open(GOOGLE_CLIENT_SECRET_PATH) as f: + config_data = json.load(f) + +oauth = OAuth() + +google = oauth.remote_app( + 'google', + consumer_key=config_data['web']['client_id'], + consumer_secret=config_data['web']['client_secret'], + request_token_params={'scope': 'email'}, + base_url='https://www.googleapis.com/oauth2/v1/', + request_token_url=None, + access_token_method='POST', + access_token_url='https://accounts.google.com/o/oauth2/token', + authorize_url='https://accounts.google.com/o/oauth2/auth', +) + + +@oauth_blueprint.route('/login') +def login(): + return google.authorize(callback=url_for('oauth.authorized', _external=True)) + + +@oauth_blueprint.route('/logout') +def logout(): + session.pop('google_token', None) + return redirect(url_for('index')) + + +@oauth_blueprint.route('/login/authorized') +def authorized(): + response = google.authorized_response() + if response is None or response.get('access_token') is None: + return jsonify({'error': 'Access denied'}), 401 + + #TODO: + session['google_token'] = (response['access_token'], '') + user_info = google.get('userinfo') + + google_user_id = user_info.data['id'] + email = user_info.data['email'] + name = user_info.data.get('name', email.split('@')[0]) + + existing_user = User.query.filter_by(email=email).first() + if existing_user: + return jsonify({'message': f'Logged in as: {name} ({email})'}) + else: + password = generate_password() + user = User.create_user(username=name, password=password) + core_db.session.flush() + + #TODO: + manager = Managers(code=google_user_id, name=f"{name} manager", user=user) + #core_db.session.add(manager) + core_db.session.commit() + + #core_db.session.flush() + + #user.manager = manager + user.email = email + #core_db.session.add(user) + #core_db.session.add(user) + core_db.session.commit() + + return jsonify({'message': f'New user created: {name} ({email}) with password: {password}'}) + + +@google.tokengetter +def get_google_oauth_token(): + return session.get('google_token') + + +def generate_password(length=12): + characters = string.ascii_letters + string.digits + string.punctuation + return ''.join(secrets.choice(characters) for _ in range(length)) diff --git a/backend/src/database/wrapped_models.py b/backend/src/database/wrapped_models.py index 157a6b75..1a92f0b6 100644 --- a/backend/src/database/wrapped_models.py +++ b/backend/src/database/wrapped_models.py @@ -5,8 +5,7 @@ import jwt from flask import url_for from passlib.context import CryptContext -from sqlalchemy import func, true - +from sqlalchemy import func from src.application import config, exceptions from src.database import generated_models from src.database.db import db @@ -98,3 +97,7 @@ def photo_url(self) -> str: if self.photo: return url_for("public_blueprint.freelancer_photo", freelancer_id=self.id) return None + + +class Managers(generated_models.Manager): + pass \ No newline at end of file From 2f97169dd0d02141a038218454d7549c5addede5 Mon Sep 17 00:00:00 2001 From: Khvatov Dmitry Date: Tue, 5 Dec 2023 00:45:46 +0100 Subject: [PATCH 02/36] Fix user creation --- backend/.gitignore | 1 + backend/src/application/oauth.py | 29 ++++++++++++-------------- backend/src/database/wrapped_models.py | 2 +- docker-compose.dev.yml | 1 + 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/backend/.gitignore b/backend/.gitignore index cf08a16d..9f7a86fe 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -2,3 +2,4 @@ logs/ data/ *.bak aieye_credentials.json +client_secret_apps.json diff --git a/backend/src/application/oauth.py b/backend/src/application/oauth.py index 0ebbe0c6..37f8e463 100644 --- a/backend/src/application/oauth.py +++ b/backend/src/application/oauth.py @@ -5,12 +5,11 @@ from flask import Blueprint, redirect, url_for, session, jsonify from flask_oauthlib.client import OAuth -from backend.src.application.config import GOOGLE_CLIENT_SECRET_PATH -from backend.src.database.db import db as core_db -from backend.src.database.wrapped_models import User, Managers +from src.application.config import GOOGLE_CLIENT_SECRET_PATH +from src.database.db import db as core_db +from src.database.wrapped_models import User, Managers - -oauth_blueprint = Blueprint('oauth', import_name=__name__, url_prefix="/oauth/",) +oauth_blueprint = Blueprint('oauth', import_name=__name__, url_prefix="/oauth/", ) with open(GOOGLE_CLIENT_SECRET_PATH) as f: config_data = json.load(f) @@ -41,13 +40,14 @@ def logout(): return redirect(url_for('index')) +## go to http://127.0.0.1:5000/oauth/login @oauth_blueprint.route('/login/authorized') def authorized(): response = google.authorized_response() if response is None or response.get('access_token') is None: return jsonify({'error': 'Access denied'}), 401 - #TODO: + # TODO: session['google_token'] = (response['access_token'], '') user_info = google.get('userinfo') @@ -60,20 +60,17 @@ def authorized(): return jsonify({'message': f'Logged in as: {name} ({email})'}) else: password = generate_password() - user = User.create_user(username=name, password=password) - core_db.session.flush() - #TODO: - manager = Managers(code=google_user_id, name=f"{name} manager", user=user) - #core_db.session.add(manager) - core_db.session.commit() + user = User.create_user(username=name, password=password) - #core_db.session.flush() + manager = Managers(code=google_user_id, name=f"{name} manager") + core_db.session.add(manager) + core_db.session.flush() # Flush here if necessary - #user.manager = manager + # Associate manager with user + user.manager_id = manager.id user.email = email - #core_db.session.add(user) - #core_db.session.add(user) + core_db.session.add(user) core_db.session.commit() return jsonify({'message': f'New user created: {name} ({email}) with password: {password}'}) diff --git a/backend/src/database/wrapped_models.py b/backend/src/database/wrapped_models.py index 1a92f0b6..7dbfdf1e 100644 --- a/backend/src/database/wrapped_models.py +++ b/backend/src/database/wrapped_models.py @@ -100,4 +100,4 @@ def photo_url(self) -> str: class Managers(generated_models.Manager): - pass \ No newline at end of file + pass diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index e4c483e8..7ecbcb1e 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -20,6 +20,7 @@ services: REQUIREMENTS_FILE: requirements.dev.txt volumes: - ./logs:/app/backend/logs + - ./backend:/app/backend - ./backend/src:/app/backend/src - ./backend/tests:/app/backend/tests ports: From a8e61a60bcf29c3d92ad30350de63ddd05454536 Mon Sep 17 00:00:00 2001 From: "Sergey A." Date: Tue, 5 Dec 2023 15:04:57 +0500 Subject: [PATCH 03/36] google auth/login WIP, update --- backend/src/application/app_core.py | 10 +++++----- backend/src/application/oauth.py | 19 +++++++++++++++---- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/backend/src/application/app_core.py b/backend/src/application/app_core.py index 747604cf..bc7509d6 100644 --- a/backend/src/application/app_core.py +++ b/backend/src/application/app_core.py @@ -22,7 +22,7 @@ from . import config, exceptions from .urls import api_blueprint, public_blueprint -from .oauth import oauth_blueprint, oauth +from .oauth import oauth_blueprint class FlaskLogged(Flask): @@ -51,11 +51,11 @@ def _store_authorized_user_in_context(): if not auth_header: return - splitted = [item.strip() for item in auth_header.split(" ")] - if not splitted or splitted[0].upper() not in ["BEARER", "TOKEN", "JWT"]: + split = [item.strip() for item in auth_header.split(" ")] + if not split or split[0].upper() not in ["BEARER", "TOKEN", "JWT"]: return - token = splitted[-1] + token = split[-1] if not token: return @@ -93,7 +93,7 @@ def before_request(): with app.app_context(): app.register_blueprint(api_blueprint) app.register_blueprint(public_blueprint) - app.register_blueprint(oauth_blueprint, url_prefix='/oauth', oauth=oauth) + app.register_blueprint(oauth_blueprint, url_prefix='/oauth') try: with app.config["AIEYE_CREDENTIALS_PATH"].open() as file: diff --git a/backend/src/application/oauth.py b/backend/src/application/oauth.py index 37f8e463..9c593b45 100644 --- a/backend/src/application/oauth.py +++ b/backend/src/application/oauth.py @@ -4,11 +4,13 @@ from flask import Blueprint, redirect, url_for, session, jsonify from flask_oauthlib.client import OAuth - from src.application.config import GOOGLE_CLIENT_SECRET_PATH from src.database.db import db as core_db from src.database.wrapped_models import User, Managers +from src.application.stub_views import anonymous_user_required, authorized_user_required +from src.database import wrapped_schemas + oauth_blueprint = Blueprint('oauth', import_name=__name__, url_prefix="/oauth/", ) with open(GOOGLE_CLIENT_SECRET_PATH) as f: @@ -17,7 +19,7 @@ oauth = OAuth() google = oauth.remote_app( - 'google', + name='google', consumer_key=config_data['web']['client_id'], consumer_secret=config_data['web']['client_secret'], request_token_params={'scope': 'email'}, @@ -29,11 +31,13 @@ ) +@anonymous_user_required @oauth_blueprint.route('/login') def login(): return google.authorize(callback=url_for('oauth.authorized', _external=True)) +@authorized_user_required @oauth_blueprint.route('/logout') def logout(): session.pop('google_token', None) @@ -41,6 +45,7 @@ def logout(): ## go to http://127.0.0.1:5000/oauth/login +@anonymous_user_required @oauth_blueprint.route('/login/authorized') def authorized(): response = google.authorized_response() @@ -56,8 +61,10 @@ def authorized(): name = user_info.data.get('name', email.split('@')[0]) existing_user = User.query.filter_by(email=email).first() + if existing_user: - return jsonify({'message': f'Logged in as: {name} ({email})'}) + if not existing_user.manager_id: + return jsonify({'error': 'Access denied. This user is not a Manager.'}), 401 else: password = generate_password() @@ -73,7 +80,11 @@ def authorized(): core_db.session.add(user) core_db.session.commit() - return jsonify({'message': f'New user created: {name} ({email}) with password: {password}'}) + existing_user = user + + token = existing_user.encode_auth_token() + current_user = wrapped_schemas.UserSchema().dump(existing_user) + return jsonify({"token": token, "current_user": current_user}) @google.tokengetter From aa79c5ebfe1a8cce3960c98dc8d6744cf2001796 Mon Sep 17 00:00:00 2001 From: "Sergey A." Date: Tue, 5 Dec 2023 18:51:37 +0500 Subject: [PATCH 04/36] google auth/login WIP, update --- backend/src/application/oauth.py | 31 +++++++++++++++---------------- backend/src/utils.py | 7 +++++++ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/backend/src/application/oauth.py b/backend/src/application/oauth.py index 9c593b45..2517a36d 100644 --- a/backend/src/application/oauth.py +++ b/backend/src/application/oauth.py @@ -1,15 +1,13 @@ import json -import secrets -import string -from flask import Blueprint, redirect, url_for, session, jsonify -from flask_oauthlib.client import OAuth +from flask import Blueprint, redirect, url_for, session, jsonify, g, current_app +from flask_oauthlib.client import OAuth, OAuthException from src.application.config import GOOGLE_CLIENT_SECRET_PATH -from src.database.db import db as core_db -from src.database.wrapped_models import User, Managers - from src.application.stub_views import anonymous_user_required, authorized_user_required from src.database import wrapped_schemas +from src.database.db import db as core_db +from src.database.wrapped_models import User, Managers +from src.utils import generate_password oauth_blueprint = Blueprint('oauth', import_name=__name__, url_prefix="/oauth/", ) @@ -40,7 +38,8 @@ def login(): @authorized_user_required @oauth_blueprint.route('/logout') def logout(): - session.pop('google_token', None) + g.token_is_deprecated = True + # session.pop('google_token', None) return redirect(url_for('index')) @@ -52,9 +51,10 @@ def authorized(): if response is None or response.get('access_token') is None: return jsonify({'error': 'Access denied'}), 401 - # TODO: - session['google_token'] = (response['access_token'], '') - user_info = google.get('userinfo') + try: + user_info = google.get('userinfo') + except OAuthException as exc: + return jsonify({'error': str(exc)}), 401 google_user_id = user_info.data['id'] email = user_info.data['email'] @@ -63,11 +63,14 @@ def authorized(): existing_user = User.query.filter_by(email=email).first() if existing_user: + current_app.logger.debug(f"Specified user with email {email} found") if not existing_user.manager_id: + current_app.logger.debug(f"Specified user with email {email} is not a manager") return jsonify({'error': 'Access denied. This user is not a Manager.'}), 401 else: password = generate_password() + current_app.logger.debug(f"Creation of a new user with email {email} started") user = User.create_user(username=name, password=password) manager = Managers(code=google_user_id, name=f"{name} manager") @@ -79,6 +82,7 @@ def authorized(): user.email = email core_db.session.add(user) core_db.session.commit() + current_app.logger.debug(f"Creation of a new user with email {email} completed") existing_user = user @@ -90,8 +94,3 @@ def authorized(): @google.tokengetter def get_google_oauth_token(): return session.get('google_token') - - -def generate_password(length=12): - characters = string.ascii_letters + string.digits + string.punctuation - return ''.join(secrets.choice(characters) for _ in range(length)) diff --git a/backend/src/utils.py b/backend/src/utils.py index b9f37599..94fab91f 100644 --- a/backend/src/utils.py +++ b/backend/src/utils.py @@ -1,3 +1,5 @@ +import secrets +import string from enum import Enum from typing import Optional, Type, TypeVar @@ -28,3 +30,8 @@ def get_full_user_name(obj) -> Optional[str]: # Use username as last-resort fallback when actual data not available return result or getattr(obj, "username", None) or None + + +def generate_password(length=12): + characters = string.ascii_letters + string.digits + string.punctuation + return ''.join(secrets.choice(characters) for _ in range(length)) \ No newline at end of file From 9769f0463e4a8d585de35e396522518f458e40e9 Mon Sep 17 00:00:00 2001 From: "Sergey A." Date: Tue, 5 Dec 2023 19:05:06 +0500 Subject: [PATCH 05/36] google auth/login WIP, update --- backend/src/application/oauth.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/backend/src/application/oauth.py b/backend/src/application/oauth.py index 2517a36d..ebd41d4a 100644 --- a/backend/src/application/oauth.py +++ b/backend/src/application/oauth.py @@ -1,19 +1,22 @@ import json -from flask import Blueprint, redirect, url_for, session, jsonify, g, current_app +from flask import Blueprint, url_for, session, jsonify, current_app from flask_oauthlib.client import OAuth, OAuthException from src.application.config import GOOGLE_CLIENT_SECRET_PATH -from src.application.stub_views import anonymous_user_required, authorized_user_required +from src.application.stub_views import anonymous_user_required from src.database import wrapped_schemas from src.database.db import db as core_db from src.database.wrapped_models import User, Managers from src.utils import generate_password -oauth_blueprint = Blueprint('oauth', import_name=__name__, url_prefix="/oauth/", ) -with open(GOOGLE_CLIENT_SECRET_PATH) as f: - config_data = json.load(f) +def load_google_config(): + with open(GOOGLE_CLIENT_SECRET_PATH) as f: + return json.load(f) + +config_data = load_google_config() +oauth_blueprint = Blueprint('oauth', import_name=__name__, url_prefix="/oauth/", ) oauth = OAuth() google = oauth.remote_app( @@ -35,15 +38,7 @@ def login(): return google.authorize(callback=url_for('oauth.authorized', _external=True)) -@authorized_user_required -@oauth_blueprint.route('/logout') -def logout(): - g.token_is_deprecated = True - # session.pop('google_token', None) - return redirect(url_for('index')) - - -## go to http://127.0.0.1:5000/oauth/login +# go to http://127.0.0.1:5000/oauth/login @anonymous_user_required @oauth_blueprint.route('/login/authorized') def authorized(): @@ -54,7 +49,7 @@ def authorized(): try: user_info = google.get('userinfo') except OAuthException as exc: - return jsonify({'error': str(exc)}), 401 + return jsonify({'error': f'OAuth Exception: {str(exc)}'}), 401 google_user_id = user_info.data['id'] email = user_info.data['email'] From ca55410d513e12b2822ac611db26a97b2a24d022 Mon Sep 17 00:00:00 2001 From: "Sergey A." Date: Wed, 6 Dec 2023 10:26:10 +0500 Subject: [PATCH 06/36] google auth/login WIP, logging, refactoring --- backend/src/application/oauth.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/backend/src/application/oauth.py b/backend/src/application/oauth.py index ebd41d4a..e4c00a6a 100644 --- a/backend/src/application/oauth.py +++ b/backend/src/application/oauth.py @@ -2,6 +2,7 @@ from flask import Blueprint, url_for, session, jsonify, current_app from flask_oauthlib.client import OAuth, OAuthException +from http import HTTPStatus from src.application.config import GOOGLE_CLIENT_SECRET_PATH from src.application.stub_views import anonymous_user_required from src.database import wrapped_schemas @@ -43,13 +44,20 @@ def login(): @oauth_blueprint.route('/login/authorized') def authorized(): response = google.authorized_response() - if response is None or response.get('access_token') is None: - return jsonify({'error': 'Access denied'}), 401 + + if response is None: + current_app.logger.error(f"google.authorized_response() is None") + return jsonify({'error': 'Access denied'}), HTTPStatus.UNAUTHORIZED + + if response.get('access_token') is None: + current_app.logger.error(f"Unable to retrieve access_token") + return jsonify({'error': 'Access denied'}), HTTPStatus.UNAUTHORIZED try: user_info = google.get('userinfo') except OAuthException as exc: - return jsonify({'error': f'OAuth Exception: {str(exc)}'}), 401 + current_app.logger.error(f"Unable to retrieve userinfo. Details: {exc}") + return jsonify({'error': f'OAuth Exception: {str(exc)}'}), HTTPStatus.UNAUTHORIZED google_user_id = user_info.data['id'] email = user_info.data['email'] @@ -61,7 +69,7 @@ def authorized(): current_app.logger.debug(f"Specified user with email {email} found") if not existing_user.manager_id: current_app.logger.debug(f"Specified user with email {email} is not a manager") - return jsonify({'error': 'Access denied. This user is not a Manager.'}), 401 + return jsonify({'error': 'Access denied. This user is not a Manager.'}), HTTPStatus.UNAUTHORIZED else: password = generate_password() From 0ec555e3a3fee59d2abc35ea40639bc49df8b611 Mon Sep 17 00:00:00 2001 From: "Sergey A." Date: Wed, 6 Dec 2023 13:39:30 +0500 Subject: [PATCH 07/36] add typing --- backend/src/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/utils.py b/backend/src/utils.py index 94fab91f..d4d39c2a 100644 --- a/backend/src/utils.py +++ b/backend/src/utils.py @@ -32,6 +32,6 @@ def get_full_user_name(obj) -> Optional[str]: return result or getattr(obj, "username", None) or None -def generate_password(length=12): +def generate_password(length=12) -> str: characters = string.ascii_letters + string.digits + string.punctuation - return ''.join(secrets.choice(characters) for _ in range(length)) \ No newline at end of file + return ''.join(secrets.choice(characters) for _ in range(length)) From 21241fb57a67cf2974c7186e398c157d0cad75b8 Mon Sep 17 00:00:00 2001 From: "Sergey A." Date: Wed, 6 Dec 2023 14:15:11 +0500 Subject: [PATCH 08/36] "google token not found" bugfix --- backend/src/application/oauth.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/src/application/oauth.py b/backend/src/application/oauth.py index e4c00a6a..333db9a6 100644 --- a/backend/src/application/oauth.py +++ b/backend/src/application/oauth.py @@ -53,6 +53,9 @@ def authorized(): current_app.logger.error(f"Unable to retrieve access_token") return jsonify({'error': 'Access denied'}), HTTPStatus.UNAUTHORIZED + # preserve the Google Oauth access token in the session + session['google_token'] = (response['access_token'], 'Google Access Token') + try: user_info = google.get('userinfo') except OAuthException as exc: From 27d6bd14b901ede7238d51628f112bde87116666 Mon Sep 17 00:00:00 2001 From: "Sergey A." Date: Thu, 7 Dec 2023 13:49:42 +0500 Subject: [PATCH 09/36] update comment --- backend/src/application/oauth.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/application/oauth.py b/backend/src/application/oauth.py index 333db9a6..fdbe837c 100644 --- a/backend/src/application/oauth.py +++ b/backend/src/application/oauth.py @@ -81,7 +81,9 @@ def authorized(): manager = Managers(code=google_user_id, name=f"{name} manager") core_db.session.add(manager) - core_db.session.flush() # Flush here if necessary + # flushing the session ensures that the primary key of the manager is available for use in + # the user.manager_id field + core_db.session.flush() # Associate manager with user user.manager_id = manager.id From 307be0c4e95cacc9ea1979f3039cc4e856737c48 Mon Sep 17 00:00:00 2001 From: "Sergey A." Date: Thu, 7 Dec 2023 14:26:44 +0500 Subject: [PATCH 10/36] fix broken test --- backend/tests/core/test_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/core/test_core.py b/backend/tests/core/test_core.py index 42da0e6c..f02d4ead 100644 --- a/backend/tests/core/test_core.py +++ b/backend/tests/core/test_core.py @@ -22,7 +22,7 @@ def test_token_expired_message(self, client, ordinary_user): token = jwt.encode(payload, config.SECRET_KEY, algorithm=config.JWT_ALGORITHM) time.sleep(3) # uri не имеет значения, любое. - response = utils.auth_call(client.get, "/api/apps/", token) + response = utils.auth_call(client.get, "/api/freelancers/", token) assert response.status_code == HTTPStatus.UNAUTHORIZED @pytest.mark.local From 6ef6f2c0b7271517972bd4f4e8591e3da04d94b6 Mon Sep 17 00:00:00 2001 From: "Sergey A." Date: Thu, 7 Dec 2023 14:31:26 +0500 Subject: [PATCH 11/36] add kwargs to User.create_user class method for passing additional fields --- backend/src/application/oauth.py | 3 +-- backend/src/database/wrapped_models.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/application/oauth.py b/backend/src/application/oauth.py index fdbe837c..efb0f49f 100644 --- a/backend/src/application/oauth.py +++ b/backend/src/application/oauth.py @@ -77,7 +77,7 @@ def authorized(): password = generate_password() current_app.logger.debug(f"Creation of a new user with email {email} started") - user = User.create_user(username=name, password=password) + user = User.create_user(username=name, password=password, email=email) manager = Managers(code=google_user_id, name=f"{name} manager") core_db.session.add(manager) @@ -87,7 +87,6 @@ def authorized(): # Associate manager with user user.manager_id = manager.id - user.email = email core_db.session.add(user) core_db.session.commit() current_app.logger.debug(f"Creation of a new user with email {email} completed") diff --git a/backend/src/database/wrapped_models.py b/backend/src/database/wrapped_models.py index 7dbfdf1e..37daf292 100644 --- a/backend/src/database/wrapped_models.py +++ b/backend/src/database/wrapped_models.py @@ -31,7 +31,7 @@ def get_password_context(cls): return cls._pwcontext @classmethod - def create_user(cls, username: str, password: str, is_admin: bool = False): + def create_user(cls, username: str, password: str, is_admin: bool = False, **kwargs): # Проверяем, что нет пользователя с таким же username existing = User.query.filter( func.lower(User.username) == func.lower(username) @@ -45,6 +45,7 @@ def create_user(cls, username: str, password: str, is_admin: bool = False): username=username, password=cls.encode_password(password), is_admin=is_admin, + **kwargs, ) db.session.add(user) db.session.commit() From 60d3fed13bcbb563432e58ba0f8adee1350ed9f8 Mon Sep 17 00:00:00 2001 From: "Sergey A." Date: Fri, 15 Dec 2023 13:45:27 +0500 Subject: [PATCH 12/36] change incorrect webpackChunkName --- client/src/router/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/router/index.js b/client/src/router/index.js index 90ac0f3c..4c307137 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -14,7 +14,7 @@ const page_proposals = { path: '/proposals', name: 'Proposals', component: () => - import(/* webpackChunkName: "Proposals2" */ '@/views/Proposals'), + import(/* webpackChunkName: "Proposals" */ '@/views/Proposals'), meta: {title: 'Proposals', visible_for_all: true}, }; From 8ab72475e18c20cc0e0c22f1a12d0e342c83825c Mon Sep 17 00:00:00 2001 From: "Sergey A." Date: Fri, 15 Dec 2023 19:41:00 +0500 Subject: [PATCH 13/36] google auth --- backend/src/application/oauth.py | 67 ++++++++++---------------------- client/public/index.html | 2 + client/src/App.vue | 63 ++++++++++++++++++++++++++++-- client/src/config.js | 1 + 4 files changed, 84 insertions(+), 49 deletions(-) diff --git a/backend/src/application/oauth.py b/backend/src/application/oauth.py index efb0f49f..2ce781a4 100644 --- a/backend/src/application/oauth.py +++ b/backend/src/application/oauth.py @@ -1,8 +1,10 @@ import json - -from flask import Blueprint, url_for, session, jsonify, current_app -from flask_oauthlib.client import OAuth, OAuthException from http import HTTPStatus + +from flask import Blueprint, jsonify, current_app, request +from google.auth.transport import requests +from google.oauth2 import id_token + from src.application.config import GOOGLE_CLIENT_SECRET_PATH from src.application.stub_views import anonymous_user_required from src.database import wrapped_schemas @@ -18,53 +20,31 @@ def load_google_config(): config_data = load_google_config() oauth_blueprint = Blueprint('oauth', import_name=__name__, url_prefix="/oauth/", ) -oauth = OAuth() - -google = oauth.remote_app( - name='google', - consumer_key=config_data['web']['client_id'], - consumer_secret=config_data['web']['client_secret'], - request_token_params={'scope': 'email'}, - base_url='https://www.googleapis.com/oauth2/v1/', - request_token_url=None, - access_token_method='POST', - access_token_url='https://accounts.google.com/o/oauth2/token', - authorize_url='https://accounts.google.com/o/oauth2/auth', -) @anonymous_user_required -@oauth_blueprint.route('/login') -def login(): - return google.authorize(callback=url_for('oauth.authorized', _external=True)) - - -# go to http://127.0.0.1:5000/oauth/login -@anonymous_user_required -@oauth_blueprint.route('/login/authorized') +@oauth_blueprint.route('/login/authorized', methods=['POST']) def authorized(): - response = google.authorized_response() - - if response is None: - current_app.logger.error(f"google.authorized_response() is None") - return jsonify({'error': 'Access denied'}), HTTPStatus.UNAUTHORIZED + if "credential" not in request.json: + return jsonify({'error': 'No credentials'}), HTTPStatus.BAD_REQUEST - if response.get('access_token') is None: - current_app.logger.error(f"Unable to retrieve access_token") - return jsonify({'error': 'Access denied'}), HTTPStatus.UNAUTHORIZED + credential = request.json["credential"] + if not credential: + return jsonify({'error': 'No credentials'}), HTTPStatus.BAD_REQUEST - # preserve the Google Oauth access token in the session - session['google_token'] = (response['access_token'], 'Google Access Token') + client_id = config_data['web']['client_id'] try: - user_info = google.get('userinfo') - except OAuthException as exc: - current_app.logger.error(f"Unable to retrieve userinfo. Details: {exc}") - return jsonify({'error': f'OAuth Exception: {str(exc)}'}), HTTPStatus.UNAUTHORIZED + idinfo = id_token.verify_oauth2_token(credential, + requests.Request(), client_id) + except: + return jsonify({'error': 'Token verification failed'}), HTTPStatus.BAD_REQUEST + + current_app.logger.debug(f"idinfo = {idinfo}") - google_user_id = user_info.data['id'] - email = user_info.data['email'] - name = user_info.data.get('name', email.split('@')[0]) + google_user_id = idinfo['sub'] + email = idinfo['email'] + name = idinfo['name'] existing_user = User.query.filter_by(email=email).first() @@ -96,8 +76,3 @@ def authorized(): token = existing_user.encode_auth_token() current_user = wrapped_schemas.UserSchema().dump(existing_user) return jsonify({"token": token, "current_user": current_user}) - - -@google.tokengetter -def get_google_oauth_token(): - return session.get('google_token') diff --git a/client/public/index.html b/client/public/index.html index 41235286..60851c7e 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -6,6 +6,8 @@ <%= htmlWebpackPlugin.options.title %> + +