Skip to content

Commit

Permalink
M2-4180 Track user login operations added.
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan-g-scnt committed Dec 11, 2023
1 parent 8313194 commit cf8427b
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 19 deletions.
26 changes: 9 additions & 17 deletions src/apps/authentication/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from jose import JWTError, jwt
from pydantic import ValidationError

from apps.authentication.api.auth_utils import auth_user
from apps.authentication.deps import get_current_token, get_current_user
from apps.authentication.domain.login import UserLogin, UserLoginRequest
from apps.authentication.domain.logout import UserLogoutRequest
Expand All @@ -16,17 +17,13 @@
TokenPayload,
TokenPurpose,
)
from apps.authentication.errors import (
AuthenticationError,
InvalidCredentials,
InvalidRefreshToken,
)
from apps.authentication.errors import AuthenticationError, InvalidRefreshToken
from apps.authentication.services.security import AuthenticationService
from apps.logs.user_activity_log import user_activity_login_log
from apps.shared.domain.response import Response
from apps.shared.response import EmptyResponse
from apps.users import UsersCRUD
from apps.users.domain import PublicUser, User
from apps.users.errors import UserNotFound
from apps.users.services.user_device import UserDeviceService
from config import settings
from infrastructure.database import atomic
Expand All @@ -36,20 +33,15 @@
async def get_token(
user_login_schema: UserLoginRequest = Body(...),
session=Depends(get_session),
user=Depends(auth_user),
user_activity_log=Depends(user_activity_login_log),
) -> Response[UserLogin]:
"""Generate the JWT access token."""
async with atomic(session):
try:
user: User = await AuthenticationService(
session
).authenticate_user(user_login_schema)
if user_login_schema.device_id:
await UserDeviceService(session, user.id).add_device(
user_login_schema.device_id
)
except UserNotFound:
raise InvalidCredentials(email=user_login_schema.email)

if user_login_schema.device_id:
await UserDeviceService(session, user.id).add_device(
user_login_schema.device_id
)
if user.email_encrypted != user_login_schema.email:
user = await UsersCRUD(session).update_encrypted_email(
user, user_login_schema.email
Expand Down
25 changes: 25 additions & 0 deletions src/apps/authentication/api/auth_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from fastapi import Body, Depends

from apps.authentication.domain.login import UserLoginRequest
from apps.authentication.errors import InvalidCredentials
from apps.authentication.services.security import AuthenticationService
from apps.users.domain import User
from apps.users.errors import UserNotFound
from infrastructure.database import atomic
from infrastructure.database.deps import get_session


async def auth_user(
user_login_schema: UserLoginRequest = Body(...),
session=Depends(get_session),
) -> User:
async with atomic(session):
try:
user: User = await AuthenticationService(
session
).authenticate_user(user_login_schema)

except UserNotFound:
raise InvalidCredentials(email=user_login_schema.email)

return user
12 changes: 12 additions & 0 deletions src/apps/logs/crud/user_activity_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from apps.logs.db.schemas import UserActivityLogSchema
from infrastructure.database.crud import BaseCRUD


class UserActivityLogCRUD(BaseCRUD[UserActivityLogSchema]):
schema_class = UserActivityLogSchema

async def save(
self, schema: UserActivityLogSchema
) -> UserActivityLogSchema:
"""Return UserActivityLogSchema instance."""
return await self._create(schema)
23 changes: 22 additions & 1 deletion src/apps/logs/db/schemas.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from sqlalchemy import Boolean, Column, String
from sqlalchemy import Boolean, Column, ForeignKey, String
from sqlalchemy.dialects.postgresql import JSONB

from infrastructure.database.base import Base
Expand All @@ -16,3 +16,24 @@ class NotificationLogSchema(Base):
notification_descriptions_updated = Column(Boolean(), nullable=False)
notifications_in_queue_updated = Column(Boolean(), nullable=False)
scheduled_notifications_updated = Column(Boolean(), nullable=False)


class UserActivityLogSchema(Base):
__tablename__ = "user_activity_logs"

user_id = Column(
ForeignKey("users.id", ondelete="RESTRICT"),
nullable=False,
index=True,
)
device_id = Column(String(), nullable=True)
event_type = Column(String(), nullable=False)
event = Column(String(), nullable=False)
user_agent = Column(String(), nullable=True)
mindlogger_content = Column(String(), nullable=False)

def __repr__(self):
return (
f"UserActivityLogSchema(id='{self.id}', user_id='{self.user_id}',"
f" target='{self.target}', action='{self.action}')"
)
9 changes: 9 additions & 0 deletions src/apps/logs/domain/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from enum import Enum


class UserActivityEventType(str, Enum):
LOGIN = "login"


class UserActivityEvent(str, Enum):
LOGIN = "login"
12 changes: 12 additions & 0 deletions src/apps/logs/services/user_activity_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from apps.logs.crud.user_activity_log import UserActivityLogCRUD
from apps.logs.db.schemas import UserActivityLogSchema


class UserActivityLogService:
def __init__(self, session):
self.session = session

async def create_log(
self, schema: UserActivityLogSchema
) -> UserActivityLogSchema:
return await UserActivityLogCRUD(self.session).save(schema)
35 changes: 35 additions & 0 deletions src/apps/logs/user_activity_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from fastapi import Body, Depends
from starlette.requests import Request

from apps.authentication.api.auth_utils import auth_user
from apps.authentication.domain.login import UserLoginRequest
from apps.logs.db.schemas import UserActivityLogSchema
from apps.logs.domain.constants import UserActivityEvent, UserActivityEventType
from apps.logs.services.user_activity_log import UserActivityLogService
from infrastructure.database.deps import get_session
from infrastructure.http.deps import get_mindlogger_content_source
from infrastructure.http.domain import MindloggerContentSource


async def user_activity_login_log(
request: Request,
user_login_schema: UserLoginRequest = Body(...),
session=Depends(get_session),
user=Depends(auth_user),
mindlogger_content=Depends(get_mindlogger_content_source),
) -> None:
if (
mindlogger_content == MindloggerContentSource.undefined.name
and user_login_schema.device_id
):
mindlogger_content = MindloggerContentSource.mobile.name

schema = UserActivityLogSchema(
user_id=user.id,
device_id=user_login_schema.device_id,
event_type=UserActivityEventType.LOGIN,
event=UserActivityEvent.LOGIN,
user_agent=request.headers.get("user-agent"),
mindlogger_content=mindlogger_content,
)
await UserActivityLogService(session).create_log(schema)
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""add-user-activity-logs
Revision ID: bc2fcc0b0cd1
Revises: 75c9ca1f506b
Create Date: 2023-11-30 10:41:15.308713
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "bc2fcc0b0cd1"
down_revision = "75c9ca1f506b"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"user_activity_logs",
sa.Column(
"id",
postgresql.UUID(as_uuid=True),
server_default=sa.text("gen_random_uuid()"),
nullable=False,
),
sa.Column(
"created_at",
sa.DateTime(),
server_default=sa.text("timezone('utc', now())"),
nullable=True,
),
sa.Column(
"updated_at",
sa.DateTime(),
server_default=sa.text("timezone('utc', now())"),
nullable=True,
),
sa.Column("migrated_date", sa.DateTime(), nullable=True),
sa.Column("migrated_updated", sa.DateTime(), nullable=True),
sa.Column("is_deleted", sa.Boolean(), nullable=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("device_id", sa.String(), nullable=True),
sa.Column("event_type", sa.String(), nullable=False),
sa.Column("event", sa.String(), nullable=False),
sa.Column("user_agent", sa.String(), nullable=True),
sa.Column("mindlogger_content", sa.String(), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
name=op.f("fk_user_activity_logs_user_id_users"),
ondelete="RESTRICT",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_user_activity_logs")),
)
op.create_index(
op.f("ix_user_activity_logs_user_id"),
"user_activity_logs",
["user_id"],
unique=False,
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(
op.f("ix_user_activity_logs_user_id"), table_name="user_activity_logs"
)
op.drop_table("user_activity_logs")
# ### end Alembic commands ###
2 changes: 1 addition & 1 deletion src/infrastructure/http/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ async def get_mindlogger_content_source(
return getattr(
MindloggerContentSource,
request.headers.get(
"mindlogger-content-source", MindloggerContentSource.web.name
"mindlogger-content-source", MindloggerContentSource.undefined.name
),
)

Expand Down
2 changes: 2 additions & 0 deletions src/infrastructure/http/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ class MindloggerContentSource(str, Enum):

web = "web"
admin = "admin"
mobile = "mobile"
undefined = "undefined"

0 comments on commit cf8427b

Please sign in to comment.