@crombolo/backoffice-host 0.13.2 → 0.14.4

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.
Files changed (45) hide show
  1. package/assets/device.js +180 -0
  2. package/assets/login.js +89 -2
  3. package/assets/remote-approval.js +134 -0
  4. package/package.json +2 -2
  5. package/plugin/assistant/umbraco-package.json +1 -1
  6. package/plugin/branding/branding.js +9 -14
  7. package/plugin/branding/localization-en.js +11 -0
  8. package/plugin/branding/theme.css +2 -0
  9. package/plugin/localizations/ar.js +3 -39
  10. package/plugin/localizations/bs.js +3 -29
  11. package/plugin/localizations/cs.js +3 -24
  12. package/plugin/localizations/cy.js +3 -32
  13. package/plugin/localizations/da.js +3 -41
  14. package/plugin/localizations/de.js +3 -27
  15. package/plugin/localizations/es.js +3 -19
  16. package/plugin/localizations/fr.js +3 -26
  17. package/plugin/localizations/hr.js +3 -29
  18. package/plugin/localizations/it.js +3 -29
  19. package/plugin/localizations/ja.js +3 -11
  20. package/plugin/localizations/ko.js +3 -11
  21. package/plugin/localizations/nb.js +3 -18
  22. package/plugin/localizations/nl.js +3 -29
  23. package/plugin/localizations/pl.js +3 -11
  24. package/plugin/localizations/pt-br.js +0 -10
  25. package/plugin/localizations/pt.js +3 -39
  26. package/plugin/localizations/ru.js +3 -9
  27. package/plugin/localizations/sv.js +3 -20
  28. package/plugin/localizations/tr.js +3 -26
  29. package/plugin/localizations/uk.js +3 -9
  30. package/plugin/localizations/umbraco-package.json +22 -2
  31. package/plugin/localizations/vi.js +3 -42
  32. package/plugin/localizations/zh-tw.js +0 -6
  33. package/plugin/localizations/zh.js +0 -6
  34. package/plugin/seo/umbraco-package.json +1 -1
  35. package/plugin/sign-in/entry.js +20 -0
  36. package/plugin/sign-in/sign-in-dialog.js +127 -0
  37. package/plugin/sign-in/umbraco-package.json +15 -0
  38. package/plugin/signed-in-devices.js +90 -0
  39. package/plugin/two-factor-setup.js +147 -0
  40. package/plugin/umbraco-package.json +79 -27
  41. package/plugin/welcome-dashboard.js +2 -5
  42. package/plugin/workflow-designer.js +245 -170
  43. package/src/login-assets.ts +5 -2
  44. package/src/shell.ts +65 -1
  45. package/plugin/branding/sysinfo.js +0 -89
@@ -3,26 +3,6 @@
3
3
  // plugin/branding/localization-en.js curates, and "content type" where
4
4
  // Umbraco says "document type".
5
5
  export default {
6
- "login": {
7
- "userInviteWelcomeMessage": "Hej och välkommen till Crombolo! Inom bara 1 minut är du redo att börja, vi behöver bara att du ställer in ett lösenord.",
8
- "userInviteExpiredMessage": "Välkommen till Crombolo! Tyvärr har din inbjudan gått ut. Vänligen kontakta din administratör och be dem skicka den igen."
9
- },
10
- "user": {
11
- "changePasswordDescription": "Du kan byta ditt lösenord för Crombolo backoffice genom att fylla i nedanstående formulär och klicka på knappen \"Ändra lösenord\".",
12
- "createUserHelp": "Skapa nya användare för att ge dom åtkomst till Crombolo. När en ny användare skapas kommer ett lösenord genereras som du kan dela med användaren.",
13
- "inviteUserHelp": "Bjud in nya användare för att ge dom åtkomst till Crombolo. Ett e-postmeddelande kommer skikcas till användaren med information om hur man loggar in i Crombolo. Inbjudningar är giltiga i 72 timmar.",
14
- "noConsole": "Inaktivera tillgång till Crombolo",
15
- "documentType": "Innehållstyp"
16
- },
17
- "visuallyHiddenTexts": {
18
- "searchOverlayTitle": "Sök i Crombolo backoffice"
19
- },
20
- "paste": {
21
- "errorMessage": "Texten du försöker klistra in innehåller specialtecken och/eller formateringstaggar. Detta kan bero på att texten kommer från t.ex. Microsoft Word. Crombolo kan ta bort specialtecken och formateringstaggar automatiskt så att innehållet lämpar sig bättre för webbpublicering."
22
- },
23
- "dashboard": {
24
- "nothinghappens": "Om Crombolo inte öppnas kan det bero på att du måste tillåta poppuppfönster att öppnas från denna webbplats"
25
- },
26
6
  "actions": {
27
7
  "changeDocType": "Ändra innehållstyp",
28
8
  "exportDocumentType": "Exportera innehållstyp",
@@ -42,5 +22,8 @@ export default {
42
22
  },
43
23
  "treeHeaders": {
44
24
  "documentTypes": "Innehållstyper"
25
+ },
26
+ "user": {
27
+ "documentType": "Innehållstyp"
45
28
  }
46
29
  }
@@ -3,32 +3,6 @@
3
3
  // plugin/branding/localization-en.js curates, and "content type" where
4
4
  // Umbraco says "document type".
5
5
  export default {
6
- "user": {
7
- "changePasswordDescription": "Aşağıdaki formu doldurarak ve 'Şifreyi Değiştir' düğmesini tıklayarak Crombolo Arka Ofisine erişim şifrenizi değiştirebilirsiniz",
8
- "createUserHelp": "Crombolo'ya erişim sağlamak için yeni kullanıcılar oluşturun. Yeni bir kullanıcı oluşturulduğunda, kullanıcıyla paylaşabileceğiniz bir şifre oluşturulacaktır.",
9
- "inviteUserHelp": "Yeni kullanıcıları Crombolo'ya erişim vermeleri için davet edin. Kullanıcıya, Crombolo'da nasıl oturum açılacağına dair bilgiler içeren bir davet e-postası gönderilecektir. Davetler 72 saat sürer.",
10
- "noConsole": "Crombolo Erişimini Devre Dışı Bırak",
11
- "userCreatedSuccessHelp": "Yeni kullanıcı başarıyla oluşturuldu. Crombolo'da oturum açmak için aşağıdaki şifreyi kullanın.",
12
- "userInvitedSuccessHelp": "Yeni kullanıcıya, Crombolo'da nasıl oturum açılacağına ilişkin ayrıntıları içeren bir davetiye gönderildi.",
13
- "userinviteWelcomeMessage": "Merhabalar, Crombolo'ya hoş geldiniz! Sadece 1 dakika içinde hazır olacaksınız, sadece bir şifre belirlemeniz.",
14
- "userinviteExpiredMessage": "Crombolo'ya hoş geldiniz! Maalesef davetinizin süresi doldu. Lütfen yöneticinizle iletişime geçin ve yeniden göndermesini isteyin.",
15
- "documentType": "İçerik Türü"
16
- },
17
- "languages": {
18
- "defaultLanguageHelp": "Bir Crombolo sitesinde yalnızca bir varsayılan dil ayarı olabilir."
19
- },
20
- "paste": {
21
- "errorMessage": "Yapıştırmaya çalıştığınız metin özel karakterler veya biçimlendirme içeriyor. Bunun nedeni Microsoft Word'den metin kopyalanması olabilir. Crombolo, özel karakterleri veya biçimlendirmeyi otomatik olarak kaldırabilir, böylece yapıştırılan içerik web için daha uygun olacaktır."
22
- },
23
- "dashboard": {
24
- "nothinghappens": "Crombolo açılmıyorsa, bu siteden pop-up'lara izin vermeniz gerekebilir"
25
- },
26
- "logViewer": {
27
- "searchUmbracoSource": "Crombolo Kaynağını Ara",
28
- "searchWithinUmbracoSourceCodeOnGithub": "Github'da Crombolo kaynak kodu içinde arama",
29
- "searchUmbracoIssues": "Crombolo Sorunlarını Ara",
30
- "searchUmbracoIssuesOnGithub": "Github'da Crombolo Sorunlarını Ara"
31
- },
32
6
  "actions": {
33
7
  "changeDocType": "İçerik Türünü Değiştir",
34
8
  "exportDocumentType": "İçerik Türünü Dışa Aktar",
@@ -70,6 +44,9 @@ export default {
70
44
  "treeHeaders": {
71
45
  "documentTypes": "İçerik Türleri"
72
46
  },
47
+ "user": {
48
+ "documentType": "İçerik Türü"
49
+ },
73
50
  "references": {
74
51
  "labelUsedByDocumentTypes": "İçerik Türlerinde Kullanılır"
75
52
  },
@@ -3,15 +3,6 @@
3
3
  // plugin/branding/localization-en.js curates, and "content type" where
4
4
  // Umbraco says "document type".
5
5
  export default {
6
- "user": {
7
- "changePasswordDescription": "Ви можете змінити свій пароль для доступу до адміністративної панелі Crombolo, заповнивши нижченаведені поля та натиснувши кнопку 'Змінити пароль'",
8
- "createUserHelp": "Створюйте нових користувачів, яким потрібний доступ до адміністративної панелі Crombolo. При створенні користувача генерується новий первинний пароль, який потрібно повідомити користувачеві.",
9
- "inviteUserHelp": "Запросіть нових користувачів, яким потрібний доступ до адміністративної панелі Crombolo. Запрошеному буде надіслано електронного листа з інструкціями щодо доступу до Crombolo.",
10
- "noConsole": "Вимкнути доступ до адміністративної панелі Crombolo",
11
- "userInvitedSuccessHelp": "Новому користувачеві було надіслано запрошення, яке містить інструкції для входу в панель Crombolo.",
12
- "userinviteWelcomeMessage": "Привіт і ласкаво просимо до Crombolo! Все буде готове протягом декількох хвилин, нам потрібно задати пароль для входу.",
13
- "documentType": "Тип контенту"
14
- },
15
6
  "actions": {
16
7
  "changeDocType": "Змінити тип контенту"
17
8
  },
@@ -42,5 +33,8 @@ export default {
42
33
  },
43
34
  "treeHeaders": {
44
35
  "documentTypes": "Типи контенту"
36
+ },
37
+ "user": {
38
+ "documentType": "Тип контенту"
45
39
  }
46
40
  }
@@ -2,9 +2,29 @@
2
2
  "$schema": "../../../backoffice-dist/dist/umbraco-package-schema.json",
3
3
  "name": "Crombolo Localizations",
4
4
  "id": "Crombolo.Localizations",
5
- "version": "0.13.2",
6
- "allowPublicAccess": false,
5
+ "version": "0.14.4",
6
+ "allowPublicAccess": true,
7
7
  "extensions": [
8
+ {
9
+ "type": "localization",
10
+ "alias": "Crombolo.Localization.EN",
11
+ "name": "Crombolo English Overrides",
12
+ "weight": 0,
13
+ "meta": {
14
+ "culture": "en"
15
+ },
16
+ "js": "/crombolo-plugin/branding/localization-en.js"
17
+ },
18
+ {
19
+ "type": "localization",
20
+ "alias": "Crombolo.Localization.EN_US",
21
+ "name": "Crombolo English (United States) Overrides",
22
+ "weight": 0,
23
+ "meta": {
24
+ "culture": "en-us"
25
+ },
26
+ "js": "/crombolo-plugin/branding/localization-en.js"
27
+ },
8
28
  {
9
29
  "type": "localization",
10
30
  "alias": "Crombolo.Localization.Overrides.AR",
@@ -3,48 +3,6 @@
3
3
  // plugin/branding/localization-en.js curates, and "content type" where
4
4
  // Umbraco says "document type".
5
5
  export default {
6
- "login": {
7
- "instruction": "Đăng nhập vào Crombolo"
8
- },
9
- "user": {
10
- "changePasswordDescription": "Bạn có thể thay đổi mật khẩu của mình để truy cập vào Crombolo backoffice bằng cách điền vào mẫu dưới đây và nhấp vào nút 'Đổi mật khẩu'",
11
- "createUserHelp": "Tạo người dùng mới để cấp quyền truy cập cho họ vào Crombolo. Khi một người dùng mới được tạo, một mật khẩu sẽ được tạo ra mà bạn có thể chia sẻ với người dùng đó.",
12
- "inviteUserHelp": "Mời người dùng mới để cấp quyền truy cập cho họ vào Crombolo. Một email mời sẽ được gửi đến người dùng với thông tin về cách đăng nhập vào Crombolo. Thời gian hiệu lực của lời mời là 72 giờ.",
13
- "noConsole": "Vô hiệu hóa quyền truy cập Crombolo",
14
- "userCreatedSuccessHelp": "Người dùng mới đã được tạo thành công. Để đăng nhập vào Crombolo, hãy sử dụng mật khẩu bên dưới.",
15
- "userInvitedSuccessHelp": "Một lời mời đã được gửi đến người dùng mới với thông tin chi tiết về cách đăng nhập vào Crombolo.",
16
- "userinviteWelcomeMessage": "Xin chào và chào mừng bạn đến với Crombolo! Chỉ trong 1 phút, bạn sẽ sẵn sàng sử dụng. Chúng tôi chỉ cần bạn tạo mật khẩu và thêm hình đại diện cho tài khoản.",
17
- "userinviteExpiredMessage": "Chào mừng bạn đến với Crombolo! Rất tiếc, lời mời của bạn đã hết hạn. Vui lòng liên hệ với quản trị viên của bạn và yêu cầu họ gửi lại lời mời.",
18
- "documentType": "Loại nội dung"
19
- },
20
- "general": {
21
- "umbracoInfo": "Thông tin Crombolo"
22
- },
23
- "buttons": {
24
- "viewSystemDetails": "Xem thông tin hệ thống Crombolo CMS và số phiên bản"
25
- },
26
- "visuallyHiddenTexts": {
27
- "searchOverlayTitle": "Tìm kiếm trong Crombolo backoffice"
28
- },
29
- "languages": {
30
- "defaultLanguageHelp": "Một trang Crombolo chỉ có thể có một ngôn ngữ mặc định được thiết lập."
31
- },
32
- "paste": {
33
- "errorMessage": "Văn bản bạn đang cố dán chứa các ký tự hoặc định dạng đặc biệt. Điều này có thể do việc sao chép văn bản từ Microsoft Word. Crombolo có thể tự động loại bỏ các ký tự hoặc định dạng đặc biệt, vì vậy nội dung được dán sẽ phù hợp hơn với web."
34
- },
35
- "dashboard": {
36
- "nothinghappens": "Nếu Crombolo không mở, bạn có thể cần cho phép cửa sổ bật lên từ trang này"
37
- },
38
- "logViewer": {
39
- "searchUmbracoSource": "Tìm kiếm mã nguồn Crombolo",
40
- "searchWithinUmbracoSourceCodeOnGithub": "Tìm trong mã nguồn Crombolo trên GitHub",
41
- "searchUmbracoIssues": "Tìm kiếm vấn đề của Crombolo",
42
- "searchUmbracoIssuesOnGithub": "Tìm vấn đề của Crombolo trên GitHub"
43
- },
44
- "defaultdialogs": {
45
- "linkYourConfirm": "Bạn sắp liên kết tài khoản Crombolo và {0} của mình và bạn sẽ được chuyển hướng đến {0} để xác nhận.",
46
- "unLinkYourConfirm": "Bạn sắp hủy liên kết tài khoản Crombolo và {0} của mình và bạn sẽ bị đăng xuất."
47
- },
48
6
  "actions": {
49
7
  "changeDocType": "Thay đổi loại nội dung",
50
8
  "exportDocumentType": "Xuất khẩu loại nội dung",
@@ -94,6 +52,9 @@ export default {
94
52
  "treeHeaders": {
95
53
  "documentTypes": "Loại nội dung"
96
54
  },
55
+ "user": {
56
+ "documentType": "Loại nội dung"
57
+ },
97
58
  "references": {
98
59
  "labelUsedByDocumentTypes": "Được sử dụng trong các Loại Nội dung"
99
60
  },
@@ -3,12 +3,6 @@
3
3
  // plugin/branding/localization-en.js curates, and "content type" where
4
4
  // Umbraco says "document type".
5
5
  export default {
6
- "paste": {
7
- "errorMessage": "您所粘貼的文本含有特殊字元或格式,Crombolo將清除以適應網頁。"
8
- },
9
- "dashboard": {
10
- "nothinghappens": "如果Crombolo沒有打開,您可能需要允許彈出式視窗。"
11
- },
12
6
  "actions": {
13
7
  "changeDocType": "改變內容類型",
14
8
  "exportDocumentType": "匯出內容類型",
@@ -3,12 +3,6 @@
3
3
  // plugin/branding/localization-en.js curates, and "content type" where
4
4
  // Umbraco says "document type".
5
5
  export default {
6
- "paste": {
7
- "errorMessage": "您所粘贴的文本含有特殊字符或格式,Crombolo将清除以适应网页。"
8
- },
9
- "dashboard": {
10
- "nothinghappens": "如果Crombolo没有打开,您可能需要允许弹出式窗口。"
11
- },
12
6
  "actions": {
13
7
  "changeDocType": "改变内容类型",
14
8
  "exportDocumentType": "导出内容类型",
@@ -2,7 +2,7 @@
2
2
  "$schema": "../../../backoffice-dist/dist/umbraco-package-schema.json",
3
3
  "name": "Crombolo Search & sharing",
4
4
  "id": "Crombolo.Seo",
5
- "version": "0.13.2",
5
+ "version": "0.14.4",
6
6
  "allowPublicAccess": false,
7
7
  "extensions": [
8
8
  {
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Puts crombolo's sign-in dialog in place of the client's. The client opens
3
+ * `Umb.Modal.AppAuth` when a session ends while the backoffice is open, and
4
+ * draws it before any private manifest has loaded — so it is replaced from a
5
+ * public app entry point, which the client runs before it routes.
6
+ *
7
+ * Re-registered under the same alias rather than through `overwrites`: the
8
+ * modal is looked up with `byTypeAndAlias`, which does not resolve overwrites.
9
+ */
10
+ export const DIALOG_ALIAS = 'Umb.Modal.AppAuth'
11
+
12
+ export const onInit = (_host, extensionRegistry) => {
13
+ extensionRegistry.unregister(DIALOG_ALIAS)
14
+ extensionRegistry.register({
15
+ type: 'modal',
16
+ alias: DIALOG_ALIAS,
17
+ name: 'Crombolo Sign-in Dialog',
18
+ element: () => import('./sign-in-dialog.js'),
19
+ })
20
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * The dialog shown when a session ends while the backoffice is open: crombolo's
3
+ * own, laid out as the login page is. Signing in opens the login page in a
4
+ * pop-up, as the client's dialog does, so what was on screen stays put.
5
+ *
6
+ * Closes once whichever comes first reports the session back: the sign-in it
7
+ * started, or another tab signing in.
8
+ */
9
+ import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'
10
+ import { css, html, nothing } from '@umbraco-cms/backoffice/external/lit'
11
+ import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'
12
+ import { UMB_SERVER_CONTEXT } from '@umbraco-cms/backoffice/server'
13
+
14
+ /** The identity provider the server's authorize endpoint knows; nothing displays it. */
15
+ const PROVIDER = 'Umbraco'
16
+ const LOGO = '/umbraco/management/api/v1/security/back-office/graphics/login-logo-alternative'
17
+
18
+ export class CromboloSignInDialogElement extends UmbModalBaseElement {
19
+ static properties = {
20
+ _logo: { state: true },
21
+ _busy: { state: true },
22
+ _error: { state: true },
23
+ }
24
+
25
+ #auth
26
+ #closed = false
27
+
28
+ constructor() {
29
+ super()
30
+ this.consumeContext(UMB_SERVER_CONTEXT, (server) => {
31
+ this._logo = `${server?.getServerUrl() ?? ''}${LOGO}`
32
+ })
33
+ this.consumeContext(UMB_AUTH_CONTEXT, (auth) => {
34
+ this.#auth = auth
35
+ this.observe(auth?.isAuthorized, (authorized) => {
36
+ if (authorized) this.#close()
37
+ })
38
+ })
39
+ }
40
+
41
+ get #timedOut() {
42
+ return this.data?.userLoginState === 'timedOut'
43
+ }
44
+
45
+ #close() {
46
+ if (this.#closed) return
47
+ this.#closed = true
48
+ this.value = { success: true }
49
+ this._submitModal()
50
+ }
51
+
52
+ async #signIn() {
53
+ if (!this.#auth) return
54
+ this._busy = true
55
+ this._error = undefined
56
+ try {
57
+ // A pop-up when the session timed out, so the page underneath survives;
58
+ // otherwise the whole window goes to the login page.
59
+ await this.#auth.makeAuthorizationRequest(PROVIDER, !this.#timedOut)
60
+ if (this.#auth.getIsAuthorized()) this.#close()
61
+ } catch {
62
+ this._error = 'Signing in did not complete. Try again.'
63
+ } finally {
64
+ this._busy = false
65
+ }
66
+ }
67
+
68
+ render() {
69
+ return html`<div id="layout">
70
+ <uui-box id="card">
71
+ ${this._logo ? html`<img id="logo" src=${this._logo} alt="" />` : nothing}
72
+ <h1 class="uui-h3">${this.#timedOut ? 'Your session has ended' : 'Sign in'}</h1>
73
+ ${this.#timedOut ? html`<p>Sign in again to carry on where you left off.</p>` : nothing}
74
+ ${this._error ? html`<p id="error">${this._error}</p>` : nothing}
75
+ <uui-button
76
+ look="primary"
77
+ color="positive"
78
+ label="Sign in"
79
+ .state=${this._busy ? 'waiting' : undefined}
80
+ @click=${() => this.#signIn()}
81
+ ></uui-button>
82
+ </uui-box>
83
+ </div>`
84
+ }
85
+
86
+ static styles = css`
87
+ :host {
88
+ display: block;
89
+ width: 100vw;
90
+ height: 100vh;
91
+ background: var(--uui-color-surface-alt, #f4f4f4);
92
+ }
93
+ #layout {
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: center;
97
+ height: 100%;
98
+ padding: 16px;
99
+ box-sizing: border-box;
100
+ }
101
+ #card {
102
+ width: 100%;
103
+ max-width: 400px;
104
+ }
105
+ #logo {
106
+ display: block;
107
+ height: 48px;
108
+ margin-bottom: var(--uui-size-space-4, 12px);
109
+ }
110
+ h1 {
111
+ margin: 0 0 var(--uui-size-space-4, 12px);
112
+ }
113
+ p {
114
+ margin: 0 0 var(--uui-size-space-5, 18px);
115
+ }
116
+ #error {
117
+ color: var(--uui-color-danger, #d42054);
118
+ }
119
+ uui-button {
120
+ width: 100%;
121
+ }
122
+ `
123
+ }
124
+
125
+ customElements.define('crombolo-sign-in-dialog', CromboloSignInDialogElement)
126
+
127
+ export default CromboloSignInDialogElement
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "../../../backoffice-dist/dist/umbraco-package-schema.json",
3
+ "name": "Crombolo Sign-in Dialog",
4
+ "id": "Crombolo.SignInDialog",
5
+ "version": "0.14.4",
6
+ "allowPublicAccess": true,
7
+ "extensions": [
8
+ {
9
+ "type": "appEntryPoint",
10
+ "alias": "Crombolo.AppEntryPoint.SignInDialog",
11
+ "name": "Crombolo Sign-in Dialog",
12
+ "js": "/crombolo-plugin/sign-in/entry.js"
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The terminals signed in as the current person (story 107), in their profile:
3
+ * each one named, when it signed in and was last used, and signed out from here.
4
+ */
5
+ import { css, html, nothing } from '@umbraco-cms/backoffice/external/lit'
6
+ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
7
+
8
+ const API = '/umbraco/management/api/v1/security/back-office/devices'
9
+
10
+ const when = (iso) => (iso ? new Date(iso).toLocaleString() : '—')
11
+
12
+ export default class CromboloSignedInDevicesElement extends UmbLitElement {
13
+ static properties = { _devices: { state: true }, _error: { state: true } }
14
+
15
+ constructor() {
16
+ super()
17
+ this._devices = undefined
18
+ this._error = ''
19
+ }
20
+
21
+ connectedCallback() {
22
+ super.connectedCallback()
23
+ this.#load()
24
+ }
25
+
26
+ async #load() {
27
+ try {
28
+ const response = await fetch(API, { credentials: 'include' })
29
+ this._devices = response.ok ? await response.json() : []
30
+ } catch {
31
+ this._error = 'Could not load your signed-in devices.'
32
+ }
33
+ }
34
+
35
+ async #signOut(id) {
36
+ const response = await fetch(`${API}/${id}`, { method: 'DELETE', credentials: 'include' })
37
+ if (!response.ok) this._error = 'That device could not be signed out.'
38
+ await this.#load()
39
+ }
40
+
41
+ render() {
42
+ if (!this._devices) return nothing
43
+ return html`<uui-box headline="Signed-in devices">
44
+ ${this._error ? html`<p class="error">${this._error}</p>` : nothing}
45
+ ${
46
+ this._devices.length === 0
47
+ ? html`<p>No terminals are signed in as you.</p>`
48
+ : html`<ul>
49
+ ${this._devices.map(
50
+ (device) => html`<li>
51
+ <div>
52
+ <strong>${device.name ?? 'A terminal'}</strong>
53
+ <small>signed in ${when(device.signedInAt)}, last used ${when(device.lastUsedAt)}</small>
54
+ </div>
55
+ <uui-button look="secondary" label="Sign out" @click=${() => this.#signOut(device.id)}></uui-button>
56
+ </li>`,
57
+ )}
58
+ </ul>`
59
+ }
60
+ </uui-box>`
61
+ }
62
+
63
+ static styles = css`
64
+ ul {
65
+ list-style: none;
66
+ margin: 0;
67
+ padding: 0;
68
+ }
69
+ li {
70
+ display: flex;
71
+ justify-content: space-between;
72
+ align-items: center;
73
+ gap: var(--uui-size-space-4);
74
+ padding: var(--uui-size-space-3) 0;
75
+ border-bottom: 1px solid var(--uui-color-divider);
76
+ }
77
+ li div {
78
+ display: flex;
79
+ flex-direction: column;
80
+ }
81
+ small {
82
+ color: var(--uui-color-text-alt);
83
+ }
84
+ .error {
85
+ color: var(--uui-color-danger);
86
+ }
87
+ `
88
+ }
89
+
90
+ customElements.define('crombolo-signed-in-devices', CromboloSignedInDevicesElement)
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Sets up an authenticator app for the signed-in user (story 032): the QR code,
3
+ * the first code to prove the app has it, then the recovery codes, shown once.
4
+ * Umbraco's own setup screen throws the recovery codes away, hence this one.
5
+ */
6
+
7
+ import { UmbCurrentUserRepository } from '@umbraco-cms/backoffice/current-user'
8
+ import { css, html, nothing } from '@umbraco-cms/backoffice/external/lit'
9
+ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
10
+
11
+ const endpoint = (providerName) =>
12
+ `${(document.querySelector('base')?.getAttribute('href') ?? '/umbraco/').replace(/\/$/, '')}/management/api/v1/user/current/2fa/${encodeURIComponent(providerName)}`
13
+
14
+ export default class CromboloTwoFactorSetupElement extends UmbLitElement {
15
+ static properties = {
16
+ providerName: { attribute: false },
17
+ displayName: { attribute: false },
18
+ close: { attribute: false },
19
+ _setup: { state: true },
20
+ _error: { state: true },
21
+ _codes: { state: true },
22
+ _busy: { state: true },
23
+ }
24
+
25
+ constructor() {
26
+ super()
27
+ this.providerName = ''
28
+ this.displayName = ''
29
+ this.close = () => {}
30
+ this._setup = undefined
31
+ this._error = undefined
32
+ this._codes = undefined
33
+ this._busy = false
34
+ this.#repository = new UmbCurrentUserRepository(this)
35
+ }
36
+
37
+ #repository
38
+
39
+ async firstUpdated() {
40
+ const response = await fetch(endpoint(this.providerName), {
41
+ credentials: 'include',
42
+ headers: { accept: 'application/json' },
43
+ })
44
+ const body = await response.json().catch(() => ({}))
45
+ if (!response.ok || !body.secret) {
46
+ this._error = body.detail ?? 'Two-factor sign-in cannot be set up right now.'
47
+ return
48
+ }
49
+ this._setup = body
50
+ }
51
+
52
+ async #submit(event) {
53
+ event.preventDefault()
54
+ const code = new FormData(event.target).get('code')?.toString().trim()
55
+ if (!code) return
56
+ this._busy = true
57
+ this._error = undefined
58
+ const response = await fetch(endpoint(this.providerName), {
59
+ method: 'POST',
60
+ credentials: 'include',
61
+ headers: { accept: 'application/json', 'content-type': 'application/json' },
62
+ body: JSON.stringify({ code, secret: this._setup.secret }),
63
+ })
64
+ const body = await response.json().catch(() => ({}))
65
+ this._busy = false
66
+ if (!response.ok) {
67
+ this._error = body.detail ?? 'That code is not right. Try the next one.'
68
+ return
69
+ }
70
+ this._codes = body.recoveryCodes ?? []
71
+ }
72
+
73
+ async #done() {
74
+ await this.#repository.requestMfaLoginProviders()
75
+ this.close()
76
+ }
77
+
78
+ #copy() {
79
+ navigator.clipboard?.writeText(this._codes.join('\n'))
80
+ }
81
+
82
+ render() {
83
+ return html`<umb-body-layout headline=${this.displayName || 'Authenticator app'}>
84
+ ${this._codes ? this.#renderCodes() : this.#renderSetup()}
85
+ </umb-body-layout>`
86
+ }
87
+
88
+ #renderSetup() {
89
+ if (!this._setup)
90
+ return this._error
91
+ ? html`<p class="error">${this._error}</p>
92
+ <div slot="actions"><uui-button label="Close" @click=${() => this.close()}></uui-button></div>`
93
+ : html`<uui-loader-bar></uui-loader-bar>`
94
+ return html`<form id="setup" @submit=${this.#submit} novalidate>
95
+ <p>Scan this code with your authenticator app, then enter the six-digit code it shows.</p>
96
+ <img class="qr" src=${this._setup.qrCodeSetupImageUrl} alt="QR code for your authenticator app" />
97
+ <p class="secret">Or enter this key by hand: <code>${this._setup.secret}</code></p>
98
+ <uui-form-layout-item>
99
+ <uui-label slot="label" for="code" required>Code from the app</uui-label>
100
+ <uui-input id="code" name="code" inputmode="numeric" autocomplete="one-time-code" label="Code from the app" required></uui-input>
101
+ </uui-form-layout-item>
102
+ ${this._error ? html`<p class="error">${this._error}</p>` : nothing}
103
+ </form>
104
+ <div slot="actions">
105
+ <uui-button label="Cancel" @click=${() => this.close()}></uui-button>
106
+ <uui-button type="submit" form="setup" look="primary" color="positive" label="Turn on" .state=${this._busy ? 'waiting' : undefined}
107
+ @click=${() => this.renderRoot.querySelector('#setup')?.requestSubmit()}></uui-button>
108
+ </div>`
109
+ }
110
+
111
+ #renderCodes() {
112
+ return html`<p>Two-factor sign-in is on. Keep these recovery codes somewhere safe: each one
113
+ signs you in once if you lose your phone. They will not be shown again.</p>
114
+ <ul class="codes">${this._codes.map((code) => html`<li><code>${code}</code></li>`)}</ul>
115
+ <div slot="actions">
116
+ <uui-button label="Copy codes" @click=${this.#copy}></uui-button>
117
+ <uui-button look="primary" label="Done" @click=${this.#done}></uui-button>
118
+ </div>`
119
+ }
120
+
121
+ static styles = css`
122
+ .qr {
123
+ display: block;
124
+ width: 200px;
125
+ height: 200px;
126
+ margin: var(--uui-size-space-4) auto;
127
+ background: #fff;
128
+ }
129
+ .secret code,
130
+ .codes code {
131
+ font-family: monospace;
132
+ word-break: break-all;
133
+ }
134
+ .codes {
135
+ display: grid;
136
+ grid-template-columns: repeat(2, 1fr);
137
+ gap: var(--uui-size-space-2);
138
+ padding: 0;
139
+ list-style: none;
140
+ }
141
+ .error {
142
+ color: var(--uui-color-danger);
143
+ }
144
+ `
145
+ }
146
+
147
+ customElements.define('crombolo-two-factor-setup', CromboloTwoFactorSetupElement)