@things-factory/auth-base 10.1.223 → 10.1.227

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 (44) hide show
  1. package/dist-client/domain-required.d.ts +1 -1
  2. package/dist-client/domain-required.js +1 -1
  3. package/dist-client/domain-required.js.map +1 -1
  4. package/dist-client/tsconfig.tsbuildinfo +1 -1
  5. package/dist-server/controllers/confirm-identity.d.ts +8 -0
  6. package/dist-server/controllers/confirm-identity.js +33 -0
  7. package/dist-server/controllers/confirm-identity.js.map +1 -0
  8. package/dist-server/controllers/signin.d.ts +5 -0
  9. package/dist-server/controllers/signin.js +38 -31
  10. package/dist-server/controllers/signin.js.map +1 -1
  11. package/dist-server/router/webauthn-router.js +20 -1
  12. package/dist-server/router/webauthn-router.js.map +1 -1
  13. package/dist-server/service/audit/audit-kinds.js +2 -0
  14. package/dist-server/service/audit/audit-kinds.js.map +1 -1
  15. package/dist-server/service/identity-confirmation/identity-confirmation.d.ts +18 -0
  16. package/dist-server/service/identity-confirmation/identity-confirmation.js +84 -0
  17. package/dist-server/service/identity-confirmation/identity-confirmation.js.map +1 -0
  18. package/dist-server/service/index.d.ts +1 -0
  19. package/dist-server/service/index.js +3 -1
  20. package/dist-server/service/index.js.map +1 -1
  21. package/dist-server/service/privilege/privilege-directive.js +17 -1
  22. package/dist-server/service/privilege/privilege-directive.js.map +1 -1
  23. package/dist-server/service/role/role-mutation.js +18 -6
  24. package/dist-server/service/role/role-mutation.js.map +1 -1
  25. package/dist-server/service/role/role-query.d.ts +5 -0
  26. package/dist-server/service/role/role-query.js +18 -0
  27. package/dist-server/service/role/role-query.js.map +1 -1
  28. package/dist-server/tsconfig.tsbuildinfo +1 -1
  29. package/dist-server/utils/access-token-cookie.js +3 -0
  30. package/dist-server/utils/access-token-cookie.js.map +1 -1
  31. package/dist-server/utils/build-auth-url.d.ts +1 -1
  32. package/dist-server/utils/build-auth-url.js +2 -2
  33. package/dist-server/utils/build-auth-url.js.map +1 -1
  34. package/package.json +5 -5
  35. package/tests/confirm-identity-db.test.ts +85 -0
  36. package/tests/identity-confirmation.test.ts +146 -0
  37. package/tests/role-mutation-db.test.ts +20 -4
  38. package/tests/role-privileges-db.test.ts +26 -0
  39. package/translations/en.json +3 -1
  40. package/translations/es.json +3 -1
  41. package/translations/ja.json +3 -1
  42. package/translations/ko.json +3 -1
  43. package/translations/ms.json +3 -1
  44. package/translations/zh.json +3 -1
@@ -6,6 +6,7 @@ exports.setSessionAccessToken = setSessionAccessToken;
6
6
  exports.clearAccessTokenCookie = clearAccessTokenCookie;
7
7
  const env_1 = require("@things-factory/env");
8
8
  const max_age_js_1 = require("../constants/max-age.js");
9
+ const identity_confirmation_js_1 = require("../service/identity-confirmation/identity-confirmation.js");
9
10
  const accessTokenCookieKey = env_1.config.get('accessTokenCookieKey', 'access_token');
10
11
  function getAccessTokenCookie(context) {
11
12
  return context?.cookies?.get(accessTokenCookieKey);
@@ -37,6 +38,8 @@ function clearAccessTokenCookie(context) {
37
38
  sameSite: 'Lax'
38
39
  };
39
40
  context.cookies.set(accessTokenCookieKey, '', cookie);
41
+ /* A confirmation that it was the person ends with the sign-in it confirmed (ADR-0168). */
42
+ (0, identity_confirmation_js_1.forgetConfirmation)(context);
40
43
  /*
41
44
  * TODO clear i18next cookie as well - need to support domain
42
45
  * https://github.com/hatiolab/things-factory/issues/70
@@ -1 +1 @@
1
- {"version":3,"file":"access-token-cookie.js","sourceRoot":"","sources":["../../server/utils/access-token-cookie.ts"],"names":[],"mappings":";;AAKA,oDAEC;AAED,oDAWC;AAED,sDASC;AAED,wDAgBC;AAjDD,6CAA4C;AAC5C,wDAAiD;AAEjD,MAAM,oBAAoB,GAAG,YAAM,CAAC,GAAG,CAAC,sBAAsB,EAAE,cAAc,CAAC,CAAA;AAE/E,SAAgB,oBAAoB,CAAC,OAAO;IAC1C,OAAO,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,oBAAoB,CAAC,CAAA;AACpD,CAAC;AAED,SAAgB,oBAAoB,CAAC,OAAO,EAAE,KAAK;IACjD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IAE1B,IAAI,MAAM,GAAG;QACX,MAAM;QACN,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,oBAAO;QACf,QAAQ,EAAE,KAAK;KAChB,CAAA;IAED,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;AAC1D,CAAC;AAED,SAAgB,qBAAqB,CAAC,OAAO;IAC3C,0FAA0F;IAC1F,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAE9B,OAAO,CAAC,OAAO,GAAG;QAChB,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,QAAQ,EAAE,IAAI,CAAC,IAAI;QACnB,MAAM,EAAE,IAAI,CAAC,KAAK;KACnB,CAAA;AACH,CAAC;AAED,SAAgB,sBAAsB,CAAC,OAAO;IAC5C,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IAE1B,IAAI,MAAM,GAAG;QACX,MAAM;QACN,QAAQ,EAAE,IAAI;QACd,QAAQ,EAAE,KAAK;KAChB,CAAA;IAED,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,EAAE,EAAE,MAAM,CAAC,CAAA;IACrD;;;OAGG;IACH,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,EAAE,MAAM,CAAC,CAAA;IAC1C,OAAO,CAAC,OAAO,GAAG,IAAI,CAAA;AACxB,CAAC","sourcesContent":["import { config } from '@things-factory/env'\nimport { MAX_AGE } from '../constants/max-age.js'\n\nconst accessTokenCookieKey = config.get('accessTokenCookieKey', 'access_token')\n\nexport function getAccessTokenCookie(context) {\n return context?.cookies?.get(accessTokenCookieKey)\n}\n\nexport function setAccessTokenCookie(context, token) {\n const { secure } = context\n\n var cookie = {\n secure,\n httpOnly: true,\n maxAge: MAX_AGE,\n sameSite: 'Lax'\n }\n\n context.cookies.set(accessTokenCookieKey, token, cookie)\n}\n\nexport function setSessionAccessToken(context) {\n /* koa-session 을 사용하는 경우에는, cookie 직접 설정이 작동되지 않는다. 그런 경우에는 session에 설정해서 cookie를 변경한다. */\n const { user } = context.state\n\n context.session = {\n id: user.id,\n userType: user.type,\n status: user.state\n }\n}\n\nexport function clearAccessTokenCookie(context) {\n const { secure } = context\n\n var cookie = {\n secure,\n httpOnly: true,\n sameSite: 'Lax'\n }\n\n context.cookies.set(accessTokenCookieKey, '', cookie)\n /*\n * TODO clear i18next cookie as well - need to support domain\n * https://github.com/hatiolab/things-factory/issues/70\n */\n context.cookies.set('i18next', '', cookie)\n context.session = null\n}\n"]}
1
+ {"version":3,"file":"access-token-cookie.js","sourceRoot":"","sources":["../../server/utils/access-token-cookie.ts"],"names":[],"mappings":";;AAMA,oDAEC;AAED,oDAWC;AAED,sDASC;AAED,wDAkBC;AApDD,6CAA4C;AAC5C,wDAAiD;AACjD,wGAA8F;AAE9F,MAAM,oBAAoB,GAAG,YAAM,CAAC,GAAG,CAAC,sBAAsB,EAAE,cAAc,CAAC,CAAA;AAE/E,SAAgB,oBAAoB,CAAC,OAAO;IAC1C,OAAO,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,oBAAoB,CAAC,CAAA;AACpD,CAAC;AAED,SAAgB,oBAAoB,CAAC,OAAO,EAAE,KAAK;IACjD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IAE1B,IAAI,MAAM,GAAG;QACX,MAAM;QACN,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,oBAAO;QACf,QAAQ,EAAE,KAAK;KAChB,CAAA;IAED,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;AAC1D,CAAC;AAED,SAAgB,qBAAqB,CAAC,OAAO;IAC3C,0FAA0F;IAC1F,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAE9B,OAAO,CAAC,OAAO,GAAG;QAChB,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,QAAQ,EAAE,IAAI,CAAC,IAAI;QACnB,MAAM,EAAE,IAAI,CAAC,KAAK;KACnB,CAAA;AACH,CAAC;AAED,SAAgB,sBAAsB,CAAC,OAAO;IAC5C,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IAE1B,IAAI,MAAM,GAAG;QACX,MAAM;QACN,QAAQ,EAAE,IAAI;QACd,QAAQ,EAAE,KAAK;KAChB,CAAA;IAED,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,EAAE,EAAE,MAAM,CAAC,CAAA;IACrD,0FAA0F;IAC1F,IAAA,6CAAkB,EAAC,OAAO,CAAC,CAAA;IAC3B;;;OAGG;IACH,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,EAAE,MAAM,CAAC,CAAA;IAC1C,OAAO,CAAC,OAAO,GAAG,IAAI,CAAA;AACxB,CAAC","sourcesContent":["import { config } from '@things-factory/env'\nimport { MAX_AGE } from '../constants/max-age.js'\nimport { forgetConfirmation } from '../service/identity-confirmation/identity-confirmation.js'\n\nconst accessTokenCookieKey = config.get('accessTokenCookieKey', 'access_token')\n\nexport function getAccessTokenCookie(context) {\n return context?.cookies?.get(accessTokenCookieKey)\n}\n\nexport function setAccessTokenCookie(context, token) {\n const { secure } = context\n\n var cookie = {\n secure,\n httpOnly: true,\n maxAge: MAX_AGE,\n sameSite: 'Lax'\n }\n\n context.cookies.set(accessTokenCookieKey, token, cookie)\n}\n\nexport function setSessionAccessToken(context) {\n /* koa-session 을 사용하는 경우에는, cookie 직접 설정이 작동되지 않는다. 그런 경우에는 session에 설정해서 cookie를 변경한다. */\n const { user } = context.state\n\n context.session = {\n id: user.id,\n userType: user.type,\n status: user.state\n }\n}\n\nexport function clearAccessTokenCookie(context) {\n const { secure } = context\n\n var cookie = {\n secure,\n httpOnly: true,\n sameSite: 'Lax'\n }\n\n context.cookies.set(accessTokenCookieKey, '', cookie)\n /* A confirmation that it was the person ends with the sign-in it confirmed (ADR-0168). */\n forgetConfirmation(context)\n /*\n * TODO clear i18next cookie as well - need to support domain\n * https://github.com/hatiolab/things-factory/issues/70\n */\n context.cookies.set('i18next', '', cookie)\n context.session = null\n}\n"]}
@@ -26,7 +26,7 @@
26
26
  * 아예 없고 어떤 경우엔 문자열 null 이다. 그러면 new URL(...) 이 던지고 **로그인과 OAuth
27
27
  * 콜백이 런타임에 깨진다.** 타입도 맞고 테스트도 도니까 띄워서 로그인해 봐야 안다.
28
28
  *
29
- * 그래서 Koa 2 의 정의를 그대로 펼쳐 쓴다. protocol 과 host 는 두 판 모두 「이 요청이 들어온
29
+ * 그래서 Koa 2 의 정의를 그대로 펼쳐 쓴다. protocol 과 host 는 두 버전 모두 「이 요청이 들어온
30
30
  * 곳」이고 app.proxy 설정도 그대로 존중한다. **오늘 값은 바뀌지 않는다.**
31
31
  *
32
32
  * ── config 의 protocol 이 마지막에 이긴다 ───────────────────────────────────
@@ -36,7 +36,7 @@ const protocol = env_1.config.get('protocol');
36
36
  * 아예 없고 어떤 경우엔 문자열 null 이다. 그러면 new URL(...) 이 던지고 **로그인과 OAuth
37
37
  * 콜백이 런타임에 깨진다.** 타입도 맞고 테스트도 도니까 띄워서 로그인해 봐야 안다.
38
38
  *
39
- * 그래서 Koa 2 의 정의를 그대로 펼쳐 쓴다. protocol 과 host 는 두 판 모두 「이 요청이 들어온
39
+ * 그래서 Koa 2 의 정의를 그대로 펼쳐 쓴다. protocol 과 host 는 두 버전 모두 「이 요청이 들어온
40
40
  * 곳」이고 app.proxy 설정도 그대로 존중한다. **오늘 값은 바뀌지 않는다.**
41
41
  *
42
42
  * ── config 의 protocol 이 마지막에 이긴다 ───────────────────────────────────
@@ -71,7 +71,7 @@ function buildAuthURL(pathname, context) {
71
71
  * 콜백 주소를 만드는 이 자리에서 런타임에 깨진다.** 타입도 맞고 테스트도 도니까 띄워서
72
72
  * 로그인해 봐야 안다.
73
73
  *
74
- * 그래서 Koa 2 의 정의를 그대로 펼쳐 쓴다. protocol 과 host 는 두 판 모두 「이 요청이
74
+ * 그래서 Koa 2 의 정의를 그대로 펼쳐 쓴다. protocol 과 host 는 두 버전 모두 「이 요청이
75
75
  * 들어온 곳」이고 app.proxy 설정도 그대로 존중한다. **오늘 값은 바뀌지 않는다.**
76
76
  */
77
77
  var url = new url_1.URL(`${context.request.protocol}://${context.request.host}`);
@@ -1 +1 @@
1
- {"version":3,"file":"build-auth-url.js","sourceRoot":"","sources":["../../server/utils/build-auth-url.ts"],"names":[],"mappings":";;AA6CA,oCA0CC;AAvFD,6BAAyB;AAEzB,6CAA4C;AAE5C;;;GAGG;AACH,MAAM,QAAQ,GAAW,YAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,SAAgB,YAAY,CAAC,QAAgB,EAAE,OAAwB;IAErE,MAAM,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAA;IAC7D,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAA;IACxD,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAA;IAExD,IAAI,gBAAgB,IAAI,YAAY,EAAE,CAAC;QACrC,IAAI,GAAG,GAAQ,IAAI,SAAG,CAAC,GAAG,gBAAgB,MAAM,YAAY,EAAE,CAAC,CAAA;QAC/D,IAAI,YAAY,EAAE,CAAC;YACjB,GAAG,CAAC,IAAI,GAAG,YAAY,CAAA;QACzB,CAAC;IACH,CAAC;SAAM,CAAC;QACR;;;;;;;;;;;;;;;;;;;WAmBG;QACD,IAAI,GAAG,GAAQ,IAAI,SAAG,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,MAAM,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;IACjF,CAAC;IAED,IAAI,QAAQ,EAAE,CAAC;QACb,GAAG,CAAC,QAAQ,GAAG,QAAQ,CAAA;IACzB,CAAC;IAED,GAAG,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAEvB,OAAO,GAAG,CAAC,IAAI,CAAA;AACjB,CAAC","sourcesContent":["import { URL } from 'url'\n\nimport { config } from '@things-factory/env'\n\n/*\n * 프록시가 헤더를 안 실어 주면서 TLS 종료를 하는 배치를 위한 못. 모듈이 실릴 때 한 번 읽는다\n * — 옮겨 오기 전에도 같은 자리에서 같은 방식으로 읽고 있었다.\n */\nconst protocol: string = config.get('protocol')\n\n/**\n * **이 요청이 들어온 우리 주소**에 경로를 붙여 절대 주소를 만든다.\n *\n * 로그인·OAuth 의 주소를 화면과 저장 값에 내려 줄 때 쓴다.\n *\n * buildAuthURL('/oauth/authorize', context)\n * buildAuthURL('/oauth2-client/callback', context)\n *\n * ── 왜 공용인가 ─────────────────────────────────────────────────────────────\n * 이 열두 줄이 auth-base 와 oauth2-client 에 **똑같이 베껴져** 있었다(2026-09-04).\n * x-forwarded 셋 읽기 · 폴백 · protocol 덮기 · pathname 붙이기가 같은 코드였다.\n * 한쪽만 고치는 일이 실제로 났다 — 그래서 한 집으로 모았다.\n *\n * ── 프록시 뒤에서는 프록시가 본 주소를 쓴다 ─────────────────────────────────\n * 리버스 프록시를 거치면 소켓이 보는 것은 프록시와의 연결이라 공개 주소가 아니다. 그때는\n * 프록시가 실어 주는 x-forwarded-* 가 유일한 단서다.\n *\n * ── 폴백에서 request.origin 을 쓰지 않는 이유 ───────────────────────────────\n * Koa 2 에서는 origin 이 정확히 우리가 원하는 값이다.\n *\n * [읽었음] node_modules/koa/lib/request.js:98\n * get origin() { return `${this.protocol}://${this.host}` }\n *\n * 그런데 **Koa 3 은 그 뜻을 바꾼다** — 현재 호스트가 아니라 브라우저가 보낸 Origin 헤더를\n * 준다(CORS 표준에 맞춘 변경, koajs/koa v3.0.0 릴리즈 노트). 그 헤더는 같은 출처 요청에는\n * 아예 없고 어떤 경우엔 문자열 null 이다. 그러면 new URL(...) 이 던지고 **로그인과 OAuth\n * 콜백이 런타임에 깨진다.** 타입도 맞고 테스트도 도니까 띄워서 로그인해 봐야 안다.\n *\n * 그래서 Koa 2 의 정의를 그대로 펼쳐 쓴다. protocol 과 host 는 두 판 모두 「이 요청이 들어온\n * 곳」이고 app.proxy 설정도 그대로 존중한다. **오늘 값은 바뀌지 않는다.**\n *\n * ── config 의 protocol 이 마지막에 이긴다 ───────────────────────────────────\n * 프록시가 헤더를 안 실어 주면서 TLS 종료를 하는 배치가 있다. 그때 여기서 읽은 것은 http 인데\n * 브라우저가 아는 것은 https 다. 그 경우를 설정으로 못 박는다.\n */\nexport function buildAuthURL(pathname: string, context: ResolverContext): string {\n\n const originalProtocol = context.headers['x-forwarded-proto']\n const originalHost = context.headers['x-forwarded-host']\n const originalPort = context.headers['x-forwarded-port']\n\n if (originalProtocol && originalHost) {\n var url: URL = new URL(`${originalProtocol}://${originalHost}`)\n if (originalPort) {\n url.port = originalPort\n }\n } else {\n /*\n * ── 왜 request.origin 을 쓰지 않나 ──────────────────────────────────────\n *\n * 여기가 하려는 일은 **이 요청이 들어온 우리 주소**를 얻는 것이다. 위의 x-forwarded-*\n * 가 없을 때(프록시를 안 거친 직접 접속) 그 값을 요청에서 읽는다.\n *\n * 전에는 context.request.origin 이었다. Koa 2 에서는 그것이 정확히 우리가 원하는 값이다.\n *\n * [읽었음] node_modules/koa/lib/request.js:98\n * get origin() { return `${this.protocol}://${this.host}` }\n *\n * 그런데 **Koa 3 은 그 뜻을 바꾼다** — 현재 호스트가 아니라 브라우저가 보낸 Origin 헤더를\n * 준다(CORS 표준에 맞춘 변경, koajs/koa v3.0.0 릴리즈 노트). 그 헤더는 같은 출처 요청에는\n * 아예 없고 어떤 경우엔 문자열 null 이다. 그러면 new URL(...) 이 던지고, **로그인·OAuth\n * 콜백 주소를 만드는 이 자리에서 런타임에 깨진다.** 타입도 맞고 테스트도 도니까 띄워서\n * 로그인해 봐야 안다.\n *\n * 그래서 Koa 2 의 정의를 그대로 펼쳐 쓴다. protocol 과 host 는 두 판 모두 「이 요청이\n * 들어온 곳」이고 app.proxy 설정도 그대로 존중한다. **오늘 값은 바뀌지 않는다.**\n */\n var url: URL = new URL(`${context.request.protocol}://${context.request.host}`)\n }\n\n if (protocol) {\n url.protocol = protocol\n }\n\n url.pathname = pathname\n\n return url.href\n}\n"]}
1
+ {"version":3,"file":"build-auth-url.js","sourceRoot":"","sources":["../../server/utils/build-auth-url.ts"],"names":[],"mappings":";;AA6CA,oCA0CC;AAvFD,6BAAyB;AAEzB,6CAA4C;AAE5C;;;GAGG;AACH,MAAM,QAAQ,GAAW,YAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,SAAgB,YAAY,CAAC,QAAgB,EAAE,OAAwB;IAErE,MAAM,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAA;IAC7D,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAA;IACxD,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAA;IAExD,IAAI,gBAAgB,IAAI,YAAY,EAAE,CAAC;QACrC,IAAI,GAAG,GAAQ,IAAI,SAAG,CAAC,GAAG,gBAAgB,MAAM,YAAY,EAAE,CAAC,CAAA;QAC/D,IAAI,YAAY,EAAE,CAAC;YACjB,GAAG,CAAC,IAAI,GAAG,YAAY,CAAA;QACzB,CAAC;IACH,CAAC;SAAM,CAAC;QACR;;;;;;;;;;;;;;;;;;;WAmBG;QACD,IAAI,GAAG,GAAQ,IAAI,SAAG,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,MAAM,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;IACjF,CAAC;IAED,IAAI,QAAQ,EAAE,CAAC;QACb,GAAG,CAAC,QAAQ,GAAG,QAAQ,CAAA;IACzB,CAAC;IAED,GAAG,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAEvB,OAAO,GAAG,CAAC,IAAI,CAAA;AACjB,CAAC","sourcesContent":["import { URL } from 'url'\n\nimport { config } from '@things-factory/env'\n\n/*\n * 프록시가 헤더를 안 실어 주면서 TLS 종료를 하는 배치를 위한 못. 모듈이 실릴 때 한 번 읽는다\n * — 옮겨 오기 전에도 같은 자리에서 같은 방식으로 읽고 있었다.\n */\nconst protocol: string = config.get('protocol')\n\n/**\n * **이 요청이 들어온 우리 주소**에 경로를 붙여 절대 주소를 만든다.\n *\n * 로그인·OAuth 의 주소를 화면과 저장 값에 내려 줄 때 쓴다.\n *\n * buildAuthURL('/oauth/authorize', context)\n * buildAuthURL('/oauth2-client/callback', context)\n *\n * ── 왜 공용인가 ─────────────────────────────────────────────────────────────\n * 이 열두 줄이 auth-base 와 oauth2-client 에 **똑같이 베껴져** 있었다(2026-09-04).\n * x-forwarded 셋 읽기 · 폴백 · protocol 덮기 · pathname 붙이기가 같은 코드였다.\n * 한쪽만 고치는 일이 실제로 났다 — 그래서 한 집으로 모았다.\n *\n * ── 프록시 뒤에서는 프록시가 본 주소를 쓴다 ─────────────────────────────────\n * 리버스 프록시를 거치면 소켓이 보는 것은 프록시와의 연결이라 공개 주소가 아니다. 그때는\n * 프록시가 실어 주는 x-forwarded-* 가 유일한 단서다.\n *\n * ── 폴백에서 request.origin 을 쓰지 않는 이유 ───────────────────────────────\n * Koa 2 에서는 origin 이 정확히 우리가 원하는 값이다.\n *\n * [읽었음] node_modules/koa/lib/request.js:98\n * get origin() { return `${this.protocol}://${this.host}` }\n *\n * 그런데 **Koa 3 은 그 뜻을 바꾼다** — 현재 호스트가 아니라 브라우저가 보낸 Origin 헤더를\n * 준다(CORS 표준에 맞춘 변경, koajs/koa v3.0.0 릴리즈 노트). 그 헤더는 같은 출처 요청에는\n * 아예 없고 어떤 경우엔 문자열 null 이다. 그러면 new URL(...) 이 던지고 **로그인과 OAuth\n * 콜백이 런타임에 깨진다.** 타입도 맞고 테스트도 도니까 띄워서 로그인해 봐야 안다.\n *\n * 그래서 Koa 2 의 정의를 그대로 펼쳐 쓴다. protocol 과 host 는 두 버전 모두 「이 요청이 들어온\n * 곳」이고 app.proxy 설정도 그대로 존중한다. **오늘 값은 바뀌지 않는다.**\n *\n * ── config 의 protocol 이 마지막에 이긴다 ───────────────────────────────────\n * 프록시가 헤더를 안 실어 주면서 TLS 종료를 하는 배치가 있다. 그때 여기서 읽은 것은 http 인데\n * 브라우저가 아는 것은 https 다. 그 경우를 설정으로 못 박는다.\n */\nexport function buildAuthURL(pathname: string, context: ResolverContext): string {\n\n const originalProtocol = context.headers['x-forwarded-proto']\n const originalHost = context.headers['x-forwarded-host']\n const originalPort = context.headers['x-forwarded-port']\n\n if (originalProtocol && originalHost) {\n var url: URL = new URL(`${originalProtocol}://${originalHost}`)\n if (originalPort) {\n url.port = originalPort\n }\n } else {\n /*\n * ── 왜 request.origin 을 쓰지 않나 ──────────────────────────────────────\n *\n * 여기가 하려는 일은 **이 요청이 들어온 우리 주소**를 얻는 것이다. 위의 x-forwarded-*\n * 가 없을 때(프록시를 안 거친 직접 접속) 그 값을 요청에서 읽는다.\n *\n * 전에는 context.request.origin 이었다. Koa 2 에서는 그것이 정확히 우리가 원하는 값이다.\n *\n * [읽었음] node_modules/koa/lib/request.js:98\n * get origin() { return `${this.protocol}://${this.host}` }\n *\n * 그런데 **Koa 3 은 그 뜻을 바꾼다** — 현재 호스트가 아니라 브라우저가 보낸 Origin 헤더를\n * 준다(CORS 표준에 맞춘 변경, koajs/koa v3.0.0 릴리즈 노트). 그 헤더는 같은 출처 요청에는\n * 아예 없고 어떤 경우엔 문자열 null 이다. 그러면 new URL(...) 이 던지고, **로그인·OAuth\n * 콜백 주소를 만드는 이 자리에서 런타임에 깨진다.** 타입도 맞고 테스트도 도니까 띄워서\n * 로그인해 봐야 안다.\n *\n * 그래서 Koa 2 의 정의를 그대로 펼쳐 쓴다. protocol 과 host 는 두 버전 모두 「이 요청이\n * 들어온 곳」이고 app.proxy 설정도 그대로 존중한다. **오늘 값은 바뀌지 않는다.**\n */\n var url: URL = new URL(`${context.request.protocol}://${context.request.host}`)\n }\n\n if (protocol) {\n url.protocol = protocol\n }\n\n url.pathname = pathname\n\n return url.href\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/auth-base",
3
- "version": "10.1.223",
3
+ "version": "10.1.227",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -35,10 +35,10 @@
35
35
  "@reduxjs/toolkit": "^2.2.5",
36
36
  "@simplewebauthn/browser": "^13.0.0",
37
37
  "@simplewebauthn/server": "^13.0.0",
38
- "@things-factory/email-base": "^10.1.208",
38
+ "@things-factory/email-base": "^10.1.227",
39
39
  "@things-factory/env": "^10.1.149",
40
- "@things-factory/shell": "^10.1.208",
41
- "@things-factory/task-lease": "^10.1.208",
40
+ "@things-factory/shell": "^10.1.227",
41
+ "@things-factory/task-lease": "^10.1.227",
42
42
  "@things-factory/utils": "^10.1.179",
43
43
  "@types/webappsec-credential-management": "^0.6.9",
44
44
  "jsonwebtoken": "^9.0.0",
@@ -50,5 +50,5 @@
50
50
  "passport-jwt": "^4.0.0",
51
51
  "passport-local": "^1.0.0"
52
52
  },
53
- "gitHead": "868f2026b8373371ad04f42b0a097964d0d0ed0f"
53
+ "gitHead": "f66591e3864aab08a6fb93fb1fcf1c9de6b546ac"
54
54
  }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Confirming it is still the signed-in person (ADR-0168) by password, through the real controller on the sqlite
3
+ * harness: a miss counts toward the same lock as signing in and leaves no confirmation; a match leaves one; both
4
+ * are audited as `auth.confirm-identity`.
5
+ */
6
+
7
+ import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase, authDataSource } from './db'
8
+ import { loadCompiled } from './compiled'
9
+
10
+ const { User } = loadCompiled('service/user/user')
11
+ const { AuditAction } = loadCompiled('service/audit/audit-action')
12
+ const { flushAudit } = loadCompiled('service/audit/audit-recorder')
13
+ const { confirmIdentity } = loadCompiled('controllers/confirm-identity')
14
+ const { verifyPassword } = loadCompiled('controllers/signin')
15
+ const { isRecentlyConfirmed } = loadCompiled('service/identity-confirmation/identity-confirmation')
16
+
17
+ const PASSWORD = 'Passw0rd!'
18
+
19
+ let domain: any
20
+ let kim: any
21
+
22
+ function request(user: any) {
23
+ const jar = new Map<string, string>()
24
+ return {
25
+ state: { user, domain, tx: authDataSource().manager },
26
+ cookies: { get: (name: string) => jar.get(name) || undefined, set: (name: string, value: string) => jar.set(name, value) }
27
+ } as any
28
+ }
29
+
30
+ const byPassword = (context: any, password: string) => confirmIdentity('password', context, (user: any) => verifyPassword(user, password, context))
31
+
32
+ async function audits() {
33
+ await flushAudit()
34
+ const rows = await getRepository(AuditAction).find({ where: { kind: 'auth.confirm-identity' }, order: { createdAt: 'ASC' } })
35
+ return rows.map(row => [row.outcome, row.code, row.detail?.method])
36
+ }
37
+
38
+ beforeAll(async () => {
39
+ await openAuthDatabase()
40
+ })
41
+
42
+ afterAll(async () => {
43
+ await closeAuthDatabase()
44
+ })
45
+
46
+ beforeEach(async () => {
47
+ await resetAuthDatabase()
48
+ domain = await getRepository(Domain).save({ name: 'office', subdomain: 'office' })
49
+ const salt = User.generateSalt()
50
+ kim = await getRepository(User).save({
51
+ username: 'kim',
52
+ email: 'kim@example.com',
53
+ name: 'kim',
54
+ salt,
55
+ password: User.encode(PASSWORD, salt),
56
+ status: 'activated',
57
+ domains: [domain]
58
+ })
59
+ })
60
+
61
+ it('leaves a confirmation for the person when the password matches', async () => {
62
+ const context = request(kim)
63
+ await byPassword(context, PASSWORD)
64
+
65
+ expect(isRecentlyConfirmed(context, kim.id)).toBe(true)
66
+ expect(await audits()).toEqual([['ok', null, 'password']])
67
+ })
68
+
69
+ it('a miss leaves none, counts toward the lock, and is audited with its code', async () => {
70
+ const context = request(kim)
71
+ await expect(byPassword(context, 'wrong')).rejects.toMatchObject({ errorCode: 'password-not-matched' })
72
+
73
+ expect(isRecentlyConfirmed(context, kim.id)).toBe(false)
74
+ expect((await getRepository(User).findOneBy({ id: kim.id })).failCount).toBe(1)
75
+ expect(await audits()).toEqual([['error', 'password-not-matched', 'password']])
76
+ })
77
+
78
+ it('the fifth miss locks the account, and a locked account confirms nothing', async () => {
79
+ for (let i = 0; i < 4; i++) await expect(byPassword(request(kim), 'wrong')).rejects.toMatchObject({ errorCode: 'password-not-matched' })
80
+ await expect(byPassword(request(kim), 'wrong')).rejects.toMatchObject({ errorCode: 'user-locked' })
81
+
82
+ const context = request(kim)
83
+ await expect(byPassword(context, PASSWORD)).rejects.toMatchObject({ errorCode: 'user-locked' })
84
+ expect(isRecentlyConfirmed(context, kim.id)).toBe(false)
85
+ })
@@ -0,0 +1,146 @@
1
+ /**
2
+ * `@recentConfirmation` (ADR-0168) - the seam. A door that asks for a recent confirmation opens only when the
3
+ * person confirmed in this browser within the last ten minutes, as themself; passing does not lengthen it. Built as SDL the way
4
+ * `privilege-directive.test.ts` builds its schema - the wrapper sees only the field, `context.state` and the cookies.
5
+ */
6
+
7
+ import { graphql } from 'graphql'
8
+ import { makeExecutableSchema } from '@graphql-tools/schema'
9
+ import jwt from 'jsonwebtoken'
10
+
11
+ import { loadCompiled } from './compiled'
12
+
13
+ const { privilegeDirectiveTypeDefs, privilegeDirectiveResolver } = loadCompiled('service/privilege/privilege-directive')
14
+ const confirmation = loadCompiled('service/identity-confirmation/identity-confirmation')
15
+ const { SECRET } = loadCompiled('utils/get-secret')
16
+ const { User } = loadCompiled('service/user/user')
17
+
18
+ const DOMAIN = { id: 'domain-1', subdomain: 'office' }
19
+ const KIM = { id: 'kim', domains: [DOMAIN] }
20
+ const LEE = { id: 'lee', domains: [DOMAIN] }
21
+
22
+ const typeDefs = `
23
+ type Query {
24
+ myPayslips: String @privilege(category: "office-self", privilege: "query") @recentConfirmation
25
+ myLeave: String @privilege(category: "office-self", privilege: "query")
26
+ }
27
+ `
28
+
29
+ function buildSchema(defs = typeDefs) {
30
+ return privilegeDirectiveResolver(
31
+ makeExecutableSchema({
32
+ typeDefs: [privilegeDirectiveTypeDefs, confirmation.recentConfirmationDirectiveTypeDefs, defs],
33
+ resolvers: { Query: { myPayslips: () => 'slips', myLeave: () => 'leave' } }
34
+ })
35
+ )
36
+ }
37
+
38
+ /** A browser: the cookies it holds between requests. */
39
+ function browser() {
40
+ const jar = new Map<string, string>()
41
+ const options = new Map<string, any>()
42
+ return {
43
+ jar,
44
+ options,
45
+ cookies: {
46
+ get: (name: string) => jar.get(name) || undefined,
47
+ set: (name: string, value: string, option: any) => (jar.set(name, value), options.set(name, option))
48
+ }
49
+ }
50
+ }
51
+
52
+ function run(schema: any, field: string, user: any, at: ReturnType<typeof browser>) {
53
+ return graphql({ schema, source: `{ ${field} }`, contextValue: { state: { domain: DOMAIN, user }, cookies: at.cookies } })
54
+ }
55
+
56
+ const refusalOf = (result: any) => result.errors?.[0]?.extensions?.refusal?.code
57
+
58
+ beforeEach(() => {
59
+ User.hasPrivilege = jest.fn().mockResolvedValue(true)
60
+ ;(process as any).domainOwnerGranted = jest.fn().mockResolvedValue(false)
61
+ ;(process as any).superUserGranted = jest.fn().mockResolvedValue(false)
62
+ })
63
+
64
+ afterEach(() => {
65
+ delete (process as any).domainOwnerGranted
66
+ delete (process as any).superUserGranted
67
+ })
68
+
69
+ it('refuses a door that asks for it when the person has not confirmed, with a code the screen can name', async () => {
70
+ const result = await run(buildSchema(), 'myPayslips', KIM, browser())
71
+
72
+ expect(result.data?.myPayslips ?? null).toBeNull()
73
+ expect(refusalOf(result)).toBe('identity-confirmation-required')
74
+ expect(result.errors?.[0]?.extensions?.code).toBe('IDENTITY_CONFIRMATION_REQUIRED')
75
+ })
76
+
77
+ it('opens it after the person confirmed, and does not lengthen the confirmation as it passes', async () => {
78
+ const at = browser()
79
+ confirmation.rememberConfirmation({ cookies: at.cookies }, KIM.id)
80
+ const given = at.jar.get('identity_confirmed')
81
+
82
+ await new Promise(resolve => setTimeout(resolve, 1100))
83
+ const result = await run(buildSchema(), 'myPayslips', KIM, at)
84
+
85
+ expect(result.errors).toBeUndefined()
86
+ expect(result.data).toEqual({ myPayslips: 'slips' })
87
+ expect(at.jar.get('identity_confirmed')).toBe(given)
88
+ expect(at.options.get('identity_confirmed')).toMatchObject({ httpOnly: true, maxAge: 10 * 60 * 1000 })
89
+ })
90
+
91
+ it('holds for ten minutes from the moment it was given, and says until when', () => {
92
+ const at = browser()
93
+ const before = Date.now()
94
+ confirmation.rememberConfirmation({ cookies: at.cookies }, KIM.id)
95
+
96
+ const until = confirmation.confirmedUntil({ cookies: at.cookies }, KIM.id).getTime()
97
+ expect(until).toBeGreaterThanOrEqual(Math.floor(before / 1000) * 1000 + 10 * 60 * 1000)
98
+ expect(until).toBeLessThanOrEqual(Date.now() + 10 * 60 * 1000)
99
+ expect(confirmation.confirmedUntil({ cookies: at.cookies }, LEE.id)).toBeNull()
100
+ })
101
+
102
+ it("does not open on another person's confirmation", async () => {
103
+ const at = browser()
104
+ confirmation.rememberConfirmation({ cookies: at.cookies }, LEE.id)
105
+
106
+ expect(refusalOf(await run(buildSchema(), 'myPayslips', KIM, at))).toBe('identity-confirmation-required')
107
+ })
108
+
109
+ it('does not open on an expired confirmation, a forged one, or one let go', async () => {
110
+ const expired = browser()
111
+ expired.jar.set('identity_confirmed', jwt.sign({ sub: KIM.id, purpose: 'identity-confirmation' }, SECRET, { expiresIn: -10 }))
112
+ expect(refusalOf(await run(buildSchema(), 'myPayslips', KIM, expired))).toBe('identity-confirmation-required')
113
+
114
+ const forged = browser()
115
+ forged.jar.set('identity_confirmed', jwt.sign({ sub: KIM.id, purpose: 'identity-confirmation' }, 'another secret', { expiresIn: 300 }))
116
+ expect(refusalOf(await run(buildSchema(), 'myPayslips', KIM, forged))).toBe('identity-confirmation-required')
117
+
118
+ const signedIn = browser()
119
+ signedIn.jar.set('identity_confirmed', jwt.sign({ sub: KIM.id }, SECRET, { expiresIn: 300 }))
120
+ expect(refusalOf(await run(buildSchema(), 'myPayslips', KIM, signedIn))).toBe('identity-confirmation-required')
121
+
122
+ const leftAlone = browser()
123
+ confirmation.rememberConfirmation({ cookies: leftAlone.cookies }, KIM.id)
124
+ confirmation.forgetConfirmation({ cookies: leftAlone.cookies })
125
+ expect(refusalOf(await run(buildSchema(), 'myPayslips', KIM, leftAlone))).toBe('identity-confirmation-required')
126
+ })
127
+
128
+ it('asks the privilege first - a person without it is refused for the privilege, confirmed or not', async () => {
129
+ User.hasPrivilege = jest.fn().mockResolvedValue(false)
130
+ const at = browser()
131
+ confirmation.rememberConfirmation({ cookies: at.cookies }, KIM.id)
132
+
133
+ const result = await run(buildSchema(), 'myPayslips', KIM, at)
134
+ expect(result.errors?.[0]?.extensions?.code).not.toBe('IDENTITY_CONFIRMATION_REQUIRED')
135
+ })
136
+
137
+ it('leaves a door that does not ask alone', async () => {
138
+ const result = await run(buildSchema(), 'myLeave', KIM, browser())
139
+
140
+ expect(result.errors).toBeUndefined()
141
+ expect(result.data).toEqual({ myLeave: 'leave' })
142
+ })
143
+
144
+ it('refuses to build a door that asks for the confirmation without a privilege beside it', () => {
145
+ expect(() => buildSchema('type Query { myPayslips: String @recentConfirmation myLeave: String }')).toThrow('has no @privilege beside it')
146
+ })
@@ -154,7 +154,7 @@ describe('지우기 — 하나', () => {
154
154
  it('가진 사람이 있으면 그 이유로 거절한다', async () => {
155
155
  const role = await makeRole('in-use', child, [boardQuery], [actor])
156
156
 
157
- await expect(mutation.deleteRole(role.id, context(child))).rejects.toThrow('currently in use')
157
+ expect(await refusalCodeOf(mutation.deleteRole(role.id, context(child)))).toBe('role-in-use')
158
158
  expect(await getRepository(Role).findOneBy({ id: role.id })).not.toBeNull()
159
159
  })
160
160
 
@@ -181,15 +181,31 @@ describe('지우기 — 여럿', () => {
181
181
  expect(await getRepository(Role).findOneBy({ id: theirs.id })).not.toBeNull()
182
182
  })
183
183
 
184
- it('사용 중인 역할은 남긴다 — 하나 지우기와 같은 규칙이다', async () => {
184
+ it('★ 사용 중인 역할이 하나라도 있으면 아무것도 안 지우고 그 이름과 사람 수로 거절한다 — 하나 지우기와 같은 거절이다', async () => {
185
185
  const unused = await makeRole('unused', child)
186
186
  const inUse = await makeRole('in-use', child, [], [actor])
187
187
 
188
- await mutation.deleteRoles([unused.id, inUse.id], context(child))
188
+ let refusal: any
189
+ try {
190
+ await mutation.deleteRoles([unused.id, inUse.id], context(child))
191
+ } catch (error) {
192
+ refusal = error
193
+ }
189
194
 
190
- expect(await getRepository(Role).findOneBy({ id: unused.id })).toBeNull()
195
+ expect(refusal?.code).toBe('role-in-use')
196
+ expect(refusal?.params).toEqual({ names: 'in-use (1)' })
197
+ expect(await getRepository(Role).findOneBy({ id: unused.id })).not.toBeNull()
191
198
  expect(await getRepository(Role).findOneBy({ id: inUse.id })).not.toBeNull()
192
199
  })
200
+
201
+ it('아무도 안 가진 역할은 함께 지운다', async () => {
202
+ const one = await makeRole('one', child)
203
+ const two = await makeRole('two', child)
204
+
205
+ expect(await mutation.deleteRoles([one.id, two.id], context(child))).toBe(true)
206
+ expect(await getRepository(Role).findOneBy({ id: one.id })).toBeNull()
207
+ expect(await getRepository(Role).findOneBy({ id: two.id })).toBeNull()
208
+ })
193
209
  })
194
210
 
195
211
  /**
@@ -159,6 +159,32 @@ describe('권한 수', () => {
159
159
  })
160
160
  })
161
161
 
162
+ /* 가진 사람 수 — 가진 사람이 있는 역할은 지우지 않으므로 목록이 지우기 전에 보인다(2026-10-28). */
163
+ describe('가진 사람 수', () => {
164
+ async function holder(roles: any[]) {
165
+ const salt = User.generateSalt()
166
+ const tag = Math.random().toString(36).slice(2, 8)
167
+ return await getRepository(User).save({ username: `h-${tag}`, email: `h-${tag}@example.com`, name: 'Lee', salt, status: 'activated', roles })
168
+ }
169
+
170
+ it('그 역할을 가진 사람만 센다', async () => {
171
+ const mine = await getRepository(Role).save({ name: 'mine', domain: child, privileges: [] })
172
+ const other = await getRepository(Role).save({ name: 'other', domain: child, privileges: [] })
173
+ await holder([mine])
174
+ await holder([mine, other])
175
+ await holder([other])
176
+
177
+ expect(await query.userCount(mine)).toBe(2)
178
+ expect(await query.userCount(other)).toBe(2)
179
+ })
180
+
181
+ it('아무도 없으면 0 이다', async () => {
182
+ const role = await getRepository(Role).save({ name: 'blank', domain: child, privileges: [] })
183
+
184
+ expect(await query.userCount(role)).toBe(0)
185
+ })
186
+ })
187
+
162
188
  /**
163
189
  * 한 사람의 역할 — `rolePrivileges` 와 **같은 결함을 갖고 있던** 자리.
164
190
  *
@@ -13,6 +13,7 @@
13
13
  "error.password used in the past": "password used in the past",
14
14
  "error.role-name-stands-inherited": "a role called {name} is already visible here, defined in {domain}. stand this domain's own of that name only if that is what you mean to do.",
15
15
  "error.role-name-taken-here": "this domain already has a role called {name}.",
16
+ "error.role-in-use": "{names} are held by users and cannot be deleted. Take them off the users first.",
16
17
  "error.subdomain not found": "domain not found",
17
18
  "error.token or password is invalid": "token or password is invalid",
18
19
  "error.unavailable-domain": "unavailable domain",
@@ -266,5 +267,6 @@
266
267
  "privilege.allows.audit.query": "see everyone’s sign-ins, privilege use and refusals, and role changes",
267
268
  "task.auth-base.audit-reading.name": "Reading the audit log",
268
269
  "task.auth-base.audit-reading.description": "Reads everyone's sign-ins, privilege use and refusals, and role changes in the domain.",
269
- "error.access-request-task-mismatch": "the work you picked does not include the refused privilege."
270
+ "error.access-request-task-mismatch": "the work you picked does not include the refused privilege.",
271
+ "error.identity confirmation required": "confirm it is you to see this"
270
272
  }
@@ -13,6 +13,7 @@
13
13
  "error.password used in the past": "contraseña usada anteriormente",
14
14
  "error.role-name-stands-inherited": "ya se muestra aquí un rol llamado {name}, definido en {domain}. cree en este dominio uno propio con ese nombre solo si es lo que pretende hacer.",
15
15
  "error.role-name-taken-here": "este dominio ya tiene un rol llamado {name}.",
16
+ "error.role-in-use": "{names} están asignados a usuarios y no se pueden eliminar. Quítelos primero de los usuarios.",
16
17
  "error.subdomain not found": "dominio no encontrado",
17
18
  "error.token or password is invalid": "el token o la contraseña no son válidos",
18
19
  "error.unavailable-domain": "dominio no disponible",
@@ -266,5 +267,6 @@
266
267
  "privilege.allows.audit.query": "ver los inicios de sesión, el uso y rechazo de privilegios y los cambios de rol de todos",
267
268
  "task.auth-base.audit-reading.name": "Consultar el registro de auditoría",
268
269
  "task.auth-base.audit-reading.description": "Lee los inicios de sesión, el uso y rechazo de privilegios y los cambios de rol de todos en el dominio.",
269
- "error.access-request-task-mismatch": "la tarea elegida no incluye el privilegio rechazado."
270
+ "error.access-request-task-mismatch": "la tarea elegida no incluye el privilegio rechazado.",
271
+ "error.identity confirmation required": "confirme que es usted para ver esto"
270
272
  }
@@ -13,6 +13,7 @@
13
13
  "error.password used in the past": "過去に使用されたパスワードです.",
14
14
  "error.role-name-stands-inherited": "{name} ロールは {domain} に定義されており、ここからも見えています。この$t(term.domain)に同じ名前で別に立てるつもりの場合のみ作成します。",
15
15
  "error.role-name-taken-here": "この$t(term.domain)には既に {name} ロールがあります。",
16
+ "error.role-in-use": "{names} はユーザーに付与されているため削除できません。先にユーザーから外してください。",
16
17
  "error.subdomain not found": "サブドメインが見つかりません.",
17
18
  "error.token or password is invalid": "トークンまたはパスワードが無効です.",
18
19
  "error.unavailable-domain": "使用できない$t(term.domain)です.",
@@ -266,5 +267,6 @@
266
267
  "privilege.allows.audit.query": "全ユーザーのログイン、権限の使用と拒否、ロール変更の記録を見ることができます。",
267
268
  "task.auth-base.audit-reading.name": "監査ログの閲覧",
268
269
  "task.auth-base.audit-reading.description": "全ユーザーのログイン、権限の使用と拒否、ロール変更の記録を読む。",
269
- "error.access-request-task-mismatch": "選んだ業務に拒否された権限が含まれていません。"
270
+ "error.access-request-task-mismatch": "選んだ業務に拒否された権限が含まれていません。",
271
+ "error.identity confirmation required": "本人確認が必要です。"
270
272
  }
@@ -13,6 +13,7 @@
13
13
  "error.password used in the past": "과거에 사용된 비밀번호입니다.",
14
14
  "error.role-name-stands-inherited": "{name} 역할은 {domain}에 정의되어 현재 도메인에 상속되어 있습니다. 상속된 역할을 대체하려는 경우에만 이 $t(term.domain)에 같은 이름의 역할을 생성해 주세요.",
15
15
  "error.role-name-taken-here": "이 $t(term.domain)에 이미 {name} 역할이 있습니다.",
16
+ "error.role-in-use": "{names} 역할은 사용자에게 부여되어 있어 지울 수 없습니다. 사용자에게서 먼저 떼어 주세요.",
16
17
  "error.subdomain not found": "서브도메인을 찾을 수 없습니다.",
17
18
  "error.token or password is invalid": "토큰 또는 비밀번호가 유효하지 않습니다.",
18
19
  "error.unavailable-domain": "사용할 수 없는 $t(term.domain)입니다.",
@@ -266,5 +267,6 @@
266
267
  "privilege.allows.audit.query": "모든 사용자의 로그인, 권한 사용과 거절, 역할 변경 기록을 볼 수 있습니다.",
267
268
  "task.auth-base.audit-reading.name": "감사 기록 열람",
268
269
  "task.auth-base.audit-reading.description": "모든 사용자의 로그인, 권한 사용과 거절, 역할 변경 기록을 읽는다.",
269
- "error.access-request-task-mismatch": "선택한 업무에 접근이 거부된 권한이 포함되어 있지 않습니다."
270
+ "error.access-request-task-mismatch": "선택한 업무에 접근이 거부된 권한이 포함되어 있지 않습니다.",
271
+ "error.identity confirmation required": "본인 확인이 필요합니다."
270
272
  }
@@ -13,6 +13,7 @@
13
13
  "error.password used in the past": "Kata laluan telah digunakan dalam masa lampau",
14
14
  "error.role-name-stands-inherited": "peranan bernama {name} ditakrifkan dalam {domain} dan kelihatan di sini juga. Bina milik domain ini dengan nama yang sama hanya jika itu memang yang dimaksudkan.",
15
15
  "error.role-name-taken-here": "domain ini sudah mempunyai peranan bernama {name}.",
16
+ "error.role-in-use": "{names} dipegang oleh pengguna dan tidak boleh dipadam. Tanggalkan daripada pengguna dahulu.",
16
17
  "error.subdomain not found": "Domain tidak ditemui",
17
18
  "error.token or password is invalid": "Token atau kata laluan tidak sah",
18
19
  "error.unavailable-domain": "Domain tidak tersedia",
@@ -266,5 +267,6 @@
266
267
  "privilege.allows.audit.query": "boleh melihat log masuk, penggunaan dan penolakan keistimewaan, serta perubahan peranan semua orang.",
267
268
  "task.auth-base.audit-reading.name": "Membaca log audit",
268
269
  "task.auth-base.audit-reading.description": "Membaca log masuk, penggunaan dan penolakan keistimewaan, serta perubahan peranan semua orang dalam domain.",
269
- "error.access-request-task-mismatch": "tugas yang dipilih tidak termasuk keistimewaan yang ditolak."
270
+ "error.access-request-task-mismatch": "tugas yang dipilih tidak termasuk keistimewaan yang ditolak.",
271
+ "error.identity confirmation required": "sahkan ini anda untuk melihat ini"
270
272
  }
@@ -13,6 +13,7 @@
13
13
  "error.password used in the past": "使用过的密码!",
14
14
  "error.role-name-stands-inherited": "名为 {name} 的角色定义在 {domain},在此处也可见。只有确实要在该$t(term.domain)中另行建立同名角色时才创建。",
15
15
  "error.role-name-taken-here": "该$t(term.domain)中已有名为 {name} 的角色。",
16
+ "error.role-in-use": "{names} 已分配给用户,无法删除。请先从用户中移除。",
16
17
  "error.subdomain not found": "用户域查询失败!",
17
18
  "error.token or password is invalid": "令牌或密码无效!",
18
19
  "error.unavailable-domain": "不可用的$t(term.domain)名",
@@ -266,5 +267,6 @@
266
267
  "privilege.allows.audit.query": "可以查看所有用户的登录、权限使用与拒绝以及角色变更记录。",
267
268
  "task.auth-base.audit-reading.name": "查看审计日志",
268
269
  "task.auth-base.audit-reading.description": "读取所有用户的登录、权限使用与拒绝以及角色变更记录。",
269
- "error.access-request-task-mismatch": "所选业务不包含被拒绝的权限。"
270
+ "error.access-request-task-mismatch": "所选业务不包含被拒绝的权限。",
271
+ "error.identity confirmation required": "需要确认身份。"
270
272
  }