-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[feat] Maintain last_login and last_active timestamps per user
- Loading branch information
1 parent
45ecb12
commit 474d5a4
Showing
10 changed files
with
89 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
migrations/versions/2024-10-07T09:24:49Z_user_last_login_and_activity.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
"""user: last_login and activity | ||
Revision ID: 000dd4f966a4 | ||
Revises: 2c3086217445 | ||
Create Date: 2024-10-07 09:24:49.803805+00:00 | ||
""" | ||
|
||
from typing import Union | ||
|
||
from alembic import op | ||
import sqlalchemy as sa | ||
|
||
from fixbackend.sqlalechemy_extensions import UTCDateTime | ||
|
||
# revision identifiers, used by Alembic. | ||
revision: str = "000dd4f966a4" | ||
down_revision: Union[str, None] = "2c3086217445" | ||
|
||
|
||
def upgrade() -> None: | ||
op.add_column("user", sa.Column("last_login", UTCDateTime(timezone=True), nullable=True)) | ||
op.add_column("user", sa.Column("last_active", UTCDateTime(timezone=True), nullable=True)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,7 +22,7 @@ | |
from sqlalchemy import select | ||
from sqlalchemy.ext.asyncio import AsyncSession | ||
|
||
from fixbackend.auth.auth_backend import session_cookie_name, FixJWTStrategy | ||
from fixbackend.auth.auth_backend import SessionCookie, FixJWTStrategy | ||
from fixbackend.auth.models import User | ||
from fixbackend.auth.models.orm import UserMFARecoveryCode | ||
from fixbackend.auth.schemas import OTPConfig | ||
|
@@ -62,7 +62,7 @@ async def publish(self, event: Event) -> None: | |
|
||
class InMemoryInvitationRepo(InvitationRepository): | ||
|
||
def __init__(self) -> None: | ||
def __init__(self) -> None: # noqa | ||
pass | ||
|
||
async def get_invitation_by_email(self, email: str) -> Optional[WorkspaceInvitation]: | ||
|
@@ -90,7 +90,7 @@ async def update_invitation( | |
|
||
|
||
class InMemoryRoleRepository(RoleRepository): | ||
def __init__(self) -> None: | ||
def __init__(self) -> None: # noqa | ||
self.roles: List[UserRole] = [] | ||
|
||
@override | ||
|
@@ -206,20 +206,20 @@ async def test_registration_flow( | |
# verified can login | ||
response = await api_client.post("/api/auth/jwt/login", data=login_json) | ||
assert response.status_code == 204 | ||
auth_cookie = response.cookies.get(session_cookie_name) | ||
auth_cookie = response.cookies.get(SessionCookie) | ||
assert auth_cookie is not None | ||
|
||
# role is set on login | ||
auth_token = jwt.api_jwt.decode_complete(auth_cookie, options={"verify_signature": False}) | ||
assert auth_token["payload"]["permissions"] == {str(workspace.id): workspace_owner_permissions.value} | ||
|
||
# workspace can be listed | ||
response = await api_client.get("/api/workspaces/", cookies={session_cookie_name: auth_cookie}) | ||
response = await api_client.get("/api/workspaces/", cookies={SessionCookie: auth_cookie}) | ||
workspace_json = response.json()[0] | ||
assert workspace_json.get("name") == user.email | ||
|
||
# workspace can be viewed by an owner | ||
response = await api_client.get(f"/api/workspaces/{workspace.id}", cookies={session_cookie_name: auth_cookie}) | ||
response = await api_client.get(f"/api/workspaces/{workspace.id}", cookies={SessionCookie: auth_cookie}) | ||
assert response.status_code == 200 | ||
workspace_json = response.json() | ||
assert workspace_json.get("name") == user.email | ||
|
@@ -238,15 +238,15 @@ async def test_registration_flow( | |
|
||
# password can be reset only with providing a current one | ||
response = await api_client.patch( | ||
"/api/users/me", json={"password": "[email protected]"}, cookies={session_cookie_name: auth_cookie} | ||
"/api/users/me", json={"password": "[email protected]"}, cookies={SessionCookie: auth_cookie} | ||
) | ||
assert response.status_code == 400 | ||
|
||
# password can be reset with providing a current one | ||
response = await api_client.patch( | ||
"/api/users/me", | ||
json={"password": "FooBar123456789123456789", "current_password": registration_json["password"]}, | ||
cookies={session_cookie_name: auth_cookie}, | ||
cookies={SessionCookie: auth_cookie}, | ||
) | ||
assert response.status_code == 200 | ||
|
||
|
@@ -278,16 +278,16 @@ async def test_mfa_flow( | |
login_json = {"username": registration_json["email"], "password": registration_json["password"]} | ||
response = await api_client.post("/api/auth/jwt/login", data=login_json) | ||
assert response.status_code == 204 | ||
auth_cookie = response.cookies.get(session_cookie_name) | ||
auth_cookie = response.cookies.get(SessionCookie) | ||
assert auth_cookie is not None | ||
|
||
# mfa can be added and enabled | ||
response = await api_client.post("/api/auth/mfa/add", cookies={session_cookie_name: auth_cookie}) | ||
response = await api_client.post("/api/auth/mfa/add", cookies={SessionCookie: auth_cookie}) | ||
assert response.status_code == 200 | ||
otp_config = OTPConfig.model_validate(response.json()) | ||
totp = TOTP(otp_config.secret) | ||
response = await api_client.post( | ||
"/api/auth/mfa/enable", data={"otp": totp.now()}, cookies={session_cookie_name: auth_cookie} | ||
"/api/auth/mfa/enable", data={"otp": totp.now()}, cookies={SessionCookie: auth_cookie} | ||
) | ||
assert response.status_code == 204 | ||
|
||
|
@@ -301,13 +301,13 @@ async def test_mfa_flow( | |
|
||
# mfa can-not be disabled without valid otp | ||
response = await api_client.post( | ||
"/api/auth/mfa/disable", data={"otp": "wrong"}, cookies={session_cookie_name: auth_cookie} | ||
"/api/auth/mfa/disable", data={"otp": "wrong"}, cookies={SessionCookie: auth_cookie} | ||
) | ||
assert response.status_code == 428 | ||
|
||
# mfa can be disabled with otp | ||
response = await api_client.post( | ||
"/api/auth/mfa/disable", data={"otp": totp.now()}, cookies={session_cookie_name: auth_cookie} | ||
"/api/auth/mfa/disable", data={"otp": totp.now()}, cookies={SessionCookie: auth_cookie} | ||
) | ||
assert response.status_code == 204 | ||
|
||
|
@@ -316,12 +316,12 @@ async def test_mfa_flow( | |
assert response.status_code == 204 | ||
|
||
# enable mfa again | ||
response = await api_client.post("/api/auth/mfa/add", cookies={session_cookie_name: auth_cookie}) | ||
response = await api_client.post("/api/auth/mfa/add", cookies={SessionCookie: auth_cookie}) | ||
assert response.status_code == 200 | ||
otp_config = OTPConfig.model_validate(response.json()) | ||
totp = TOTP(otp_config.secret) | ||
response = await api_client.post( | ||
"/api/auth/mfa/enable", data={"otp": totp.now()}, cookies={session_cookie_name: auth_cookie} | ||
"/api/auth/mfa/enable", data={"otp": totp.now()}, cookies={SessionCookie: auth_cookie} | ||
) | ||
assert response.status_code == 204 | ||
|
||
|
@@ -348,14 +348,14 @@ async def test_mfa_flow( | |
|
||
# mfa can-not be disabled without valid recovery code | ||
response = await api_client.post( | ||
"/api/auth/mfa/disable", data={"recovery_code": "wrong"}, cookies={session_cookie_name: auth_cookie} | ||
"/api/auth/mfa/disable", data={"recovery_code": "wrong"}, cookies={SessionCookie: auth_cookie} | ||
) | ||
assert response.status_code == 428 | ||
|
||
# mfa can be disabled with recovery code | ||
response = await api_client.post( | ||
"/api/auth/mfa/disable", | ||
data={"recovery_code": otp_config.recovery_codes[1]}, | ||
cookies={session_cookie_name: auth_cookie}, | ||
cookies={SessionCookie: auth_cookie}, | ||
) | ||
assert response.status_code == 204 |