Coverage for gws-app/gws/plugin/account/account_action.py: 0%
82 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""Action for the account onboarding procedure."""
3from typing import Optional, cast
5import gws
6import gws.config.util
7import gws.base.action
8import gws.lib.mime
10from . import core, helper
13@gws.ext.config.action('account')
14class Config(gws.base.action.Config):
15 """Account management for end users, including onboarding."""
16 pass
19@gws.ext.props.action('account')
20class Props(gws.base.action.Props):
21 pass
24class MfaProps(gws.Data):
25 """MFA method offered to the user during onboarding."""
27 index: int
28 """Index of the method in the helper's ``mfa`` list, starting with 1."""
29 title: str
30 """Title of the method."""
31 qrCode: str
32 """QR code of the key URI as a data URL, empty if the method has no key URI."""
35class OnboardingStartRequest(gws.Request):
36 """Request to start the onboarding."""
38 tc: str
39 """Temporary code from the onboarding link."""
42class OnboardingStartResponse(gws.Response):
43 """Response to the onboarding start."""
45 tc: str
46 """New temporary code for the next step."""
49class OnboardingSavePasswordRequest(gws.Request):
50 """Request to set the password during onboarding."""
52 tc: str
53 """Temporary code from the previous step."""
54 email: str
55 """Email address, must match the account's email."""
56 password1: str
57 """New password."""
58 password2: str
59 """New password, repeated."""
62class OnboardingSavePasswordResponse(gws.Response):
63 """Response to setting the password."""
65 tc: str
66 """New temporary code for the next step, if the onboarding is not complete."""
67 ok: bool
68 """``False`` if the email does not match or the password is invalid."""
69 complete: bool
70 """``True`` if the account is active, ``False`` if an MFA method must be selected."""
71 completionUrl: str
72 """URL to go to when the onboarding is complete."""
73 mfaList: list[MfaProps]
74 """MFA methods to choose from."""
77class OnboardingSaveMfaRequest(gws.Request):
78 """Request to select an MFA method during onboarding."""
80 tc: str
81 """Temporary code from the previous step."""
82 mfaIndex: Optional[int]
83 """Index of the selected method."""
86class OnboardingSaveMfaResponse(gws.Response):
87 """Response to selecting an MFA method."""
89 complete: bool
90 """``True`` when the onboarding is complete."""
91 completionUrl: str
92 """URL to go to when the onboarding is complete."""
95@gws.ext.object.action('account')
96class Object(gws.base.action.Object):
97 """Account action, provides the onboarding API for the client.
99 The onboarding steps are: start, set the password, select an MFA method (only if MFA methods are configured).
100 Each step requires the temporary code returned by the previous step, the first one the code from the onboarding link.
101 """
103 h: helper.Object
104 """The account helper."""
106 def configure(self):
107 self.h = cast(helper.Object, self.root.app.helper('account'))
109 @gws.ext.command.api('accountOnboardingStart')
110 def onboarding_start(self, req: gws.WebRequester, p: OnboardingStartRequest) -> OnboardingStartResponse:
111 """Start the account onboarding."""
113 account = self.get_account_by_tc(p.tc, core.Category.onboarding, core.Status.new)
114 self.h.set_status(account, core.Status.onboarding)
115 return OnboardingStartResponse(
116 tc=self.h.generate_tc(account, core.Category.onboarding)
117 )
119 @gws.ext.command.api('accountOnboardingSavePassword')
120 def onboarding_save_password(self, req: gws.WebRequester, p: OnboardingSavePasswordRequest) -> OnboardingSavePasswordResponse:
121 """Set the password of the account."""
123 account = self.get_account_by_tc(p.tc, core.Category.onboarding, core.Status.onboarding)
125 p1 = p.password1
126 p2 = p.password2
128 if account.get('email') != p.email or p1 != p2 or not self.h.validate_password(p1):
129 return OnboardingSavePasswordResponse(
130 ok=False,
131 tc=self.h.generate_tc(account, core.Category.onboarding),
132 )
134 self.h.set_password(account, p1)
136 mfa = self.h.mfa_options(account)
137 if mfa:
138 mfa_secret = self.h.generate_mfa_secret(account)
139 return OnboardingSavePasswordResponse(
140 ok=True,
141 complete=False,
142 mfaList=self.mfa_props(account, mfa_secret),
143 tc=self.h.generate_tc(account, core.Category.onboarding),
144 )
146 self.h.set_status(account, core.Status.active)
147 self.h.clear_tc(account)
148 return OnboardingSavePasswordResponse(
149 ok=True,
150 complete=True,
151 completionUrl=self.h.onboardingCompletionUrl,
152 )
154 @gws.ext.command.api('accountOnboardingSaveMfa')
155 def onboarding_save_mfa(self, req: gws.WebRequester, p: OnboardingSaveMfaRequest) -> OnboardingSaveMfaResponse:
156 """Save the selected MFA method and activate the account."""
158 account = self.get_account_by_tc(p.tc, core.Category.onboarding, core.Status.onboarding)
160 self.h.set_mfa(account, p.mfaIndex)
161 self.h.set_status(account, core.Status.active)
162 self.h.clear_tc(account)
164 return OnboardingSaveMfaResponse(
165 complete=True,
166 completionUrl=self.h.onboardingCompletionUrl,
167 )
169 ##
171 def get_account_by_tc(self, tc, category, expected_status):
172 """Find an account by a temporary code.
174 The code is invalidated.
176 Args:
177 tc: Temporary code.
178 category: Expected code category.
179 expected_status: Expected account status.
181 Returns:
182 The account record.
184 Raises:
185 gws.ForbiddenError: If no valid account is found.
186 """
188 try:
189 account = self.h.get_account_by_tc(tc, category, expected_status)
190 except helper.Error as exc:
191 raise gws.ForbiddenError() from exc
193 if not account:
194 raise gws.ForbiddenError(f'account: {tc=} not found')
196 return account
198 def mfa_props(self, account: dict, mfa_secret):
199 """Create props for the MFA methods available to an account.
201 Args:
202 account: Account record.
203 mfa_secret: MFA secret for the QR codes.
205 Returns:
206 A list of ``MfaProps``.
207 """
209 ps = []
211 for mo in self.h.mfa_options(account):
212 ps.append(MfaProps(
213 index=mo.index,
214 title=mo.title,
215 qrCode=self.h.qr_code_for_mfa(account, mo, mfa_secret)
216 ))
218 return ps