Skip to content

Commit 785cabb

Browse files
committed
implements reject_user_account
1 parent 22cf0e2 commit 785cabb

File tree

5 files changed

+182
-1
lines changed

5 files changed

+182
-1
lines changed

api/specs/web-server/_users.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ async def approve_user_account(_body: UserApprove): ...
170170
"/admin/users:reject",
171171
status_code=status.HTTP_204_NO_CONTENT,
172172
tags=_extra_tags,
173-
include_in_schema=False, # UNDER DEVELOPMENT
174173
)
175174
async def reject_user_account(_body: UserReject): ...
176175

services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,6 +1422,22 @@ paths:
14221422
responses:
14231423
'204':
14241424
description: Successful Response
1425+
/v0/admin/users:reject:
1426+
post:
1427+
tags:
1428+
- users
1429+
- admin
1430+
summary: Reject User Account
1431+
operationId: reject_user_account
1432+
requestBody:
1433+
content:
1434+
application/json:
1435+
schema:
1436+
$ref: '#/components/schemas/UserReject'
1437+
required: true
1438+
responses:
1439+
'204':
1440+
description: Successful Response
14251441
/v0/admin/users:search:
14261442
get:
14271443
tags:
@@ -17261,6 +17277,16 @@ components:
1726117277
required:
1726217278
- read
1726317279
title: UserNotificationPatch
17280+
UserReject:
17281+
properties:
17282+
email:
17283+
type: string
17284+
format: email
17285+
title: Email
17286+
type: object
17287+
required:
17288+
- email
17289+
title: UserReject
1726417290
UserStatus:
1726517291
type: string
1726617292
enum:

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,25 @@ async def approve_user_account(request: web.Request) -> web.Response:
273273
assert pre_registration_id # nosec
274274

275275
return web.json_response(status=status.HTTP_204_NO_CONTENT)
276+
277+
278+
@routes.post(f"/{API_VTAG}/admin/users:reject", name="reject_user_account")
279+
@login_required
280+
@permission_required("admin.users.write")
281+
@_handle_users_exceptions
282+
async def reject_user_account(request: web.Request) -> web.Response:
283+
req_ctx = UsersRequestContext.model_validate(request)
284+
assert req_ctx.product_name # nosec
285+
286+
rejection_data = await parse_request_body_as(UserReject, request)
287+
288+
# Reject the user account, passing the current user's ID as the reviewer
289+
pre_registration_id = await _users_service.reject_user_account(
290+
request.app,
291+
pre_registration_email=rejection_data.email,
292+
product_name=req_ctx.product_name,
293+
reviewer_id=req_ctx.user_id,
294+
)
295+
assert pre_registration_id # nosec
296+
297+
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
@@ -499,3 +499,53 @@ async def approve_user_account(
499499
)
500500

501501
return pre_registration_id
502+
503+
504+
async def reject_user_account(
505+
app: web.Application,
506+
*,
507+
pre_registration_email: LowerCaseEmailStr,
508+
product_name: ProductName,
509+
reviewer_id: UserID,
510+
) -> int:
511+
"""Reject a user account based on their pre-registration email.
512+
513+
Args:
514+
app: The web application instance
515+
pre_registration_email: Email of the pre-registered user to reject
516+
product_name: Product name for which the user is being rejected
517+
reviewer_id: ID of the user rejecting the account
518+
519+
Returns:
520+
int: The ID of the rejected pre-registration record
521+
522+
Raises:
523+
ValueError: If no pre-registration is found for the email/product
524+
"""
525+
engine = get_asyncpg_engine(app)
526+
527+
# First, find the pre-registration entry matching the email and product
528+
pre_registrations, _ = await _users_repository.list_user_pre_registrations(
529+
engine,
530+
filter_by_pre_email=pre_registration_email,
531+
filter_by_product_name=product_name,
532+
filter_by_account_request_status=AccountRequestStatus.PENDING,
533+
)
534+
535+
if not pre_registrations:
536+
msg = f"No pending pre-registration found for email {pre_registration_email} in product {product_name}"
537+
raise ValueError(msg)
538+
539+
# There should be only one registration matching these criteria
540+
pre_registration = pre_registrations[0]
541+
pre_registration_id = pre_registration["id"]
542+
543+
# Update the pre-registration status to REJECTED using the reviewer's ID
544+
await _users_repository.review_user_pre_registration(
545+
engine,
546+
pre_registration_id=pre_registration_id,
547+
reviewed_by=reviewer_id,
548+
new_status=AccountRequestStatus.REJECTED,
549+
)
550+
551+
return pre_registration_id

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,3 +326,87 @@ async def test_list_users_for_admin(
326326
for item in filtered_page_data:
327327
user = UserForAdminGet(**item)
328328
assert user.registered is False # Pending users are not registered
329+
330+
331+
@pytest.mark.parametrize(
332+
"user_role",
333+
[
334+
UserRole.PRODUCT_OWNER,
335+
],
336+
)
337+
async def test_reject_user_account(
338+
client: TestClient,
339+
logged_user: UserInfoDict,
340+
account_request_form: dict[str, Any],
341+
faker: Faker,
342+
product_name: ProductName,
343+
):
344+
assert client.app
345+
346+
# 1. Create a pre-registered user
347+
form_data = account_request_form.copy()
348+
form_data["firstName"] = faker.first_name()
349+
form_data["lastName"] = faker.last_name()
350+
form_data["email"] = faker.email()
351+
352+
resp = await client.post(
353+
"/v0/admin/users:pre-register",
354+
json=form_data,
355+
headers={X_PRODUCT_NAME_HEADER: product_name},
356+
)
357+
pre_registered_data, _ = await assert_status(resp, status.HTTP_200_OK)
358+
pre_registered_email = pre_registered_data["email"]
359+
360+
# 2. Verify the user is in PENDING status
361+
url = client.app.router["list_users_for_admin"].url_for()
362+
resp = await client.get(
363+
f"{url}?status=PENDING", headers={X_PRODUCT_NAME_HEADER: product_name}
364+
)
365+
data, _ = await assert_status(resp, status.HTTP_200_OK)
366+
367+
pending_emails = [user["email"] for user in data if user["status"] is None]
368+
assert pre_registered_email in pending_emails
369+
370+
# 3. Reject the pre-registered user
371+
url = client.app.router["reject_user_account"].url_for()
372+
resp = await client.post(
373+
f"{url}",
374+
headers={X_PRODUCT_NAME_HEADER: product_name},
375+
json={"email": pre_registered_email},
376+
)
377+
await assert_status(resp, status.HTTP_204_NO_CONTENT)
378+
379+
# 4. Verify the user is no longer in PENDING status
380+
url = client.app.router["list_users_for_admin"].url_for()
381+
resp = await client.get(
382+
f"{url}?status=PENDING", headers={X_PRODUCT_NAME_HEADER: product_name}
383+
)
384+
pending_data, _ = await assert_status(resp, status.HTTP_200_OK)
385+
pending_emails = [user["email"] for user in pending_data]
386+
assert pre_registered_email not in pending_emails
387+
388+
# 5. Verify the user is now in REJECTED status
389+
# First get user details to check status
390+
resp = await client.get(
391+
"/v0/admin/users:search",
392+
params={"email": pre_registered_email},
393+
headers={X_PRODUCT_NAME_HEADER: product_name},
394+
)
395+
found, _ = await assert_status(resp, status.HTTP_200_OK)
396+
assert len(found) == 1
397+
398+
# Check that account_request_status is REJECTED
399+
user_data = found[0]
400+
assert user_data["account_request_status"] == "REJECTED"
401+
assert user_data["account_request_reviewed_by"] == logged_user["id"]
402+
assert user_data["account_request_reviewed_at"] is not None
403+
404+
# 6. Verify that a rejected user cannot be approved
405+
url = client.app.router["approve_user_account"].url_for()
406+
resp = await client.post(
407+
f"{url}",
408+
headers={X_PRODUCT_NAME_HEADER: product_name},
409+
json={"email": pre_registered_email},
410+
)
411+
# Should fail as the account is already reviewed
412+
assert resp.status == status.HTTP_400_BAD_REQUEST

0 commit comments

Comments
 (0)