Skip to content

Commit babf591

Browse files
authored
Merge pull request #2737 from ORCID/aromanovv/PD-4790-add-2fa-to-account-settings-password-reset
Aromanovv/pd 4790 add 2fa to account settings password reset
2 parents 7c97cfc + 1b7743a commit babf591

21 files changed

Lines changed: 1181 additions & 179 deletions

angular.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,10 @@
494494
"input": "projects/orcid-ui-docs/public"
495495
}
496496
],
497-
"styles": ["projects/orcid-ui-docs/src/styles.scss"]
497+
"styles": [
498+
"projects/orcid-ui-docs/src/styles.scss",
499+
"src/assets/css/tailwind.css"
500+
]
498501
},
499502
"configurations": {
500503
"production": {

projects/orcid-ui-docs/src/app/app.routes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,11 @@ export const routes: Routes = [
6363
(m) => m.SkeletonPlaceholderPageComponent
6464
),
6565
},
66+
{
67+
path: 'two-factor-auth-form',
68+
loadComponent: () =>
69+
import('./pages/two-factor-auth-form-page.component').then(
70+
(m) => m.TwoFactorAuthFormPageComponent
71+
),
72+
},
6673
]

projects/orcid-ui-docs/src/app/docs-shell.component.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ <h1 class="docs-logo">Orcid UI</h1>
3232
<a routerLink="/skeleton-placeholder" routerLinkActive="active">
3333
Skeleton Placeholder
3434
</a>
35+
<a routerLink="/two-factor-auth-form" routerLinkActive="active">
36+
Two Factor Authentication Form
37+
</a>
3538

3639
<div class="nav-section">Directives</div>
3740
<a routerLink="/accent-button" routerLinkActive="active">
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<app-documentation-page
2+
title="Two Factor Authentication Form"
3+
description="The &lt;app-two-factor-auth-form&gt; component is a subform that validates a user's 2fa or recovery code."
4+
>
5+
<div controls>
6+
<p>Customize the subform to preview different configurations:</p>
7+
8+
<div style="display: grid; gap: 16px; margin-bottom: 24px">
9+
<mat-checkbox [(ngModel)]="showHelpText">
10+
Display help text / controls
11+
</mat-checkbox>
12+
13+
<mat-checkbox [(ngModel)]="showAlert"> Display alert </mat-checkbox>
14+
15+
<button
16+
style="width: 100px; height: 40px; margin-left: 10px"
17+
mat-button
18+
(click)="form.reset()"
19+
>
20+
Reset form
21+
</button>
22+
</div>
23+
</div>
24+
25+
<div examples>
26+
<form [formGroup]="form" class="w-[537px]">
27+
<app-two-factor-auth-form
28+
[showAlert]="showAlert"
29+
[showHelpText]="showHelpText"
30+
codeControlName="twoFactorCode"
31+
recoveryControlName="twoFactorRecoveryCode"
32+
>
33+
</app-two-factor-auth-form>
34+
</form>
35+
</div>
36+
37+
<div usage style="font-size: 14px">
38+
<p>
39+
This component is a sub-form that must be placed inside a parent
40+
<code>[formGroup]</code>. It manages the UI switching and validation logic
41+
between the 2FA code and recovery code inputs.
42+
</p>
43+
44+
<p>To implement this component, use the code highlighted in yellow.</p>
45+
46+
<h4>1. Template</h4>
47+
<p>
48+
Place the component inside your existing form. Ensure the control names
49+
passed here match your FormGroup definition.
50+
</p>
51+
<pre><code class="language-html">&lt;form [formGroup]="form" (ngSubmit)="save()"&gt;
52+
&lt;!-- Other inputs... --&gt;
53+
54+
&#64;if (twoFactorEnabled) &#123;
55+
<span style="background-color: #fff59d; color: #000;">&lt;app-two-factor-auth-form</span>
56+
<span style="background-color: #fff59d; color: #000;">codeControlName="twoFactorCode"</span>
57+
<span style="background-color: #fff59d; color: #000;">recoveryControlName="twoFactorRecoveryCode"</span>
58+
<span style="background-color: #fff59d; color: #000;">[showAlert]="true"&gt;</span>
59+
<span style="background-color: #fff59d; color: #000;">&lt;/app-two-factor-auth-form&gt;</span>
60+
&#125;
61+
&lt;/form&gt;</code></pre>
62+
63+
<h4>2. Form Configuration</h4>
64+
<p>
65+
Initialize the controls in your parent component. Note that
66+
<code>Validators.required</code> is managed automatically by the child
67+
component and should not be added here.
68+
</p>
69+
<pre><code class="language-typescript">this.form = this.fb.group(&#123;
70+
// ... other controls
71+
<span style="background-color: #fff59d; color: #000;">twoFactorCode: [null, [Validators.minLength(6), Validators.maxLength(6)]],</span>
72+
<span style="background-color: #fff59d; color: #000;">twoFactorRecoveryCode: [null, [Validators.minLength(10), Validators.maxLength(10)]],</span>
73+
&#125;);</code></pre>
74+
75+
<h4>3. Handling Submit & Responses</h4>
76+
<p>
77+
Use <code>@ViewChild</code> to access the component. This allows you to
78+
delegate backend error mapping and focus management.
79+
</p>
80+
<pre><code class="language-typescript"><span style="background-color: #fff59d; color: #000;">@ViewChild(TwoFactorAuthFormComponent) twoFactorComponent: TwoFactorAuthFormComponent;</span>
81+
82+
save() &#123;
83+
if (this.form.valid) &#123;
84+
this.service.update(this.form.value).subscribe(response => &#123;
85+
// Pass backend response to child to handle errors/focus
86+
<span style="background-color: #fff59d; color: #000;">this.twoFactorComponent?.processBackendResponse(response);</span>
87+
<span style="color: #666">// Used when we don't know if the user</span>
88+
<span style="color: #666">// has 2fa enabled (e.g. signin)</span>
89+
<span style="background-color: #fff59d; color: #000;">this.twoFactorEnabled = response.twoFactorEnabled;</span>
90+
&#125;);
91+
&#125;
92+
&#125;</code></pre>
93+
94+
<h4>4. Interface Definition</h4>
95+
<p>
96+
The endpoint payload/response object needs to extend the
97+
<strong><code>TwoFactorAuthForm</code></strong> interface/pojo.
98+
</p>
99+
<pre><code class="language-typescript">export interface TwoFactorAuthForm &#123;
100+
invalidTwoFactorCode?: boolean;
101+
invalidTwoFactorRecoveryCode?: boolean;
102+
twoFactorCode?: string;
103+
twoFactorRecoveryCode?: string;
104+
twoFactorEnabled?: boolean;
105+
&#125;</code></pre>
106+
107+
<h4>5. Backend Implementation</h4>
108+
<p>
109+
The backend should use the
110+
<code>TwoFactorAuthenticationManager</code> to validate the request.
111+
</p>
112+
<p>
113+
If no codes are provided, the backend will assume it is a two-step form
114+
flow: it will set the <code>twoFactorEnabled</code> flag to true and
115+
return the form so the UI can display the inputs. Otherwise, it will
116+
validate the codes, assign error flags if necessary, and return the form.
117+
</p>
118+
<p>
119+
In case the 2FA check passes successfully, the manager returns
120+
<code>true</code> and the function proceeds to the next step.
121+
</p>
122+
<pre><code class="language-java"><span style="background-color: #fff59d; color: #000;">if (!twoFactorAuthenticationManager.validateTwoFactorAuthForm(orcid, form)) &#123;</span>
123+
<span style="background-color: #fff59d; color: #000;">return form;</span>
124+
<span style="background-color: #fff59d; color: #000;">&#125;</span></code></pre>
125+
</div>
126+
<div inputs>
127+
<ul style="font-size: 14px">
128+
<li>
129+
<code style="font-weight: bold">codeControlName</code>:
130+
<code>string</code>. The name of the form control for the 6-digit
131+
authentication code. (default <code>'twoFactorCode'</code>)
132+
</li>
133+
<li>
134+
<code style="font-weight: bold">recoveryControlName</code>:
135+
<code>string</code>. The name of the form control for the 10-character
136+
recovery code. (default <code>'twoFactorRecoveryCode'</code>)
137+
</li>
138+
<li>
139+
<code style="font-weight: bold">showAlert</code>: <code>boolean</code>.
140+
Whether to display the notice alert indicating 2FA is active. (default
141+
<code>false</code>)
142+
</li>
143+
<li>
144+
<code style="font-weight: bold">showHelpText</code>:
145+
<code>boolean</code>. Whether to display the helper links (e.g. "Use a
146+
recovery code instead") below the inputs. (default <code>true</code>)
147+
</li>
148+
</ul>
149+
</div>
150+
</app-documentation-page>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
section {
2+
display: grid;
3+
gap: 1.25rem;
4+
}
5+
6+
app-two-factor-auth-from {
7+
margin-top: 0.5rem;
8+
}
9+
10+
pre {
11+
background: #1e1e1e;
12+
color: #fff;
13+
padding: 0.75rem 1rem;
14+
border-radius: 4px;
15+
font-size: 0.85rem;
16+
overflow-x: auto;
17+
}
18+
19+
::ng-deep {
20+
mat-form-field {
21+
width: 100%;
22+
}
23+
.mat-mdc-form-field-subscript-wrapper {
24+
display: none;
25+
}
26+
.mat-mdc-form-field-infix {
27+
padding-top: 8px !important;
28+
padding-bottom: 8px !important;
29+
}
30+
31+
.mat-mdc-text-field-wrapper {
32+
height: 40px !important;
33+
}
34+
mat-hint,
35+
mat-label {
36+
font-size: 12px;
37+
}
38+
mat-error {
39+
font-size: 12px;
40+
}
41+
}
42+
43+
form {
44+
font-size: 14px;
45+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Component, OnInit } from '@angular/core'
2+
import { CommonModule } from '@angular/common'
3+
import {
4+
FormsModule,
5+
ReactiveFormsModule,
6+
UntypedFormBuilder,
7+
UntypedFormGroup,
8+
Validators,
9+
} from '@angular/forms'
10+
import { MatSelectModule } from '@angular/material/select'
11+
import { MatFormFieldModule } from '@angular/material/form-field'
12+
import { MatInputModule } from '@angular/material/input'
13+
import { MatIconModule } from '@angular/material/icon'
14+
import { TwoFactorAuthFormComponent } from '@orcid/ui'
15+
import { DocumentationPageComponent } from '../components/documentation-page/documentation-page.component'
16+
import '@angular/localize/init'
17+
import { MatCheckboxModule } from '@angular/material/checkbox'
18+
19+
@Component({
20+
selector: 'two-factor-auth-form-page',
21+
standalone: true,
22+
imports: [
23+
CommonModule,
24+
FormsModule,
25+
MatSelectModule,
26+
MatFormFieldModule,
27+
MatInputModule,
28+
MatCheckboxModule,
29+
MatIconModule,
30+
TwoFactorAuthFormComponent,
31+
DocumentationPageComponent,
32+
ReactiveFormsModule,
33+
],
34+
styleUrls: ['./two-factor-auth-form-page.component.scss'],
35+
templateUrl: './two-factor-auth-form-page.component.html',
36+
})
37+
export class TwoFactorAuthFormPageComponent implements OnInit {
38+
showAlert = false
39+
showHelpText = true
40+
form: UntypedFormGroup
41+
42+
constructor(private _fb: UntypedFormBuilder) {}
43+
44+
ngOnInit() {
45+
this.form = this._fb.group({
46+
twoFactorCode: [null, [Validators.minLength(6), Validators.maxLength(6)]],
47+
twoFactorRecoveryCode: [
48+
null,
49+
[Validators.minLength(10), Validators.maxLength(10)],
50+
],
51+
})
52+
}
53+
}

0 commit comments

Comments
 (0)