@oneuptime/common 11.0.3 → 11.0.5

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 (228) hide show
  1. package/Models/DatabaseModels/GlobalConfig.ts +19 -0
  2. package/Models/DatabaseModels/GlobalOidc.ts +351 -0
  3. package/Models/DatabaseModels/GlobalOidcProject.ts +265 -0
  4. package/Models/DatabaseModels/GlobalSso.ts +312 -0
  5. package/Models/DatabaseModels/GlobalSsoProject.ts +268 -0
  6. package/Models/DatabaseModels/Index.ts +8 -0
  7. package/Models/DatabaseModels/Project.ts +31 -0
  8. package/Models/DatabaseModels/StatusPage.ts +82 -0
  9. package/Server/API/StatusPageAPI.ts +2 -0
  10. package/Server/Infrastructure/GlobalCache.ts +11 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/{1781587937032-MigrationName.ts → 1781750000000-MigrationName.ts} +2 -2
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1782000000000-AddGlobalSsoAndOidc.ts +176 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1782100000000-AddStatusPageImageAltText.ts +25 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1782200000000-AddRequireSsoForLoginToGlobalProviders.ts +25 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1782300000000-MoveRequireSsoForLoginToGlobalConfig.ts +38 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1782310000000-MigrationName.ts +299 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1782400000000-RemoveIsTestedFromGlobalSsoAndOidc.ts +21 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -2
  19. package/Server/Middleware/UserAuthorization.ts +113 -42
  20. package/Server/Services/CephClusterService.ts +22 -10
  21. package/Server/Services/CloudResourceService.ts +22 -10
  22. package/Server/Services/DockerHostService.ts +22 -10
  23. package/Server/Services/DockerSwarmClusterService.ts +22 -10
  24. package/Server/Services/GlobalConfigService.ts +50 -0
  25. package/Server/Services/GlobalOidcProjectService.ts +85 -0
  26. package/Server/Services/GlobalOidcService.ts +10 -0
  27. package/Server/Services/GlobalSsoProjectService.ts +85 -0
  28. package/Server/Services/GlobalSsoService.ts +10 -0
  29. package/Server/Services/HostService.ts +22 -10
  30. package/Server/Services/Index.ts +8 -0
  31. package/Server/Services/KubernetesClusterService.ts +22 -10
  32. package/Server/Services/PodmanHostService.ts +22 -10
  33. package/Server/Services/ProjectService.ts +44 -1
  34. package/Server/Services/ProxmoxClusterService.ts +22 -10
  35. package/Server/Services/RumApplicationService.ts +22 -10
  36. package/Server/Services/ServerlessFunctionService.ts +22 -10
  37. package/Server/Utils/Cookie.ts +39 -5
  38. package/Server/Utils/JsonWebToken.ts +7 -0
  39. package/Server/Utils/ValidateGlobalProviderProjectTeams.ts +119 -0
  40. package/Tests/Server/Infrastructure/GlobalCache.test.ts +43 -0
  41. package/Tests/Server/Middleware/UserAuthorization.test.ts +51 -13
  42. package/Tests/Server/Middleware/UserAuthorizationSSOProvider.test.ts +163 -0
  43. package/Tests/Server/Utils/CookieSSOToken.test.ts +130 -0
  44. package/Types/JsonWebTokenData.ts +3 -0
  45. package/Types/SSO/SsoProviderType.ts +8 -0
  46. package/UI/Components/Accordion/Accordion.tsx +5 -1
  47. package/UI/Components/CardSelect/CardSelect.tsx +6 -1
  48. package/UI/Components/CategoryCheckbox/Index.tsx +2 -1
  49. package/UI/Components/Charts/Area/AreaChart.tsx +4 -8
  50. package/UI/Components/Charts/Bar/BarChart.tsx +4 -8
  51. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +4 -8
  52. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +1 -1
  53. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +1 -1
  54. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +1 -1
  55. package/UI/Components/Charts/Line/LineChart.tsx +4 -8
  56. package/UI/Components/CodeEditor/CodeEditor.tsx +2 -0
  57. package/UI/Components/CollapsibleSection/CollapsibleSection.tsx +8 -1
  58. package/UI/Components/Dropdown/Dropdown.tsx +2 -0
  59. package/UI/Components/EditionLabel/EditionLabel.tsx +68 -16
  60. package/UI/Components/EntityDropdown/EntityDropdown.tsx +3 -0
  61. package/UI/Components/FilePicker/FilePicker.tsx +2 -0
  62. package/UI/Components/Forms/Fields/ColorPicker.tsx +2 -0
  63. package/UI/Components/Forms/Fields/FieldLabel.tsx +4 -0
  64. package/UI/Components/Forms/Fields/FormField.tsx +72 -15
  65. package/UI/Components/Forms/Fields/IconPicker.tsx +2 -0
  66. package/UI/Components/Forms/Validation.ts +107 -23
  67. package/UI/Components/Input/Input.tsx +4 -0
  68. package/UI/Components/Link/Link.tsx +23 -0
  69. package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
  70. package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +3 -0
  71. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +63 -2
  72. package/UI/Components/Modal/Modal.tsx +4 -1
  73. package/UI/Components/Radio/Radio.tsx +2 -0
  74. package/UI/Components/RadioButtons/GroupRadioButtons.tsx +6 -1
  75. package/UI/Components/Tabs/Tabs.tsx +63 -0
  76. package/UI/Components/TextArea/TextArea.tsx +2 -0
  77. package/UI/Components/TimePicker/TimePicker.tsx +2 -0
  78. package/UI/Components/Toggle/Toggle.tsx +2 -1
  79. package/UI/Components/Tooltip/Tooltip.tsx +6 -1
  80. package/build/dist/Models/DatabaseModels/GlobalConfig.js +20 -0
  81. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  82. package/build/dist/Models/DatabaseModels/GlobalOidc.js +379 -0
  83. package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -0
  84. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js +276 -0
  85. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js.map +1 -0
  86. package/build/dist/Models/DatabaseModels/GlobalSso.js +341 -0
  87. package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -0
  88. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js +279 -0
  89. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js.map +1 -0
  90. package/build/dist/Models/DatabaseModels/Index.js +8 -0
  91. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  92. package/build/dist/Models/DatabaseModels/Project.js +32 -0
  93. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/StatusPage.js +84 -0
  95. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  96. package/build/dist/Server/API/StatusPageAPI.js +2 -0
  97. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  98. package/build/dist/Server/Infrastructure/GlobalCache.js +13 -0
  99. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  100. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/{1781587937032-MigrationName.js → 1781750000000-MigrationName.js} +3 -3
  101. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/{1781587937032-MigrationName.js.map → 1781750000000-MigrationName.js.map} +1 -1
  102. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782000000000-AddGlobalSsoAndOidc.js +73 -0
  103. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782000000000-AddGlobalSsoAndOidc.js.map +1 -0
  104. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782100000000-AddStatusPageImageAltText.js +14 -0
  105. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782100000000-AddStatusPageImageAltText.js.map +1 -0
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782200000000-AddRequireSsoForLoginToGlobalProviders.js +14 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782200000000-AddRequireSsoForLoginToGlobalProviders.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782300000000-MoveRequireSsoForLoginToGlobalConfig.js +23 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782300000000-MoveRequireSsoForLoginToGlobalConfig.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782310000000-MigrationName.js +106 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782310000000-MigrationName.js.map +1 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782400000000-RemoveIsTestedFromGlobalSsoAndOidc.js +14 -0
  113. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782400000000-RemoveIsTestedFromGlobalSsoAndOidc.js.map +1 -0
  114. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -2
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  116. package/build/dist/Server/Middleware/UserAuthorization.js +77 -34
  117. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  118. package/build/dist/Server/Services/CephClusterService.js +19 -2
  119. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  120. package/build/dist/Server/Services/CloudResourceService.js +19 -2
  121. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  122. package/build/dist/Server/Services/DockerHostService.js +19 -2
  123. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  124. package/build/dist/Server/Services/DockerSwarmClusterService.js +19 -2
  125. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  126. package/build/dist/Server/Services/GlobalConfigService.js +55 -0
  127. package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
  128. package/build/dist/Server/Services/GlobalOidcProjectService.js +80 -0
  129. package/build/dist/Server/Services/GlobalOidcProjectService.js.map +1 -0
  130. package/build/dist/Server/Services/GlobalOidcService.js +9 -0
  131. package/build/dist/Server/Services/GlobalOidcService.js.map +1 -0
  132. package/build/dist/Server/Services/GlobalSsoProjectService.js +80 -0
  133. package/build/dist/Server/Services/GlobalSsoProjectService.js.map +1 -0
  134. package/build/dist/Server/Services/GlobalSsoService.js +9 -0
  135. package/build/dist/Server/Services/GlobalSsoService.js.map +1 -0
  136. package/build/dist/Server/Services/HostService.js +19 -2
  137. package/build/dist/Server/Services/HostService.js.map +1 -1
  138. package/build/dist/Server/Services/Index.js +8 -0
  139. package/build/dist/Server/Services/Index.js.map +1 -1
  140. package/build/dist/Server/Services/KubernetesClusterService.js +19 -2
  141. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  142. package/build/dist/Server/Services/PodmanHostService.js +19 -2
  143. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  144. package/build/dist/Server/Services/ProjectService.js +36 -1
  145. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  146. package/build/dist/Server/Services/ProxmoxClusterService.js +19 -2
  147. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  148. package/build/dist/Server/Services/RumApplicationService.js +19 -2
  149. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  150. package/build/dist/Server/Services/ServerlessFunctionService.js +19 -2
  151. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  152. package/build/dist/Server/Utils/Cookie.js +32 -3
  153. package/build/dist/Server/Utils/Cookie.js.map +1 -1
  154. package/build/dist/Server/Utils/JsonWebToken.js +6 -0
  155. package/build/dist/Server/Utils/JsonWebToken.js.map +1 -1
  156. package/build/dist/Server/Utils/ValidateGlobalProviderProjectTeams.js +66 -0
  157. package/build/dist/Server/Utils/ValidateGlobalProviderProjectTeams.js.map +1 -0
  158. package/build/dist/Types/SSO/SsoProviderType.js +9 -0
  159. package/build/dist/Types/SSO/SsoProviderType.js.map +1 -0
  160. package/build/dist/UI/Components/Accordion/Accordion.js +5 -3
  161. package/build/dist/UI/Components/Accordion/Accordion.js.map +1 -1
  162. package/build/dist/UI/Components/CardSelect/CardSelect.js +1 -1
  163. package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
  164. package/build/dist/UI/Components/CategoryCheckbox/Index.js +1 -1
  165. package/build/dist/UI/Components/CategoryCheckbox/Index.js.map +1 -1
  166. package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -6
  167. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  168. package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -6
  169. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  170. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +8 -8
  171. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  172. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +1 -1
  173. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  174. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +1 -1
  175. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  176. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +1 -1
  177. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  178. package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -6
  179. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  180. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +1 -1
  181. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  182. package/build/dist/UI/Components/CollapsibleSection/CollapsibleSection.js +4 -2
  183. package/build/dist/UI/Components/CollapsibleSection/CollapsibleSection.js.map +1 -1
  184. package/build/dist/UI/Components/Dropdown/Dropdown.js +1 -1
  185. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  186. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +31 -5
  187. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  188. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +2 -2
  189. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  190. package/build/dist/UI/Components/FilePicker/FilePicker.js +1 -1
  191. package/build/dist/UI/Components/FilePicker/FilePicker.js.map +1 -1
  192. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +1 -1
  193. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
  194. package/build/dist/UI/Components/Forms/Fields/FieldLabel.js +1 -1
  195. package/build/dist/UI/Components/Forms/Fields/FieldLabel.js.map +1 -1
  196. package/build/dist/UI/Components/Forms/Fields/FormField.js +58 -22
  197. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  198. package/build/dist/UI/Components/Forms/Fields/IconPicker.js +1 -1
  199. package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
  200. package/build/dist/UI/Components/Forms/Validation.js +64 -15
  201. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  202. package/build/dist/UI/Components/Input/Input.js +1 -1
  203. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  204. package/build/dist/UI/Components/Link/Link.js +22 -1
  205. package/build/dist/UI/Components/Link/Link.js.map +1 -1
  206. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
  207. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
  208. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +2 -2
  209. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
  210. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +46 -2
  211. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  212. package/build/dist/UI/Components/Modal/Modal.js +1 -1
  213. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  214. package/build/dist/UI/Components/Radio/Radio.js +1 -1
  215. package/build/dist/UI/Components/Radio/Radio.js.map +1 -1
  216. package/build/dist/UI/Components/RadioButtons/GroupRadioButtons.js +1 -1
  217. package/build/dist/UI/Components/RadioButtons/GroupRadioButtons.js.map +1 -1
  218. package/build/dist/UI/Components/Tabs/Tabs.js +50 -1
  219. package/build/dist/UI/Components/Tabs/Tabs.js.map +1 -1
  220. package/build/dist/UI/Components/TextArea/TextArea.js +1 -1
  221. package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
  222. package/build/dist/UI/Components/TimePicker/TimePicker.js +1 -1
  223. package/build/dist/UI/Components/TimePicker/TimePicker.js.map +1 -1
  224. package/build/dist/UI/Components/Toggle/Toggle.js +1 -1
  225. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  226. package/build/dist/UI/Components/Tooltip/Tooltip.js +6 -1
  227. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  228. package/package.json +1 -1
@@ -0,0 +1,130 @@
1
+ import CookieUtil from "../../../Server/Utils/Cookie";
2
+ import JSONWebToken from "../../../Server/Utils/JsonWebToken";
3
+ import { JSONObject } from "../../../Types/JSON";
4
+ import JSONWebTokenData from "../../../Types/JsonWebTokenData";
5
+ import Email from "../../../Types/Email";
6
+ import Name from "../../../Types/Name";
7
+ import ObjectID from "../../../Types/ObjectID";
8
+ import SsoProviderType from "../../../Types/SSO/SsoProviderType";
9
+ import User from "../../../Models/DatabaseModels/User";
10
+ import { describe, expect, test } from "@jest/globals";
11
+
12
+ /*
13
+ * Tests for the SSO-provider discriminator that CookieUtil.getSSOToken stamps
14
+ * onto a per-project SSO token. These run real JWT signing (no mocks): the JWT
15
+ * secret falls back to EncryptionSecret = "secret" when ENCRYPTION_SECRET is
16
+ * unset (see Common/Server/EnvironmentConfig.ts), so no env setup is required.
17
+ */
18
+ describe("CookieUtil.getSSOToken - SSO provider discriminator", () => {
19
+ const buildUser: () => User = (): User => {
20
+ const user: User = new User();
21
+ user.id = ObjectID.generate();
22
+ user.name = new Name("Test User");
23
+ user.email = new Email("test@oneuptime.com");
24
+ return user;
25
+ };
26
+
27
+ test("token carries userId and projectId (round-trips through JSONWebToken.decode)", () => {
28
+ const user: User = buildUser();
29
+ const projectId: ObjectID = ObjectID.generate();
30
+
31
+ const token: string = CookieUtil.getSSOToken({
32
+ user,
33
+ projectId,
34
+ });
35
+
36
+ expect(typeof token).toBe("string");
37
+ expect(token.length).toBeGreaterThan(0);
38
+
39
+ const decoded: JSONWebTokenData = JSONWebToken.decode(token);
40
+
41
+ expect(decoded.userId?.toString()).toBe(user.id!.toString());
42
+ expect(decoded.projectId?.toString()).toBe(projectId.toString());
43
+ });
44
+
45
+ test("token carries the ssoProviderId + ssoProviderType discriminator when provided", () => {
46
+ const user: User = buildUser();
47
+ const projectId: ObjectID = ObjectID.generate();
48
+ const ssoProviderId: ObjectID = ObjectID.generate();
49
+
50
+ const token: string = CookieUtil.getSSOToken({
51
+ user,
52
+ projectId,
53
+ ssoProviderId,
54
+ ssoProviderType: SsoProviderType.GlobalSSO,
55
+ });
56
+
57
+ /*
58
+ * The discriminator lives on the raw JWT payload. JSONWebToken.decode()
59
+ * intentionally re-shapes the payload into JSONWebTokenData and does not
60
+ * surface ssoProviderId / ssoProviderType, so we assert the round-trip on
61
+ * the raw payload (decodeJsonPayload), which is the actual transport layer.
62
+ */
63
+ const payload: JSONObject = JSONWebToken.decodeJsonPayload(token);
64
+
65
+ expect(payload["userId"]).toBe(user.id!.toString());
66
+ expect(payload["projectId"]).toBe(projectId.toString());
67
+ expect(payload["ssoProviderId"]).toBe(ssoProviderId.toString());
68
+ expect(payload["ssoProviderType"]).toBe(SsoProviderType.GlobalSSO);
69
+ });
70
+
71
+ test("each SsoProviderType enum value round-trips on the token", () => {
72
+ const user: User = buildUser();
73
+ const projectId: ObjectID = ObjectID.generate();
74
+ const ssoProviderId: ObjectID = ObjectID.generate();
75
+
76
+ const providerTypes: Array<SsoProviderType> = [
77
+ SsoProviderType.ProjectSSO,
78
+ SsoProviderType.ProjectOIDC,
79
+ SsoProviderType.GlobalSSO,
80
+ SsoProviderType.GlobalOIDC,
81
+ ];
82
+
83
+ for (const providerType of providerTypes) {
84
+ const token: string = CookieUtil.getSSOToken({
85
+ user,
86
+ projectId,
87
+ ssoProviderId,
88
+ ssoProviderType: providerType,
89
+ });
90
+
91
+ const payload: JSONObject = JSONWebToken.decodeJsonPayload(token);
92
+
93
+ expect(payload["ssoProviderId"]).toBe(ssoProviderId.toString());
94
+ expect(payload["ssoProviderType"]).toBe(providerType);
95
+ }
96
+ });
97
+
98
+ test("ssoProviderId / ssoProviderType are absent (undefined) when not provided (legacy token shape)", () => {
99
+ const user: User = buildUser();
100
+ const projectId: ObjectID = ObjectID.generate();
101
+
102
+ const token: string = CookieUtil.getSSOToken({
103
+ user,
104
+ projectId,
105
+ });
106
+
107
+ const payload: JSONObject = JSONWebToken.decodeJsonPayload(token);
108
+
109
+ // No discriminator was provided, so neither field should carry a value.
110
+ expect(payload["ssoProviderId"]).toBeUndefined();
111
+ expect(payload["ssoProviderType"]).toBeUndefined();
112
+ });
113
+
114
+ test("ssoProviderId without ssoProviderType still records the provider id", () => {
115
+ const user: User = buildUser();
116
+ const projectId: ObjectID = ObjectID.generate();
117
+ const ssoProviderId: ObjectID = ObjectID.generate();
118
+
119
+ const token: string = CookieUtil.getSSOToken({
120
+ user,
121
+ projectId,
122
+ ssoProviderId,
123
+ });
124
+
125
+ const payload: JSONObject = JSONWebToken.decodeJsonPayload(token);
126
+
127
+ expect(payload["ssoProviderId"]).toBe(ssoProviderId.toString());
128
+ expect(payload["ssoProviderType"]).toBeUndefined();
129
+ });
130
+ });
@@ -2,6 +2,7 @@ import Email from "./Email";
2
2
  import { JSONObject } from "./JSON";
3
3
  import Name from "./Name";
4
4
  import ObjectID from "./ObjectID";
5
+ import SsoProviderType from "./SSO/SsoProviderType";
5
6
 
6
7
  export default interface JSONWebTokenData extends JSONObject {
7
8
  userId: ObjectID;
@@ -12,4 +13,6 @@ export default interface JSONWebTokenData extends JSONObject {
12
13
  projectId?: ObjectID | undefined; // for SSO logins.
13
14
  isGlobalLogin: boolean; // If this is OneUptime username and password login. This is true, if this is SSO login. Then, this is false.
14
15
  sessionId?: ObjectID | undefined;
16
+ ssoProviderId?: ObjectID | undefined; // which SSO provider (Project or Global SSO/OIDC) issued this per-project SSO token.
17
+ ssoProviderType?: SsoProviderType | undefined;
15
18
  }
@@ -0,0 +1,8 @@
1
+ enum SsoProviderType {
2
+ ProjectSSO = "ProjectSSO",
3
+ ProjectOIDC = "ProjectOIDC",
4
+ GlobalSSO = "GlobalSSO",
5
+ GlobalOIDC = "GlobalOIDC",
6
+ }
7
+
8
+ export default SsoProviderType;
@@ -60,7 +60,9 @@ const Accordion: FunctionComponent<ComponentProps> = (
60
60
  className = "-ml-5 -mr-5 p-5 mt-1";
61
61
  }
62
62
 
63
- const accordionId: string = `accordion-content-${React.useId()}`;
63
+ const generatedId: string = React.useId();
64
+ const accordionId: string = `accordion-content-${generatedId}`;
65
+ const accordionTitleId: string = `accordion-title-${generatedId}`;
64
66
 
65
67
  const handleKeyDown: (event: React.KeyboardEvent) => void = (
66
68
  event: React.KeyboardEvent,
@@ -84,6 +86,7 @@ const Accordion: FunctionComponent<ComponentProps> = (
84
86
  tabIndex={0}
85
87
  aria-expanded={isOpen}
86
88
  aria-controls={accordionId}
89
+ aria-labelledby={props.title ? accordionTitleId : undefined}
87
90
  onClick={() => {
88
91
  setIsOpen(!isOpen);
89
92
  }}
@@ -121,6 +124,7 @@ const Accordion: FunctionComponent<ComponentProps> = (
121
124
  }`}
122
125
  >
123
126
  <div
127
+ id={accordionTitleId}
124
128
  className={`text-gray-900 leading-snug ${props.titleClassName || ""}`}
125
129
  >
126
130
  {props.title}
@@ -30,6 +30,7 @@ export interface ComponentProps {
30
30
  error?: string | undefined;
31
31
  tabIndex?: number | undefined;
32
32
  dataTestId?: string | undefined;
33
+ ariaLabelledby?: string | undefined;
33
34
  // Force single-column (1 item per row). Default: responsive 1/2/3 grid.
34
35
  singleColumn?: boolean | undefined;
35
36
  }
@@ -67,7 +68,11 @@ const CardSelect: FunctionComponent<ComponentProps> = (
67
68
 
68
69
  return (
69
70
  <div data-testid={props.dataTestId}>
70
- <div role="radiogroup" aria-label="Select an option">
71
+ <div
72
+ role="radiogroup"
73
+ aria-label="Select an option"
74
+ aria-labelledby={props.ariaLabelledby}
75
+ >
71
76
  {groups.map((group: RenderGroup, groupIndex: number) => {
72
77
  return (
73
78
  <div key={groupIndex} className={groupIndex > 0 ? "mt-8" : ""}>
@@ -19,6 +19,7 @@ export interface CategoryCheckboxProps
19
19
  initialValue?: undefined | Array<CategoryCheckboxValue | BaseModel>;
20
20
  error?: string | undefined;
21
21
  dataTestId?: string | undefined;
22
+ ariaLabelledby?: string | undefined;
22
23
  }
23
24
 
24
25
  const CategoryCheckbox: FunctionComponent<CategoryCheckboxProps> = (
@@ -152,7 +153,7 @@ const CategoryCheckbox: FunctionComponent<CategoryCheckboxProps> = (
152
153
  };
153
154
 
154
155
  return (
155
- <div>
156
+ <div role="group" aria-labelledby={props.ariaLabelledby}>
156
157
  {getCategory(undefined, categories.length === 0)}
157
158
  {categories.map((category: CheckboxCategory, i: number) => {
158
159
  return (
@@ -108,11 +108,6 @@ const AreaChartElement: FunctionComponent<AreaInternalProps> = (
108
108
  });
109
109
  }, [props.exemplarPoints, props.xAxis]);
110
110
 
111
- const className: string = props.heightInPx ? `` : "h-80";
112
- const style: React.CSSProperties = props.heightInPx
113
- ? { height: `${props.heightInPx}px` }
114
- : {};
115
-
116
111
  const hasNoData: boolean =
117
112
  !props.data ||
118
113
  props.data.length === 0 ||
@@ -134,10 +129,11 @@ const AreaChartElement: FunctionComponent<AreaInternalProps> = (
134
129
  typeof yAxisMaxOption === "number" ? { maxValue: yAxisMaxOption } : {};
135
130
 
136
131
  return (
137
- <div className="relative">
132
+ <div
133
+ className="relative flex flex-1"
134
+ style={props.heightInPx ? { height: `${props.heightInPx}px` } : undefined}
135
+ >
138
136
  <AreaChart
139
- className={className}
140
- style={style}
141
137
  data={records}
142
138
  tickGap={30}
143
139
  index={"Time"}
@@ -54,11 +54,6 @@ const BarChartElement: FunctionComponent<BarInternalProps> = (
54
54
  setRecords(records);
55
55
  }, [props.data]);
56
56
 
57
- const className: string = props.heightInPx ? `` : "h-80";
58
- const style: React.CSSProperties = props.heightInPx
59
- ? { height: `${props.heightInPx}px` }
60
- : {};
61
-
62
57
  const hasNoData: boolean =
63
58
  !props.data ||
64
59
  props.data.length === 0 ||
@@ -67,10 +62,11 @@ const BarChartElement: FunctionComponent<BarInternalProps> = (
67
62
  });
68
63
 
69
64
  return (
70
- <div className="relative">
65
+ <div
66
+ className="relative flex flex-1"
67
+ style={props.heightInPx ? { height: `${props.heightInPx}px` } : undefined}
68
+ >
71
69
  <BarChart
72
- className={className}
73
- style={style}
74
70
  data={records}
75
71
  tickGap={30}
76
72
  index={"Time"}
@@ -49,7 +49,6 @@ export interface Chart {
49
49
  export interface ComponentProps {
50
50
  charts: Array<Chart>;
51
51
  hideCard?: boolean | undefined;
52
- heightInPx?: number | undefined;
53
52
  chartCssClass?: string | undefined;
54
53
  }
55
54
 
@@ -80,7 +79,6 @@ const ChartGroup: FunctionComponent<ComponentProps> = (
80
79
  key={index}
81
80
  {...(chart.props as LineChartProps)}
82
81
  syncid={syncId}
83
- heightInPx={props.heightInPx}
84
82
  exemplarPoints={chart.exemplarPoints}
85
83
  onExemplarClick={chart.onExemplarClick}
86
84
  showLegend={showLegend}
@@ -92,7 +90,6 @@ const ChartGroup: FunctionComponent<ComponentProps> = (
92
90
  key={index}
93
91
  {...(chart.props as BarChartProps)}
94
92
  syncid={syncId}
95
- heightInPx={props.heightInPx}
96
93
  showLegend={showLegend}
97
94
  />
98
95
  );
@@ -102,7 +99,6 @@ const ChartGroup: FunctionComponent<ComponentProps> = (
102
99
  key={index}
103
100
  {...(chart.props as AreaChartProps)}
104
101
  syncid={syncId}
105
- heightInPx={props.heightInPx}
106
102
  exemplarPoints={chart.exemplarPoints}
107
103
  onExemplarClick={chart.onExemplarClick}
108
104
  showLegend={showLegend}
@@ -244,14 +240,14 @@ const ChartGroup: FunctionComponent<ComponentProps> = (
244
240
  return (
245
241
  <>
246
242
  {renderMetricInfoModal()}
247
- <div className="space-y-3">
243
+ <div className="space-y-3 flex-col flex flex-1 w-full">
248
244
  {props.charts.map((chart: Chart, index: number) => {
249
245
  return (
250
246
  <div
251
247
  key={index}
252
- className={`bg-white ${props.chartCssClass || ""}`}
248
+ className={`bg-white ${props.chartCssClass || ""} flex-1 flex-col flex w-full`}
253
249
  >
254
- <div className="px-5 pt-4 pb-4">
250
+ <div className="px-5 pt-4 pb-4 flex flex-col flex-1">
255
251
  <div className="mb-3 pb-3 border-b border-gray-100">
256
252
  <div className="flex items-center">
257
253
  <h3 className="text-sm font-semibold text-gray-800 tracking-tight">
@@ -265,10 +261,10 @@ const ChartGroup: FunctionComponent<ComponentProps> = (
265
261
  </p>
266
262
  )}
267
263
  </div>
264
+ {getChartContent(chart, index)}
268
265
  {chart.seriesControls ? (
269
266
  <div className="mb-3">{chart.seriesControls}</div>
270
267
  ) : null}
271
- {getChartContent(chart, index)}
272
268
  </div>
273
269
  </div>
274
270
  );
@@ -689,7 +689,7 @@ const AreaChart: React.ForwardRefExoticComponent<
689
689
  }
690
690
 
691
691
  return (
692
- <div ref={ref} className={cx("h-80 w-full", className)} {...other}>
692
+ <div ref={ref} className={cx("flex-1 w-full", className)} {...other}>
693
693
  <ResponsiveContainer>
694
694
  <RechartsAreaChart
695
695
  data={data}
@@ -780,7 +780,7 @@ const BarChart: React.ForwardRefExoticComponent<
780
780
  return (
781
781
  <div
782
782
  ref={forwardedRef}
783
- className={cx("h-80 w-full", className)}
783
+ className={cx("flex-1 w-full", className)}
784
784
  data-tremor-id="tremor-raw"
785
785
  {...other}
786
786
  >
@@ -695,7 +695,7 @@ const LineChart: React.ForwardRefExoticComponent<
695
695
  }
696
696
 
697
697
  return (
698
- <div ref={ref} className={cx("h-80 w-full", className)} {...other}>
698
+ <div ref={ref} className={cx("flex-1 w-full", className)} {...other}>
699
699
  <ResponsiveContainer>
700
700
  <RechartsLineChart
701
701
  data={data}
@@ -86,11 +86,6 @@ const LineChartElement: FunctionComponent<LineInternalProps> = (
86
86
  });
87
87
  }, [props.exemplarPoints, props.xAxis]);
88
88
 
89
- const className: string = props.heightInPx ? `` : "h-80";
90
- const style: React.CSSProperties = props.heightInPx
91
- ? { height: `${props.heightInPx}px` }
92
- : {};
93
-
94
89
  const hasNoData: boolean =
95
90
  !props.data ||
96
91
  props.data.length === 0 ||
@@ -114,10 +109,11 @@ const LineChartElement: FunctionComponent<LineInternalProps> = (
114
109
  typeof yAxisMaxOption === "number" ? { maxValue: yAxisMaxOption } : {};
115
110
 
116
111
  return (
117
- <div className="relative">
112
+ <div
113
+ className="relative flex flex-1"
114
+ style={props.heightInPx ? { height: `${props.heightInPx}px` } : undefined}
115
+ >
118
116
  <LineChart
119
- className={className}
120
- style={style}
121
117
  data={records}
122
118
  tickGap={30}
123
119
  index={"Time"}
@@ -25,6 +25,7 @@ export interface ComponentProps {
25
25
  value?: string | undefined;
26
26
  showLineNumbers?: boolean | undefined;
27
27
  disableSpellCheck?: boolean | undefined;
28
+ ariaLabelledby?: string | undefined;
28
29
  }
29
30
 
30
31
  const CodeEditor: FunctionComponent<ComponentProps> = (
@@ -119,6 +120,7 @@ const CodeEditor: FunctionComponent<ComponentProps> = (
119
120
  return (
120
121
  <div
121
122
  data-testid={props.dataTestId}
123
+ aria-labelledby={props.ariaLabelledby}
122
124
  onClick={() => {
123
125
  if (props.onClick) {
124
126
  props.onClick();
@@ -46,6 +46,9 @@ const CollapsibleSection: FunctionComponent<ComponentProps> = (
46
46
 
47
47
  const variant: CollapsibleSectionVariant = props.variant || "default";
48
48
 
49
+ // Associate the role="button" header with its visible title text (WCAG 4.1.2).
50
+ const collapsibleTitleId: string = `collapsible-title-${React.useId()}`;
51
+
49
52
  const getContainerClassName: () => string = (): string => {
50
53
  const baseClassName: string = props.className || "";
51
54
 
@@ -97,6 +100,7 @@ const CollapsibleSection: FunctionComponent<ComponentProps> = (
97
100
  }
98
101
  }}
99
102
  aria-expanded={!isCollapsed}
103
+ aria-labelledby={collapsibleTitleId}
100
104
  >
101
105
  <div className="flex items-center flex-1 min-w-0">
102
106
  <Icon
@@ -105,7 +109,10 @@ const CollapsibleSection: FunctionComponent<ComponentProps> = (
105
109
  />
106
110
  <div className="flex-1 min-w-0">
107
111
  <div className="flex items-center">
108
- <span className="text-sm font-medium text-gray-900 truncate">
112
+ <span
113
+ id={collapsibleTitleId}
114
+ className="text-sm font-medium text-gray-900 truncate"
115
+ >
109
116
  {props.title}
110
117
  </span>
111
118
  {isCollapsed && props.badge && (
@@ -59,6 +59,7 @@ export interface ComponentProps {
59
59
  id?: string | undefined;
60
60
  dataTestId?: string | undefined;
61
61
  ariaLabel?: string | undefined;
62
+ ariaLabelledby?: string | undefined;
62
63
  }
63
64
 
64
65
  const Dropdown: FunctionComponent<ComponentProps> = (
@@ -537,6 +538,7 @@ const Dropdown: FunctionComponent<ComponentProps> = (
537
538
  props.onFocus?.();
538
539
  }}
539
540
  aria-label={props.ariaLabel}
541
+ aria-labelledby={props.ariaLabelledby}
540
542
  aria-invalid={props.error ? true : undefined}
541
543
  aria-describedby={props.error ? errorId : undefined}
542
544
  classNames={{
@@ -45,6 +45,7 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
45
45
  const [validationError, setValidationError] = useState<string>("");
46
46
  const [successMessage, setSuccessMessage] = useState<string>("");
47
47
  const [isValidating, setIsValidating] = useState<boolean>(false);
48
+ const [isChangingLicense, setIsChangingLicense] = useState<boolean>(false);
48
49
  const licenseInputEditedRef: React.MutableRefObject<boolean> =
49
50
  useRef<boolean>(false);
50
51
 
@@ -318,6 +319,22 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
318
319
  setIsDialogOpen(false);
319
320
  setValidationError("");
320
321
  setSuccessMessage("");
322
+ setIsChangingLicense(false);
323
+ };
324
+
325
+ const handleStartChangingLicense: () => void = () => {
326
+ setIsChangingLicense(true);
327
+ setValidationError("");
328
+ setSuccessMessage("");
329
+ setLicenseKeyInput("");
330
+ licenseInputEditedRef.current = true;
331
+ };
332
+
333
+ const handleCancelChangingLicense: () => void = () => {
334
+ setIsChangingLicense(false);
335
+ setValidationError("");
336
+ licenseInputEditedRef.current = false;
337
+ setLicenseKeyInput(globalConfig?.enterpriseLicenseKey || "");
321
338
  };
322
339
 
323
340
  const handlePrimaryAction: () => void = () => {
@@ -371,6 +388,7 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
371
388
  licenseInputEditedRef.current = false;
372
389
  setLicenseKeyInput((payload["licenseKey"] as string) || trimmedKey);
373
390
  setSuccessMessage("License validated successfully.");
391
+ setIsChangingLicense(false);
374
392
 
375
393
  await fetchGlobalConfig();
376
394
  } catch (err) {
@@ -396,8 +414,10 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
396
414
  }
397
415
  };
398
416
 
399
- const shouldShowEnterpriseValidationButton: boolean =
400
- IS_ENTERPRISE_EDITION && !licenseValid;
417
+ const showLicenseKeyInput: boolean =
418
+ IS_ENTERPRISE_EDITION && (!licenseValid || isChangingLicense);
419
+
420
+ const shouldShowEnterpriseValidationButton: boolean = showLicenseKeyInput;
401
421
 
402
422
  const modalSubmitButtonText: string | undefined = IS_ENTERPRISE_EDITION
403
423
  ? shouldShowEnterpriseValidationButton
@@ -611,6 +631,20 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
611
631
  </div>
612
632
  )}
613
633
 
634
+ {!configError &&
635
+ !isConfigLoading &&
636
+ licenseValid &&
637
+ !isChangingLicense && (
638
+ <div className="-ml-3">
639
+ <Button
640
+ title="Change license key"
641
+ icon={IconProp.Edit}
642
+ buttonStyle={ButtonStyleType.NORMAL}
643
+ onClick={handleStartChangingLicense}
644
+ />
645
+ </div>
646
+ )}
647
+
614
648
  {!configError &&
615
649
  !isConfigLoading &&
616
650
  !licenseValid &&
@@ -632,11 +666,13 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
632
666
  <Alert type={AlertType.SUCCESS} title={successMessage} />
633
667
  )}
634
668
 
635
- {!licenseValid && (
669
+ {showLicenseKeyInput && (
636
670
  <>
637
671
  <div>
638
672
  <label className="text-sm font-medium text-gray-700">
639
- License Key
673
+ {isChangingLicense
674
+ ? "New License Key"
675
+ : "License Key"}
640
676
  </label>
641
677
  <Input
642
678
  value={licenseKeyInput}
@@ -656,18 +692,34 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
656
692
  />
657
693
  )}
658
694
 
659
- <p className="text-xs text-gray-500">
660
- You have installed Enterprise Edition of OneUptime.
661
- You need to validate your license key. Need a license
662
- key? Contact our sales team at{" "}
663
- <a
664
- href="mailto:sales@oneuptime.com"
665
- className="font-medium text-indigo-600 hover:text-indigo-700"
666
- >
667
- sales@oneuptime.com
668
- </a>
669
- .
670
- </p>
695
+ {isChangingLicense ? (
696
+ <div className="flex items-center justify-between gap-3">
697
+ <p className="text-xs text-gray-500">
698
+ Enter the new enterprise license key and validate
699
+ it to replace the current one. Your existing
700
+ license stays active until the new key is
701
+ validated.
702
+ </p>
703
+ <Button
704
+ title="Cancel"
705
+ buttonStyle={ButtonStyleType.NORMAL}
706
+ onClick={handleCancelChangingLicense}
707
+ />
708
+ </div>
709
+ ) : (
710
+ <p className="text-xs text-gray-500">
711
+ You have installed Enterprise Edition of OneUptime.
712
+ You need to validate your license key. Need a
713
+ license key? Contact our sales team at{" "}
714
+ <a
715
+ href="mailto:sales@oneuptime.com"
716
+ className="font-medium text-indigo-600 hover:text-indigo-700"
717
+ >
718
+ sales@oneuptime.com
719
+ </a>
720
+ .
721
+ </p>
722
+ )}
671
723
  </>
672
724
  )}
673
725
  </>
@@ -86,6 +86,7 @@ export interface EntityDropdownProps {
86
86
  id?: string | undefined;
87
87
  dataTestId?: string | undefined;
88
88
  ariaLabel?: string | undefined;
89
+ ariaLabelledby?: string | undefined;
89
90
  disabled?: boolean | undefined;
90
91
 
91
92
  /*
@@ -1115,6 +1116,7 @@ const EntityDropdown: FunctionComponent<EntityDropdownProps> = (
1115
1116
  <button
1116
1117
  type="button"
1117
1118
  disabled={props.disabled}
1119
+ aria-labelledby={props.ariaLabelledby}
1118
1120
  onClick={(): void => {
1119
1121
  if (props.disabled) {
1120
1122
  return;
@@ -1204,6 +1206,7 @@ const EntityDropdown: FunctionComponent<EntityDropdownProps> = (
1204
1206
  aria-autocomplete="list"
1205
1207
  aria-expanded={isOpen}
1206
1208
  aria-label={props.ariaLabel}
1209
+ aria-labelledby={props.ariaLabelledby}
1207
1210
  aria-invalid={props.error ? true : undefined}
1208
1211
  data-testid={props.dataTestId}
1209
1212
  role="combobox"
@@ -32,6 +32,7 @@ export interface ComponentProps {
32
32
  isMultiFilePicker?: boolean | undefined;
33
33
  tabIndex?: number | undefined;
34
34
  error?: string | undefined;
35
+ ariaLabelledby?: string | undefined;
35
36
  }
36
37
 
37
38
  type UploadStatus = {
@@ -449,6 +450,7 @@ const FilePicker: FunctionComponent<ComponentProps> = (
449
450
  id="file-upload"
450
451
  name="file-upload"
451
452
  type="file"
453
+ aria-labelledby={props.ariaLabelledby}
452
454
  className="sr-only"
453
455
  />
454
456
  </label>
@@ -24,6 +24,7 @@ export interface ComponentProps {
24
24
  dataTestId?: string | undefined;
25
25
  onEnterPress?: (() => void) | undefined;
26
26
  error?: string | undefined;
27
+ ariaLabelledby?: string | undefined;
27
28
  }
28
29
 
29
30
  const ColorPicker: FunctionComponent<ComponentProps> = (
@@ -83,6 +84,7 @@ const ColorPicker: FunctionComponent<ComponentProps> = (
83
84
  readOnly={true}
84
85
  type={InputType.TEXT}
85
86
  tabIndex={props.tabIndex}
87
+ ariaLabelledby={props.ariaLabelledby}
86
88
  onChange={(value: string) => {
87
89
  if (!value) {
88
90
  return handleChange("");
@@ -5,6 +5,8 @@ import React, { FunctionComponent, ReactElement } from "react";
5
5
 
6
6
  export interface ComponentProps {
7
7
  title: string;
8
+ id?: string | undefined;
9
+ htmlFor?: string | undefined;
8
10
  required?: boolean | undefined;
9
11
  sideLink?: FormFieldSideLink | undefined;
10
12
  description?: string | ReactElement | undefined;
@@ -26,6 +28,8 @@ const FieldLabelElement: FunctionComponent<ComponentProps> = (
26
28
  return (
27
29
  <>
28
30
  <label
31
+ id={props.id}
32
+ htmlFor={props.htmlFor}
29
33
  className={
30
34
  props.className ||
31
35
  `block ${