Skip to content

Commit d873f11

Browse files
Users search improvments (#270)
* Users search improvments * Users to org moved to backend * Update users with additional data * Migrate users when refreshing * BE request for users change * PR comments * submodules updated * removed unused code * PR comments * Submodules merge
1 parent 054e88c commit d873f11

File tree

7 files changed

+146
-72
lines changed

7 files changed

+146
-72
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Kratos data
2+
3+
Revision ID: 7aa933ec5de9
4+
Revises: 05bbef1eec3f
5+
Create Date: 2024-11-19 16:04:00.539685
6+
7+
"""
8+
9+
from alembic import op
10+
import sqlalchemy as sa
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "7aa933ec5de9"
15+
down_revision = "05bbef1eec3f"
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# ### commands auto generated by Alembic - please adjust! ###
22+
op.add_column("user", sa.Column("email", sa.String(), nullable=True))
23+
op.add_column("user", sa.Column("verified", sa.Boolean(), nullable=True))
24+
op.add_column("user", sa.Column("created_at", sa.DateTime(), nullable=True))
25+
op.add_column("user", sa.Column("metadata_public", sa.JSON(), nullable=True))
26+
op.add_column("user", sa.Column("sso_provider", sa.String(), nullable=True))
27+
op.create_unique_constraint(None, "user", ["email"])
28+
# ### end Alembic commands ###
29+
30+
31+
def downgrade():
32+
# ### commands auto generated by Alembic - please adjust! ###
33+
op.drop_constraint(None, "user", type_="unique")
34+
op.drop_column("user", "sso_provider")
35+
op.drop_column("user", "metadata_public")
36+
op.drop_column("user", "created_at")
37+
op.drop_column("user", "verified")
38+
op.drop_column("user", "email")
39+
# ### end Alembic commands ###

app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from starlette.routing import Route, Mount
4444

4545
from controller.project.manager import check_in_deletion_projects
46+
from controller.user.manager import migrate_kratos_users
4647
from route_prefix import (
4748
PREFIX_ORGANIZATION,
4849
PREFIX_PROJECT,
@@ -70,6 +71,7 @@
7071
logger = logging.getLogger(__name__)
7172

7273
init_config()
74+
migrate_kratos_users()
7375
fastapi_app = FastAPI()
7476

7577

controller/auth/kratos.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
from datetime import datetime, timedelta
66
from urllib.parse import quote
77

8+
from controller.user import manager
9+
10+
811
logging.basicConfig(level=logging.INFO)
912
logger: logging.Logger = logging.getLogger(__name__)
1013
logger.setLevel(logging.DEBUG)
@@ -17,19 +20,19 @@
1720
KRATOS_IDENTITY_CACHE_TIMEOUT = timedelta(minutes=30)
1821

1922

20-
def __get_cached_values() -> Dict[str, Dict[str, Any]]:
23+
def get_cached_values(update_db_users: bool = True) -> Dict[str, Dict[str, Any]]:
2124
global KRATOS_IDENTITY_CACHE
2225
if not KRATOS_IDENTITY_CACHE or len(KRATOS_IDENTITY_CACHE) == 0:
23-
__refresh_identity_cache()
26+
__refresh_identity_cache(update_db_users)
2427
elif (
2528
KRATOS_IDENTITY_CACHE["collected"] + KRATOS_IDENTITY_CACHE_TIMEOUT
2629
< datetime.now()
2730
):
28-
__refresh_identity_cache()
31+
__refresh_identity_cache(update_db_users)
2932
return KRATOS_IDENTITY_CACHE
3033

3134

32-
def __refresh_identity_cache():
35+
def __refresh_identity_cache(update_db_users: bool = True) -> None:
3336
global KRATOS_IDENTITY_CACHE
3437
request = requests.get(f"{KRATOS_ADMIN_URL}/identities")
3538
if request.ok:
@@ -54,6 +57,9 @@ def __refresh_identity_cache():
5457
else:
5558
KRATOS_IDENTITY_CACHE = {}
5659

60+
if update_db_users:
61+
manager.migrate_kratos_users()
62+
5763

5864
def __get_link_from_kratos_request(request: requests.Response) -> str:
5965
# rel=next only if there is more than 1 page
@@ -71,7 +77,7 @@ def __get_link_from_kratos_request(request: requests.Response) -> str:
7177
def __get_identity(user_id: str, only_simple: bool = True) -> Dict[str, Any]:
7278
if not isinstance(user_id, str):
7379
user_id = str(user_id)
74-
cache = __get_cached_values()
80+
cache = get_cached_values()
7581
if user_id in cache:
7682
if only_simple:
7783
return cache[user_id]["simple"]
@@ -117,7 +123,7 @@ def __parse_identity_to_simple(identity: Dict[str, Any]) -> Dict[str, str]:
117123

118124

119125
def get_userid_from_mail(user_mail: str) -> str:
120-
values = __get_cached_values()
126+
values = get_cached_values()
121127
for key in values:
122128
if key == "collected":
123129
continue

controller/user/manager.py

Lines changed: 57 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from typing import Any, Dict, List
2-
from submodules.model import User, enums
1+
from typing import Any, Dict, List, Optional
2+
from submodules.model import User, daemon, enums
33
from submodules.model.business_objects import user, user_activity, general
44
from controller.auth import kratos
55
from submodules.model.exceptions import EntityNotFoundException
@@ -91,11 +91,17 @@ def remove_organization_from_user(user_mail: str) -> None:
9191
user.remove_organization(user_id, with_commit=True)
9292

9393

94-
def get_active_users(minutes: int, order_by_interaction: bool) -> User:
94+
def get_active_users_filtered(
95+
minutes: Optional[int] = None,
96+
sort_key: Optional[str] = None,
97+
sort_direction: Optional[str] = None,
98+
offset: Optional[int] = None,
99+
limit: Optional[int] = None,
100+
) -> User:
95101
now = datetime.now()
96102
last_interaction_range = (now - timedelta(minutes=minutes)) if minutes > 0 else None
97-
return user_activity.get_active_users_in_range(
98-
last_interaction_range, order_by_interaction
103+
return user.get_active_users_after_filter(
104+
last_interaction_range, sort_key, sort_direction, offset, limit
99105
)
100106

101107

@@ -104,53 +110,53 @@ def update_last_interaction(user_id: str) -> None:
104110
user_activity.update_last_interaction(user_id)
105111

106112

107-
def get_mapped_sorted_paginated_users(
108-
active_users: Dict[str, Any],
109-
sort_key: str,
110-
sort_direction: int,
111-
offset: int,
112-
limit: int,
113-
) -> List[Dict[str, Any]]:
114-
115-
final_users = []
116-
save_len_final_users = 0
117-
118-
# mapping users with the users in kratos
119-
active_users_ids = list(active_users.keys())
120-
121-
for user_id in active_users_ids:
122-
get_user = kratos.__get_identity(user_id, False)["identity"]
123-
if get_user and get_user["traits"]["email"] is not None:
124-
get_user["email"] = get_user["traits"]["email"]
125-
get_user["verified"] = get_user["verifiable_addresses"][0]["verified"]
126-
active_user_by_id = active_users[user_id]
127-
get_user["last_interaction"] = active_user_by_id["last_interaction"]
128-
get_user["role"] = active_user_by_id["role"]
129-
get_user["organization"] = active_user_by_id["organizationName"]
130-
131-
public_meta = get_user["metadata_public"]
132-
get_user["sso_provider"] = (
133-
public_meta.get("registration_scope", {}).get("provider_id", None)
134-
if public_meta
135-
else None
136-
)
137-
138-
final_users.append(get_user)
139-
save_len_final_users += 1
140-
141-
final_users = sorted(
142-
final_users,
143-
key=lambda x: (x[sort_key] is None, x.get(sort_key, "")),
144-
reverse=sort_direction == -1,
145-
)
146-
147-
# paginating users
148-
final_users = final_users[offset : offset + limit]
149-
150-
return final_users, save_len_final_users
151-
152-
153113
def delete_user(user_id: str) -> None:
154114
user.delete(user_id, with_commit=True)
155115
user_activity.delete_user_activity(user_id, with_commit=True)
156116
kratos.__refresh_identity_cache()
117+
118+
119+
def migrate_kratos_users() -> None:
120+
# this is only supposed to be called during startup of the application
121+
daemon.run_with_db_token(__migrate_kratos_users)
122+
123+
124+
def __migrate_kratos_users():
125+
users_kratos = kratos.get_cached_values(False)
126+
users_database = user.get_all()
127+
128+
for user_database in users_database:
129+
user_id = str(user_database.id)
130+
user_identity = users_kratos[user_id]["identity"]
131+
132+
if user_database.email != user_identity["traits"]["email"]:
133+
user_database.email = user_identity["traits"]["email"]
134+
if (
135+
user_database.verified
136+
!= user_identity["verifiable_addresses"][0]["verified"]
137+
):
138+
user_database.verified = user_identity["verifiable_addresses"][0][
139+
"verified"
140+
]
141+
if (
142+
user_database.created_at
143+
!= user_identity["verifiable_addresses"][0]["created_at"]
144+
):
145+
user_database.created_at = user_identity["verifiable_addresses"][0][
146+
"created_at"
147+
]
148+
if user_database.metadata_public != user_identity["metadata_public"]:
149+
user_database.metadata_public = user_identity["metadata_public"]
150+
sso_provider = (
151+
(
152+
user_identity["metadata_public"]
153+
.get("registration_scope", {})
154+
.get("provider_id", None)
155+
)
156+
if user_identity["metadata_public"]
157+
else None
158+
)
159+
if user_database.sso_provider != sso_provider:
160+
user_database.sso_provider = sso_provider
161+
162+
general.commit()

fast_api/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,3 +440,7 @@ class UpdateCustomerButton(BaseModel):
440440
location: Optional[CustomerButtonLocation] = None
441441
visible: Optional[StrictBool] = None
442442
config: Optional[Dict[StrictStr, Any]] = None
443+
444+
445+
class MissingUsersBody(BaseModel):
446+
user_ids: List[StrictStr]

fast_api/routes/organization.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
DeleteOrganizationBody,
1111
DeleteUserBody,
1212
MappedSortedPaginatedUsers,
13+
MissingUsersBody,
1314
RemoveUserToOrganizationBody,
1415
UserLanguageDisplay,
1516
)
@@ -24,7 +25,8 @@
2425
from controller.user import manager as user_manager
2526

2627
from fast_api.routes.client_response import get_silent_success, pack_json_result
27-
from submodules.model.business_objects import organization
28+
from submodules.model import events
29+
from submodules.model.business_objects import organization, user
2830
from submodules.model.util import sql_alchemy_to_dict
2931
from util import notification
3032

@@ -273,31 +275,32 @@ def get_mapped_sorted_paginated_users(
273275
request: Request, body: MappedSortedPaginatedUsers = Body(...)
274276
):
275277
auth_manager.check_admin_access(request.state.info)
276-
active_users = user_manager.get_active_users(body.filter_minutes, None)
278+
count_users = user_manager.get_active_users_filtered(body.filter_minutes)
279+
active_users = user_manager.get_active_users_filtered(
280+
body.filter_minutes, body.sort_key, body.sort_direction, body.offset, body.limit
281+
)
277282
active_users = [
278283
{
279284
"id": str(user.id),
280285
"last_interaction": (
281286
user.last_interaction.isoformat() if user.last_interaction else None
282287
),
283288
"role": user.role,
284-
"organizationName": (
285-
organization_manager.get_organization_by_id(str(user.organization_id))[
286-
"name"
287-
]
288-
if user.organization_id
289-
else ""
290-
),
289+
"organization": user.organization_name,
290+
"email": user.email,
291+
"verified": user.verified,
292+
"created_at": user.created_at.isoformat(),
293+
"metadata_public": user.metadata_public,
294+
"sso_provider": user.sso_provider,
291295
}
292296
for user in active_users
293297
]
294-
active_users = {user["id"]: user for user in active_users}
295298

296-
data, final_len = user_manager.get_mapped_sorted_paginated_users(
297-
active_users, body.sort_key, body.sort_direction, body.offset, body.limit
298-
)
299299
return pack_json_result(
300-
{"mappedSortedPaginatedUsers": data, "fullCountUsers": final_len},
300+
{
301+
"mappedSortedPaginatedUsers": active_users,
302+
"fullCountUsers": len(count_users),
303+
},
301304
wrap_for_frontend=False, # needed because it's used like this on the frontend (kratos values)
302305
)
303306

@@ -307,3 +310,17 @@ def delete_user(request: Request, body: DeleteUserBody = Body(...)):
307310
auth_manager.check_admin_access(request.state.info)
308311
user_manager.delete_user(body.user_id)
309312
return get_silent_success()
313+
314+
315+
@router.post("/missing-users-interaction")
316+
def get_missing_users_interaction(request: Request, body: MissingUsersBody = Body(...)):
317+
auth_manager.check_admin_access(request.state.info)
318+
data = user.get_missing_users(body.user_ids)
319+
return pack_json_result(data, wrap_for_frontend=False)
320+
321+
322+
@router.get("/user-to-organization")
323+
def get_user_to_organization(request: Request):
324+
auth_manager.check_admin_access(request.state.info)
325+
data = user.get_user_to_organization()
326+
return pack_json_result(data, wrap_for_frontend=False)

0 commit comments

Comments
 (0)