Skip to content

Commit 99e97b5

Browse files
committed
✨ users: Implement user account approval functionality with appropriate status code and service logic
1 parent dc4e942 commit 99e97b5

File tree

5 files changed

+91
-7
lines changed

5 files changed

+91
-7
lines changed

api/specs/web-server/_users.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ async def list_users_for_admin(
160160

161161
@router.post(
162162
"/admin/users:approve",
163-
response_model=Envelope[Page[UserForAdminGet]],
163+
status_code=status.HTTP_204_NO_CONTENT,
164164
tags=_extra_tags,
165165
)
166166
async def approve_user_account(_body: UserApprove): ...

services/web/server/src/simcore_service_webserver/security/_authz_access_roles.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
""" Defines different user roles and its associated permission
1+
"""Defines different user roles and its associated permission
22
3-
This definition is consumed by the security._access_model to build an access model for the framework
4-
The access model is created upon setting up of the security subsystem
3+
This definition is consumed by the security._access_model to build an access model for the framework
4+
The access model is created upon setting up of the security subsystem
55
"""
66

7-
87
from simcore_postgres_database.models.users import UserRole
98
from typing_extensions import ( # https://docs.pydantic.dev/latest/api/standard_library_types/#typeddict
109
TypedDict,
@@ -106,6 +105,7 @@ class PermissionDict(TypedDict, total=False):
106105
"product.details.*",
107106
"product.invitations.create",
108107
"admin.users.read",
108+
"admin.users.write",
109109
],
110110
inherits=[UserRole.TESTER],
111111
),

services/web/server/src/simcore_service_webserver/users/_users_rest.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from models_library.api_schemas_webserver.users import (
88
MyProfileGet,
99
MyProfilePatch,
10+
UserApprove,
1011
UserForAdminGet,
1112
UserGet,
1213
UsersForAdminListQueryParams,
@@ -248,3 +249,25 @@ async def pre_register_user_for_admin(request: web.Request) -> web.Response:
248249
return envelope_json_response(
249250
user_profile.model_dump(**_RESPONSE_MODEL_MINIMAL_POLICY)
250251
)
252+
253+
254+
@routes.post(f"/{API_VTAG}/admin/users:approve", name="approve_user_account")
255+
@login_required
256+
@permission_required("admin.users.write")
257+
@_handle_users_exceptions
258+
async def approve_user_account(request: web.Request) -> web.Response:
259+
req_ctx = UsersRequestContext.model_validate(request)
260+
assert req_ctx.product_name # nosec
261+
262+
approval_data = await parse_request_body_as(UserApprove, request)
263+
264+
# Approve the user account, passing the current user's ID as the reviewer
265+
pre_registration_id = await _users_service.approve_user_account(
266+
request.app,
267+
pre_registration_email=approval_data.email,
268+
product_name=req_ctx.product_name,
269+
reviewer_id=req_ctx.user_id,
270+
)
271+
assert pre_registration_id # nosec
272+
273+
return web.json_response(status=status.HTTP_204_NO_CONTENT)

services/web/server/src/simcore_service_webserver/users/_users_service.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,3 +449,53 @@ async def update_my_profile(
449449
user_id=user_id,
450450
update=ToUserUpdateDB.from_api(update),
451451
)
452+
453+
454+
async def approve_user_account(
455+
app: web.Application,
456+
*,
457+
pre_registration_email: LowerCaseEmailStr,
458+
product_name: ProductName,
459+
reviewer_id: UserID,
460+
) -> int:
461+
"""Approve a user account based on their pre-registration email.
462+
463+
Args:
464+
app: The web application instance
465+
pre_registration_email: Email of the pre-registered user to approve
466+
product_name: Product name for which the user is being approved
467+
reviewer_id: ID of the user approving the account
468+
469+
Returns:
470+
int: The ID of the approved pre-registration record
471+
472+
Raises:
473+
ValueError: If no pre-registration is found for the email/product
474+
"""
475+
engine = get_asyncpg_engine(app)
476+
477+
# First, find the pre-registration entry matching the email and product
478+
pre_registrations, _ = await _users_repository.list_user_pre_registrations(
479+
engine,
480+
filter_by_pre_email=pre_registration_email,
481+
filter_by_product_name=product_name,
482+
filter_by_account_request_status=AccountRequestStatus.PENDING,
483+
)
484+
485+
if not pre_registrations:
486+
msg = f"No pending pre-registration found for email {pre_registration_email} in product {product_name}"
487+
raise ValueError(msg)
488+
489+
# There should be only one registration matching these criteria
490+
pre_registration = pre_registrations[0]
491+
pre_registration_id = pre_registration["id"]
492+
493+
# Update the pre-registration status to APPROVED using the reviewer's ID
494+
await _users_repository.review_user_pre_registration(
495+
engine,
496+
pre_registration_id=pre_registration_id,
497+
reviewed_by=reviewer_id,
498+
new_status=AccountRequestStatus.APPROVED,
499+
)
500+
501+
return pre_registration_id

services/web/server/tests/unit/with_dbs/03/test_users_rest_registration.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,12 +230,22 @@ async def test_list_users_for_admin(
230230
)
231231
data, _ = await assert_status(resp, status.HTTP_200_OK)
232232

233-
pending_emails = [user["email"] for user in data if user["status"] == "PENDING"]
233+
pending_emails = [user["email"] for user in data if user["status"] is None]
234234
for pre_user in pre_registered_users:
235235
assert pre_user["email"] in pending_emails
236236

237-
# 2. Register one of the pre-registered users
237+
# 2. Register one of the pre-registered users: approve + create account
238238
registered_email = pre_registered_users[0]["email"]
239+
240+
url = client.app.router["approve_user_account"].url_for()
241+
resp = await client.post(
242+
f"{url}",
243+
headers={X_PRODUCT_NAME_HEADER: product_name},
244+
json={"email": registered_email},
245+
)
246+
await assert_status(resp, status.HTTP_204_NO_CONTENT)
247+
248+
# Emulates user accepting invitation link
239249
new_user = await simcore_service_webserver.login._auth_service.create_user(
240250
client.app,
241251
email=registered_email,
@@ -246,6 +256,7 @@ async def test_list_users_for_admin(
246256

247257
# 3. Test filtering by status
248258
# a. Check PENDING filter (should exclude the registered user)
259+
url = client.app.router["list_users_for_admin"].url_for()
249260
resp = await client.get(
250261
f"{url}?status=PENDING", headers={X_PRODUCT_NAME_HEADER: product_name}
251262
)

0 commit comments

Comments
 (0)