@things-factory/auth-ui 10.1.41 → 10.1.46

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 (43) hide show
  1. package/dist-client/components/abstract-password-reset.d.ts +6 -0
  2. package/dist-client/components/abstract-password-reset.js +16 -1
  3. package/dist-client/components/abstract-password-reset.js.map +1 -1
  4. package/dist-client/components/accept-invitation.js +2 -2
  5. package/dist-client/components/accept-invitation.js.map +1 -1
  6. package/dist-client/components/auth-badge.d.ts +20 -0
  7. package/dist-client/components/auth-badge.js +50 -0
  8. package/dist-client/components/auth-badge.js.map +1 -0
  9. package/dist-client/components/auth-badge.test.js +37 -0
  10. package/dist-client/components/auth-badge.test.js.map +1 -0
  11. package/dist-client/components/create-domain-popup-theme.test.d.ts +1 -0
  12. package/dist-client/components/create-domain-popup-theme.test.js +16 -0
  13. package/dist-client/components/create-domain-popup-theme.test.js.map +1 -0
  14. package/dist-client/components/create-domain-popup.js +3 -1
  15. package/dist-client/components/create-domain-popup.js.map +1 -1
  16. package/dist-client/entries/auth/verify.d.ts +25 -0
  17. package/dist-client/entries/auth/verify.js +81 -0
  18. package/dist-client/entries/auth/verify.js.map +1 -0
  19. package/dist-client/pages/role/role-list-page.d.ts +8 -0
  20. package/dist-client/pages/role/role-list-page.js +73 -24
  21. package/dist-client/pages/role/role-list-page.js.map +1 -1
  22. package/dist-client/pages/role/role-template-gap.test.d.ts +1 -0
  23. package/dist-client/pages/role/role-template-gap.test.js +30 -0
  24. package/dist-client/pages/role/role-template-gap.test.js.map +1 -0
  25. package/dist-client/pages/user/user-list-page.d.ts +1 -0
  26. package/dist-client/pages/user/user-list-page.js +6 -21
  27. package/dist-client/pages/user/user-list-page.js.map +1 -1
  28. package/dist-client/tsconfig.tsbuildinfo +1 -1
  29. package/dist-client/utils/{invitation-token.d.ts → link-token.d.ts} +5 -4
  30. package/dist-client/utils/{invitation-token.js → link-token.js} +6 -5
  31. package/dist-client/utils/link-token.js.map +1 -0
  32. package/dist-client/utils/link-token.test.d.ts +1 -0
  33. package/dist-client/utils/{invitation-token.test.js → link-token.test.js} +17 -17
  34. package/dist-client/utils/link-token.test.js.map +1 -0
  35. package/package.json +5 -5
  36. package/translations/en.json +5 -0
  37. package/translations/ja.json +5 -0
  38. package/translations/ko.json +5 -0
  39. package/translations/ms.json +5 -0
  40. package/translations/zh.json +5 -0
  41. package/dist-client/utils/invitation-token.js.map +0 -1
  42. package/dist-client/utils/invitation-token.test.js.map +0 -1
  43. /package/dist-client/{utils/invitation-token.test.d.ts → components/auth-badge.test.d.ts} +0 -0
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Taking the invitation token out of the link, and out of the address bar. (Pure rule.)
2
+ * Taking the token out of a mailed link, and out of the address bar. (Pure rule.)
3
3
  *
4
4
  * ## Why the token is in the fragment
5
5
  *
6
6
  * A browser does not send what follows `#`, so a link shaped `…/accept-invitation#<token>`
7
- * reaches the server as `/accept-invitation` and nothing else. The token never appears in an
7
+ * reaches the server as `/accept-invitation` and nothing else — and the same for
8
+ * `/reset-password`, `/unlock-user` and `/verify`. The token never appears in an
8
9
  * access log, a proxy log, or a `Referer` sent onward to some other site.
9
10
  *
10
11
  * That leaves the places the browser itself keeps it: the address bar and the history entry.
@@ -19,10 +20,10 @@
19
20
  * anchor — to the server as a credential attempt. It answers empty instead, and the page shows
20
21
  * the box where the code from the mail can be pasted.
21
22
  */
22
- export type InvitationLink = {
23
+ export type LinkToken = {
23
24
  /** The token, or an empty string when the link carries none this rule will read. */
24
25
  token: string;
25
26
  /** The same URL without its fragment, for the caller to replace the address with. */
26
27
  clean: string;
27
28
  };
28
- export declare function readInvitationToken(href: string): InvitationLink;
29
+ export declare function readLinkToken(href: string): LinkToken;
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Taking the invitation token out of the link, and out of the address bar. (Pure rule.)
2
+ * Taking the token out of a mailed link, and out of the address bar. (Pure rule.)
3
3
  *
4
4
  * ## Why the token is in the fragment
5
5
  *
6
6
  * A browser does not send what follows `#`, so a link shaped `…/accept-invitation#<token>`
7
- * reaches the server as `/accept-invitation` and nothing else. The token never appears in an
7
+ * reaches the server as `/accept-invitation` and nothing else — and the same for
8
+ * `/reset-password`, `/unlock-user` and `/verify`. The token never appears in an
8
9
  * access log, a proxy log, or a `Referer` sent onward to some other site.
9
10
  *
10
11
  * That leaves the places the browser itself keeps it: the address bar and the history entry.
@@ -19,9 +20,9 @@
19
20
  * anchor — to the server as a credential attempt. It answers empty instead, and the page shows
20
21
  * the box where the code from the mail can be pasted.
21
22
  */
22
- /** 16 bytes as hex, which is what `makeInvitationToken` produces. */
23
+ /** 16 bytes as hex, which is what `makeInvitationToken` and `makeVerificationToken` produce. */
23
24
  const TOKEN = /^[0-9a-f]{32}$/;
24
- export function readInvitationToken(href) {
25
+ export function readLinkToken(href) {
25
26
  if (!href) {
26
27
  return { token: '', clean: '' };
27
28
  }
@@ -37,4 +38,4 @@ export function readInvitationToken(href) {
37
38
  const raw = decodeURIComponent(href.slice(hash + 1)).replace(/^invite=/, '');
38
39
  return { token: TOKEN.test(raw) ? raw : '', clean };
39
40
  }
40
- //# sourceMappingURL=invitation-token.js.map
41
+ //# sourceMappingURL=link-token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-token.js","sourceRoot":"","sources":["../../client/utils/link-token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,gGAAgG;AAChG,MAAM,KAAK,GAAG,gBAAgB,CAAA;AAS9B,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAA;IACjC,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAE9B,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;QACb,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;IACnC,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;IAEjC;;;OAGG;IACH,MAAM,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;IAE5E,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,CAAA;AACrD,CAAC","sourcesContent":["/**\n * Taking the token out of a mailed link, and out of the address bar. (Pure rule.)\n *\n * ## Why the token is in the fragment\n *\n * A browser does not send what follows `#`, so a link shaped `…/accept-invitation#<token>`\n * reaches the server as `/accept-invitation` and nothing else — and the same for\n * `/reset-password`, `/unlock-user` and `/verify`. The token never appears in an\n * access log, a proxy log, or a `Referer` sent onward to some other site.\n *\n * That leaves the places the browser itself keeps it: the address bar and the history entry.\n * Both are cleared by replacing the URL with the one this returns as `clean`, which is why the\n * two answers come back together — reading the token and removing it are one act, and a caller\n * that did the first without the second would leave it on screen.\n *\n * ## What it refuses to read\n *\n * A token is hex from `crypto.randomBytes(16)`. Anything else in the fragment is not one, and\n * taking it anyway would send whatever a link happened to carry — a tracking fragment, a page\n * anchor — to the server as a credential attempt. It answers empty instead, and the page shows\n * the box where the code from the mail can be pasted.\n */\n\n/** 16 bytes as hex, which is what `makeInvitationToken` and `makeVerificationToken` produce. */\nconst TOKEN = /^[0-9a-f]{32}$/\n\nexport type LinkToken = {\n /** The token, or an empty string when the link carries none this rule will read. */\n token: string\n /** The same URL without its fragment, for the caller to replace the address with. */\n clean: string\n}\n\nexport function readLinkToken(href: string): LinkToken {\n if (!href) {\n return { token: '', clean: '' }\n }\n\n const hash = href.indexOf('#')\n\n if (hash < 0) {\n return { token: '', clean: href }\n }\n\n const clean = href.slice(0, hash)\n\n /*\n * `#invite=<token>` as well as a bare `#<token>`. Both shapes turn up in mail clients that\n * rewrite links, and neither costs anything to accept.\n */\n const raw = decodeURIComponent(href.slice(hash + 1)).replace(/^invite=/, '')\n\n return { token: TOKEN.test(raw) ? raw : '', clean }\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- import { readInvitationToken } from './invitation-token';
1
+ import { readLinkToken } from './link-token';
2
2
  /**
3
3
  * Reading the invitation token out of the link, and handing back the link without it.
4
4
  *
@@ -10,51 +10,51 @@ const TOKEN = '0123456789abcdef0123456789abcdef';
10
10
  const PAGE = 'https://acme.example.com/accept-invitation';
11
11
  describe('a link with a token in it', () => {
12
12
  it('reads a bare fragment', () => {
13
- expect(readInvitationToken(`${PAGE}#${TOKEN}`)).toEqual({ token: TOKEN, clean: PAGE });
13
+ expect(readLinkToken(`${PAGE}#${TOKEN}`)).toEqual({ token: TOKEN, clean: PAGE });
14
14
  });
15
15
  it('reads the named form some mail clients produce', () => {
16
- expect(readInvitationToken(`${PAGE}#invite=${TOKEN}`)).toEqual({ token: TOKEN, clean: PAGE });
16
+ expect(readLinkToken(`${PAGE}#invite=${TOKEN}`)).toEqual({ token: TOKEN, clean: PAGE });
17
17
  });
18
18
  it('reads one that arrived percent-encoded', () => {
19
- expect(readInvitationToken(`${PAGE}#invite%3D${TOKEN}`).token).toEqual(TOKEN);
19
+ expect(readLinkToken(`${PAGE}#invite%3D${TOKEN}`).token).toEqual(TOKEN);
20
20
  });
21
21
  it('hands back the address without the fragment', () => {
22
22
  /* ⚠ What the caller replaces the address with. Without it the token stays on screen and in
23
23
  this history entry, which is the half of the hiding the fragment does not do by itself. */
24
- expect(readInvitationToken(`${PAGE}?x=1#${TOKEN}`).clean).toEqual(`${PAGE}?x=1`);
24
+ expect(readLinkToken(`${PAGE}?x=1#${TOKEN}`).clean).toEqual(`${PAGE}?x=1`);
25
25
  });
26
26
  });
27
27
  describe('a link with nothing usable in it', () => {
28
28
  it('has no fragment at all', () => {
29
- expect(readInvitationToken(PAGE)).toEqual({ token: '', clean: PAGE });
29
+ expect(readLinkToken(PAGE)).toEqual({ token: '', clean: PAGE });
30
30
  });
31
31
  it('has an empty fragment', () => {
32
- expect(readInvitationToken(`${PAGE}#`)).toEqual({ token: '', clean: PAGE });
32
+ expect(readLinkToken(`${PAGE}#`)).toEqual({ token: '', clean: PAGE });
33
33
  });
34
34
  it('⚠ carries something that is not a token', () => {
35
35
  /*
36
36
  * ⚠ A page anchor, a tracking fragment, anything a link happened to pick up. Sending it as a
37
37
  * credential attempt would be this page volunteering whatever it was given.
38
38
  */
39
- expect(readInvitationToken(`${PAGE}#section-2`).token).toEqual('');
40
- expect(readInvitationToken(`${PAGE}#utm_source=mail`).token).toEqual('');
41
- expect(readInvitationToken(`${PAGE}#<script>`).token).toEqual('');
39
+ expect(readLinkToken(`${PAGE}#section-2`).token).toEqual('');
40
+ expect(readLinkToken(`${PAGE}#utm_source=mail`).token).toEqual('');
41
+ expect(readLinkToken(`${PAGE}#<script>`).token).toEqual('');
42
42
  });
43
43
  it('carries something token-shaped but the wrong length', () => {
44
- expect(readInvitationToken(`${PAGE}#0123456789abcdef`).token).toEqual('');
45
- expect(readInvitationToken(`${PAGE}#${TOKEN}ff`).token).toEqual('');
44
+ expect(readLinkToken(`${PAGE}#0123456789abcdef`).token).toEqual('');
45
+ expect(readLinkToken(`${PAGE}#${TOKEN}ff`).token).toEqual('');
46
46
  });
47
47
  it('carries hex in the wrong case', () => {
48
48
  /* `makeInvitationToken` produces lower-case hex. Anything else did not come from us. */
49
- expect(readInvitationToken(`${PAGE}#${TOKEN.toUpperCase()}`).token).toEqual('');
49
+ expect(readLinkToken(`${PAGE}#${TOKEN.toUpperCase()}`).token).toEqual('');
50
50
  });
51
51
  it('still hands back a clean address to replace with', () => {
52
52
  /* Even a fragment that is not ours should not be left sitting in the bar for this page. */
53
- expect(readInvitationToken(`${PAGE}#section-2`).clean).toEqual(PAGE);
53
+ expect(readLinkToken(`${PAGE}#section-2`).clean).toEqual(PAGE);
54
54
  });
55
55
  it('is given nothing at all', () => {
56
- expect(readInvitationToken('')).toEqual({ token: '', clean: '' });
57
- expect(readInvitationToken(undefined)).toEqual({ token: '', clean: '' });
56
+ expect(readLinkToken('')).toEqual({ token: '', clean: '' });
57
+ expect(readLinkToken(undefined)).toEqual({ token: '', clean: '' });
58
58
  });
59
59
  });
60
- //# sourceMappingURL=invitation-token.test.js.map
60
+ //# sourceMappingURL=link-token.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-token.test.js","sourceRoot":"","sources":["../../client/utils/link-token.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { readLinkToken } from './link-token'\n\n/**\n * Reading the invitation token out of the link, and handing back the link without it.\n *\n * The two answers travel together because removing the token is not optional: the fragment keeps\n * it out of server and proxy logs, and replacing the address is what keeps it out of the browser\n * history and off the screen. A caller that read it and forgot to replace would undo the point.\n */\n\nconst TOKEN = '0123456789abcdef0123456789abcdef'\nconst PAGE = 'https://acme.example.com/accept-invitation'\n\ndescribe('a link with a token in it', () => {\n it('reads a bare fragment', () => {\n expect(readLinkToken(`${PAGE}#${TOKEN}`)).toEqual({ token: TOKEN, clean: PAGE })\n })\n\n it('reads the named form some mail clients produce', () => {\n expect(readLinkToken(`${PAGE}#invite=${TOKEN}`)).toEqual({ token: TOKEN, clean: PAGE })\n })\n\n it('reads one that arrived percent-encoded', () => {\n expect(readLinkToken(`${PAGE}#invite%3D${TOKEN}`).token).toEqual(TOKEN)\n })\n\n it('hands back the address without the fragment', () => {\n /* ⚠ What the caller replaces the address with. Without it the token stays on screen and in\n this history entry, which is the half of the hiding the fragment does not do by itself. */\n expect(readLinkToken(`${PAGE}?x=1#${TOKEN}`).clean).toEqual(`${PAGE}?x=1`)\n })\n})\n\ndescribe('a link with nothing usable in it', () => {\n it('has no fragment at all', () => {\n expect(readLinkToken(PAGE)).toEqual({ token: '', clean: PAGE })\n })\n\n it('has an empty fragment', () => {\n expect(readLinkToken(`${PAGE}#`)).toEqual({ token: '', clean: PAGE })\n })\n\n it('⚠ carries something that is not a token', () => {\n /*\n * ⚠ A page anchor, a tracking fragment, anything a link happened to pick up. Sending it as a\n * credential attempt would be this page volunteering whatever it was given.\n */\n expect(readLinkToken(`${PAGE}#section-2`).token).toEqual('')\n expect(readLinkToken(`${PAGE}#utm_source=mail`).token).toEqual('')\n expect(readLinkToken(`${PAGE}#<script>`).token).toEqual('')\n })\n\n it('carries something token-shaped but the wrong length', () => {\n expect(readLinkToken(`${PAGE}#0123456789abcdef`).token).toEqual('')\n expect(readLinkToken(`${PAGE}#${TOKEN}ff`).token).toEqual('')\n })\n\n it('carries hex in the wrong case', () => {\n /* `makeInvitationToken` produces lower-case hex. Anything else did not come from us. */\n expect(readLinkToken(`${PAGE}#${TOKEN.toUpperCase()}`).token).toEqual('')\n })\n\n it('still hands back a clean address to replace with', () => {\n /* Even a fragment that is not ours should not be left sitting in the bar for this page. */\n expect(readLinkToken(`${PAGE}#section-2`).clean).toEqual(PAGE)\n })\n\n it('is given nothing at all', () => {\n expect(readLinkToken('')).toEqual({ token: '', clean: '' })\n expect(readLinkToken(undefined as any)).toEqual({ token: '', clean: '' })\n })\n})\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/auth-ui",
3
- "version": "10.1.41",
3
+ "version": "10.1.46",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -38,11 +38,11 @@
38
38
  "@operato/p13n": "^10.0.0",
39
39
  "@operato/popup": "^10.15.0",
40
40
  "@operato/styles": "^10.0.0",
41
- "@things-factory/auth-base": "^10.1.40",
42
- "@things-factory/component-ui": "^10.1.41",
43
- "@things-factory/more-base": "^10.1.40",
41
+ "@things-factory/auth-base": "^10.1.46",
42
+ "@things-factory/component-ui": "^10.1.46",
43
+ "@things-factory/more-base": "^10.1.46",
44
44
  "base64url": "^3.0.1",
45
45
  "clipboard": "^2.0.6"
46
46
  },
47
- "gitHead": "4f41243edca4c965bc6992a10fb52d75a58fd3a9"
47
+ "gitHead": "d3467cde550453f3348fed0ed6c79a12a6ba0db4"
48
48
  }
@@ -116,6 +116,7 @@
116
116
  "field.user-type": "user type",
117
117
  "field.username": "user id",
118
118
  "field.username or email": "username or email",
119
+ "field.vs template": "vs template",
119
120
  "field.webhook": "webhook",
120
121
  "label.access token": "access token",
121
122
  "label.activated": "activated",
@@ -184,6 +185,7 @@
184
185
  "text.a role of this name is already visible": "a role of this name is already visible",
185
186
  "text.account exists so joins immediately": "This account already exists. Adding it lets them in right away.",
186
187
  "text.account will be created with the default password": "No account uses this address yet. One is created with the default password, which you have to tell them yourself.",
188
+ "text.add template privileges": "adds the {count} privileges the {template} template grants and this role lacks: {list}",
187
189
  "text.add_domain_owner_description": "grant another domain member owner permission.",
188
190
  "text.already in this domain": "already here",
189
191
  "text.an application signs in on its own behalf": "an application signs in on its own behalf, with its own key and its own privileges",
@@ -248,6 +250,7 @@
248
250
  "text.invalid-username": "use an id or an email address",
249
251
  "text.invitation resent": "invitation sent again.",
250
252
  "text.loading": "loading…",
253
+ "text.missing against template": "{count} missing",
251
254
  "text.new password": "new password",
252
255
  "text.no account and no default password": "No account uses this address, and this system has no default password. They have to sign up first.",
253
256
  "text.no account so an invitation will be mailed": "no account yet, so an invitation will be mailed. they choose their own password.",
@@ -336,7 +339,9 @@
336
339
  "text.your account is not activated": "your account is not activated.",
337
340
  "text.your email address": "your email address",
338
341
  "title.accept invitation": "accept invitation",
342
+ "title.activate account": "activate account",
339
343
  "title.add domain owner": "add domain owner",
344
+ "title.add template privileges": "add template privileges",
340
345
  "title.attribute model": "attribute model",
341
346
  "title.attribute-item list": "attribute-item list",
342
347
  "title.attributes": "attributes",
@@ -102,6 +102,7 @@
102
102
  "field.user-id or email": "ユーザーIDまたはメール",
103
103
  "field.user-type": "ユーザー タイプ",
104
104
  "field.username": "ユーザーID",
105
+ "field.vs template": "テンプレート比",
105
106
  "field.webhook": "webhook",
106
107
  "label.access token": "アクセストークン",
107
108
  "label.activated": "使用中",
@@ -158,6 +159,7 @@
158
159
  "text.a blank seat opens nothing until you give it privileges": "A blank seat opens nothing until you give it privileges.",
159
160
  "text.a role of this name exists and will be added to": "A role of this name already exists. It will be used as it stands and these privileges added to it.",
160
161
  "text.a role of this name is already visible": "同じ名前のロールが既に見えています",
162
+ "text.add template privileges": "{template} テンプレートが付与し、このロールにない権限 {count}件を追加します: {list}",
161
163
  "text.already in this domain": "already here",
162
164
  "text.an application signs in on its own behalf": "アプリケーションは自身のキーと権限でログインします",
163
165
  "text.appliance": "端末装置",
@@ -209,6 +211,7 @@
209
211
  "text.invalid-email": "メールアドレスの形式が違います",
210
212
  "text.invalid-username": "IDまたはメールアドレスを入力してください",
211
213
  "text.invitation resent": "招待メールを再送しました。",
214
+ "text.missing against template": "{count}件不足",
212
215
  "text.new password": "新しいパスワード",
213
216
  "text.no account so an invitation will be mailed": "アカウントがないため招待メールを送ります。パスワードは本人が決めます。",
214
217
  "text.no domain available": "使用できる$t(term.domain)がありません.",
@@ -267,6 +270,8 @@
267
270
  "text.your account is not activated": "アカウントがアクティブ化されていません.",
268
271
  "text.your email address": "ユーザー メール",
269
272
  "title.accept invitation": "招待を受ける",
273
+ "title.activate account": "アカウント有効化",
274
+ "title.add template privileges": "テンプレートの権限を追加",
270
275
  "title.attribute model": "属性モデル",
271
276
  "title.attribute-item list": "属性アイテム リスト",
272
277
  "title.attributes": "属性",
@@ -116,6 +116,7 @@
116
116
  "field.user-type": "사용자유형",
117
117
  "field.username": "사용자 아이디",
118
118
  "field.username or email": "사용자명 또는 이메일",
119
+ "field.vs template": "템플릿 대비",
119
120
  "field.webhook": "webhook",
120
121
  "label.access token": "액세스 토큰",
121
122
  "label.activated": "사용 중",
@@ -184,6 +185,7 @@
184
185
  "text.a role of this name is already visible": "같은 이름의 역할이 이미 보입니다",
185
186
  "text.account exists so joins immediately": "이미 계정이 있습니다. 넣으면 바로 들어옵니다.",
186
187
  "text.account will be created with the default password": "이 이메일로 가입한 계정이 없습니다. 기본 비밀번호로 계정을 만들어 넣습니다. 비밀번호는 직접 알려 주셔야 합니다.",
188
+ "text.add template privileges": "{template} 템플릿이 여는데 이 역할에 없는 권한 {count}개를 더합니다: {list}",
187
189
  "text.add_domain_owner_description": "$t(term.domain) 멤버 중 한 명에게 소유자 권한을 부여합니다.",
188
190
  "text.already in this domain": "이미 있음",
189
191
  "text.an application signs in on its own behalf": "응용프로그램은 자기 키와 자기 권한으로 스스로 로그인합니다",
@@ -247,6 +249,7 @@
247
249
  "text.invalid-username": "아이디나 이메일 주소를 적으십시오",
248
250
  "text.invitation resent": "초대 메일을 다시 보냈습니다.",
249
251
  "text.loading": "읽는 중…",
252
+ "text.missing against template": "{count}개 빠짐",
250
253
  "text.new password": "새 비밀번호",
251
254
  "text.no account and no default password": "이 이메일로 가입한 계정이 없고, 이 시스템에는 기본 비밀번호가 설정되어 있지 않습니다. 그 사람이 먼저 가입해야 부를 수 있습니다.",
252
255
  "text.no account so an invitation will be mailed": "계정이 없어 초대 메일을 보냅니다. 비밀번호는 본인이 정합니다.",
@@ -335,7 +338,9 @@
335
338
  "text.your account is not activated": "계정이 활성화되지 않았습니다.",
336
339
  "text.your email address": "사용자 이메일",
337
340
  "title.accept invitation": "초대 수락",
341
+ "title.activate account": "계정 활성화",
338
342
  "title.add domain owner": "$t(term.domain) 소유자 추가",
343
+ "title.add template privileges": "템플릿의 권한 더하기",
339
344
  "title.attribute model": "속성 모델",
340
345
  "title.attribute-item list": "속성 아이템 리스트",
341
346
  "title.attributes": "속성",
@@ -102,6 +102,7 @@
102
102
  "field.user-id or email": "ID pengguna atau e-mel",
103
103
  "field.user-type": "jenis pengguna",
104
104
  "field.username": "ID pengguna",
105
+ "field.vs template": "berbanding templat",
105
106
  "field.webhook": "webhook",
106
107
  "label.access token": "token akses",
107
108
  "label.activated": "diaktifkan",
@@ -158,6 +159,7 @@
158
159
  "text.a blank seat opens nothing until you give it privileges": "A blank seat opens nothing until you give it privileges.",
159
160
  "text.a role of this name exists and will be added to": "A role of this name already exists. It will be used as it stands and these privileges added to it.",
160
161
  "text.a role of this name is already visible": "peranan dengan nama ini sudah kelihatan",
162
+ "text.add template privileges": "menambah {count} keizinan yang diberi oleh templat {template} tetapi tiada pada peranan ini: {list}",
161
163
  "text.already in this domain": "already here",
162
164
  "text.an application signs in on its own behalf": "aplikasi log masuk bagi pihaknya sendiri, dengan kunci dan keizinannya sendiri",
163
165
  "text.appliance": "perkakas terminal",
@@ -211,6 +213,7 @@
211
213
  "text.invalid-email": "itu bukan alamat emel",
212
214
  "text.invalid-username": "gunakan id atau alamat emel",
213
215
  "text.invitation resent": "jemputan dihantar semula.",
216
+ "text.missing against template": "{count} tiada",
214
217
  "text.new password": "kata laluan baru",
215
218
  "text.no account so an invitation will be mailed": "tiada akaun lagi, jadi jemputan akan dihantar melalui e-mel. mereka memilih kata laluan sendiri.",
216
219
  "text.no domain available": "maaf, tiada domain untuk dipilih",
@@ -268,6 +271,8 @@
268
271
  "text.your account is not activated": "akaun anda belum diaktifkan",
269
272
  "text.your email address": "alamat emel anda",
270
273
  "title.accept invitation": "terima jemputan",
274
+ "title.activate account": "aktifkan akaun",
275
+ "title.add template privileges": "tambah keizinan templat",
271
276
  "title.attribute model": "model atribut",
272
277
  "title.attribute-item list": "senarai ciri-ciri",
273
278
  "title.attributes": "ciri-ciri",
@@ -102,6 +102,7 @@
102
102
  "field.user-id or email": "用户名或电子邮件",
103
103
  "field.user-type": "用户类型",
104
104
  "field.username": "用户名",
105
+ "field.vs template": "对照模板",
105
106
  "field.webhook": "webhook",
106
107
  "label.access token": "访问令牌",
107
108
  "label.activated": "已启用",
@@ -158,6 +159,7 @@
158
159
  "text.a blank seat opens nothing until you give it privileges": "A blank seat opens nothing until you give it privileges.",
159
160
  "text.a role of this name exists and will be added to": "A role of this name already exists. It will be used as it stands and these privileges added to it.",
160
161
  "text.a role of this name is already visible": "同名角色已可见",
162
+ "text.add template privileges": "添加 {template} 模板授予而此角色缺少的 {count} 项权限: {list}",
161
163
  "text.already in this domain": "already here",
162
164
  "text.an application signs in on its own behalf": "应用程序以自己的密钥和权限登录",
163
165
  "text.appliance": "终端设备",
@@ -210,6 +212,7 @@
210
212
  "text.invalid-email": "电子邮件地址格式不正确",
211
213
  "text.invalid-username": "请输入 ID 或电子邮件地址",
212
214
  "text.invitation resent": "已重新发送邀请邮件。",
215
+ "text.missing against template": "缺少 {count} 项",
213
216
  "text.new password": "新密码",
214
217
  "text.no account so an invitation will be mailed": "尚无账号,将发送邀请邮件,由本人设置密码。",
215
218
  "text.no domain available": "抱歉,没有$t(term.domain)名",
@@ -268,6 +271,8 @@
268
271
  "text.your account is not activated": "您的账号还没验证",
269
272
  "text.your email address": "您的邮件地址",
270
273
  "title.accept invitation": "接受邀请",
274
+ "title.activate account": "激活帐户",
275
+ "title.add template privileges": "添加模板权限",
271
276
  "title.attribute model": "属性模型",
272
277
  "title.attribute-item list": "属性列表",
273
278
  "title.attributes": "属性",
@@ -1 +0,0 @@
1
- {"version":3,"file":"invitation-token.js","sourceRoot":"","sources":["../../client/utils/invitation-token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,qEAAqE;AACrE,MAAM,KAAK,GAAG,gBAAgB,CAAA;AAS9B,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAA;IACjC,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAE9B,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;QACb,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;IACnC,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;IAEjC;;;OAGG;IACH,MAAM,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;IAE5E,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,CAAA;AACrD,CAAC","sourcesContent":["/**\n * Taking the invitation token out of the link, and out of the address bar. (Pure rule.)\n *\n * ## Why the token is in the fragment\n *\n * A browser does not send what follows `#`, so a link shaped `…/accept-invitation#<token>`\n * reaches the server as `/accept-invitation` and nothing else. The token never appears in an\n * access log, a proxy log, or a `Referer` sent onward to some other site.\n *\n * That leaves the places the browser itself keeps it: the address bar and the history entry.\n * Both are cleared by replacing the URL with the one this returns as `clean`, which is why the\n * two answers come back together — reading the token and removing it are one act, and a caller\n * that did the first without the second would leave it on screen.\n *\n * ## What it refuses to read\n *\n * A token is hex from `crypto.randomBytes(16)`. Anything else in the fragment is not one, and\n * taking it anyway would send whatever a link happened to carry — a tracking fragment, a page\n * anchor — to the server as a credential attempt. It answers empty instead, and the page shows\n * the box where the code from the mail can be pasted.\n */\n\n/** 16 bytes as hex, which is what `makeInvitationToken` produces. */\nconst TOKEN = /^[0-9a-f]{32}$/\n\nexport type InvitationLink = {\n /** The token, or an empty string when the link carries none this rule will read. */\n token: string\n /** The same URL without its fragment, for the caller to replace the address with. */\n clean: string\n}\n\nexport function readInvitationToken(href: string): InvitationLink {\n if (!href) {\n return { token: '', clean: '' }\n }\n\n const hash = href.indexOf('#')\n\n if (hash < 0) {\n return { token: '', clean: href }\n }\n\n const clean = href.slice(0, hash)\n\n /*\n * `#invite=<token>` as well as a bare `#<token>`. Both shapes turn up in mail clients that\n * rewrite links, and neither costs anything to accept.\n */\n const raw = decodeURIComponent(href.slice(hash + 1)).replace(/^invite=/, '')\n\n return { token: TOKEN.test(raw) ? raw : '', clean }\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"invitation-token.test.js","sourceRoot":"","sources":["../../client/utils/invitation-token.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { readInvitationToken } from './invitation-token'\n\n/**\n * Reading the invitation token out of the link, and handing back the link without it.\n *\n * The two answers travel together because removing the token is not optional: the fragment keeps\n * it out of server and proxy logs, and replacing the address is what keeps it out of the browser\n * history and off the screen. A caller that read it and forgot to replace would undo the point.\n */\n\nconst TOKEN = '0123456789abcdef0123456789abcdef'\nconst PAGE = 'https://acme.example.com/accept-invitation'\n\ndescribe('a link with a token in it', () => {\n it('reads a bare fragment', () => {\n expect(readInvitationToken(`${PAGE}#${TOKEN}`)).toEqual({ token: TOKEN, clean: PAGE })\n })\n\n it('reads the named form some mail clients produce', () => {\n expect(readInvitationToken(`${PAGE}#invite=${TOKEN}`)).toEqual({ token: TOKEN, clean: PAGE })\n })\n\n it('reads one that arrived percent-encoded', () => {\n expect(readInvitationToken(`${PAGE}#invite%3D${TOKEN}`).token).toEqual(TOKEN)\n })\n\n it('hands back the address without the fragment', () => {\n /* ⚠ What the caller replaces the address with. Without it the token stays on screen and in\n this history entry, which is the half of the hiding the fragment does not do by itself. */\n expect(readInvitationToken(`${PAGE}?x=1#${TOKEN}`).clean).toEqual(`${PAGE}?x=1`)\n })\n})\n\ndescribe('a link with nothing usable in it', () => {\n it('has no fragment at all', () => {\n expect(readInvitationToken(PAGE)).toEqual({ token: '', clean: PAGE })\n })\n\n it('has an empty fragment', () => {\n expect(readInvitationToken(`${PAGE}#`)).toEqual({ token: '', clean: PAGE })\n })\n\n it('⚠ carries something that is not a token', () => {\n /*\n * ⚠ A page anchor, a tracking fragment, anything a link happened to pick up. Sending it as a\n * credential attempt would be this page volunteering whatever it was given.\n */\n expect(readInvitationToken(`${PAGE}#section-2`).token).toEqual('')\n expect(readInvitationToken(`${PAGE}#utm_source=mail`).token).toEqual('')\n expect(readInvitationToken(`${PAGE}#<script>`).token).toEqual('')\n })\n\n it('carries something token-shaped but the wrong length', () => {\n expect(readInvitationToken(`${PAGE}#0123456789abcdef`).token).toEqual('')\n expect(readInvitationToken(`${PAGE}#${TOKEN}ff`).token).toEqual('')\n })\n\n it('carries hex in the wrong case', () => {\n /* `makeInvitationToken` produces lower-case hex. Anything else did not come from us. */\n expect(readInvitationToken(`${PAGE}#${TOKEN.toUpperCase()}`).token).toEqual('')\n })\n\n it('still hands back a clean address to replace with', () => {\n /* Even a fragment that is not ours should not be left sitting in the bar for this page. */\n expect(readInvitationToken(`${PAGE}#section-2`).clean).toEqual(PAGE)\n })\n\n it('is given nothing at all', () => {\n expect(readInvitationToken('')).toEqual({ token: '', clean: '' })\n expect(readInvitationToken(undefined as any)).toEqual({ token: '', clean: '' })\n })\n})\n"]}