Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f97a754
google auth/login WIP
varnie Dec 4, 2023
537745d
Merge branch 'dev' into google-auth
kagel Dec 4, 2023
2f97169
Fix user creation
kagel Dec 4, 2023
a8e61a6
google auth/login WIP, update
varnie Dec 5, 2023
aa79c5e
google auth/login WIP, update
varnie Dec 5, 2023
9769f04
google auth/login WIP, update
varnie Dec 5, 2023
ca55410
google auth/login WIP, logging, refactoring
varnie Dec 6, 2023
0ec555e
add typing
varnie Dec 6, 2023
a42d7b1
Merge remote-tracking branch 'origin/dev' into google-auth
varnie Dec 6, 2023
21241fb
"google token not found" bugfix
varnie Dec 6, 2023
27d6bd1
update comment
varnie Dec 7, 2023
307be0c
fix broken test
varnie Dec 7, 2023
6ef6f2c
add kwargs to User.create_user class method for passing additional fi…
varnie Dec 7, 2023
60d3fed
change incorrect webpackChunkName
varnie Dec 15, 2023
a857010
Merge remote-tracking branch 'origin/dev' into google-auth
varnie Dec 15, 2023
8ab7247
google auth
varnie Dec 15, 2023
3a5bd5e
update
varnie Dec 15, 2023
2b18e0c
some fixes for situation where user gets logged out
varnie Dec 15, 2023
658b709
add better logging
varnie Dec 16, 2023
2dcf63b
fix handleCredentialsResponse
varnie Dec 16, 2023
4bf2c04
fix dialog closing bug
varnie Dec 18, 2023
f188b31
delete debug logs
varnie Dec 18, 2023
7e46bf7
simplify checks
varnie Dec 18, 2023
671ce08
this was wrong, fix it
varnie Dec 18, 2023
810d839
alter users registration process
varnie Dec 20, 2023
3c83c98
work on new task - managers invitation and registration
varnie Dec 21, 2023
c96cece
fix typo
varnie Dec 21, 2023
3bf62bd
InvitePage itself
varnie Dec 21, 2023
22a97a8
continue working on a task
varnie Dec 22, 2023
d62d923
add email settings
varnie Dec 27, 2023
8b6c5f6
minor changes
varnie Dec 27, 2023
1a3fe6d
resolve merge conflicts
varnie Dec 27, 2023
f4347fb
Merge branch 'dev' into google-auth
varnie Dec 27, 2023
c24fe44
improve invitation email template
varnie Dec 28, 2023
e57c639
Merge remote-tracking branch 'origin/google-auth' into google-auth
varnie Dec 28, 2023
be69ef4
UI fixes
varnie Dec 28, 2023
89cb1fd
minor improvements
varnie Dec 28, 2023
9b940a4
Keep env in .env
kagel Jan 1, 2024
539b99e
Invite user bugfixes
kagel Jan 1, 2024
e6d4916
Fix user invite schema
kagel Jan 1, 2024
5c7afc8
Various bugfixes + logging
kagel Jan 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FLASK_APP=app.py
FLASK_RUN_HOST=0.0.0.0
FLASK_DEBUG=true
DB_HOST=db:5434
SECRET_KEY=muiguyoNoo0kaip1nah3phoi1ouFeuc5aexeeKai2aeY9pohqu5Enohthohng4to

MAIL_SERVER="localhost"
MAIL_PORT=465
MAIL_USERNAME=""
MAIL_PASSWORD=""
MAIL_DEFAULT_SENDER=""
MAIL_USE_TLS=false
MAIL_USE_SSL=true
1 change: 1 addition & 0 deletions backend/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ __pycache__
data
venv
logs
client_secret_apps.json
5 changes: 0 additions & 5 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ FROM python:3.9-alpine
WORKDIR /app/backend

ARG REQUIREMENTS_FILE
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
ENV FLASK_DEBUG=true
ENV DB_HOST=db:5434
ENV SECRET_KEY=muiguyoNoo0kaip1nah3phoi1ouFeuc5aexeeKai2aeY9pohqu5Enohthohng4to

COPY requirements.txt requirements.txt
COPY requirements.dev.txt requirements.dev.txt
Expand Down
2 changes: 2 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ sqlalchemy-utils == 0.39.0
werkzeug == 2.3.7
gevent
psycogreen
flask_oauthlib==0.9.6
Flask-Mail == 0.9.1
14 changes: 9 additions & 5 deletions backend/src/application/app_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

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
from flask_mail import Mail

# Добавление автоматически сгенерированных моделей.
try:
Expand All @@ -22,6 +23,7 @@

from . import config, exceptions
from .urls import api_blueprint, public_blueprint
from .oauth import oauth_blueprint


class FlaskLogged(Flask):
Expand Down Expand Up @@ -50,11 +52,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

Expand Down Expand Up @@ -86,12 +88,14 @@ def before_request():

# enable CORS
CORS(app, resources={r"/*": {"origins": config.FLASK_HTTP_ORIGIN}})

mail = Mail()
core_db.init_app(app)
mail.init_app(app)

with app.app_context():
app.register_blueprint(api_blueprint)
app.register_blueprint(public_blueprint)
app.register_blueprint(oauth_blueprint, url_prefix='/oauth')

try:
with app.config["AIEYE_CREDENTIALS_PATH"].open() as file:
Expand Down
16 changes: 16 additions & 0 deletions backend/src/application/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,24 @@
FLASK_RUN_HOST = "0.0.0.0"
FLASK_HTTP_ORIGIN = "*"

MAIL_SERVER = "localhost"
MAIL_PORT = 465
MAIL_USERNAME = ""
MAIL_PASSWORD = ""
MAIL_DEFAULT_SENDER = ""
MAIL_USE_TLS = False
MAIL_USE_SSL = True

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():
global DEBUG, SQLALCHEMY_DATABASE_URI, JWT_EXPIRATION_DELTA, SECRET_KEY
global JSONIFY_MIMETYPE, FLASK_RUN_PORT, FLASK_RUN_HOST, FLASK_HTTP_ORIGIN
global MAIL_SERVER, MAIL_PORT, MAIL_USERNAME, MAIL_PASSWORD, MAIL_DEFAULT_SENDER, MAIL_USE_SSL, MAIL_USE_TLS

DEBUG = os.getenv("FLASK_DEBUG", "true").upper() == "TRUE"
SQLALCHEMY_DATABASE_URI = (
Expand All @@ -45,3 +55,9 @@ def init_config():
FLASK_RUN_PORT = int(os.getenv("FLASK_RUN_PORT", "5000") or "5000")
FLASK_RUN_HOST = os.getenv("FLASK_RUN_HOST", "0.0.0.0")
FLASK_HTTP_ORIGIN = os.getenv("FLASK_HTTP_ORIGIN", "*")

MAIL_SERVER = os.getenv("MAIL_SERVER", "")
MAIL_PORT = os.getenv("MAIL_PORT", 465)
MAIL_USERNAME = os.getenv("MAIL_USERNAME", "")
MAIL_PASSWORD = os.getenv("MAIL_PASSWORD", "")
MAIL_DEFAULT_SENDER = os.getenv("MAIL_DEFAULT_SENDER", "")
62 changes: 62 additions & 0 deletions backend/src/application/oauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import json
from http import HTTPStatus

from flask import Blueprint, jsonify, current_app, request
from google.auth.exceptions import GoogleAuthError
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
from src.database.db import db as core_db
from src.database.wrapped_models import User, Managers
from src.utils import generate_password


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/", )


@anonymous_user_required
@oauth_blueprint.route('/login/authorized', methods=['POST'])
def authorized():
if "credential" not in request.json or not request.json["credential"]:
return jsonify({'error': 'Invalid request. No credentials.'}), HTTPStatus.BAD_REQUEST

credential = request.json["credential"]
client_id = config_data['web']['client_id']
try:
decoded_token = id_token.verify_oauth2_token(credential,
requests.Request(), client_id)
except GoogleAuthError as exc:
current_app.logger.error(f"Unable to decode token: {exc}")
return jsonify({'error': 'Token verification failed'}), HTTPStatus.BAD_REQUEST

current_app.logger.debug(f"Successfully decoded token: {decoded_token}")

email = decoded_token['email']
name = decoded_token['name']

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.is_admin:
current_app.logger.error(f"Specified user with email {email} already exists and he/she is not an admin")
return jsonify({'error': 'This user already exists and he/she is not an admin.'}), HTTPStatus.UNAUTHORIZED
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, email=email, is_admin=True)
current_app.logger.debug(f"Creation of a new user with email {email} completed")
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})
13 changes: 11 additions & 2 deletions backend/src/application/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
validate,
validates_schema,
)
from werkzeug.exceptions import BadRequest

from src.application.enums import GenerateProposalStyle, ProposalStatus
from src.application.marshmallow_ma import ma
from src.database import models
from werkzeug.exceptions import BadRequest

# pylint: disable=invalid-name

VALIDATION_EXCEPTIONS = (BadRequest, TypeError, ValueError, ValidationError)


DATE_FORMAT = "%Y-%m-%d"


Expand Down Expand Up @@ -140,3 +140,12 @@ class BulkSkillsSchema(Schema):
class ChangeProposal2StatusRequestSchema(Schema):
proposal_id = fields.Integer(allow_none=False, required=True)
status = fields.Integer(required=True, validate=validate.OneOf(ProposalStatus.values()))


class InviteUserRequestSchema(Schema):
email = fields.Email(allow_none=False, required=True)


class RegistrationRequestSchema(Schema):
last_name = fields.String(required=False, allow_none=True)
first_name = fields.String(required=False, allow_none=True)
6 changes: 6 additions & 0 deletions backend/src/application/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,9 @@
"/freelancer_photo/<int:freelancer_id>.png",
view_func=views.FreelancerPhoto().as_view("freelancer_photo"),
)
api_blueprint.add_url_rule(
"/invite/", view_func=views.InviteUser.as_view("invite_user")
)
public_blueprint.add_url_rule(
"/team_member_registration/<uuid>/", view_func=views.TeamMemberRegistrationPage.as_view("team_member_registration_page")
)
131 changes: 128 additions & 3 deletions backend/src/application/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
from json import JSONDecodeError

import marshmallow
from flask import current_app, g, jsonify, request, send_file, render_template
from flask import current_app, g, jsonify, request, send_file, render_template, url_for
from flask.views import MethodView
from flask_mail import Message
from marshmallow import EXCLUDE, ValidationError
from sqlalchemy import Integer, distinct, func, or_, text, desc, asc
from sqlalchemy.orm import joinedload, aliased
Expand All @@ -34,9 +35,10 @@
from src.database.generated_models import Manager, FreelancerSkill, Questions, \
FreelancersQuestionsAnswers, Candidate, CandidateSkill
from src.database.generated_models import Skill, SkillCategory
from src.database.models import Application, Proposal, Proposal2StatusRel, ProposalQuestionAnswer
from src.database.models import Application, Proposal, Proposal2StatusRel, ProposalQuestionAnswer, RegistrationRequest
from src.database.wrapped_models import Freelancer, User
from src.database.wrapped_schemas import without_id
from src.utils import generate_password

PrepareAppDataResult = namedtuple("PrepareAppDataResult", ["app", "errors"])

Expand Down Expand Up @@ -161,6 +163,129 @@ def post(self):
return jsonify({"response": response})


class InviteUser(MethodView):
decorators = [admin_user_required]
methods = ["POST"]

def post(self):
try:
data = schemas.InviteUserRequestSchema().load(request.json)
current_app.logger.debug(
'InviteUser view POST call'
)
except schemas.VALIDATION_EXCEPTIONS as error:
raise exceptions.BadRequest(str(error))

email = data["email"]

if User.query.filter(func.lower(User.email) == func.lower(email)).count():
return jsonify({"message": "This user's email is already occupied, try another please."}), 500

self.send_invitation_email(email)
return {"message": f"Invitation email sent to {email}"}, 200

@staticmethod
def send_invitation_email(email):
registration_request = RegistrationRequest(email=email)
db.session.add(registration_request)
db.session.commit()

uuid = registration_request.uuid
registration_link = url_for('public_blueprint.team_member_registration_page', uuid=uuid, _external=True)

subject = "Invitation to Register"
body = f"""
Hello!
You have been invited to register on our website.
Follow the link to complete your registration: {registration_link}
"""

message = Message(subject, recipients=[email], body=body)

try:
mail = current_app.extensions['mail']
current_app.logger.debug(f"Sending invitation email to {email}...")
mail.send(message)
current_app.logger.debug(f"Invitation email sent")
except Exception as exc:
current_app.logger.error(f"Error sending invitation email to {email}: {str(exc)}")
raise exceptions.BadRequest(
f"Unable to send email to {email}."
)
else:
current_app.logger.debug("Email sent")

return {"message": f"Invitation email sent to {email}"}, 200


class TeamMemberRegistrationPage(MethodView):
methods = ["GET", "POST"]

def get(self, uuid):
registration_request = RegistrationRequest.query.filter_by(uuid=uuid).first()
current_app.logger.info(f"Registration request: {registration_request}")
if not registration_request:
current_app.logger.error(f"Registration request not found: {uuid}")
return jsonify({"error": "Registration request not found!"}), 404

if not registration_request.active:
current_app.logger.error(f"Registration request has already been processed: {uuid}")
return jsonify({"error": "Registration request has already been processed!"}), 404

registration_link = url_for('public_blueprint.team_member_registration_page', uuid=uuid)
return render_template("team_member_registration.html", registration_link=registration_link)

def post(self, uuid):
registration_request = RegistrationRequest.query.filter_by(uuid=uuid).first()
current_app.logger.info(f"Registration request: {registration_request}")
if not registration_request:
current_app.logger.error(f"Registration request not found: {uuid}")
return jsonify({"error": "Registration request not found!"}), 404

if not registration_request.active:
current_app.logger.error(f"Registration request has already been processed: {uuid}")
return jsonify({"error": "Registration request has already been processed!"}), 404

try:
data = schemas.RegistrationRequestSchema().load(request.json)
except schemas.VALIDATION_EXCEPTIONS as error:
current_app.logger.error(f"Error validating registration request: {str(error)}")
return jsonify({"error": str(error)}), 404

try:
user = User.create_user(email=registration_request.email,
username=registration_request.email,
password=generate_password(),
first_name=data['first_name'],
last_name=data['last_name'])
except exceptions.BadRequest as exc:
current_app.logger.error(f"Error creating user: {str(exc)}")
return jsonify({"error": exc.message}), 404

manager_code = uuid.replace("-", "")[:32]

try:
manager = Manager(code=manager_code, name=f"{data['first_name']} {data['last_name']}")
db.session.add(manager)
# flushing the session ensures that the primary key of the manager is available for use in
# the user.manager_id field
db.session.flush()

# Associate manager with user
user.manager_id = manager.id
db.session.add(user)

registration_request.active = False
db.session.add(registration_request)
db.session.commit()
except Exception as exc:
current_app.logger.error(f"Error creating manager for user: {str(exc)}")
return jsonify({"error": str(exc)}), 500

current_app.logger.info(f"User {user.username} created successfully")
return jsonify({"message": "Registration successful!"}), 201


class GenerateProposal(MethodView):
decorators = [authorized_user_required]
methods = ["POST"]
Expand All @@ -169,7 +294,7 @@ def post(self):
try:
data = schemas.GenerateProposalRequestSchema().load(request.json)
current_app.logger.debug(
f'GenerateProposal view POST call'
'GenerateProposal view POST call'
)
except schemas.VALIDATION_EXCEPTIONS as error:
raise exceptions.BadRequest(str(error))
Expand Down
Loading