@masmarino/gabarit 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -85
- package/accordion/README.md +56 -0
- package/alert/README.md +87 -0
- package/app-shell/README.md +133 -0
- package/auth/README.md +155 -0
- package/auth-activate/README.md +119 -0
- package/auth-login/README.md +113 -0
- package/auth-register/README.md +104 -0
- package/auth-reset-password/README.md +120 -0
- package/autocomplete/README.md +77 -0
- package/avatar/README.md +34 -0
- package/avatar-group/README.md +47 -0
- package/badge/README.md +177 -0
- package/bar-chart/README.md +44 -0
- package/breadcrumb/README.md +38 -0
- package/button/README.md +175 -0
- package/card/README.md +179 -0
- package/checkbox/README.md +50 -0
- package/checkbox-group/README.md +78 -0
- package/confirm-danger-modal/README.md +77 -0
- package/copy-button/README.md +90 -0
- package/copy-field/README.md +54 -0
- package/date-picker/README.md +84 -0
- package/description-list/README.md +63 -0
- package/dimension-card/README.md +39 -0
- package/disclosure/README.md +48 -0
- package/divider/README.md +31 -0
- package/drawer/README.md +49 -0
- package/empty-state/README.md +84 -0
- package/fesm2022/masmarino-gabarit-accordion.mjs +243 -0
- package/fesm2022/masmarino-gabarit-accordion.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-alert.mjs +208 -0
- package/fesm2022/masmarino-gabarit-alert.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-app-shell.mjs +378 -0
- package/fesm2022/masmarino-gabarit-app-shell.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-auth-activate.mjs +415 -0
- package/fesm2022/masmarino-gabarit-auth-activate.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-auth-login.mjs +467 -0
- package/fesm2022/masmarino-gabarit-auth-login.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-auth-register.mjs +457 -0
- package/fesm2022/masmarino-gabarit-auth-register.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-auth-reset-password.mjs +364 -0
- package/fesm2022/masmarino-gabarit-auth-reset-password.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-auth.mjs +844 -0
- package/fesm2022/masmarino-gabarit-auth.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-autocomplete.mjs +475 -0
- package/fesm2022/masmarino-gabarit-autocomplete.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-avatar-group.mjs +202 -0
- package/fesm2022/masmarino-gabarit-avatar-group.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-avatar.mjs +107 -0
- package/fesm2022/masmarino-gabarit-avatar.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-badge.mjs +449 -0
- package/fesm2022/masmarino-gabarit-badge.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-bar-chart.mjs +408 -0
- package/fesm2022/masmarino-gabarit-bar-chart.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-breadcrumb.mjs +59 -0
- package/fesm2022/masmarino-gabarit-breadcrumb.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-button.mjs +374 -0
- package/fesm2022/masmarino-gabarit-button.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-card.mjs +400 -0
- package/fesm2022/masmarino-gabarit-card.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-chart.mjs +1289 -0
- package/fesm2022/masmarino-gabarit-chart.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-checkbox-group.mjs +335 -0
- package/fesm2022/masmarino-gabarit-checkbox-group.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-checkbox.mjs +170 -0
- package/fesm2022/masmarino-gabarit-checkbox.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-confirm-danger-modal.mjs +387 -0
- package/fesm2022/masmarino-gabarit-confirm-danger-modal.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-copy-button.mjs +407 -0
- package/fesm2022/masmarino-gabarit-copy-button.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-copy-field.mjs +229 -0
- package/fesm2022/masmarino-gabarit-copy-field.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-date-picker.mjs +1242 -0
- package/fesm2022/masmarino-gabarit-date-picker.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-description-list.mjs +127 -0
- package/fesm2022/masmarino-gabarit-description-list.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-dimension-card.mjs +162 -0
- package/fesm2022/masmarino-gabarit-dimension-card.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-disclosure.mjs +154 -0
- package/fesm2022/masmarino-gabarit-disclosure.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-divider.mjs +90 -0
- package/fesm2022/masmarino-gabarit-divider.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-docs.mjs +16 -2
- package/fesm2022/masmarino-gabarit-docs.mjs.map +1 -1
- package/fesm2022/masmarino-gabarit-drawer.mjs +209 -0
- package/fesm2022/masmarino-gabarit-drawer.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-empty-state.mjs +226 -0
- package/fesm2022/masmarino-gabarit-empty-state.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-file-upload.mjs +335 -0
- package/fesm2022/masmarino-gabarit-file-upload.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-floating-panel.mjs +24 -0
- package/fesm2022/masmarino-gabarit-floating-panel.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-format.mjs +345 -0
- package/fesm2022/masmarino-gabarit-format.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-funnel-chart.mjs +247 -0
- package/fesm2022/masmarino-gabarit-funnel-chart.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-gauge-bar.mjs +180 -0
- package/fesm2022/masmarino-gabarit-gauge-bar.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-git-field.mjs +127 -0
- package/fesm2022/masmarino-gabarit-git-field.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-icon-marker.mjs +150 -0
- package/fesm2022/masmarino-gabarit-icon-marker.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-icon.mjs +308 -0
- package/fesm2022/masmarino-gabarit-icon.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-input.mjs +493 -0
- package/fesm2022/masmarino-gabarit-input.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-job-graph.mjs +216 -0
- package/fesm2022/masmarino-gabarit-job-graph.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-job-status.mjs +104 -0
- package/fesm2022/masmarino-gabarit-job-status.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-line-chart.mjs +574 -0
- package/fesm2022/masmarino-gabarit-line-chart.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-list-card.mjs +320 -0
- package/fesm2022/masmarino-gabarit-list-card.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-list-row.mjs +58 -0
- package/fesm2022/masmarino-gabarit-list-row.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-list-toolbar.mjs +369 -0
- package/fesm2022/masmarino-gabarit-list-toolbar.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-menu.mjs +464 -0
- package/fesm2022/masmarino-gabarit-menu.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-mfa-enrollment.mjs +897 -0
- package/fesm2022/masmarino-gabarit-mfa-enrollment.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-mfa-settings.mjs +629 -0
- package/fesm2022/masmarino-gabarit-mfa-settings.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-modal.mjs +260 -0
- package/fesm2022/masmarino-gabarit-modal.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-nav-tabs.mjs +332 -0
- package/fesm2022/masmarino-gabarit-nav-tabs.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-notification-dot.mjs +119 -0
- package/fesm2022/masmarino-gabarit-notification-dot.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-page-header.mjs +78 -0
- package/fesm2022/masmarino-gabarit-page-header.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-page-layout.mjs +157 -0
- package/fesm2022/masmarino-gabarit-page-layout.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-pagination.mjs +415 -0
- package/fesm2022/masmarino-gabarit-pagination.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-panel.mjs +80 -0
- package/fesm2022/masmarino-gabarit-panel.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-passkey-settings.mjs +606 -0
- package/fesm2022/masmarino-gabarit-passkey-settings.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-pie-chart.mjs +317 -0
- package/fesm2022/masmarino-gabarit-pie-chart.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-popover.mjs +142 -0
- package/fesm2022/masmarino-gabarit-popover.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-radio-group.mjs +202 -0
- package/fesm2022/masmarino-gabarit-radio-group.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-save-status.mjs +142 -0
- package/fesm2022/masmarino-gabarit-save-status.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-search-bar.mjs +576 -0
- package/fesm2022/masmarino-gabarit-search-bar.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-secret-reveal.mjs +348 -0
- package/fesm2022/masmarino-gabarit-secret-reveal.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-segmented-control.mjs +272 -0
- package/fesm2022/masmarino-gabarit-segmented-control.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-select.mjs +512 -0
- package/fesm2022/masmarino-gabarit-select.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-skeleton-list.mjs +189 -0
- package/fesm2022/masmarino-gabarit-skeleton-list.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-skeleton.mjs +109 -0
- package/fesm2022/masmarino-gabarit-skeleton.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-slider.mjs +239 -0
- package/fesm2022/masmarino-gabarit-slider.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-sparkline.mjs +276 -0
- package/fesm2022/masmarino-gabarit-sparkline.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-spinner.mjs +86 -0
- package/fesm2022/masmarino-gabarit-spinner.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-stat-grid.mjs +139 -0
- package/fesm2022/masmarino-gabarit-stat-grid.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-stat-tile.mjs +312 -0
- package/fesm2022/masmarino-gabarit-stat-tile.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-stepper.mjs +219 -0
- package/fesm2022/masmarino-gabarit-stepper.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-switch.mjs +151 -0
- package/fesm2022/masmarino-gabarit-switch.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-table.mjs +206 -0
- package/fesm2022/masmarino-gabarit-table.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-tabs.mjs +210 -0
- package/fesm2022/masmarino-gabarit-tabs.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-tag-input.mjs +288 -0
- package/fesm2022/masmarino-gabarit-tag-input.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-tag.mjs +155 -0
- package/fesm2022/masmarino-gabarit-tag.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-textarea.mjs +369 -0
- package/fesm2022/masmarino-gabarit-textarea.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-timeline-chart.mjs +521 -0
- package/fesm2022/masmarino-gabarit-timeline-chart.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-toaster.mjs +220 -0
- package/fesm2022/masmarino-gabarit-toaster.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-tooltip.mjs +185 -0
- package/fesm2022/masmarino-gabarit-tooltip.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-tree.mjs +258 -0
- package/fesm2022/masmarino-gabarit-tree.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit-user-chip.mjs +106 -0
- package/fesm2022/masmarino-gabarit-user-chip.mjs.map +1 -0
- package/fesm2022/masmarino-gabarit.mjs +83 -24785
- package/fesm2022/masmarino-gabarit.mjs.map +1 -1
- package/file-upload/README.md +47 -0
- package/funnel-chart/README.md +39 -0
- package/gauge-bar/README.md +35 -0
- package/git-field/README.md +34 -0
- package/icon/README.md +52 -0
- package/icon-marker/README.md +37 -0
- package/input/README.md +120 -0
- package/job-graph/README.md +76 -0
- package/job-status/README.md +34 -0
- package/line-chart/README.md +52 -0
- package/list-card/README.md +144 -0
- package/list-row/README.md +72 -0
- package/list-toolbar/README.md +52 -0
- package/menu/README.md +90 -0
- package/mfa-enrollment/README.md +189 -0
- package/mfa-settings/README.md +154 -0
- package/modal/README.md +72 -0
- package/nav-tabs/README.md +106 -0
- package/notification-dot/README.md +43 -0
- package/package.json +329 -1
- package/page-header/README.md +56 -0
- package/page-layout/README.md +76 -0
- package/pagination/README.md +73 -0
- package/panel/README.md +56 -0
- package/passkey-settings/README.md +120 -0
- package/pie-chart/README.md +85 -0
- package/popover/README.md +62 -0
- package/radio-group/README.md +47 -0
- package/save-status/README.md +44 -0
- package/search-bar/README.md +84 -0
- package/secret-reveal/README.md +60 -0
- package/segmented-control/README.md +79 -0
- package/select/README.md +61 -0
- package/skeleton/README.md +49 -0
- package/skeleton-list/README.md +40 -0
- package/slider/README.md +44 -0
- package/sparkline/README.md +37 -0
- package/spinner/README.md +41 -0
- package/stat-grid/README.md +47 -0
- package/stat-tile/README.md +75 -0
- package/stepper/README.md +67 -0
- package/switch/README.md +51 -0
- package/table/README.md +75 -0
- package/tabs/README.md +34 -0
- package/tag/README.md +34 -0
- package/tag-input/README.md +45 -0
- package/textarea/README.md +76 -0
- package/timeline-chart/README.md +49 -0
- package/toaster/README.md +92 -0
- package/tooltip/README.md +50 -0
- package/tree/README.md +79 -0
- package/types/masmarino-gabarit-accordion.d.ts +54 -0
- package/types/masmarino-gabarit-alert.d.ts +65 -0
- package/types/masmarino-gabarit-app-shell.d.ts +102 -0
- package/types/masmarino-gabarit-auth-activate.d.ts +70 -0
- package/types/masmarino-gabarit-auth-login.d.ts +63 -0
- package/types/masmarino-gabarit-auth-register.d.ts +66 -0
- package/types/masmarino-gabarit-auth-reset-password.d.ts +53 -0
- package/types/masmarino-gabarit-auth.d.ts +543 -0
- package/types/masmarino-gabarit-autocomplete.d.ts +140 -0
- package/types/masmarino-gabarit-avatar-group.d.ts +60 -0
- package/types/masmarino-gabarit-avatar.d.ts +31 -0
- package/types/masmarino-gabarit-badge.d.ts +139 -0
- package/types/masmarino-gabarit-bar-chart.d.ts +111 -0
- package/types/masmarino-gabarit-breadcrumb.d.ts +12 -0
- package/types/masmarino-gabarit-button.d.ts +108 -0
- package/types/masmarino-gabarit-card.d.ts +102 -0
- package/types/masmarino-gabarit-chart.d.ts +361 -0
- package/types/masmarino-gabarit-checkbox-group.d.ts +113 -0
- package/types/masmarino-gabarit-checkbox.d.ts +49 -0
- package/types/masmarino-gabarit-confirm-danger-modal.d.ts +94 -0
- package/types/masmarino-gabarit-copy-button.d.ts +111 -0
- package/types/masmarino-gabarit-copy-field.d.ts +64 -0
- package/types/masmarino-gabarit-date-picker.d.ts +399 -0
- package/types/masmarino-gabarit-description-list.d.ts +40 -0
- package/types/masmarino-gabarit-dimension-card.d.ts +55 -0
- package/types/masmarino-gabarit-disclosure.d.ts +44 -0
- package/types/masmarino-gabarit-divider.d.ts +20 -0
- package/types/masmarino-gabarit-docs.d.ts +2 -1
- package/types/masmarino-gabarit-drawer.d.ts +51 -0
- package/types/masmarino-gabarit-empty-state.d.ts +67 -0
- package/types/masmarino-gabarit-file-upload.d.ts +95 -0
- package/types/masmarino-gabarit-floating-panel.d.ts +14 -0
- package/types/masmarino-gabarit-format.d.ts +54 -0
- package/types/masmarino-gabarit-funnel-chart.d.ts +78 -0
- package/types/masmarino-gabarit-gauge-bar.d.ts +54 -0
- package/types/masmarino-gabarit-git-field.d.ts +39 -0
- package/types/masmarino-gabarit-icon-marker.d.ts +47 -0
- package/types/masmarino-gabarit-icon.d.ts +23 -0
- package/types/masmarino-gabarit-input.d.ts +181 -0
- package/types/masmarino-gabarit-job-graph.d.ts +63 -0
- package/types/masmarino-gabarit-job-status.d.ts +20 -0
- package/types/masmarino-gabarit-line-chart.d.ts +153 -0
- package/types/masmarino-gabarit-list-card.d.ts +87 -0
- package/types/masmarino-gabarit-list-row.d.ts +14 -0
- package/types/masmarino-gabarit-list-toolbar.d.ts +100 -0
- package/types/masmarino-gabarit-menu.d.ts +93 -0
- package/types/masmarino-gabarit-mfa-enrollment.d.ts +142 -0
- package/types/masmarino-gabarit-mfa-settings.d.ts +89 -0
- package/types/masmarino-gabarit-modal.d.ts +60 -0
- package/types/masmarino-gabarit-nav-tabs.d.ts +65 -0
- package/types/masmarino-gabarit-notification-dot.d.ts +34 -0
- package/types/masmarino-gabarit-page-header.d.ts +20 -0
- package/types/masmarino-gabarit-page-layout.d.ts +46 -0
- package/types/masmarino-gabarit-pagination.d.ts +109 -0
- package/types/masmarino-gabarit-panel.d.ts +21 -0
- package/types/masmarino-gabarit-passkey-settings.d.ts +94 -0
- package/types/masmarino-gabarit-pie-chart.d.ts +94 -0
- package/types/masmarino-gabarit-popover.d.ts +33 -0
- package/types/masmarino-gabarit-radio-group.d.ts +66 -0
- package/types/masmarino-gabarit-save-status.d.ts +39 -0
- package/types/masmarino-gabarit-search-bar.d.ts +183 -0
- package/types/masmarino-gabarit-secret-reveal.d.ts +95 -0
- package/types/masmarino-gabarit-segmented-control.d.ts +76 -0
- package/types/masmarino-gabarit-select.d.ts +146 -0
- package/types/masmarino-gabarit-skeleton-list.d.ts +54 -0
- package/types/masmarino-gabarit-skeleton.d.ts +26 -0
- package/types/masmarino-gabarit-slider.d.ts +73 -0
- package/types/masmarino-gabarit-sparkline.d.ts +70 -0
- package/types/masmarino-gabarit-spinner.d.ts +20 -0
- package/types/masmarino-gabarit-stat-grid.d.ts +39 -0
- package/types/masmarino-gabarit-stat-tile.d.ts +79 -0
- package/types/masmarino-gabarit-stepper.d.ts +67 -0
- package/types/masmarino-gabarit-switch.d.ts +42 -0
- package/types/masmarino-gabarit-table.d.ts +60 -0
- package/types/masmarino-gabarit-tabs.d.ts +40 -0
- package/types/masmarino-gabarit-tag-input.d.ts +78 -0
- package/types/masmarino-gabarit-tag.d.ts +35 -0
- package/types/masmarino-gabarit-textarea.d.ts +109 -0
- package/types/masmarino-gabarit-timeline-chart.d.ts +145 -0
- package/types/masmarino-gabarit-toaster.d.ts +62 -0
- package/types/masmarino-gabarit-tooltip.d.ts +39 -0
- package/types/masmarino-gabarit-tree.d.ts +71 -0
- package/types/masmarino-gabarit-user-chip.d.ts +26 -0
- package/types/masmarino-gabarit.d.ts +83 -6599
- package/user-chip/README.md +42 -0
package/auth/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Auth kit
|
|
2
|
+
|
|
3
|
+
The public sign-in pages and the account's second-factor settings, as components an application uses
|
|
4
|
+
as they are: login (with the MFA challenge), free registration, invitation activation, the new
|
|
5
|
+
password after an administrator's reset, the mandatory
|
|
6
|
+
first MFA enrolment (authenticator app or passkey, then backup codes), and the account cards for the
|
|
7
|
+
authenticator app and the passkeys. Every application that uses them gets the same behaviour, the same
|
|
8
|
+
accessibility and the same look, by construction.
|
|
9
|
+
|
|
10
|
+
The kit is **router-agnostic** (it never imports `@angular/router`: it emits outputs and shows the
|
|
11
|
+
links the application projects), **backend-agnostic** (it never talks HTTP: every call goes through a
|
|
12
|
+
port the application implements), **storage-agnostic** (it never touches `localStorage`: sessions are
|
|
13
|
+
the port's business) and **locale-agnostic** (every string is a label with an English default).
|
|
14
|
+
|
|
15
|
+
| Component | Selector | Port |
|
|
16
|
+
| --------------------------------------------------------- | ---------------------- | ---------------------------------------- |
|
|
17
|
+
| [`AuthLogin`](../auth-login/README.md) | `gbt-auth-login` | `AUTH_PORT` |
|
|
18
|
+
| [`AuthRegister`](../auth-register/README.md) | `gbt-auth-register` | `AUTH_PORT` |
|
|
19
|
+
| [`AuthActivate`](../auth-activate/README.md) | `gbt-auth-activate` | `AUTH_PORT` |
|
|
20
|
+
| [`AuthResetPassword`](../auth-reset-password/README.md) | `gbt-auth-reset-password` | `AUTH_PORT` |
|
|
21
|
+
| [`MfaEnrollment`](../mfa-enrollment/README.md) | `gbt-mfa-enrollment` | `AUTH_PORT` |
|
|
22
|
+
| [`MfaSettings`](../mfa-settings/README.md) | `gbt-mfa-settings` | `MFA_PORT` |
|
|
23
|
+
| [`PasskeySettings`](../passkey-settings/README.md) | `gbt-passkey-settings` | `MFA_PORT` (and `AUTH_PORT` if provided) |
|
|
24
|
+
| [`AuthPanel`](auth-panel/README.md) | `gbt-auth-panel` | — |
|
|
25
|
+
| [`AuthFooter`, `AuthFooterLink`](auth-footer/README.md) | `gbt-auth-footer` | — |
|
|
26
|
+
| [`TotpQr`](../mfa-enrollment/totp-qr/README.md) | `gbt-totp-qr` | — (`TOTP_QR_RENDERER`) |
|
|
27
|
+
| [`BackupCodes`](../mfa-enrollment/backup-codes/README.md) | `gbt-backup-codes` | — |
|
|
28
|
+
|
|
29
|
+
## Wiring
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// app.config.ts
|
|
33
|
+
providers: [
|
|
34
|
+
{ provide: AUTH_PORT, useExisting: AuthService }, // your service implements AuthPort
|
|
35
|
+
{ provide: MFA_PORT, useExisting: MfaService }, // your service implements MfaPort
|
|
36
|
+
{
|
|
37
|
+
provide: TOTP_QR_RENDERER, // Gabarit ships no QR encoder: bring one, loaded lazily
|
|
38
|
+
useValue: (text: string, options: TotpQrRenderOptions) =>
|
|
39
|
+
import('qrcode').then((qr) => qr.toDataURL(text, options)),
|
|
40
|
+
},
|
|
41
|
+
provideAuthLabels(MY_AUTH_LABELS), // optional: localise the whole kit once
|
|
42
|
+
]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> **Security: `TOTP_QR_RENDERER` receives the user's TOTP secret.** It is called with the full
|
|
46
|
+
> `otpauth://…?secret=…` URL, raw secret included. The renderer **must encode locally**, in the
|
|
47
|
+
> browser (a bundled library such as `qrcode`, as above): **no network call, never a hosted or
|
|
48
|
+
> third-party QR service** (a `https://…?data=otpauth…` image would hand every user's second factor
|
|
49
|
+
> to that service and its access logs). It **must not log** the text, nor send it to analytics or
|
|
50
|
+
> error reports. See [TotpQr](../mfa-enrollment/totp-qr/README.md).
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<!-- login.page.html: a thin wrapper, routed at /login -->
|
|
54
|
+
<gbt-auth-login (loggedIn)="router.navigateByUrl('/home')">
|
|
55
|
+
<img auth-logo src="logo.svg" alt="Acme" width="480" height="120" />
|
|
56
|
+
<a gbtButton variant="link" gbtAuthFooterLink routerLink="/register">Create an account</a>
|
|
57
|
+
</gbt-auth-login>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Every page takes the application's logo in its `[auth-logo]` slot (an `img`, a `picture` or an
|
|
61
|
+
`svg`) and the link of its footer in `[gbtAuthFooterLink]` (an `a[gbtButton]` in its `link` variant,
|
|
62
|
+
carrying the application's `routerLink` or `href`). A page without a projected footer link shows no
|
|
63
|
+
footer.
|
|
64
|
+
|
|
65
|
+
**`gbt-auth-register`, `gbt-auth-activate` and `gbt-auth-reset-password` also emit `(signIn)`**, from
|
|
66
|
+
their own terminal states (registration closed, account created, activation succeeded, password
|
|
67
|
+
changed, dead link), where "Sign in" is a button
|
|
68
|
+
rather than the projected link. That output is in addition to the projected `[gbtAuthFooterLink]`,
|
|
69
|
+
not instead of it: **wire both** (`(signIn)="router.navigateByUrl('/login')"` and the footer link)
|
|
70
|
+
for every path to reach the sign-in page.
|
|
71
|
+
|
|
72
|
+
## The `AuthPort` contract
|
|
73
|
+
|
|
74
|
+
- **Sessions.** `login`, `register`, `verifyMfa` and `finishPasskeyChallenge` store the session they
|
|
75
|
+
obtain, when there is one (`LoginResponse.token` not null), then the page emits its success output.
|
|
76
|
+
`confirmTotp` and `finishPasskeySetup` must **not** store theirs: the enrolment holds the session
|
|
77
|
+
back until the user has acknowledged the backup codes (shown once), then the page hands it to
|
|
78
|
+
`setToken(token)` and emits.
|
|
79
|
+
- **MFA.** A `LoginResponse` with `token: null` and an `mfaToken` sends the user to the challenge
|
|
80
|
+
(`mfaHasTotp`, `mfaHasPasskey` say what they can use) or, with `mfaSetupRequired`, to the mandatory
|
|
81
|
+
enrolment.
|
|
82
|
+
- **Configuration.** `authConfig()` is read once by the login page (a failed read hides the
|
|
83
|
+
registration link; passkeys stay "unknown", which does not block them) and by the registration page
|
|
84
|
+
(a failed read shows the form: the server has the last word).
|
|
85
|
+
|
|
86
|
+
## The error contract
|
|
87
|
+
|
|
88
|
+
A failed port call errors with `{ status, error }`: the HTTP status and the response body, the shape
|
|
89
|
+
of Angular's `HttpErrorResponse` (an adapter on `HttpClient` passes its errors through untouched). A
|
|
90
|
+
browser's WebAuthn error (a `DOMException`, no `status`) is told apart from a server answer by that
|
|
91
|
+
shape. The kit words every failure itself; the server's text is never shown. What it recognises
|
|
92
|
+
(`AUTH_PORT_ERROR_BODIES` holds the strings):
|
|
93
|
+
|
|
94
|
+
| Call | Answer | Read as |
|
|
95
|
+
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
|
|
96
|
+
| `login` | 401 | wrong username or password (any other failure: "sign-in failed") |
|
|
97
|
+
| any | 429 | too many attempts |
|
|
98
|
+
| MFA calls with an `mfaToken` | 401 `invalid or expired token` | the sign-in expired: back to the credentials |
|
|
99
|
+
| `verifyMfa`, `finishPasskeyChallenge` | 401 (any other body) | wrong code / passkey refused |
|
|
100
|
+
| `confirmTotp`, `confirm` | 400 `invalid code` | wrong code |
|
|
101
|
+
| enrolment calls | 400 `MFA is already set up` | the account got a factor meanwhile: back to the credentials |
|
|
102
|
+
| passkey calls | 503 | passkeys not available on this server |
|
|
103
|
+
| `finishPasskeySetup`, registration | 409 | this key is already registered |
|
|
104
|
+
| password-gated `MfaPort` calls | 400 `current password is incorrect` (exactly) | wrong password (never a 401, which an interceptor would take for a sign-out) |
|
|
105
|
+
| `startPasskeyRegistration`/`finish…` | 400 `too many passkeys` | the account holds the most passkeys the server keeps |
|
|
106
|
+
| `deletePasskey` | 404 | already gone: removed from the list, nobody signed out |
|
|
107
|
+
| `register` | 400 `registration is disabled` | the closed state |
|
|
108
|
+
| `register` | 400 `username is reserved` / `username …` / `email is not a valid address` / `password must be at least …` / other | the field concerned, else "check the fields" |
|
|
109
|
+
| `register` | 409 `email already in use` / other 409 | address taken / name taken |
|
|
110
|
+
| `activate`, `resetPassword` | 400 `password must be at least …` / other 400 | weak password (the link still works) / dead link |
|
|
111
|
+
|
|
112
|
+
An adapter for a backend that words these differently maps its answers onto them with `catchError`.
|
|
113
|
+
|
|
114
|
+
## Session revocation
|
|
115
|
+
|
|
116
|
+
`MfaPort.disable` and `MfaPort.deletePasskey` revoke the caller's own session on the server (MFA
|
|
117
|
+
being mandatory, removing a factor signs every device out). The cards then emit `sessionRevoked`:
|
|
118
|
+
the application must discard its local session and go to its sign-in page. As a safety net, the card
|
|
119
|
+
that revoked the session also turns inert ("You have been signed out", its `signedOut` label, and no
|
|
120
|
+
action left), so an unbound output never leaves a live card acting on a dead session; it cannot clear
|
|
121
|
+
the application's stored session, though: binding `sessionRevoked` stays mandatory.
|
|
122
|
+
|
|
123
|
+
## Activation and password-reset links
|
|
124
|
+
|
|
125
|
+
The mail should carry the token in the URL fragment (`/activate#token=…`,
|
|
126
|
+
`/reset-password#token=…`), which no server ever sees. The application's wrapper reads it once with
|
|
127
|
+
`activationToken(fragment, query)`, removes it from the address bar and history
|
|
128
|
+
(`navigateByUrl('/activate', { replaceUrl: true })`), and hands it to the `token` input of
|
|
129
|
+
`gbt-auth-activate` or `gbt-auth-reset-password`.
|
|
130
|
+
|
|
131
|
+
**The `token` input must be a one-time snapshot of the URL, never a value that reactively follows the
|
|
132
|
+
URL (e.g. a signal derived from `route.fragment`): the component resets when it changes.** Scrubbing
|
|
133
|
+
the URL would otherwise turn the token into `null` and flip the page to the dead-link view, even over
|
|
134
|
+
its success view.
|
|
135
|
+
|
|
136
|
+
## Labels
|
|
137
|
+
|
|
138
|
+
Each component has a `labels` input (a partial object) over `provideAuthLabels({ … })` over its
|
|
139
|
+
English defaults (`DEFAULT_LOGIN_LABELS`, …); nested pieces (`gbt-mfa-enrollment` inside the login
|
|
140
|
+
page, `gbt-totp-qr` and `gbt-backup-codes` inside the enrolment and the settings) read the provided
|
|
141
|
+
labels, so an application localises the whole kit in one place. Interpolated strings are functions
|
|
142
|
+
(`step(current, total)`, `codesLeft(count)`, `passkeyAdded(name)`…).
|
|
143
|
+
|
|
144
|
+
## The account cards together
|
|
145
|
+
|
|
146
|
+
`gbt-mfa-settings` and `gbt-passkey-settings` are meant to sit side by side. They share
|
|
147
|
+
`MfaSettingsState` (root-provided; provide it on a page to scope it): the factors as last read or
|
|
148
|
+
changed by either card, so the app becomes optional as soon as a passkey is added and deleting the
|
|
149
|
+
only key is flagged as removing the last factor; and which card owns the open form, so a view never
|
|
150
|
+
shows two password prompts (two primary buttons) at once.
|
|
151
|
+
|
|
152
|
+
## Theming
|
|
153
|
+
|
|
154
|
+
`--gbt-auth-panel-logo-width` (15rem) sizes the logo; `--gbt-auth-panel-logo-offset` (0) pulls it
|
|
155
|
+
up when the artwork has a transparent margin above its drawing.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# AuthActivate
|
|
2
|
+
|
|
3
|
+
The activation page of the auth kit: where the link of an invitation mail lands, outside the app shell,
|
|
4
|
+
in the sign-in panel ([`gbt-auth-panel`](../auth/auth-panel/auth-panel.ts)). The invited user chooses a
|
|
5
|
+
password; no session comes out of it: they sign in afterwards and are taken through the mandatory MFA
|
|
6
|
+
enrolment like everybody. It never talks HTTP, never reads the URL and never navigates: the backend is
|
|
7
|
+
the application's `AUTH_PORT`, the token is an input, and the page tells the application where to go
|
|
8
|
+
through its outputs.
|
|
9
|
+
|
|
10
|
+
**Selector**: `gbt-auth-activate`
|
|
11
|
+
|
|
12
|
+
## Views
|
|
13
|
+
|
|
14
|
+
| View | What shows |
|
|
15
|
+
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
16
|
+
| form | A token was given: "Activate your account", New password (with its hint), Confirm the password, one primary "Activate my account", then the projected sign-in link. |
|
|
17
|
+
| success | The server accepted: "Your account is activated", what comes next, one "Sign in" button. |
|
|
18
|
+
| invalid | No token (`null` or `''`), or the server refused it (unknown, expired or used): an error-toned state, "This link does not work", ask for a new invitation, one "Sign in". |
|
|
19
|
+
|
|
20
|
+
## Inputs
|
|
21
|
+
|
|
22
|
+
| Input | Type | Default | Role |
|
|
23
|
+
| ------------------- | ------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
24
|
+
| `token` | `string \| null` | `null` | The invitation's token, as the application read it from its URL with `activationToken(fragment, query)`. `null` (or `''`) is the dead link, without any request. A new value starts the page over. |
|
|
25
|
+
| `labels` | `Partial<ActivateLabels>` | `{}` | Strings to change, over `provideAuthLabels({ activate })` and the English defaults (`DEFAULT_ACTIVATE_LABELS`). |
|
|
26
|
+
| `minPasswordLength` | `number` | `MIN_PASSWORD_LENGTH` (8) | The server's minimum password length, checked before the round trip; also worded in the hint and the errors. |
|
|
27
|
+
| `chooseUsername` | `boolean` | `false` | For servers where the administrator invites by e-mail only: a Username field comes first, and the invitee's choice is sent as `activate(token, password, username)`. |
|
|
28
|
+
| `usernamePattern` | `RegExp` | `USERNAME_PATTERN` | The server's username rule, checked before the round trip when `chooseUsername` is on. |
|
|
29
|
+
|
|
30
|
+
## Outputs
|
|
31
|
+
|
|
32
|
+
| Output | Payload | Role |
|
|
33
|
+
| ----------- | ------- | -------------------------------------------------------------------------------- |
|
|
34
|
+
| `activated` | `void` | The server accepted the password: the success view shows. No session was opened. |
|
|
35
|
+
| `signIn` | `void` | "Sign in" was pressed in the success or dead-link view: go to the sign-in page. |
|
|
36
|
+
|
|
37
|
+
## Slots
|
|
38
|
+
|
|
39
|
+
| Selector | Role |
|
|
40
|
+
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
41
|
+
| `[auth-logo]` | The application's logo (an `img`, `picture` or `svg`), on top of the panel. Optional. |
|
|
42
|
+
| `[auth-backdrop]` | What is drawn on the graphite page, behind the panel: `gbt-git-field`, the family's animated commit graph (`@masmarino/gabarit/git-field`). Optional. |
|
|
43
|
+
| `[gbtAuthFooterLink]` | The link to the sign-in page, under the form after "Is your account already active?". An `a gbtButton variant="link"` carrying `routerLink` or `href`. No footer when absent. |
|
|
44
|
+
|
|
45
|
+
## Behaviour
|
|
46
|
+
|
|
47
|
+
- **The token.** The page sends it with the password (`activate(token, password)`) and never renders it
|
|
48
|
+
(not in the markup, not in a field). Once spent (success) or refused (a 400 other than a weak password),
|
|
49
|
+
it is dropped: the page does not send it again until a new `token` arrives.
|
|
50
|
+
- **The application's part** (it owns the URL): put the token in the link's **fragment**
|
|
51
|
+
(`/activate#token=…`) so that no server, proxy or access log ever sees it; read it once with
|
|
52
|
+
`activationToken(route.snapshot.fragment, route.snapshot.queryParamMap.get('token'))` (the query is only a
|
|
53
|
+
fallback for older links; a token not shaped like the server's is `null`); then remove it (and any other
|
|
54
|
+
parameter) from the address bar and the history entry, e.g.
|
|
55
|
+
`router.navigateByUrl('/activate', { replaceUrl: true })` after the first render, so it does not linger in
|
|
56
|
+
a screenshot or a shared tab.
|
|
57
|
+
- **The `token` input must be a one-time snapshot of the URL, never a value that reactively follows the
|
|
58
|
+
URL (e.g. a signal derived from `route.fragment`): the component resets when it changes.** Scrubbing the
|
|
59
|
+
URL as above would otherwise turn the token into `null` and flip the page to the dead-link view, even over
|
|
60
|
+
its success view. Read it into a plain field, as in the example below.
|
|
61
|
+
- **`chooseUsername`.** The intro becomes `introWithUsername`, the Username field (with `usernameHint`) comes
|
|
62
|
+
before the passwords and takes the first focus, and it is checked first (`usernameEmpty`,
|
|
63
|
+
`usernameInvalid`). The value is sent trimmed; the server keeps the final say: a 400 `username is reserved`
|
|
64
|
+
(`usernameReserved`), another 400 starting with `username ` (`usernameInvalid`) or a 409
|
|
65
|
+
(`usernameTaken`) keeps the form, in an alert, the focus in the Username field.
|
|
66
|
+
- **`minPasswordLength` must mirror the server's own rule.** It only spares a round trip: the server stays
|
|
67
|
+
the authority, and a looser value only moves the refusal to the server's answer.
|
|
68
|
+
- **Checks before the request.** Nothing is said while typing; after a first attempt the password rule and
|
|
69
|
+
the confirmation ("Confirm your password", "The passwords do not match") show under their fields, the
|
|
70
|
+
focus moves to the first wrong one, and the checks stay live.
|
|
71
|
+
- **Failures** are worded from the labels, in an alert, the focus back in the password field: a weak-password
|
|
72
|
+
400 keeps the form (the link is still good), a 429 says to wait, anything else (5xx, network) says the
|
|
73
|
+
activation failed and the same token can be retried. Any other 400 (but the username ones, with
|
|
74
|
+
`chooseUsername`) is the dead link.
|
|
75
|
+
- **Focus**: the form starts in the new-password field; the success and dead-link views move the focus to
|
|
76
|
+
their `h1` (focusable by script, not a tab stop). The passwords are emptied when the server accepted or
|
|
77
|
+
refused the link.
|
|
78
|
+
|
|
79
|
+
## Example
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// app.config.ts
|
|
83
|
+
providers: [
|
|
84
|
+
{ provide: AUTH_PORT, useClass: HttpAuthPort }, // your adapter over HttpClient
|
|
85
|
+
provideAuthLabels({ activate: { signInPrompt: 'Already signed up?' } }),
|
|
86
|
+
]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
@Component({
|
|
91
|
+
imports: [AuthActivate, Button, AuthFooterLink, RouterLink],
|
|
92
|
+
template: `
|
|
93
|
+
<gbt-auth-activate [token]="token" (signIn)="router.navigateByUrl('/login')">
|
|
94
|
+
<img auth-logo src="/logo.svg" alt="Acme" width="160" height="40" />
|
|
95
|
+
<a gbtButton variant="link" gbtAuthFooterLink routerLink="/login">Sign in</a>
|
|
96
|
+
</gbt-auth-activate>
|
|
97
|
+
`,
|
|
98
|
+
})
|
|
99
|
+
export class ActivateRoute {
|
|
100
|
+
protected readonly router = inject(Router)
|
|
101
|
+
private readonly route = inject(ActivatedRoute)
|
|
102
|
+
// Read once, then scrubbed from the address bar and the history entry.
|
|
103
|
+
protected readonly token = activationToken(
|
|
104
|
+
this.route.snapshot.fragment,
|
|
105
|
+
this.route.snapshot.queryParamMap.get('token'),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
constructor() {
|
|
109
|
+
const { fragment, queryParamMap } = this.route.snapshot
|
|
110
|
+
if (fragment !== null || queryParamMap.keys.length > 0) {
|
|
111
|
+
afterNextRender(() => void this.router.navigateByUrl('/activate', { replaceUrl: true }))
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Accessibility
|
|
118
|
+
|
|
119
|
+
See [AUDIT.md](AUDIT.md). The panel owns the page's `<main>` landmark and its `h1`.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# AuthLogin
|
|
2
|
+
|
|
3
|
+
The sign-in page of the auth kit, outside the app shell: the application's logo,
|
|
4
|
+
a heading, the username and password fields and one "Sign in" button; then,
|
|
5
|
+
when the password is accepted, the second factor. It is router-, storage- and
|
|
6
|
+
backend-agnostic: every call goes through the application's
|
|
7
|
+
[`AuthPort`](../auth/ports/auth.port.ts), and the page tells the application the
|
|
8
|
+
user is signed in with `loggedIn` (the application navigates).
|
|
9
|
+
|
|
10
|
+
**Selector**: `gbt-auth-login`
|
|
11
|
+
|
|
12
|
+
## States
|
|
13
|
+
|
|
14
|
+
| State | When | What shows |
|
|
15
|
+
| ----------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
16
|
+
| Credentials | On open, after "Back", after an expired second step. | "Sign in", the two fields, the submit button; the registration footer when registration is open and the application projected a link. |
|
|
17
|
+
| Challenge | `login` answered an `mfaToken` without `mfaSetupRequired`. | "Two-step verification", with what this user can do here: their passkey (when they have one and both the browser and the server can run it), the 6-digit code of their app (when they have one), and always a backup code. |
|
|
18
|
+
| Enrolment | `login` answered an `mfaToken` with `mfaSetupRequired` (no second factor yet). | "Two-factor authentication" and [`gbt-mfa-enrollment`](../mfa-enrollment/mfa-enrollment.ts) in a wider panel: the mandatory first set-up of an app or a passkey, then the backup codes. The session is only handed over once the user acknowledged the codes. |
|
|
19
|
+
|
|
20
|
+
## Inputs
|
|
21
|
+
|
|
22
|
+
| Input | Type | Default | Role |
|
|
23
|
+
| -------- | ---------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
24
|
+
| `labels` | `Partial<LoginLabels>` | `{}` | Strings to change, string by string, over `provideAuthLabels({ login })` and the English defaults (`DEFAULT_LOGIN_LABELS`). |
|
|
25
|
+
|
|
26
|
+
The enrolment takes its strings from `provideAuthLabels({ mfaEnrollment, totpQr, backupCodes })`.
|
|
27
|
+
|
|
28
|
+
## Outputs
|
|
29
|
+
|
|
30
|
+
| Output | Payload | Role |
|
|
31
|
+
| ---------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
32
|
+
| `loggedIn` | `void` | The user is signed in: the port stored the session (`login`, `verifyMfa`, `finishPasskeyChallenge`), or the page handed the held-back session of a finished enrolment to `AuthPort.setToken`. Navigate to the application. |
|
|
33
|
+
|
|
34
|
+
## Slots
|
|
35
|
+
|
|
36
|
+
| Selector | Role |
|
|
37
|
+
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
38
|
+
| `[auth-logo]` | The application's logo (an `img`, a `picture`, an `svg`) at the top of the panel, on every state. Give it its `alt`. |
|
|
39
|
+
| `[auth-backdrop]` | What is drawn on the graphite page, behind the panel: `gbt-git-field`, the family's animated commit graph (`@masmarino/gabarit/git-field`). Optional. |
|
|
40
|
+
| `[gbtAuthFooterLink]` | The link to the application's registration page (`<a gbtButton variant="link" gbtAuthFooterLink routerLink="/register">`). Shown under the form, after "No account yet?", only when `authConfig` says registration is open. |
|
|
41
|
+
| `[auth-notice]` | Something the application has to say before the credentials (a session that ended, a sign-in link that failed), e.g. a `gbt-alert`. First in the form, on the credentials step only. |
|
|
42
|
+
|
|
43
|
+
## Providers
|
|
44
|
+
|
|
45
|
+
| Token | Required | Role |
|
|
46
|
+
| ----------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
47
|
+
| `AUTH_PORT` | Yes | The backend: `authConfig`, `login`, `verifyMfa`, `startPasskeyChallenge`, `finishPasskeyChallenge`, `setToken`, and the enrolment's calls. |
|
|
48
|
+
| `TOTP_QR_RENDERER` | For the enrolment's QR | The QR encoder of the authenticator-app set-up. Without one the enrolment shows the secret to type by hand. |
|
|
49
|
+
| `AUTH_LABELS` (`provideAuthLabels`) | No | The application's wording, once for the whole kit. |
|
|
50
|
+
|
|
51
|
+
## Behaviour
|
|
52
|
+
|
|
53
|
+
- **Configuration.** `authConfig` is read once, when the page opens. A failed read only hides
|
|
54
|
+
the registration link, and is not taken for "no passkeys on this server": signing in never
|
|
55
|
+
depends on it.
|
|
56
|
+
- **Password step.** Only a 401 says "Incorrect username or password"; a 429 says to wait; any
|
|
57
|
+
other failure (a 5xx, a network error, an answer with neither `token` nor `mfaToken`) says the
|
|
58
|
+
sign-in failed. The password is dropped from memory as soon as the server accepted it.
|
|
59
|
+
- **Challenge.** One primary button per view: "Use a passkey" when it is offered, else "Verify".
|
|
60
|
+
A user with both factors gets the passkey first and the code form under "or". Spaces are
|
|
61
|
+
stripped from a code. A refused code empties the field; a 429 keeps it. A passkey-only user
|
|
62
|
+
whose passkey cannot run here (no WebAuthn in the browser, or passkeys off on the server) is
|
|
63
|
+
told why and given the backup-code form, never a dead end.
|
|
64
|
+
- **Passkey.** `startPasskeyChallenge`, the browser's prompt, then `finishPasskeyChallenge`. A
|
|
65
|
+
dismissed prompt is a quiet "Operation cancelled" (not an error) and keeps the screen. While
|
|
66
|
+
the prompt is open "Use a backup code" and "Back" stay usable; leaving drops the ceremony, so a
|
|
67
|
+
late answer is never sent.
|
|
68
|
+
- **Expiry.** A dead `mfaToken` (401 `invalid or expired token`, at the challenge, the passkey
|
|
69
|
+
or the enrolment) goes back to the credentials with "Your sign-in has expired, sign in again."
|
|
70
|
+
and the username kept.
|
|
71
|
+
- **Enrolment.** The page embeds `gbt-mfa-enrollment`: `completed` hands the session to
|
|
72
|
+
`AuthPort.setToken` then emits `loggedIn`; `cancelled` ("Back") returns to the credentials;
|
|
73
|
+
`expired` returns with the expiry message. The passkey option is only offered when the
|
|
74
|
+
server said `passkeysAvailable: true` and the browser has WebAuthn.
|
|
75
|
+
- **Focus.** On open the username field; after a failed sign-in the password field; on the
|
|
76
|
+
challenge the passkey button or the code field; after a failure the field (or button) to retry;
|
|
77
|
+
after "Back" the password field.
|
|
78
|
+
|
|
79
|
+
Failures are classified by status and by the body's `error` string (see
|
|
80
|
+
`AUTH_PORT_ERROR_BODIES`): the backend's own text is never shown.
|
|
81
|
+
|
|
82
|
+
## Example
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// app.config.ts
|
|
86
|
+
providers: [
|
|
87
|
+
{ provide: AUTH_PORT, useClass: HttpAuthPort }, // your adapter over HttpClient
|
|
88
|
+
{
|
|
89
|
+
provide: TOTP_QR_RENDERER,
|
|
90
|
+
useValue: (text, options) => import('qrcode').then((m) => m.toDataURL(text, options)),
|
|
91
|
+
},
|
|
92
|
+
provideAuthLabels({ login: { heading: 'Connexion', submit: 'Se connecter' /* … */ } }),
|
|
93
|
+
]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
@Component({
|
|
98
|
+
imports: [AuthLogin, Button, AuthFooterLink, RouterLink],
|
|
99
|
+
template: `
|
|
100
|
+
<gbt-auth-login (loggedIn)="router.navigateByUrl('/home')">
|
|
101
|
+
<img auth-logo src="/logo.svg" alt="Acme" width="160" height="40" />
|
|
102
|
+
<a gbtButton variant="link" gbtAuthFooterLink routerLink="/register">Create an account</a>
|
|
103
|
+
</gbt-auth-login>
|
|
104
|
+
`,
|
|
105
|
+
})
|
|
106
|
+
export class LoginPage {
|
|
107
|
+
protected readonly router = inject(Router)
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Accessibility
|
|
112
|
+
|
|
113
|
+
See [AUDIT.md](AUDIT.md). The page title (`<title>`) is the application's: set it on the route.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# AuthRegister
|
|
2
|
+
|
|
3
|
+
The free registration page of the auth kit, outside the app shell, in the sign-in panel
|
|
4
|
+
([`gbt-auth-panel`](../auth/auth-panel/auth-panel.ts)). It asks the backend whether registration is open,
|
|
5
|
+
shows the form (or "Registration is closed"), creates the account, then takes the new account through
|
|
6
|
+
the mandatory first MFA enrolment ([`gbt-mfa-enrollment`](../mfa-enrollment/mfa-enrollment.ts)) right
|
|
7
|
+
in the panel. It never talks HTTP, never touches storage and never navigates: the backend is the
|
|
8
|
+
application's `AUTH_PORT`, and the page tells the application where to go through its outputs.
|
|
9
|
+
|
|
10
|
+
**Selector**: `gbt-auth-register`
|
|
11
|
+
|
|
12
|
+
## States
|
|
13
|
+
|
|
14
|
+
| State | What shows |
|
|
15
|
+
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
16
|
+
| loading | Until `AuthPort.authConfig()` answers: a skeleton of the form (`role="status"`, `aria-busy`, a visually hidden "Loading"), no heading, never a flash of the form. |
|
|
17
|
+
| open | The form: username, email address, password, one primary "Create my account", then the projected sign-in link. Also shown when `authConfig()` fails: the server enforces the switch anyway. |
|
|
18
|
+
| closed | `registrationEnabled` is false (or the server answered "registration is disabled" to a submit): a lock state with "Registration is closed", its explanation and one "Sign in" button. |
|
|
19
|
+
| enrolment | The server answered an `mfaToken`: the panel widens (`wide`), its heading becomes "Two-factor authentication" and `gbt-mfa-enrollment` replaces the form. A passkey is offered when `authConfig().passkeysAvailable` is true and the browser supports WebAuthn. |
|
|
20
|
+
| created | The enrolment was left ("Back") or its token expired: the account exists, so the page says "Your account has been created", names it (lower-cased, as stored; "The setup has expired." first when it expired) and offers one "Sign in" button. No session is held. |
|
|
21
|
+
|
|
22
|
+
## Inputs
|
|
23
|
+
|
|
24
|
+
| Input | Type | Default | Role |
|
|
25
|
+
| ------------------- | ------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
+
| `labels` | `Partial<RegisterLabels>` | `{}` | Strings to change, over `provideAuthLabels({ register })` and the English defaults (`DEFAULT_REGISTER_LABELS`). |
|
|
27
|
+
| `minPasswordLength` | `number` | `MIN_PASSWORD_LENGTH` (8) | The server's minimum password length, checked before the round trip; also worded in the hint and the error. |
|
|
28
|
+
| `usernamePattern` | `RegExp` | `USERNAME_PATTERN` (a letter, 3 to 32) | The server's username rule, checked on the trimmed name before the round trip. Adjust the `usernameHint` and `usernameInvalid` labels with it. |
|
|
29
|
+
|
|
30
|
+
The enrolment's own strings are `MfaEnrollmentLabels`: localise them with `provideAuthLabels({ mfaEnrollment })`.
|
|
31
|
+
|
|
32
|
+
**A custom `usernamePattern` must not use the `g` or `y` regex flags (they make `.test()` stateful across
|
|
33
|
+
calls, so the same name would pass and fail in turn) and must mirror the server's own rule.** The same goes
|
|
34
|
+
for `minPasswordLength`: both only spare a round trip, the server stays the authority.
|
|
35
|
+
|
|
36
|
+
## Outputs
|
|
37
|
+
|
|
38
|
+
| Output | Payload | Role |
|
|
39
|
+
| ------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
40
|
+
| `registered` | `void` | The account exists and has a session: either the backend issued one at once (the port stored it), or the enrolment finished and the held-back session was handed to `AuthPort.setToken`. Navigate into the app. |
|
|
41
|
+
| `signIn` | `void` | "Sign in" was pressed in the closed or created state: go to the sign-in page. |
|
|
42
|
+
|
|
43
|
+
## Slots
|
|
44
|
+
|
|
45
|
+
| Selector | Role |
|
|
46
|
+
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
47
|
+
| `[auth-logo]` | The application's logo (an `img`, `picture` or `svg`), on top of the panel. Optional. |
|
|
48
|
+
| `[auth-backdrop]` | What is drawn on the graphite page, behind the panel: `gbt-git-field`, the family's animated commit graph (`@masmarino/gabarit/git-field`). Optional. |
|
|
49
|
+
| `[gbtAuthFooterLink]` | The link to the sign-in page, under the form after "Already have an account?". An `a gbtButton variant="link"` carrying `routerLink` or `href`. No footer when absent. |
|
|
50
|
+
|
|
51
|
+
## Behaviour
|
|
52
|
+
|
|
53
|
+
- **Checks before the request.** Nothing is said while the user types; after a first "Create my account"
|
|
54
|
+
every wrong field shows its rule (in place of its hint, as its only `aria-describedby`) and the focus moves
|
|
55
|
+
to the first wrong one. The checks then stay live. Nothing is sent while a field is wrong.
|
|
56
|
+
- **Sent**: `register(username.trim(), email.trim(), password)`. A second submit while in flight is ignored;
|
|
57
|
+
the button shows its spinner and is disabled. The password is emptied as soon as the server answered.
|
|
58
|
+
- **Failures** are worded from the labels, never from the backend's text, in an alert (`role="alert"`), and the
|
|
59
|
+
focus goes where the user retypes: a 400 naming a field focuses that field ("Check the fields"); a 400 naming
|
|
60
|
+
none focuses the alert; a reserved name, a taken name or address (409) focus the field concerned; a 429 says
|
|
61
|
+
to wait; anything else (5xx, network, a browser error) says the creation failed and keeps what was typed. A
|
|
62
|
+
400 "registration is disabled" switches to the closed state. The recognised bodies are in
|
|
63
|
+
`AUTH_PORT_ERROR_BODIES`.
|
|
64
|
+
- **Focus**: the form starts in the username field; the closed and created states move the focus to their
|
|
65
|
+
`h1` (focusable by script, not a tab stop) so the change is announced.
|
|
66
|
+
- **Sessions**: the page never stores one. A backend without MFA enforcement answers a `token` (the port stores
|
|
67
|
+
it): `registered` fires at once. Otherwise the enrolment keeps the session until the backup codes are
|
|
68
|
+
acknowledged, then the page hands it to `AuthPort.setToken` and emits `registered`.
|
|
69
|
+
|
|
70
|
+
## Example
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// app.config.ts
|
|
74
|
+
providers: [
|
|
75
|
+
{ provide: AUTH_PORT, useClass: HttpAuthPort }, // your adapter over HttpClient
|
|
76
|
+
{
|
|
77
|
+
provide: TOTP_QR_RENDERER,
|
|
78
|
+
useValue: (text, options) => import('qrcode').then((m) => m.toDataURL(text, options)),
|
|
79
|
+
},
|
|
80
|
+
provideAuthLabels({ register: { intro: 'Join Acme to host your repositories.' } }),
|
|
81
|
+
]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
@Component({
|
|
86
|
+
imports: [AuthRegister, Button, AuthFooterLink, RouterLink],
|
|
87
|
+
template: `
|
|
88
|
+
<gbt-auth-register
|
|
89
|
+
(registered)="router.navigateByUrl('/home')"
|
|
90
|
+
(signIn)="router.navigateByUrl('/login')"
|
|
91
|
+
>
|
|
92
|
+
<img auth-logo src="/logo.svg" alt="Acme" width="160" height="40" />
|
|
93
|
+
<a gbtButton variant="link" gbtAuthFooterLink routerLink="/login">Sign in</a>
|
|
94
|
+
</gbt-auth-register>
|
|
95
|
+
`,
|
|
96
|
+
})
|
|
97
|
+
export class RegisterRoute {
|
|
98
|
+
protected readonly router = inject(Router)
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Accessibility
|
|
103
|
+
|
|
104
|
+
See [AUDIT.md](AUDIT.md). The panel owns the page's `<main>` landmark and its `h1`.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# AuthResetPassword
|
|
2
|
+
|
|
3
|
+
The password-reset page of the auth kit: where the link of a password-reset mail lands, outside the app
|
|
4
|
+
shell, in the sign-in panel ([`gbt-auth-panel`](../auth/auth-panel/auth-panel.ts)). An administrator has
|
|
5
|
+
reset the password of an existing account; the user follows the link and chooses a new password. No
|
|
6
|
+
session comes out of it: they sign in afterwards, through their second factor as on any sign-in (a
|
|
7
|
+
password reset leaves the account's factors untouched). It never talks HTTP, never reads the URL and
|
|
8
|
+
never navigates: the backend is the application's `AUTH_PORT`, the token is an input, and the page tells
|
|
9
|
+
the application where to go through its outputs.
|
|
10
|
+
|
|
11
|
+
**Selector**: `gbt-auth-reset-password`
|
|
12
|
+
|
|
13
|
+
It is the sibling of [`gbt-auth-activate`](../auth-activate/README.md): same one-time link mechanics, same
|
|
14
|
+
form, same failure handling; only the call (`resetPassword` instead of `activate`), the output and the
|
|
15
|
+
wording differ.
|
|
16
|
+
|
|
17
|
+
## Views
|
|
18
|
+
|
|
19
|
+
| View | What shows |
|
|
20
|
+
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
21
|
+
| form | A token was given: "Choose a new password", why (an administrator reset it), New password (with its hint), Confirm the new password, one primary "Set new password", then the projected sign-in link. |
|
|
22
|
+
| success | The server accepted: "Your password has been changed", sign in with the new one, one "Sign in" button. |
|
|
23
|
+
| invalid | No token (`null` or `''`), or the server refused it (unknown, expired or used): an error-toned state, "This link does not work", ask an administrator for a new link, one "Sign in". |
|
|
24
|
+
|
|
25
|
+
## Inputs
|
|
26
|
+
|
|
27
|
+
| Input | Type | Default | Role |
|
|
28
|
+
| ------------------- | ------------------------------ | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
+
| `token` | `string \| null` | `null` | The reset link's token, as the application read it from its URL with `activationToken(fragment, null)`. `null` (or `''`) is the dead link, without any request. A new value starts the page over. |
|
|
30
|
+
| `labels` | `Partial<ResetPasswordLabels>` | `{}` | Strings to change, over `provideAuthLabels({ resetPassword })` and the English defaults (`DEFAULT_RESET_PASSWORD_LABELS`). |
|
|
31
|
+
| `minPasswordLength` | `number` | `MIN_PASSWORD_LENGTH` (8) | The server's minimum password length, checked before the round trip; also worded in the hint and the errors. |
|
|
32
|
+
|
|
33
|
+
## Outputs
|
|
34
|
+
|
|
35
|
+
| Output | Payload | Role |
|
|
36
|
+
| --------------- | ------- | ------------------------------------------------------------------------------------ |
|
|
37
|
+
| `passwordReset` | `void` | The server accepted the new password: the success view shows. No session was opened. |
|
|
38
|
+
| `signIn` | `void` | "Sign in" was pressed in the success or dead-link view: go to the sign-in page. |
|
|
39
|
+
|
|
40
|
+
The success output is `passwordReset`, not `reset`: an output named after a native DOM event would
|
|
41
|
+
also catch the `reset` events bubbling from the page's own `<form>`.
|
|
42
|
+
|
|
43
|
+
## Slots
|
|
44
|
+
|
|
45
|
+
| Selector | Role |
|
|
46
|
+
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
47
|
+
| `[auth-logo]` | The application's logo (an `img`, `picture` or `svg`), on top of the panel. Optional. |
|
|
48
|
+
| `[auth-backdrop]` | What is drawn on the graphite page, behind the panel: `gbt-git-field`, the family's animated commit graph (`@masmarino/gabarit/git-field`). Optional. |
|
|
49
|
+
| `[gbtAuthFooterLink]` | The link to the sign-in page, under the form after "Already set your new password?". An `a gbtButton variant="link"` carrying `routerLink` or `href`. No footer when absent. |
|
|
50
|
+
|
|
51
|
+
## Behaviour
|
|
52
|
+
|
|
53
|
+
- **The token.** The page sends it with the new password (`resetPassword(token, password)`) and never
|
|
54
|
+
renders it (not in the markup, not in a field). Once spent (success) or refused (a 400 other than a weak
|
|
55
|
+
password), it is dropped: the page does not send it again until a new `token` arrives.
|
|
56
|
+
- **The application's part** (it owns the URL): put the token in the link's **fragment**
|
|
57
|
+
(`/reset-password#token=…`) so that no server, proxy or access log ever sees it; read it once with
|
|
58
|
+
`activationToken(route.snapshot.fragment, null)` (the helper is shared with the activation: a token not
|
|
59
|
+
shaped like the server's, 64 hexadecimal characters by default, is `null`; pass your own `shape` as its
|
|
60
|
+
third argument if your backend's differs); then remove it (and any other parameter) from the address bar
|
|
61
|
+
and the history entry, e.g. `router.navigateByUrl('/reset-password', { replaceUrl: true })` after the
|
|
62
|
+
first render, so it does not linger in a screenshot or a shared tab. Unlike the activation, there is no
|
|
63
|
+
older `?token=` link to support: read the fragment only.
|
|
64
|
+
- **The `token` input must be a one-time snapshot of the URL, never a value that reactively follows the
|
|
65
|
+
URL (e.g. a signal derived from `route.fragment`): the component resets when it changes.** Scrubbing the
|
|
66
|
+
URL as above would otherwise turn the token into `null` and flip the page to the dead-link view, even over
|
|
67
|
+
its success view. Read it into a plain field, as in the example below.
|
|
68
|
+
- **`minPasswordLength` must mirror the server's own rule.** It only spares a round trip: the server stays
|
|
69
|
+
the authority, and a looser value only moves the refusal to the server's answer.
|
|
70
|
+
- **Checks before the request.** Nothing is said while typing; after a first attempt the password rule and
|
|
71
|
+
the confirmation ("Confirm your new password", "The passwords do not match") show under their fields, the
|
|
72
|
+
focus moves to the first wrong one, and the checks stay live.
|
|
73
|
+
- **Failures** are worded from the labels, in an alert, the focus back in the password field: a weak-password
|
|
74
|
+
400 keeps the form (the link is still good), a 429 says to wait, anything else (5xx, network) says the new
|
|
75
|
+
password could not be set and the same token can be retried. Any other 400 is the dead link: the server
|
|
76
|
+
is expected to answer one generic 400 for an unknown, expired, used or malformed token, so that nobody
|
|
77
|
+
probing links can tell them apart; the page words them all the same way. The failures are read by
|
|
78
|
+
`classifyActivateFailure`, shared with the activation.
|
|
79
|
+
- **Focus**: the form starts in the new-password field; the success and dead-link views move the focus to
|
|
80
|
+
their `h1` (focusable by script, not a tab stop). The passwords are emptied when the server accepted or
|
|
81
|
+
refused the link.
|
|
82
|
+
|
|
83
|
+
## Example
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// app.config.ts
|
|
87
|
+
providers: [
|
|
88
|
+
{ provide: AUTH_PORT, useClass: HttpAuthPort }, // your adapter over HttpClient
|
|
89
|
+
provideAuthLabels({ resetPassword: { signInPrompt: 'Password already changed?' } }),
|
|
90
|
+
]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
@Component({
|
|
95
|
+
imports: [AuthResetPassword, Button, AuthFooterLink, RouterLink],
|
|
96
|
+
template: `
|
|
97
|
+
<gbt-auth-reset-password [token]="token" (signIn)="router.navigateByUrl('/login')">
|
|
98
|
+
<img auth-logo src="/logo.svg" alt="Acme" width="160" height="40" />
|
|
99
|
+
<a gbtButton variant="link" gbtAuthFooterLink routerLink="/login">Sign in</a>
|
|
100
|
+
</gbt-auth-reset-password>
|
|
101
|
+
`,
|
|
102
|
+
})
|
|
103
|
+
export class ResetPasswordRoute {
|
|
104
|
+
protected readonly router = inject(Router)
|
|
105
|
+
private readonly route = inject(ActivatedRoute)
|
|
106
|
+
// Read once, then scrubbed from the address bar and the history entry.
|
|
107
|
+
protected readonly token = activationToken(this.route.snapshot.fragment, null)
|
|
108
|
+
|
|
109
|
+
constructor() {
|
|
110
|
+
const { fragment, queryParamMap } = this.route.snapshot
|
|
111
|
+
if (fragment !== null || queryParamMap.keys.length > 0) {
|
|
112
|
+
afterNextRender(() => void this.router.navigateByUrl('/reset-password', { replaceUrl: true }))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Accessibility
|
|
119
|
+
|
|
120
|
+
See [AUDIT.md](AUDIT.md). The panel owns the page's `<main>` landmark and its `h1`.
|