@things-factory/auth-base 10.1.198 → 10.1.203

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 (50) hide show
  1. package/dist-client/tsconfig.tsbuildinfo +1 -1
  2. package/dist-server/ai-entry/access-assistant.js +4 -3
  3. package/dist-server/ai-entry/access-assistant.js.map +1 -1
  4. package/dist-server/controllers/access-request.d.ts +8 -2
  5. package/dist-server/controllers/access-request.js +33 -6
  6. package/dist-server/controllers/access-request.js.map +1 -1
  7. package/dist-server/service/access-request/access-request-query.d.ts +7 -2
  8. package/dist-server/service/access-request/access-request-query.js +25 -4
  9. package/dist-server/service/access-request/access-request-query.js.map +1 -1
  10. package/dist-server/service/access-request/access-request-types.d.ts +7 -0
  11. package/dist-server/service/access-request/access-request-types.js +27 -1
  12. package/dist-server/service/access-request/access-request-types.js.map +1 -1
  13. package/dist-server/service/access-request/access-request.d.ts +1 -0
  14. package/dist-server/service/access-request/access-request.js +5 -0
  15. package/dist-server/service/access-request/access-request.js.map +1 -1
  16. package/dist-server/service/access-request/index.d.ts +1 -1
  17. package/dist-server/service/audit/index.d.ts +1 -1
  18. package/dist-server/service/index.d.ts +1 -1
  19. package/dist-server/service/open-fields.js +1 -0
  20. package/dist-server/service/open-fields.js.map +1 -1
  21. package/dist-server/service/role-template/index.d.ts +1 -1
  22. package/dist-server/service/role-template/role-task.d.ts +7 -0
  23. package/dist-server/service/role-template/role-task.js +16 -0
  24. package/dist-server/service/role-template/role-task.js.map +1 -1
  25. package/dist-server/tsconfig.tsbuildinfo +1 -1
  26. package/dist-server/utils/access-request-review.d.ts +12 -2
  27. package/dist-server/utils/access-request-review.js +13 -3
  28. package/dist-server/utils/access-request-review.js.map +1 -1
  29. package/package.json +2 -2
  30. package/tests/access-assistant-db.test.ts +1 -1
  31. package/tests/access-request-db.test.ts +71 -0
  32. package/tests/access-request-review.test.ts +30 -0
  33. package/translations/en.json +2 -1
  34. package/translations/es.json +2 -1
  35. package/translations/ja.json +2 -1
  36. package/translations/ko.json +2 -1
  37. package/translations/ms.json +2 -1
  38. package/translations/zh.json +2 -1
  39. package/dist-server/service/login-history/index.d.ts +0 -4
  40. package/dist-server/service/login-history/index.js +0 -8
  41. package/dist-server/service/login-history/index.js.map +0 -1
  42. package/dist-server/service/login-history/login-history-query.d.ts +0 -10
  43. package/dist-server/service/login-history/login-history-query.js +0 -80
  44. package/dist-server/service/login-history/login-history-query.js.map +0 -1
  45. package/dist-server/service/login-history/login-history-type.d.ts +0 -5
  46. package/dist-server/service/login-history/login-history-type.js +0 -21
  47. package/dist-server/service/login-history/login-history-type.js.map +0 -1
  48. package/dist-server/service/login-history/login-history.d.ts +0 -12
  49. package/dist-server/service/login-history/login-history.js +0 -58
  50. package/dist-server/service/login-history/login-history.js.map +0 -1
@@ -36,12 +36,18 @@ export interface DecisionFacts {
36
36
  } | null;
37
37
  /** The requester holds the privilege now - through some other role, since the request was made. */
38
38
  requesterHoldsIt: boolean;
39
- /** Every assignable role that grants the privilege, with how many privileges each carries. */
39
+ /** Every assignable role that grants the privilege, with how many privileges each carries and whether it holds the task asked. */
40
40
  candidates: {
41
41
  id: string;
42
42
  name: string;
43
43
  privilegeCount: number;
44
+ holdsTask?: boolean;
44
45
  }[];
46
+ /** The task the requester asked for, with its declared privileges - null when none was asked or it is gone (ADR-0103 decision 9). */
47
+ task?: {
48
+ id: string;
49
+ privileges: PrivilegeName[];
50
+ } | null;
45
51
  /** The decider's own privileges - to tell them when a role gives what they do not have. */
46
52
  deciderPrivileges: PrivilegeName[];
47
53
  /** Whether the decider is a domain owner - an owner may give anything. */
@@ -64,9 +70,13 @@ export interface DecisionIssue {
64
70
  export declare function effectiveState(state: AccessRequestState, expiresAt: Date | string | null | undefined, now: Date): AccessRequestState;
65
71
  export declare function expiryFrom(now: Date, days?: number): Date;
66
72
  export declare function reviewAccessDecision(facts: DecisionFacts): DecisionIssue[];
67
- /** The smallest candidate - what the draft card proposes by default. Ties go by name, so the answer is the same every time. */
73
+ /**
74
+ * The candidate the draft card proposes by default - one holding the task asked, if any does, then the smallest. Ties go
75
+ * by name, so the answer is the same every time; the order is `rolesGranting`'s.
76
+ */
68
77
  export declare function smallestRole<T extends {
69
78
  name: string;
70
79
  privilegeCount: number;
80
+ holdsTask?: boolean;
71
81
  }>(candidates: T[]): T | undefined;
72
82
  export declare const isBlocked: (issues: DecisionIssue[]) => boolean;
@@ -95,11 +95,18 @@ function grantIssues(facts, asked) {
95
95
  params: { role: role.name, count: more.length, privileges: more.map(nameOf).join(', ') }
96
96
  });
97
97
  }
98
+ /* Told, not refused: the role opens the refused door, and the rest of the work asked for is the decider's call. */
99
+ const missingOfTask = (facts.task?.privileges || []).filter(p => !role.privileges.some(r => same(r, p)));
100
+ if (facts.task && missingOfTask.length > 0) {
101
+ issues.push({ code: 'role-lacks-task', blocking: false, field: 'role', params: { role: role.name, task: facts.task.id, privileges: missingOfTask.map(nameOf).join(', ') } });
102
+ }
98
103
  if (!same(asked, exports.ROLE_GRANTING_PRIVILEGE) && role.privileges.some(p => same(p, exports.ROLE_GRANTING_PRIVILEGE))) {
99
104
  issues.push({ code: 'role-grants-admin', blocking: false, field: 'role', params: { role: role.name } });
100
105
  }
106
+ /* A smaller role that drops the task asked for is not a better answer - only one holding it as the chosen does counts. */
107
+ const choiceHoldsTask = !!facts.task && missingOfTask.length === 0;
101
108
  const smaller = facts.candidates
102
- .filter(c => c.id !== role.id && c.privilegeCount < role.privileges.length)
109
+ .filter(c => c.id !== role.id && c.privilegeCount < role.privileges.length && (!choiceHoldsTask || c.holdsTask))
103
110
  .sort((a, b) => a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0];
104
111
  if (smaller) {
105
112
  issues.push({ code: 'smaller-role-exists', blocking: false, field: 'role', params: { role: smaller.name, count: smaller.privilegeCount } });
@@ -113,9 +120,12 @@ function grantIssues(facts, asked) {
113
120
  }
114
121
  return issues;
115
122
  }
116
- /** The smallest candidate - what the draft card proposes by default. Ties go by name, so the answer is the same every time. */
123
+ /**
124
+ * The candidate the draft card proposes by default - one holding the task asked, if any does, then the smallest. Ties go
125
+ * by name, so the answer is the same every time; the order is `rolesGranting`'s.
126
+ */
117
127
  function smallestRole(candidates) {
118
- return [...candidates].sort((a, b) => a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0];
128
+ return [...candidates].sort((a, b) => Number(!!b.holdsTask) - Number(!!a.holdsTask) || a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0];
119
129
  }
120
130
  const isBlocked = (issues) => issues.some(issue => issue.blocking);
121
131
  exports.isBlocked = isBlocked;
@@ -1 +1 @@
1
- {"version":3,"file":"access-request-review.js","sourceRoot":"","sources":["../../server/utils/access-request-review.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;;AAyDH,wCAIC;AAED,gCAEC;AAED,oDAuCC;AA6DD,oCAEC;AAjKD,yGAAyG;AAC5F,QAAA,uBAAuB,GAAkB,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,CAAA;AAEjG,8DAA8D;AACjD,QAAA,2BAA2B,GAAG,EAAE,CAAA;AAyC7C,MAAM,IAAI,GAAG,CAAC,CAAgB,EAAE,CAAgB,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,SAAS,CAAA;AAC7G,MAAM,MAAM,GAAG,CAAC,CAAgB,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,SAAS,EAAE,CAAA;AAEnE,kFAAkF;AAClF,SAAgB,cAAc,CAAC,KAAyB,EAAE,SAA2C,EAAE,GAAS;IAC9G,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,SAAS;QAAE,OAAO,KAAK,CAAA;IACnD,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,CAAA;IAC9B,OAAO,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;AAChF,CAAC;AAED,SAAgB,UAAU,CAAC,GAAS,EAAE,IAAI,GAAG,mCAA2B;IACtE,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;AAC7D,CAAC;AAED,SAAgB,oBAAoB,CAAC,KAAoB;IACvD,MAAM,MAAM,GAAoB,EAAE,CAAA;IAClC,MAAM,EAAE,OAAO,EAAE,GAAG,KAAK,CAAA;IAEzB,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,CAAC,EAAE,IAAI,EAAE,0BAA0B,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAA;IACjF,CAAC;IAED,MAAM,KAAK,GAAG,cAAc,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,CAAA;IACzE,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,qBAAqB,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,CAAA;IACnG,CAAC;IAED,IAAI,OAAO,CAAC,WAAW,KAAK,KAAK,CAAC,SAAS,EAAE,CAAC;QAC5C,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAA;IACxE,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;QAC7E,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,uBAAuB,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAA;IAClF,CAAC;IAED,IAAI,KAAK,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QAC9B,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;YAC1C,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAA;QAC7E,CAAC;IACH,CAAC;SAAM,CAAC;QACN,MAAM,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAA;IAClG,CAAC;IAED,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;QACvB,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,eAAe;YACrB,QAAQ,EAAE,KAAK;YACf,KAAK,EAAE,SAAS;YAChB,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,YAAY,CAAC,MAAM,IAAI,EAAE,EAAE;SAClI,CAAC,CAAA;IACJ,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAED,SAAS,WAAW,CAAC,KAAoB,EAAE,KAAoB;IAC7D,MAAM,MAAM,GAAoB,EAAE,CAAA;IAElC,IAAI,KAAK,CAAC,gBAAgB,EAAE,CAAC;QAC3B,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAA;IAChH,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAChB,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,KAAK,CAAC,yBAAyB,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,eAAe;YAC/E,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,MAAM;SACd,CAAC,CAAA;QACF,OAAO,MAAM,CAAA;IACf,CAAC;IAED,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAA;IACtB,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;QAC/C,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,qBAAqB,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAA;QAClI,OAAO,MAAM,CAAA;IACf,CAAC;IAED,IAAI,KAAK,CAAC,aAAa,IAAI,CAAC,KAAK,CAAC,2BAA2B,EAAE,CAAC;QAC9D,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,wBAAwB,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;IAC7G,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAA;IACzD,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,kBAAkB;YACxB,QAAQ,EAAE,KAAK;YACf,KAAK,EAAE,MAAM;YACb,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;SACzF,CAAC,CAAA;IACJ,CAAC;IAED,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,+BAAuB,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,+BAAuB,CAAC,CAAC,EAAE,CAAC;QACzG,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;IACzG,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,UAAU;SAC7B,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;SAC1E,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,cAAc,GAAG,CAAC,CAAC,cAAc,IAAI,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACzF,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,qBAAqB,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAA;IAC7I,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;QAC3F,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,uHAAuH;YACvH,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAA;QACnI,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAED,+HAA+H;AAC/H,SAAgB,YAAY,CAAqD,UAAe;IAC9F,OAAO,CAAC,GAAG,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,cAAc,GAAG,CAAC,CAAC,cAAc,IAAI,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAC/G,CAAC;AAEM,MAAM,SAAS,GAAG,CAAC,MAAuB,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;AAA7E,QAAA,SAAS,aAAoE","sourcesContent":["/*\n * The checks an access decision passes - one function, read by the list screen, by the AI 입력 draft card\n * (its `check`, when the card is made and again when it is pressed) and by `decideAccessRequest` itself\n * (ADR-0103 decision 4: the card and the screen cannot say different things).\n *\n * Pure: the facts are collected by `access-request-facts.ts`; this decides what they mean. A model never\n * judges a role as risky - this does, the same way every time, and the model only words the result.\n *\n * Blocking issues keep the decision from being written. The rest are told to the person deciding.\n */\n\nexport type AccessRequestState = 'PENDING' | 'GRANTED' | 'DENIED' | 'WITHDRAWN' | 'EXPIRED'\n\nexport type AccessDecisionKind = 'GRANT' | 'DENY'\n\nexport type PrivilegeName = { category: string; privilege: string }\n\n/** The privilege that lets its holder put people into roles - a role carrying it hands that power on. */\nexport const ROLE_GRANTING_PRIVILEGE: PrivilegeName = { category: 'user', privilege: 'mutation' }\n\n/** How long a request waits for a decision (questions Q7). */\nexport const ACCESS_REQUEST_PENDING_DAYS = 14\n\nexport interface DecisionFacts {\n request: {\n state: AccessRequestState\n requesterId: string\n category: string\n privilege: string\n expiresAt?: Date | string | null\n } | null\n deciderId: string\n decision: AccessDecisionKind\n reason?: string | null\n /** The role chosen to grant - absent when it is not this domain's to assign. */\n role?: { id: string; name: string; privileges: PrivilegeName[] } | null\n /** A role id was named but not found in the domain's scope. */\n roleNamedButNotAssignable?: boolean\n /** The chosen role is the parent domain's - and whether the decider may give it (ADR-0103 decision 8). */\n roleIsParents?: boolean\n deciderMayChangeParentRoles?: boolean\n requester: { active: boolean; inDomain: boolean } | null\n /** The requester holds the privilege now - through some other role, since the request was made. */\n requesterHoldsIt: boolean\n /** Every assignable role that grants the privilege, with how many privileges each carries. */\n candidates: { id: string; name: string; privilegeCount: number }[]\n /** The decider's own privileges - to tell them when a role gives what they do not have. */\n deciderPrivileges: PrivilegeName[]\n /** Whether the decider is a domain owner - an owner may give anything. */\n deciderIsOwner: boolean\n /** The latest earlier denial of the same requester and privilege. */\n deniedBefore?: { by: string; at: Date | string; reason?: string | null } | null\n now: Date\n}\n\nexport interface DecisionIssue {\n code: string\n blocking: boolean\n field?: 'request' | 'decision' | 'role' | 'reason'\n params?: Record<string, string | number | boolean | null>\n}\n\nconst same = (a: PrivilegeName, b: PrivilegeName) => a.category === b.category && a.privilege === b.privilege\nconst nameOf = (p: PrivilegeName) => `${p.category}:${p.privilege}`\n\n/** A pending request past its time is expired, whatever the column still says. */\nexport function effectiveState(state: AccessRequestState, expiresAt: Date | string | null | undefined, now: Date): AccessRequestState {\n if (state !== 'PENDING' || !expiresAt) return state\n const at = new Date(expiresAt)\n return isNaN(at.getTime()) || at.getTime() > now.getTime() ? state : 'EXPIRED'\n}\n\nexport function expiryFrom(now: Date, days = ACCESS_REQUEST_PENDING_DAYS): Date {\n return new Date(now.getTime() + days * 24 * 60 * 60 * 1000)\n}\n\nexport function reviewAccessDecision(facts: DecisionFacts): DecisionIssue[] {\n const issues: DecisionIssue[] = []\n const { request } = facts\n\n if (!request) {\n return [{ code: 'access-request-not-found', blocking: true, field: 'request' }]\n }\n\n const state = effectiveState(request.state, request.expiresAt, facts.now)\n if (state !== 'PENDING') {\n issues.push({ code: 'request-not-pending', blocking: true, field: 'request', params: { state } })\n }\n\n if (request.requesterId === facts.deciderId) {\n issues.push({ code: 'own-request', blocking: true, field: 'request' })\n }\n\n if (!facts.requester || !facts.requester.inDomain || !facts.requester.active) {\n issues.push({ code: 'requester-unavailable', blocking: true, field: 'request' })\n }\n\n if (facts.decision === 'DENY') {\n if (!facts.reason || !facts.reason.trim()) {\n issues.push({ code: 'deny-needs-reason', blocking: true, field: 'reason' })\n }\n } else {\n issues.push(...grantIssues(facts, { category: request.category, privilege: request.privilege }))\n }\n\n if (facts.deniedBefore) {\n issues.push({\n code: 'denied-before',\n blocking: false,\n field: 'request',\n params: { by: facts.deniedBefore.by, at: new Date(facts.deniedBefore.at).toISOString(), reason: facts.deniedBefore.reason ?? '' }\n })\n }\n\n return issues\n}\n\nfunction grantIssues(facts: DecisionFacts, asked: PrivilegeName): DecisionIssue[] {\n const issues: DecisionIssue[] = []\n\n if (facts.requesterHoldsIt) {\n issues.push({ code: 'already-holds', blocking: true, field: 'request', params: { privilege: nameOf(asked) } })\n }\n\n if (!facts.role) {\n issues.push({\n code: facts.roleNamedButNotAssignable ? 'role-not-assignable' : 'role-required',\n blocking: true,\n field: 'role'\n })\n return issues\n }\n\n const { role } = facts\n if (!role.privileges.some(p => same(p, asked))) {\n issues.push({ code: 'role-does-not-grant', blocking: true, field: 'role', params: { role: role.name, privilege: nameOf(asked) } })\n return issues\n }\n\n if (facts.roleIsParents && !facts.deciderMayChangeParentRoles) {\n issues.push({ code: 'role-belongs-to-parent', blocking: true, field: 'role', params: { role: role.name } })\n }\n\n const more = role.privileges.filter(p => !same(p, asked))\n if (more.length > 0) {\n issues.push({\n code: 'role-grants-more',\n blocking: false,\n field: 'role',\n params: { role: role.name, count: more.length, privileges: more.map(nameOf).join(', ') }\n })\n }\n\n if (!same(asked, ROLE_GRANTING_PRIVILEGE) && role.privileges.some(p => same(p, ROLE_GRANTING_PRIVILEGE))) {\n issues.push({ code: 'role-grants-admin', blocking: false, field: 'role', params: { role: role.name } })\n }\n\n const smaller = facts.candidates\n .filter(c => c.id !== role.id && c.privilegeCount < role.privileges.length)\n .sort((a, b) => a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0]\n if (smaller) {\n issues.push({ code: 'smaller-role-exists', blocking: false, field: 'role', params: { role: smaller.name, count: smaller.privilegeCount } })\n }\n\n if (!facts.deciderIsOwner) {\n const lacking = role.privileges.filter(p => !facts.deciderPrivileges.some(d => same(d, p)))\n if (lacking.length > 0) {\n /* Told, not refused (questions Q6) - the role screen gives the same role today; ruling both is a separate decision. */\n issues.push({ code: 'decider-lacks-it', blocking: false, field: 'role', params: { privileges: lacking.map(nameOf).join(', ') } })\n }\n }\n\n return issues\n}\n\n/** The smallest candidate - what the draft card proposes by default. Ties go by name, so the answer is the same every time. */\nexport function smallestRole<T extends { name: string; privilegeCount: number }>(candidates: T[]): T | undefined {\n return [...candidates].sort((a, b) => a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0]\n}\n\nexport const isBlocked = (issues: DecisionIssue[]) => issues.some(issue => issue.blocking)\n"]}
1
+ {"version":3,"file":"access-request-review.js","sourceRoot":"","sources":["../../server/utils/access-request-review.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The checks an access decision passes - one function, read by the list screen, by the AI 입력 draft card\n * (its `check`, when the card is made and again when it is pressed) and by `decideAccessRequest` itself\n * (ADR-0103 decision 4: the card and the screen cannot say different things).\n *\n * Pure: the facts are collected by `access-request-facts.ts`; this decides what they mean. A model never\n * judges a role as risky - this does, the same way every time, and the model only words the result.\n *\n * Blocking issues keep the decision from being written. The rest are told to the person deciding.\n */\n\nexport type AccessRequestState = 'PENDING' | 'GRANTED' | 'DENIED' | 'WITHDRAWN' | 'EXPIRED'\n\nexport type AccessDecisionKind = 'GRANT' | 'DENY'\n\nexport type PrivilegeName = { category: string; privilege: string }\n\n/** The privilege that lets its holder put people into roles - a role carrying it hands that power on. */\nexport const ROLE_GRANTING_PRIVILEGE: PrivilegeName = { category: 'user', privilege: 'mutation' }\n\n/** How long a request waits for a decision (questions Q7). */\nexport const ACCESS_REQUEST_PENDING_DAYS = 14\n\nexport interface DecisionFacts {\n request: {\n state: AccessRequestState\n requesterId: string\n category: string\n privilege: string\n expiresAt?: Date | string | null\n } | null\n deciderId: string\n decision: AccessDecisionKind\n reason?: string | null\n /** The role chosen to grant - absent when it is not this domain's to assign. */\n role?: { id: string; name: string; privileges: PrivilegeName[] } | null\n /** A role id was named but not found in the domain's scope. */\n roleNamedButNotAssignable?: boolean\n /** The chosen role is the parent domain's - and whether the decider may give it (ADR-0103 decision 8). */\n roleIsParents?: boolean\n deciderMayChangeParentRoles?: boolean\n requester: { active: boolean; inDomain: boolean } | null\n /** The requester holds the privilege now - through some other role, since the request was made. */\n requesterHoldsIt: boolean\n /** Every assignable role that grants the privilege, with how many privileges each carries and whether it holds the task asked. */\n candidates: { id: string; name: string; privilegeCount: number; holdsTask?: boolean }[]\n /** The task the requester asked for, with its declared privileges - null when none was asked or it is gone (ADR-0103 decision 9). */\n task?: { id: string; privileges: PrivilegeName[] } | null\n /** The decider's own privileges - to tell them when a role gives what they do not have. */\n deciderPrivileges: PrivilegeName[]\n /** Whether the decider is a domain owner - an owner may give anything. */\n deciderIsOwner: boolean\n /** The latest earlier denial of the same requester and privilege. */\n deniedBefore?: { by: string; at: Date | string; reason?: string | null } | null\n now: Date\n}\n\nexport interface DecisionIssue {\n code: string\n blocking: boolean\n field?: 'request' | 'decision' | 'role' | 'reason'\n params?: Record<string, string | number | boolean | null>\n}\n\nconst same = (a: PrivilegeName, b: PrivilegeName) => a.category === b.category && a.privilege === b.privilege\nconst nameOf = (p: PrivilegeName) => `${p.category}:${p.privilege}`\n\n/** A pending request past its time is expired, whatever the column still says. */\nexport function effectiveState(state: AccessRequestState, expiresAt: Date | string | null | undefined, now: Date): AccessRequestState {\n if (state !== 'PENDING' || !expiresAt) return state\n const at = new Date(expiresAt)\n return isNaN(at.getTime()) || at.getTime() > now.getTime() ? state : 'EXPIRED'\n}\n\nexport function expiryFrom(now: Date, days = ACCESS_REQUEST_PENDING_DAYS): Date {\n return new Date(now.getTime() + days * 24 * 60 * 60 * 1000)\n}\n\nexport function reviewAccessDecision(facts: DecisionFacts): DecisionIssue[] {\n const issues: DecisionIssue[] = []\n const { request } = facts\n\n if (!request) {\n return [{ code: 'access-request-not-found', blocking: true, field: 'request' }]\n }\n\n const state = effectiveState(request.state, request.expiresAt, facts.now)\n if (state !== 'PENDING') {\n issues.push({ code: 'request-not-pending', blocking: true, field: 'request', params: { state } })\n }\n\n if (request.requesterId === facts.deciderId) {\n issues.push({ code: 'own-request', blocking: true, field: 'request' })\n }\n\n if (!facts.requester || !facts.requester.inDomain || !facts.requester.active) {\n issues.push({ code: 'requester-unavailable', blocking: true, field: 'request' })\n }\n\n if (facts.decision === 'DENY') {\n if (!facts.reason || !facts.reason.trim()) {\n issues.push({ code: 'deny-needs-reason', blocking: true, field: 'reason' })\n }\n } else {\n issues.push(...grantIssues(facts, { category: request.category, privilege: request.privilege }))\n }\n\n if (facts.deniedBefore) {\n issues.push({\n code: 'denied-before',\n blocking: false,\n field: 'request',\n params: { by: facts.deniedBefore.by, at: new Date(facts.deniedBefore.at).toISOString(), reason: facts.deniedBefore.reason ?? '' }\n })\n }\n\n return issues\n}\n\nfunction grantIssues(facts: DecisionFacts, asked: PrivilegeName): DecisionIssue[] {\n const issues: DecisionIssue[] = []\n\n if (facts.requesterHoldsIt) {\n issues.push({ code: 'already-holds', blocking: true, field: 'request', params: { privilege: nameOf(asked) } })\n }\n\n if (!facts.role) {\n issues.push({\n code: facts.roleNamedButNotAssignable ? 'role-not-assignable' : 'role-required',\n blocking: true,\n field: 'role'\n })\n return issues\n }\n\n const { role } = facts\n if (!role.privileges.some(p => same(p, asked))) {\n issues.push({ code: 'role-does-not-grant', blocking: true, field: 'role', params: { role: role.name, privilege: nameOf(asked) } })\n return issues\n }\n\n if (facts.roleIsParents && !facts.deciderMayChangeParentRoles) {\n issues.push({ code: 'role-belongs-to-parent', blocking: true, field: 'role', params: { role: role.name } })\n }\n\n const more = role.privileges.filter(p => !same(p, asked))\n if (more.length > 0) {\n issues.push({\n code: 'role-grants-more',\n blocking: false,\n field: 'role',\n params: { role: role.name, count: more.length, privileges: more.map(nameOf).join(', ') }\n })\n }\n\n /* Told, not refused: the role opens the refused door, and the rest of the work asked for is the decider's call. */\n const missingOfTask = (facts.task?.privileges || []).filter(p => !role.privileges.some(r => same(r, p)))\n if (facts.task && missingOfTask.length > 0) {\n issues.push({ code: 'role-lacks-task', blocking: false, field: 'role', params: { role: role.name, task: facts.task.id, privileges: missingOfTask.map(nameOf).join(', ') } })\n }\n if (!same(asked, ROLE_GRANTING_PRIVILEGE) && role.privileges.some(p => same(p, ROLE_GRANTING_PRIVILEGE))) {\n issues.push({ code: 'role-grants-admin', blocking: false, field: 'role', params: { role: role.name } })\n }\n\n /* A smaller role that drops the task asked for is not a better answer - only one holding it as the chosen does counts. */\n const choiceHoldsTask = !!facts.task && missingOfTask.length === 0\n const smaller = facts.candidates\n .filter(c => c.id !== role.id && c.privilegeCount < role.privileges.length && (!choiceHoldsTask || c.holdsTask))\n .sort((a, b) => a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0]\n if (smaller) {\n issues.push({ code: 'smaller-role-exists', blocking: false, field: 'role', params: { role: smaller.name, count: smaller.privilegeCount } })\n }\n\n if (!facts.deciderIsOwner) {\n const lacking = role.privileges.filter(p => !facts.deciderPrivileges.some(d => same(d, p)))\n if (lacking.length > 0) {\n /* Told, not refused (questions Q6) - the role screen gives the same role today; ruling both is a separate decision. */\n issues.push({ code: 'decider-lacks-it', blocking: false, field: 'role', params: { privileges: lacking.map(nameOf).join(', ') } })\n }\n }\n\n return issues\n}\n\n/**\n * The candidate the draft card proposes by default - one holding the task asked, if any does, then the smallest. Ties go\n * by name, so the answer is the same every time; the order is `rolesGranting`'s.\n */\nexport function smallestRole<T extends { name: string; privilegeCount: number; holdsTask?: boolean }>(candidates: T[]): T | undefined {\n return [...candidates].sort((a, b) => Number(!!b.holdsTask) - Number(!!a.holdsTask) || a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0]\n}\n\nexport const isBlocked = (issues: DecisionIssue[]) => issues.some(issue => issue.blocking)\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/auth-base",
3
- "version": "10.1.198",
3
+ "version": "10.1.203",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -50,5 +50,5 @@
50
50
  "passport-jwt": "^4.0.0",
51
51
  "passport-local": "^1.0.0"
52
52
  },
53
- "gitHead": "68f884a78ca3a779c2b057ba5b8f6e03f477d701"
53
+ "gitHead": "031aaae3b75b2975e6bce007b3ad7d8d74fe60c8"
54
54
  }
@@ -84,7 +84,7 @@ describe('listPendingAccessRequests', () => {
84
84
  await ask(kim)
85
85
  const answer = await assistant.pendingRequestsToDecide({ domain, user: park })
86
86
  expect(answer.requests).toEqual([
87
- expect.objectContaining({ requester: 'kim', privilege: 'warehouse:mutation', roles: [{ name: 'inbound', privilegeCount: 1 }] })
87
+ expect.objectContaining({ requester: 'kim', privilege: 'warehouse:mutation', task: null, roles: [{ name: 'inbound', privilegeCount: 1, holdsTask: false }] })
88
88
  ])
89
89
  })
90
90
 
@@ -315,3 +315,74 @@ describe('found in review (2026-10-02)', () => {
315
315
  expect(mine.requester?.id).toBe(kim.id)
316
316
  })
317
317
  })
318
+
319
+ describe('asking for a task (ADR-0103 decision 9)', () => {
320
+ const { registerRoleTask } = loadCompiled('service/role-template/role-task')
321
+ const { AccessRequestQuery } = loadCompiled('service/access-request/access-request-query')
322
+
323
+ beforeEach(() => {
324
+ ;(process as any)['ROLE_TASKS'] = {}
325
+ ;(process as any).PRIVILEGES = {
326
+ 'warehouse query': ['warehouse', 'query'],
327
+ 'warehouse mutation': ['warehouse', 'mutation'],
328
+ 'user mutation': ['user', 'mutation']
329
+ }
330
+ registerRoleTask({ id: 'warehouse.keeping', grants: [{ category: 'warehouse', axes: ['query', 'mutation'] }] })
331
+ registerRoleTask({ id: 'warehouse.reading', grants: [{ category: 'warehouse', axes: ['query'] }] })
332
+ })
333
+
334
+ it('★ the tasks holding a refused privilege are listed fewest privileges first', async () => {
335
+ const query = new AccessRequestQuery()
336
+ expect((await query.tasksGranting('warehouse', 'query')).map((one: any) => [one.taskId, one.privilegeCount])).toEqual([
337
+ ['warehouse.reading', 1],
338
+ ['warehouse.keeping', 2]
339
+ ])
340
+ expect((await query.tasksGranting('warehouse', 'mutation')).map((one: any) => one.taskId)).toEqual(['warehouse.keeping'])
341
+ })
342
+
343
+ it('★ the request keeps the task asked for, and refuses one that does not hold the refused privilege', async () => {
344
+ const request = await access.requestAccess({ ...ASK, taskId: 'warehouse.keeping' }, contextOf(kim))
345
+ expect(request.taskId).toBe('warehouse.keeping')
346
+
347
+ await expect(access.requestAccess({ ...ASK, taskId: 'warehouse.reading' }, contextOf(park))).rejects.toMatchObject({ code: 'access-request-task-mismatch' })
348
+ })
349
+
350
+ it('★ asking again with other work adds to the pending one and does not change the work it named', async () => {
351
+ await access.requestAccess({ ...ASK, taskId: 'warehouse.keeping' }, contextOf(kim))
352
+ registerRoleTask({ id: 'warehouse.receiving', grants: [{ category: 'warehouse', axes: ['mutation'] }] })
353
+ const merged = await access.requestAccess({ ...ASK, taskId: 'warehouse.receiving', reason: 'still needed' }, contextOf(kim))
354
+
355
+ expect(merged.count).toBe(2)
356
+ expect(merged.taskId).toBe('warehouse.keeping')
357
+ expect(await getRepository(AccessRequest).count()).toBe(1)
358
+ })
359
+
360
+ it('★ roles holding the whole task come first, then fewest privileges', async () => {
361
+ expect((await access.rolesGranting('warehouse', 'mutation', domain)).map((c: any) => c.name)).toEqual(['inbound', 'manager'])
362
+ const ordered = await access.rolesGranting('warehouse', 'mutation', domain, undefined, 'warehouse.keeping')
363
+ expect(ordered.map((c: any) => [c.name, c.holdsTask])).toEqual([
364
+ ['manager', true],
365
+ ['inbound', false]
366
+ ])
367
+ })
368
+
369
+ it('★ a role short of the task is told, not refused - and a smaller role short of it is not proposed', async () => {
370
+ const request = await access.requestAccess({ ...ASK, taskId: 'warehouse.keeping' }, contextOf(kim))
371
+
372
+ const short = await access.reviewDecision({ requestId: request.id, decision: 'GRANT', roleId: inbound.id }, contextOf(park))
373
+ expect(short.find((i: any) => i.code === 'role-lacks-task')).toMatchObject({ blocking: false, params: { task: 'warehouse.keeping', privileges: 'warehouse:query' } })
374
+
375
+ const whole = await access.reviewDecision({ requestId: request.id, decision: 'GRANT', roleId: manager.id }, contextOf(park))
376
+ expect(whole.map((i: any) => i.code)).not.toContain('role-lacks-task')
377
+ expect(whole.map((i: any) => i.code)).not.toContain('smaller-role-exists')
378
+ })
379
+
380
+ it('a task gone from the catalog is no error - the review says nothing of it and no role is put first', async () => {
381
+ const request = await access.requestAccess({ ...ASK, taskId: 'warehouse.keeping' }, contextOf(kim))
382
+ ;(process as any)['ROLE_TASKS'] = {}
383
+
384
+ const issues = await access.reviewDecision({ requestId: request.id, decision: 'GRANT', roleId: inbound.id }, contextOf(park))
385
+ expect(issues.map((i: any) => i.code)).not.toContain('role-lacks-task')
386
+ expect((await access.rolesGranting('warehouse', 'mutation', domain, undefined, 'warehouse.keeping')).map((c: any) => c.name)).toEqual(['inbound', 'manager'])
387
+ })
388
+ })
@@ -161,3 +161,33 @@ describe('the parent domain\'s role (ADR-0103 decision 8)', () => {
161
161
  expect(codes(facts({ roleIsParents: false }))).not.toContain('⛔role-belongs-to-parent')
162
162
  })
163
163
  })
164
+
165
+ describe('the task asked for (ADR-0103 decision 9)', () => {
166
+ const task = { id: 'warehouse.keeping', privileges: [ASKED, SEE] }
167
+
168
+ it('★ a role short of the task asked is told which privileges it lacks - not refused', () => {
169
+ const issues = reviewAccessDecision(facts({ task }))
170
+ expect(issues.find(issue => issue.code === 'role-lacks-task')).toEqual({
171
+ code: 'role-lacks-task',
172
+ blocking: false,
173
+ field: 'role',
174
+ params: { role: 'inbound', task: 'warehouse.keeping', privileges: 'warehouse:query' }
175
+ })
176
+ expect(isBlocked(issues)).toBe(false)
177
+ })
178
+
179
+ it('★ says nothing when no task was asked or the task is gone', () => {
180
+ expect(codes(facts({ task: null }))).not.toContain('⚠role-lacks-task')
181
+ expect(codes(facts())).not.toContain('⚠role-lacks-task')
182
+ })
183
+
184
+ it('★ a smaller role that drops the task is not proposed over a role holding it', () => {
185
+ const keeper = { id: 'r-keeper', name: 'keeper', privileges: [ASKED, SEE, GRANT_ROLES] }
186
+ const candidates = [
187
+ { id: 'r-keeper', name: 'keeper', privilegeCount: 3, holdsTask: true },
188
+ { id: 'r-inbound', name: 'inbound', privilegeCount: 1, holdsTask: false }
189
+ ]
190
+ expect(codes(facts({ task, role: keeper, candidates }))).not.toContain('⚠smaller-role-exists')
191
+ expect(smallestRole(candidates)?.name).toBe('keeper')
192
+ })
193
+ })
@@ -263,5 +263,6 @@
263
263
  "privilege.category.audit": "audit log",
264
264
  "privilege.allows.audit.query": "see everyone’s sign-ins, privilege use and refusals, and role changes",
265
265
  "task.auth-base.audit-reading.name": "Reading the audit log",
266
- "task.auth-base.audit-reading.description": "Reads everyone's sign-ins, privilege use and refusals, and role changes in the domain."
266
+ "task.auth-base.audit-reading.description": "Reads everyone's sign-ins, privilege use and refusals, and role changes in the domain.",
267
+ "error.access-request-task-mismatch": "the work you picked does not include the refused privilege."
267
268
  }
@@ -263,5 +263,6 @@
263
263
  "privilege.category.audit": "registro de auditoría",
264
264
  "privilege.allows.audit.query": "ver los inicios de sesión, el uso y rechazo de privilegios y los cambios de rol de todos",
265
265
  "task.auth-base.audit-reading.name": "Consultar el registro de auditoría",
266
- "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."
266
+ "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.",
267
+ "error.access-request-task-mismatch": "la tarea elegida no incluye el privilegio rechazado."
267
268
  }
@@ -263,5 +263,6 @@
263
263
  "privilege.category.audit": "監査ログ",
264
264
  "privilege.allows.audit.query": "全ユーザーのログイン、権限の使用と拒否、ロール変更の記録を見ることができます。",
265
265
  "task.auth-base.audit-reading.name": "監査ログの閲覧",
266
- "task.auth-base.audit-reading.description": "全ユーザーのログイン、権限の使用と拒否、ロール変更の記録を読む。"
266
+ "task.auth-base.audit-reading.description": "全ユーザーのログイン、権限の使用と拒否、ロール変更の記録を読む。",
267
+ "error.access-request-task-mismatch": "選んだ業務に拒否された権限が含まれていません。"
267
268
  }
@@ -263,5 +263,6 @@
263
263
  "privilege.category.audit": "감사 기록",
264
264
  "privilege.allows.audit.query": "모든 사용자의 로그인, 권한 사용과 거절, 역할 변경 기록을 볼 수 있습니다.",
265
265
  "task.auth-base.audit-reading.name": "감사 기록 열람",
266
- "task.auth-base.audit-reading.description": "모든 사용자의 로그인, 권한 사용과 거절, 역할 변경 기록을 읽는다."
266
+ "task.auth-base.audit-reading.description": "모든 사용자의 로그인, 권한 사용과 거절, 역할 변경 기록을 읽는다.",
267
+ "error.access-request-task-mismatch": "고른 업무에 거절된 권한이 들어 있지 않습니다."
267
268
  }
@@ -263,5 +263,6 @@
263
263
  "privilege.category.audit": "log audit",
264
264
  "privilege.allows.audit.query": "boleh melihat log masuk, penggunaan dan penolakan keistimewaan, serta perubahan peranan semua orang.",
265
265
  "task.auth-base.audit-reading.name": "Membaca log audit",
266
- "task.auth-base.audit-reading.description": "Membaca log masuk, penggunaan dan penolakan keistimewaan, serta perubahan peranan semua orang dalam domain."
266
+ "task.auth-base.audit-reading.description": "Membaca log masuk, penggunaan dan penolakan keistimewaan, serta perubahan peranan semua orang dalam domain.",
267
+ "error.access-request-task-mismatch": "tugas yang dipilih tidak termasuk keistimewaan yang ditolak."
267
268
  }
@@ -263,5 +263,6 @@
263
263
  "privilege.category.audit": "审计日志",
264
264
  "privilege.allows.audit.query": "可以查看所有用户的登录、权限使用与拒绝以及角色变更记录。",
265
265
  "task.auth-base.audit-reading.name": "查看审计日志",
266
- "task.auth-base.audit-reading.description": "读取所有用户的登录、权限使用与拒绝以及角色变更记录。"
266
+ "task.auth-base.audit-reading.description": "读取所有用户的登录、权限使用与拒绝以及角色变更记录。",
267
+ "error.access-request-task-mismatch": "所选业务不包含被拒绝的权限。"
267
268
  }
@@ -1,4 +0,0 @@
1
- import { LoginHistory } from './login-history.js';
2
- import { LoginHistoryQuery } from './login-history-query.js';
3
- export declare const entities: (typeof LoginHistory)[];
4
- export declare const resolvers: (typeof LoginHistoryQuery)[];
@@ -1,8 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolvers = exports.entities = void 0;
4
- const login_history_js_1 = require("./login-history.js");
5
- const login_history_query_js_1 = require("./login-history-query.js");
6
- exports.entities = [login_history_js_1.LoginHistory];
7
- exports.resolvers = [login_history_query_js_1.LoginHistoryQuery];
8
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/login-history/index.ts"],"names":[],"mappings":";;;AAAA,yDAAiD;AACjD,qEAA4D;AAE/C,QAAA,QAAQ,GAAG,CAAC,+BAAY,CAAC,CAAA;AACzB,QAAA,SAAS,GAAG,CAAC,0CAAiB,CAAC,CAAA","sourcesContent":["import { LoginHistory } from './login-history.js'\nimport { LoginHistoryQuery } from './login-history-query.js'\n\nexport const entities = [LoginHistory]\nexport const resolvers = [LoginHistoryQuery]\n"]}
@@ -1,10 +0,0 @@
1
- import { Domain, ListParam } from '@things-factory/shell';
2
- import { User } from '../user/user.js';
3
- import { LoginHistory } from './login-history.js';
4
- import { LoginHistoryList } from './login-history-type.js';
5
- export declare class LoginHistoryQuery {
6
- loginHistories(params: ListParam, context: ResolverContext): Promise<LoginHistoryList>;
7
- myLoginHistories(limit: number, context: ResolverContext): Promise<LoginHistory[]>;
8
- accessDomain(loginHistory: LoginHistory): Promise<Domain>;
9
- accessUser(loginHistory: LoginHistory): Promise<User>;
10
- }
@@ -1,80 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LoginHistoryQuery = void 0;
4
- const tslib_1 = require("tslib");
5
- const type_graphql_1 = require("type-graphql");
6
- const shell_1 = require("@things-factory/shell");
7
- const user_js_1 = require("../user/user.js");
8
- const login_history_js_1 = require("./login-history.js");
9
- const login_history_type_js_1 = require("./login-history-type.js");
10
- let LoginHistoryQuery = class LoginHistoryQuery {
11
- /* Everybody's sign-ins. `myLoginHistories` below is the one that answers about the caller,
12
- and it exists precisely because this one should not be open. */
13
- async loginHistories(params, context) {
14
- const { domain } = context.state;
15
- /*
16
- * The row's domain is `accessDomain` - there is no `domain` column, so handing `domain` to the builder failed every
17
- * call (no such column). This domain's sign-ins only; `inherited` never widens it to the parent's.
18
- */
19
- const queryBuilder = (0, shell_1.getQueryBuilderFromListParams)({
20
- params: { ...params, inherited: shell_1.InheritedValueType.None },
21
- repository: (0, shell_1.getRepository)(login_history_js_1.LoginHistory),
22
- alias: 'loginHistory'
23
- }).andWhere('loginHistory.accessDomain = :accessDomainId', { accessDomainId: domain.id });
24
- const [items, total] = await queryBuilder.getManyAndCount();
25
- return { items, total };
26
- }
27
- async myLoginHistories(limit, context) {
28
- const user = context.state.user;
29
- if (!user) {
30
- throw new Error('No user specified');
31
- }
32
- return await (0, shell_1.getRepository)(login_history_js_1.LoginHistory).find({
33
- where: { accessUser: { id: user.id } },
34
- take: limit || 10,
35
- order: { accessedAt: 'DESC' }
36
- });
37
- }
38
- async accessDomain(loginHistory) {
39
- return await (0, shell_1.getRepository)(shell_1.Domain).findOneBy({ id: loginHistory.accessDomainId });
40
- }
41
- async accessUser(loginHistory) {
42
- return await (0, shell_1.getRepository)(user_js_1.User).findOneBy({ id: loginHistory.accessUserId });
43
- }
44
- };
45
- exports.LoginHistoryQuery = LoginHistoryQuery;
46
- tslib_1.__decorate([
47
- (0, type_graphql_1.Directive)('@privilege(category: "user", privilege: "query", domainOwnerGranted: true, superUserGranted: true)'),
48
- (0, type_graphql_1.Query)(returns => login_history_type_js_1.LoginHistoryList, { description: 'To fetch multiple LoginHistories' }),
49
- tslib_1.__param(0, (0, type_graphql_1.Args)(type => shell_1.ListParam)),
50
- tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
51
- tslib_1.__metadata("design:type", Function),
52
- tslib_1.__metadata("design:paramtypes", [shell_1.ListParam, Object]),
53
- tslib_1.__metadata("design:returntype", Promise)
54
- ], LoginHistoryQuery.prototype, "loginHistories", null);
55
- tslib_1.__decorate([
56
- (0, type_graphql_1.Query)(returns => [login_history_js_1.LoginHistory]),
57
- tslib_1.__param(0, (0, type_graphql_1.Arg)('limit')),
58
- tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
59
- tslib_1.__metadata("design:type", Function),
60
- tslib_1.__metadata("design:paramtypes", [Number, Object]),
61
- tslib_1.__metadata("design:returntype", Promise)
62
- ], LoginHistoryQuery.prototype, "myLoginHistories", null);
63
- tslib_1.__decorate([
64
- (0, type_graphql_1.FieldResolver)(),
65
- tslib_1.__param(0, (0, type_graphql_1.Root)()),
66
- tslib_1.__metadata("design:type", Function),
67
- tslib_1.__metadata("design:paramtypes", [login_history_js_1.LoginHistory]),
68
- tslib_1.__metadata("design:returntype", Promise)
69
- ], LoginHistoryQuery.prototype, "accessDomain", null);
70
- tslib_1.__decorate([
71
- (0, type_graphql_1.FieldResolver)(),
72
- tslib_1.__param(0, (0, type_graphql_1.Root)()),
73
- tslib_1.__metadata("design:type", Function),
74
- tslib_1.__metadata("design:paramtypes", [login_history_js_1.LoginHistory]),
75
- tslib_1.__metadata("design:returntype", Promise)
76
- ], LoginHistoryQuery.prototype, "accessUser", null);
77
- exports.LoginHistoryQuery = LoginHistoryQuery = tslib_1.__decorate([
78
- (0, type_graphql_1.Resolver)(login_history_js_1.LoginHistory)
79
- ], LoginHistoryQuery);
80
- //# sourceMappingURL=login-history-query.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"login-history-query.js","sourceRoot":"","sources":["../../../server/service/login-history/login-history-query.ts"],"names":[],"mappings":";;;;AAAA,+CAA8F;AAE9F,iDAA2H;AAE3H,6CAAsC;AACtC,yDAAiD;AACjD,mEAA0D;AAGnD,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAC5B;sEACkE;IAG5D,AAAN,KAAK,CAAC,cAAc,CACO,MAAiB,EACnC,OAAwB;QAE/B,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAEhC;;;WAGG;QACH,MAAM,YAAY,GAAG,IAAA,qCAA6B,EAAC;YACjD,MAAM,EAAE,EAAE,GAAG,MAAM,EAAE,SAAS,EAAE,0BAAkB,CAAC,IAAI,EAAE;YACzD,UAAU,EAAE,IAAA,qBAAa,EAAC,+BAAY,CAAC;YACvC,KAAK,EAAE,cAAc;SACtB,CAAC,CAAC,QAAQ,CAAC,6CAA6C,EAAE,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAA;QAEzF,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,YAAY,CAAC,eAAe,EAAE,CAAA;QAC3D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;IACzB,CAAC;IAGK,AAAN,KAAK,CAAC,gBAAgB,CAAe,KAAa,EAAS,OAAwB;QACjF,MAAM,IAAI,GAAS,OAAO,CAAC,KAAK,CAAC,IAAI,CAAA;QACrC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAA;QACtC,CAAC;QAED,OAAO,MAAM,IAAA,qBAAa,EAAC,+BAAY,CAAC,CAAC,IAAI,CAAC;YAC5C,KAAK,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE;YACtC,IAAI,EAAE,KAAK,IAAI,EAAE;YACjB,KAAK,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE;SAC9B,CAAC,CAAA;IACJ,CAAC;IAGK,AAAN,KAAK,CAAC,YAAY,CAAS,YAA0B;QACnD,OAAO,MAAM,IAAA,qBAAa,EAAC,cAAM,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,YAAY,CAAC,cAAc,EAAE,CAAC,CAAA;IACnF,CAAC;IAGK,AAAN,KAAK,CAAC,UAAU,CAAS,YAA0B;QACjD,OAAO,MAAM,IAAA,qBAAa,EAAC,cAAI,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,YAAY,CAAC,YAAY,EAAE,CAAC,CAAA;IAC/E,CAAC;CACF,CAAA;AAhDY,8CAAiB;AAKtB;IAFL,IAAA,wBAAS,EAAC,oGAAoG,CAAC;IAC/G,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,wCAAgB,EAAE,EAAE,WAAW,EAAE,kCAAkC,EAAE,CAAC;IAErF,mBAAA,IAAA,mBAAI,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAS,CAAC,CAAA;IACvB,mBAAA,IAAA,kBAAG,GAAE,CAAA;;6CAD2B,iBAAS;;uDAiB3C;AAGK;IADL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,+BAAY,CAAC,CAAC;IACT,mBAAA,IAAA,kBAAG,EAAC,OAAO,CAAC,CAAA;IAAiB,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;yDAWzD;AAGK;IADL,IAAA,4BAAa,GAAE;IACI,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAe,+BAAY;;qDAEpD;AAGK;IADL,IAAA,4BAAa,GAAE;IACE,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAe,+BAAY;;mDAElD;4BA/CU,iBAAiB;IAD7B,IAAA,uBAAQ,EAAC,+BAAY,CAAC;GACV,iBAAiB,CAgD7B","sourcesContent":["import { Arg, Args, Ctx, Directive, FieldResolver, Query, Resolver, Root } from 'type-graphql'\n\nimport { Domain, getQueryBuilderFromListParams, InheritedValueType, getRepository, ListParam } from '@things-factory/shell'\n\nimport { User } from '../user/user.js'\nimport { LoginHistory } from './login-history.js'\nimport { LoginHistoryList } from './login-history-type.js'\n\n@Resolver(LoginHistory)\nexport class LoginHistoryQuery {\n /* Everybody's sign-ins. `myLoginHistories` below is the one that answers about the caller,\n and it exists precisely because this one should not be open. */\n @Directive('@privilege(category: \"user\", privilege: \"query\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => LoginHistoryList, { description: 'To fetch multiple LoginHistories' })\n async loginHistories(\n @Args(type => ListParam) params: ListParam,\n @Ctx() context: ResolverContext\n ): Promise<LoginHistoryList> {\n const { domain } = context.state\n\n /*\n * The row's domain is `accessDomain` - there is no `domain` column, so handing `domain` to the builder failed every\n * call (no such column). This domain's sign-ins only; `inherited` never widens it to the parent's.\n */\n const queryBuilder = getQueryBuilderFromListParams({\n params: { ...params, inherited: InheritedValueType.None },\n repository: getRepository(LoginHistory),\n alias: 'loginHistory'\n }).andWhere('loginHistory.accessDomain = :accessDomainId', { accessDomainId: domain.id })\n\n const [items, total] = await queryBuilder.getManyAndCount()\n return { items, total }\n }\n\n @Query(returns => [LoginHistory])\n async myLoginHistories(@Arg('limit') limit: number, @Ctx() context: ResolverContext): Promise<LoginHistory[]> {\n const user: User = context.state.user\n if (!user) {\n throw new Error('No user specified')\n }\n\n return await getRepository(LoginHistory).find({\n where: { accessUser: { id: user.id } },\n take: limit || 10,\n order: { accessedAt: 'DESC' }\n })\n }\n\n @FieldResolver()\n async accessDomain(@Root() loginHistory: LoginHistory) {\n return await getRepository(Domain).findOneBy({ id: loginHistory.accessDomainId })\n }\n\n @FieldResolver()\n async accessUser(@Root() loginHistory: LoginHistory) {\n return await getRepository(User).findOneBy({ id: loginHistory.accessUserId })\n }\n}\n"]}
@@ -1,5 +0,0 @@
1
- import { LoginHistory } from './login-history.js';
2
- export declare class LoginHistoryList {
3
- items: LoginHistory[];
4
- total: number;
5
- }
@@ -1,21 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LoginHistoryList = void 0;
4
- const tslib_1 = require("tslib");
5
- const type_graphql_1 = require("type-graphql");
6
- const login_history_js_1 = require("./login-history.js");
7
- let LoginHistoryList = class LoginHistoryList {
8
- };
9
- exports.LoginHistoryList = LoginHistoryList;
10
- tslib_1.__decorate([
11
- (0, type_graphql_1.Field)(type => [login_history_js_1.LoginHistory], { description: 'The list of login history items.' }),
12
- tslib_1.__metadata("design:type", Array)
13
- ], LoginHistoryList.prototype, "items", void 0);
14
- tslib_1.__decorate([
15
- (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'The total number of login history records.' }),
16
- tslib_1.__metadata("design:type", Number)
17
- ], LoginHistoryList.prototype, "total", void 0);
18
- exports.LoginHistoryList = LoginHistoryList = tslib_1.__decorate([
19
- (0, type_graphql_1.ObjectType)({ description: 'A paginated list of login history records.' })
20
- ], LoginHistoryList);
21
- //# sourceMappingURL=login-history-type.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"login-history-type.js","sourceRoot":"","sources":["../../../server/service/login-history/login-history-type.ts"],"names":[],"mappings":";;;;AAAA,+CAAoE;AAEpE,yDAAiD;AAG1C,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;CAM5B,CAAA;AANY,4CAAgB;AAE3B;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,+BAAY,CAAC,EAAE,EAAE,WAAW,EAAE,kCAAkC,EAAE,CAAC;;+CAC9D;AAGrB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,4CAA4C,EAAE,CAAC;;+CACrE;2BALF,gBAAgB;IAD5B,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,4CAA4C,EAAE,CAAC;GAC7D,gBAAgB,CAM5B","sourcesContent":["import { Field, ID, InputType, Int, ObjectType } from 'type-graphql'\n\nimport { LoginHistory } from './login-history.js'\n\n@ObjectType({ description: 'A paginated list of login history records.' })\nexport class LoginHistoryList {\n @Field(type => [LoginHistory], { description: 'The list of login history items.' })\n items: LoginHistory[]\n\n @Field(type => Int, { description: 'The total number of login history records.' })\n total: number\n}\n"]}
@@ -1,12 +0,0 @@
1
- import { Domain } from '@things-factory/shell';
2
- import { User } from '../user/user.js';
3
- export declare class LoginHistory {
4
- readonly id: string;
5
- accessDomain?: Domain;
6
- accessDomainId: string;
7
- accessUser: User;
8
- accessUserId: string;
9
- accessorIp: string;
10
- accessedAt: Date;
11
- static stamp(accessDomain: Partial<Domain>, accessUser: User, accessorIp: string): Promise<void>;
12
- }
@@ -1,58 +0,0 @@
1
- "use strict";
2
- var LoginHistory_1;
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.LoginHistory = void 0;
5
- const tslib_1 = require("tslib");
6
- const type_graphql_1 = require("type-graphql");
7
- const typeorm_1 = require("typeorm");
8
- const shell_1 = require("@things-factory/shell");
9
- const user_js_1 = require("../user/user.js");
10
- let LoginHistory = LoginHistory_1 = class LoginHistory {
11
- static async stamp(accessDomain, accessUser, accessorIp) {
12
- await (0, shell_1.getRepository)(LoginHistory_1).save({
13
- accessDomain,
14
- accessUser,
15
- accessorIp
16
- });
17
- }
18
- };
19
- exports.LoginHistory = LoginHistory;
20
- tslib_1.__decorate([
21
- (0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
22
- (0, type_graphql_1.Field)(type => type_graphql_1.ID, { description: 'Unique identifier for the login history record.' }),
23
- tslib_1.__metadata("design:type", String)
24
- ], LoginHistory.prototype, "id", void 0);
25
- tslib_1.__decorate([
26
- (0, typeorm_1.ManyToOne)(type => shell_1.Domain),
27
- (0, type_graphql_1.Field)(type => shell_1.Domain, { description: 'The domain the user logged into.' }),
28
- tslib_1.__metadata("design:type", shell_1.Domain)
29
- ], LoginHistory.prototype, "accessDomain", void 0);
30
- tslib_1.__decorate([
31
- (0, typeorm_1.RelationId)((loginHistory) => loginHistory.accessDomain),
32
- tslib_1.__metadata("design:type", String)
33
- ], LoginHistory.prototype, "accessDomainId", void 0);
34
- tslib_1.__decorate([
35
- (0, typeorm_1.ManyToOne)(type => user_js_1.User),
36
- (0, type_graphql_1.Field)(type => user_js_1.User, { description: 'The user who logged in.' }),
37
- tslib_1.__metadata("design:type", user_js_1.User)
38
- ], LoginHistory.prototype, "accessUser", void 0);
39
- tslib_1.__decorate([
40
- (0, typeorm_1.RelationId)((loginHistory) => loginHistory.accessUser),
41
- tslib_1.__metadata("design:type", String)
42
- ], LoginHistory.prototype, "accessUserId", void 0);
43
- tslib_1.__decorate([
44
- (0, typeorm_1.Column)({ nullable: true }),
45
- (0, type_graphql_1.Field)({ nullable: true, description: 'The IP address from which the login attempt was made.' }),
46
- tslib_1.__metadata("design:type", String)
47
- ], LoginHistory.prototype, "accessorIp", void 0);
48
- tslib_1.__decorate([
49
- (0, typeorm_1.CreateDateColumn)(),
50
- (0, type_graphql_1.Field)({ nullable: true, description: 'The timestamp of the login attempt.' }),
51
- tslib_1.__metadata("design:type", Date)
52
- ], LoginHistory.prototype, "accessedAt", void 0);
53
- exports.LoginHistory = LoginHistory = LoginHistory_1 = tslib_1.__decorate([
54
- (0, typeorm_1.Entity)(),
55
- (0, typeorm_1.Index)('ix_login_history_0', (loginHistory) => [loginHistory.accessDomain, loginHistory.accessUser]),
56
- (0, type_graphql_1.ObjectType)({ description: 'Records user login attempts.' })
57
- ], LoginHistory);
58
- //# sourceMappingURL=login-history.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"login-history.js","sourceRoot":"","sources":["../../../server/service/login-history/login-history.ts"],"names":[],"mappings":";;;;;AAAA,+CAAoD;AACpD,qCAAgH;AAEhH,iDAA6D;AAE7D,6CAAsC;AAK/B,IAAM,YAAY,oBAAlB,MAAM,YAAY;IA2BvB,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,YAA6B,EAAE,UAAgB,EAAE,UAAkB;QACpF,MAAM,IAAA,qBAAa,EAAC,cAAY,CAAC,CAAC,IAAI,CAAC;YACrC,YAAY;YACZ,UAAU;YACV,UAAU;SACX,CAAC,CAAA;IACJ,CAAC;CACF,CAAA;AAlCY,oCAAY;AAGd;IAFR,IAAA,gCAAsB,EAAC,MAAM,CAAC;IAC9B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAE,EAAE,EAAE,WAAW,EAAE,iDAAiD,EAAE,CAAC;;wCACnE;AAInB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,CAAC;IACzB,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,EAAE,EAAE,WAAW,EAAE,kCAAkC,EAAE,CAAC;sCAC5D,cAAM;kDAAA;AAGrB;IADC,IAAA,oBAAU,EAAC,CAAC,YAA0B,EAAE,EAAE,CAAC,YAAY,CAAC,YAAY,CAAC;;oDAChD;AAItB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,cAAI,CAAC;IACvB,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,cAAI,EAAE,EAAE,WAAW,EAAE,yBAAyB,EAAE,CAAC;sCACpD,cAAI;gDAAA;AAGhB;IADC,IAAA,oBAAU,EAAC,CAAC,YAA0B,EAAE,EAAE,CAAC,YAAY,CAAC,UAAU,CAAC;;kDAChD;AAIpB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,uDAAuD,EAAE,CAAC;;gDAC9E;AAIlB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,qCAAqC,EAAE,CAAC;sCAClE,IAAI;gDAAA;uBAzBL,YAAY;IAHxB,IAAA,gBAAM,GAAE;IACR,IAAA,eAAK,EAAC,oBAAoB,EAAE,CAAC,YAA0B,EAAE,EAAE,CAAC,CAAC,YAAY,CAAC,YAAY,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC;IACjH,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,8BAA8B,EAAE,CAAC;GAC/C,YAAY,CAkCxB","sourcesContent":["import { Field, ID, ObjectType } from 'type-graphql'\nimport { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId } from 'typeorm'\n\nimport { Domain, getRepository } from '@things-factory/shell'\n\nimport { User } from '../user/user.js'\n\n@Entity()\n@Index('ix_login_history_0', (loginHistory: LoginHistory) => [loginHistory.accessDomain, loginHistory.accessUser])\n@ObjectType({ description: 'Records user login attempts.' })\nexport class LoginHistory {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier for the login history record.' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { description: 'The domain the user logged into.' })\n accessDomain?: Domain\n\n @RelationId((loginHistory: LoginHistory) => loginHistory.accessDomain)\n accessDomainId: string\n\n @ManyToOne(type => User)\n @Field(type => User, { description: 'The user who logged in.' })\n accessUser: User\n\n @RelationId((loginHistory: LoginHistory) => loginHistory.accessUser)\n accessUserId: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'The IP address from which the login attempt was made.' })\n accessorIp: string\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'The timestamp of the login attempt.' })\n accessedAt: Date\n\n static async stamp(accessDomain: Partial<Domain>, accessUser: User, accessorIp: string): Promise<void> {\n await getRepository(LoginHistory).save({\n accessDomain,\n accessUser,\n accessorIp\n })\n }\n}\n"]}