Skip to content

Commit f47c538

Browse files
authored
✨ Enhance Account Request Flow with Pre-Registration and PO Approval Handling (#8026)
1 parent 04ffd16 commit f47c538

File tree

22 files changed

+561
-81
lines changed

22 files changed

+561
-81
lines changed

api/specs/web-server/_users_admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from models_library.generics import Envelope
2020
from models_library.rest_pagination import Page
2121
from simcore_service_webserver._meta import API_VTAG
22-
from simcore_service_webserver.users._common.schemas import PreRegisteredUserGet
22+
from simcore_service_webserver.users.schemas import PreRegisteredUserGet
2323

2424
router = APIRouter(prefix=f"/{API_VTAG}", tags=["users"])
2525

packages/models-library/src/models_library/api_schemas_webserver/users.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,8 +300,9 @@ class UserAccountGet(OutputSchema):
300300
),
301301
] = DEFAULT_FACTORY
302302

303-
# pre-registration
303+
# pre-registration NOTE: that some users have no pre-registartion and therefore all options here can be none
304304
pre_registration_id: int | None
305+
pre_registration_created: datetime | None
305306
invited_by: str | None = None
306307
account_request_status: AccountRequestStatus | None
307308
account_request_reviewed_by: UserID | None = None

services/web/server/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.69.1
1+
0.70.0

services/web/server/setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.69.1
2+
current_version = 0.70.0
33
commit = True
44
message = services/webserver api version: {current_version} → {new_version}
55
tag = False

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ openapi: 3.1.0
22
info:
33
title: simcore-service-webserver
44
description: Main service with an interface (http-API & websockets) to the web front-end
5-
version: 0.69.1
5+
version: 0.70.0
66
servers:
77
- url: ''
88
description: webserver
@@ -17166,6 +17166,12 @@ components:
1716617166
- type: integer
1716717167
- type: 'null'
1716817168
title: Preregistrationid
17169+
preRegistrationCreated:
17170+
anyOf:
17171+
- type: string
17172+
format: date-time
17173+
- type: 'null'
17174+
title: Preregistrationcreated
1716917175
invitedBy:
1717017176
anyOf:
1717117177
- type: string
@@ -17217,6 +17223,7 @@ components:
1721717223
- postalCode
1721817224
- country
1721917225
- preRegistrationId
17226+
- preRegistrationCreated
1722017227
- accountRequestStatus
1722117228
- registered
1722217229
- status

services/web/server/src/simcore_service_webserver/constants.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# pylint:disable=unused-import
22

3-
from sys import version
43
from typing import Final
54

65
from common_library.user_messages import user_message
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from common_library.user_messages import user_message
2+
from servicelib.aiohttp import status
3+
4+
from ....exception_handling import (
5+
ExceptionToHttpErrorMap,
6+
HttpErrorInfo,
7+
exception_handling_decorator,
8+
to_exceptions_handlers_map,
9+
)
10+
from ....users.exceptions import AlreadyPreRegisteredError
11+
from ..._constants import MSG_2FA_UNAVAILABLE
12+
from ...errors import SendingVerificationEmailError, SendingVerificationSmsError
13+
14+
_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = {
15+
AlreadyPreRegisteredError: HttpErrorInfo(
16+
status.HTTP_409_CONFLICT,
17+
user_message(
18+
"An account for the email {email} has been submitted. If you haven't received any updates, please contact support.",
19+
_version=1,
20+
),
21+
),
22+
SendingVerificationSmsError: HttpErrorInfo(
23+
status.HTTP_503_SERVICE_UNAVAILABLE,
24+
MSG_2FA_UNAVAILABLE,
25+
),
26+
SendingVerificationEmailError: HttpErrorInfo(
27+
status.HTTP_503_SERVICE_UNAVAILABLE,
28+
MSG_2FA_UNAVAILABLE,
29+
),
30+
}
31+
32+
33+
handle_rest_requests_exceptions = exception_handling_decorator(
34+
to_exceptions_handlers_map(_TO_HTTP_ERROR_MAP)
35+
)

services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
)
4040
from ..._models import InputSchema
4141
from ...decorators import login_required
42-
from ...errors import handle_login_exceptions
4342
from ...settings import LoginSettingsForProduct, get_plugin_settings
43+
from ._rest_exceptions import handle_rest_requests_exceptions
4444

4545
log = logging.getLogger(__name__)
4646

@@ -75,7 +75,7 @@ class LoginNextPage(NextPage[CodePageParams]): ...
7575
name="auth_resend_2fa_code",
7676
max_access_count=MAX_2FA_CODE_RESEND,
7777
)
78-
@handle_login_exceptions
78+
@handle_rest_requests_exceptions
7979
async def login(request: web.Request):
8080
"""Login: user submits an email (identification) and a password
8181
@@ -218,6 +218,7 @@ class LoginTwoFactorAuthBody(InputSchema):
218218
"auth_login_2fa",
219219
unauthorized_reason=MSG_UNAUTHORIZED_LOGIN_2FA,
220220
)
221+
@handle_rest_requests_exceptions
221222
async def login_2fa(request: web.Request):
222223
"""Login (continuation): Submits 2FA code"""
223224
product: Product = products_web.get_current_product(request)
@@ -266,6 +267,7 @@ class LogoutBody(InputSchema):
266267

267268
@routes.post(f"/{API_VTAG}/auth/logout", name="auth_logout")
268269
@login_required
270+
@handle_rest_requests_exceptions
269271
async def logout(request: web.Request) -> web.Response:
270272
user_id = request.get(RQT_USERID_KEY, -1)
271273

services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from pydantic import SecretStr, field_validator
77
from servicelib.aiohttp.requests_validation import parse_request_body_as
88
from servicelib.logging_errors import create_troubleshootting_log_kwargs
9-
from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON
109
from servicelib.request_keys import RQT_USERID_KEY
1110
from simcore_postgres_database.utils_repos import pass_or_acquire_connection
1211
from simcore_postgres_database.utils_users import UsersRepo
@@ -296,9 +295,7 @@ async def change_password(request: web.Request):
296295
if not security_service.check_password(
297296
passwords.current.get_secret_value(), user["password_hash"]
298297
):
299-
raise web.HTTPUnprocessableEntity(
300-
text=MSG_WRONG_PASSWORD, content_type=MIMETYPE_APPLICATION_JSON
301-
) # 422
298+
raise web.HTTPUnprocessableEntity(text=MSG_WRONG_PASSWORD) # 422
302299

303300
await db.update_user(
304301
dict(user),

services/web/server/src/simcore_service_webserver/login/_controller/rest/preregistration.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from servicelib.aiohttp.application_keys import APP_FIRE_AND_FORGET_TASKS_KEY
1313
from servicelib.aiohttp.requests_validation import parse_request_body_as
1414
from servicelib.logging_utils import get_log_record_extra, log_context
15-
from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON
1615
from servicelib.request_keys import RQT_USERID_KEY
1716
from servicelib.utils import fire_and_forget_task
1817

@@ -22,8 +21,9 @@
2221
from ....products.models import Product
2322
from ....security import security_service, security_web
2423
from ....security.decorators import permission_required
25-
from ....session.api import get_session
26-
from ....users.api import get_user_credentials, set_user_as_deleted
24+
from ....session import api as session_service
25+
from ....users import api as users_service
26+
from ....users._common.schemas import PreRegisteredUserGet
2727
from ....utils import MINUTE
2828
from ....utils_rate_limiting import global_rate_limit_route
2929
from ... import _preregistration_service
@@ -35,6 +35,7 @@
3535
from ..._login_service import flash_response, notify_user_logout
3636
from ...decorators import login_required
3737
from ...settings import LoginSettingsForProduct, get_plugin_settings
38+
from ._rest_exceptions import handle_rest_requests_exceptions
3839

3940
_logger = logging.getLogger(__name__)
4041

@@ -62,24 +63,30 @@ def _get_ipinfo(request: web.Request) -> dict[str, Any]:
6263
name="request_product_account",
6364
)
6465
@global_rate_limit_route(number_of_requests=30, interval_seconds=MINUTE)
66+
@handle_rest_requests_exceptions
6567
async def request_product_account(request: web.Request):
6668
product = products_web.get_current_product(request)
67-
session = await get_session(request)
69+
session = await session_service.get_session(request)
6870

6971
body = await parse_request_body_as(AccountRequestInfo, request)
7072
assert body.form # nosec
7173
assert body.captcha # nosec
7274

7375
if body.captcha != session.get(CAPTCHA_SESSION_KEY):
74-
raise web.HTTPUnprocessableEntity(
75-
text=MSG_WRONG_CAPTCHA__INVALID, content_type=MIMETYPE_APPLICATION_JSON
76-
)
76+
raise web.HTTPUnprocessableEntity(text=MSG_WRONG_CAPTCHA__INVALID)
7777
session.pop(CAPTCHA_SESSION_KEY, None)
7878

79-
# send email to fogbugz or user itself
79+
# create pre-regiatration or raise if already exists
80+
await _preregistration_service.create_pre_registration(
81+
request.app,
82+
profile=PreRegisteredUserGet.model_validate(body.form),
83+
product_name=product.name,
84+
)
85+
86+
# if created send email to fogbugz or user itself
8087
fire_and_forget_task(
8188
_preregistration_service.send_account_request_email_to_support(
82-
request,
89+
request=request,
8390
product=product,
8491
request_form=body.form,
8592
ipinfo=_get_ipinfo(request),
@@ -98,6 +105,7 @@ class _AuthenticatedContext(BaseModel):
98105
@routes.post(f"/{API_VTAG}/auth/unregister", name="unregister_account")
99106
@login_required
100107
@permission_required("user.profile.delete")
108+
@handle_rest_requests_exceptions
101109
async def unregister_account(request: web.Request):
102110
req_ctx = _AuthenticatedContext.model_validate(request)
103111
body = await parse_request_body_as(UnregisterCheck, request)
@@ -108,7 +116,9 @@ async def unregister_account(request: web.Request):
108116
)
109117

110118
# checks before deleting
111-
credentials = await get_user_credentials(request.app, user_id=req_ctx.user_id)
119+
credentials = await users_service.get_user_credentials(
120+
request.app, user_id=req_ctx.user_id
121+
)
112122
if body.email != credentials.email.lower() or not security_service.check_password(
113123
body.password.get_secret_value(), credentials.password_hash
114124
):
@@ -124,7 +134,7 @@ async def unregister_account(request: web.Request):
124134
extra=get_log_record_extra(user_id=req_ctx.user_id),
125135
):
126136
# update user table
127-
await set_user_as_deleted(request.app, user_id=req_ctx.user_id)
137+
await users_service.set_user_as_deleted(request.app, user_id=req_ctx.user_id)
128138

129139
# logout
130140
await notify_user_logout(
@@ -150,8 +160,9 @@ async def unregister_account(request: web.Request):
150160

151161
@routes.get(f"/{API_VTAG}/auth/captcha", name="create_captcha")
152162
@global_rate_limit_route(number_of_requests=30, interval_seconds=MINUTE)
163+
@handle_rest_requests_exceptions
153164
async def create_captcha(request: web.Request):
154-
session = await get_session(request)
165+
session = await session_service.get_session(request)
155166

156167
captcha_text, image_data = await _preregistration_service.create_captcha()
157168

0 commit comments

Comments
 (0)