@things-factory/auth-base 10.1.106 → 10.1.111

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 (52) hide show
  1. package/dist-server/ai-entry/access-assistant.d.ts +50 -0
  2. package/dist-server/ai-entry/access-assistant.js +145 -0
  3. package/dist-server/ai-entry/access-assistant.js.map +1 -0
  4. package/dist-server/ai-entry/access-entry-targets.d.ts +32 -0
  5. package/dist-server/ai-entry/access-entry-targets.js +197 -0
  6. package/dist-server/ai-entry/access-entry-targets.js.map +1 -0
  7. package/dist-server/ai-entry/entry-target-shape.d.ts +82 -0
  8. package/dist-server/ai-entry/entry-target-shape.js +3 -0
  9. package/dist-server/ai-entry/entry-target-shape.js.map +1 -0
  10. package/dist-server/controllers/access-request.d.ts +6 -0
  11. package/dist-server/controllers/access-request.js +104 -26
  12. package/dist-server/controllers/access-request.js.map +1 -1
  13. package/dist-server/index.d.ts +3 -0
  14. package/dist-server/index.js +3 -0
  15. package/dist-server/index.js.map +1 -1
  16. package/dist-server/service/access-request/access-request-query.d.ts +7 -0
  17. package/dist-server/service/access-request/access-request-query.js +37 -5
  18. package/dist-server/service/access-request/access-request-query.js.map +1 -1
  19. package/dist-server/service/index.d.ts +1 -1
  20. package/dist-server/service/open-fields.js +2 -0
  21. package/dist-server/service/open-fields.js.map +1 -1
  22. package/dist-server/service/privilege/privilege-directive.js +2 -2
  23. package/dist-server/service/privilege/privilege-directive.js.map +1 -1
  24. package/dist-server/service/privilege/privilege-rejection.d.ts +3 -1
  25. package/dist-server/service/privilege/privilege-rejection.js +2 -10
  26. package/dist-server/service/privilege/privilege-rejection.js.map +1 -1
  27. package/dist-server/service/role/role-query.d.ts +2 -0
  28. package/dist-server/service/role/role-query.js +12 -0
  29. package/dist-server/service/role/role-query.js.map +1 -1
  30. package/dist-server/service/user/index.d.ts +1 -1
  31. package/dist-server/service/user/user-mutation.js +1 -1
  32. package/dist-server/service/user/user-mutation.js.map +1 -1
  33. package/dist-server/tsconfig.tsbuildinfo +1 -1
  34. package/dist-server/utils/access-request-review.d.ts +3 -0
  35. package/dist-server/utils/access-request-review.js +3 -0
  36. package/dist-server/utils/access-request-review.js.map +1 -1
  37. package/dist-server/utils/assignable-roles.d.ts +11 -1
  38. package/dist-server/utils/assignable-roles.js +73 -1
  39. package/dist-server/utils/assignable-roles.js.map +1 -1
  40. package/package.json +2 -2
  41. package/tests/access-assistant-db.test.ts +155 -0
  42. package/tests/access-entry-targets-db.test.ts +138 -0
  43. package/tests/access-request-db.test.ts +84 -3
  44. package/tests/access-request-pg.test.ts +83 -0
  45. package/tests/access-request-review.test.ts +8 -0
  46. package/tests/assignable-roles-db.test.ts +79 -1
  47. package/tests/privilege-directive.test.ts +18 -0
  48. package/translations/en.json +4 -1
  49. package/translations/ja.json +4 -1
  50. package/translations/ko.json +4 -1
  51. package/translations/ms.json +4 -1
  52. package/translations/zh.json +4 -1
@@ -100,6 +100,7 @@ describe('asking', () => {
100
100
  expect(request).toMatchObject({ state: 'PENDING', category: 'warehouse', privilege: 'mutation', count: 1, reason: 'inbound from today' })
101
101
  expect(context.notify).toHaveBeenCalledTimes(1)
102
102
  expect([...context.notify.mock.calls[0][0].receivers].sort()).toEqual([owner.id, park.id].sort())
103
+ expect(context.notify.mock.calls[0][0].url).toBe(`access-requests?id=${request.id}`)
103
104
  })
104
105
 
105
106
  it('★ asking again adds to the pending one - no second row, nobody told again', async () => {
@@ -125,7 +126,7 @@ describe('asking', () => {
125
126
  })
126
127
 
127
128
  it('a privilege nobody declared is not asked for', async () => {
128
- await expect(access.requestAccess({ category: 'nothing', privilege: 'here' }, contextOf(kim))).rejects.toMatchObject({
129
+ await expect(access.requestAccess({ category: 'nothing', privilege: 'here', reason: 'x' }, contextOf(kim))).rejects.toMatchObject({
129
130
  code: 'access-request-unknown-privilege'
130
131
  })
131
132
  })
@@ -155,7 +156,9 @@ describe('deciding', () => {
155
156
 
156
157
  expect(decided).toMatchObject({ state: 'GRANTED', grantedRole: { id: inbound.id } })
157
158
  expect(await holds(kim, 'warehouse', 'mutation')).toBe(true)
158
- expect(context.notify).toHaveBeenCalledWith(expect.objectContaining({ receivers: [kim.id], title: 'access-request.notify.granted.title' }))
159
+ expect(context.notify).toHaveBeenCalledWith(
160
+ expect.objectContaining({ receivers: [kim.id], title: 'access-request.notify.granted.title', url: 'my-access-requests' })
161
+ )
159
162
  })
160
163
 
161
164
  it('★ deny keeps the reason and tells the requester', async () => {
@@ -217,7 +220,7 @@ describe('deciding', () => {
217
220
 
218
221
  it('★ a grant closes the same person\'s other requests it now answers', async () => {
219
222
  const first = await access.requestAccess(ASK, contextOf(kim))
220
- const second = await access.requestAccess({ category: 'warehouse', privilege: 'query' }, contextOf(kim))
223
+ const second = await access.requestAccess({ category: 'warehouse', privilege: 'query', reason: 'checking stock' }, contextOf(kim))
221
224
  await access.decideAccessRequest({ requestId: first.id, decision: 'GRANT', roleId: manager.id }, contextOf(owner))
222
225
 
223
226
  expect((await getRepository(AccessRequest).findOneBy({ id: second.id })).state).toBe('GRANTED')
@@ -234,3 +237,81 @@ describe('the review as the screen asks it', () => {
234
237
  expect(issues.map((i: any) => i.code).sort()).toEqual(['decider-lacks-it', 'role-grants-admin', 'role-grants-more', 'smaller-role-exists'])
235
238
  })
236
239
  })
240
+
241
+ describe('asking is not a reflex (questions Q9)', () => {
242
+ it('★ no reason, no request - blank is no reason', async () => {
243
+ for (const reason of [undefined, '', ' ']) {
244
+ await expect(access.requestAccess({ ...ASK, reason }, contextOf(kim))).rejects.toMatchObject({ code: 'access-request-needs-reason' })
245
+ }
246
+ expect(await getRepository(AccessRequest).count()).toBe(0)
247
+ })
248
+
249
+ it('★ denied lately: the same words are refused, new words are taken', async () => {
250
+ const first = await access.requestAccess(ASK, contextOf(kim))
251
+ await access.decideAccessRequest({ requestId: first.id, decision: 'DENY', reason: 'IT only' }, contextOf(park))
252
+
253
+ await expect(access.requestAccess(ASK, contextOf(kim))).rejects.toMatchObject({
254
+ code: 'access-request-same-reason-as-denied',
255
+ params: { reason: 'IT only' }
256
+ })
257
+ const again = await access.requestAccess({ ...ASK, reason: 'moved to the inbound team this week' }, contextOf(kim))
258
+ expect(again.state).toBe('PENDING')
259
+ })
260
+
261
+ it('a denial older than seven days holds nothing against the same words', async () => {
262
+ const first = await access.requestAccess(ASK, contextOf(kim))
263
+ await access.decideAccessRequest({ requestId: first.id, decision: 'DENY', reason: 'not now' }, contextOf(park))
264
+ await getRepository(AccessRequest).update({ id: first.id }, { decidedAt: new Date(Date.now() - 8 * 24 * 60 * 60 * 1000) })
265
+ expect((await access.requestAccess(ASK, contextOf(kim))).state).toBe('PENDING')
266
+ })
267
+
268
+ it('★ the popup can say how many people a request reaches - not who', async () => {
269
+ const { AccessRequestQuery } = loadCompiled('service/access-request/access-request-query')
270
+ expect(await new AccessRequestQuery().accessRequestDeciderCount(contextOf(kim))).toBe(2)
271
+ expect(await new AccessRequestQuery().accessRequestDeciderCount(contextOf(park))).toBe(1)
272
+ })
273
+ })
274
+
275
+ describe('found in review (2026-10-02)', () => {
276
+ const { AccessRequestQuery } = loadCompiled('service/access-request/access-request-query')
277
+
278
+ it('★ the list is this domain\'s only - asking for the parent\'s with inherited: Only reads nothing', async () => {
279
+ await access.requestAccess(ASK, contextOf(kim))
280
+ const savedChild = await getRepository(Domain).save({ name: 'Plant A-1', subdomain: 'plant-a-1', parent: domain })
281
+ const child = await getRepository(Domain).findOneBy({ id: savedChild.id })
282
+ const childAdmin = await makeUser('childadmin', [child])
283
+ const answer = await new AccessRequestQuery().accessRequests({ inherited: 'Only' } as any, { state: { domain: child, user: childAdmin } } as any)
284
+ expect(answer.items).toEqual([])
285
+ })
286
+
287
+ it('★ ask, withdraw, ask again: one row, and the deciders are told once', async () => {
288
+ const first = contextOf(kim)
289
+ const request = await access.requestAccess(ASK, first)
290
+ await commit(first)
291
+ await access.withdrawAccessRequest(request.id, contextOf(kim))
292
+ const again = contextOf(kim)
293
+ const back = await access.requestAccess({ ...ASK, reason: 'needed after all' }, again)
294
+ await commit(again)
295
+
296
+ expect(back.id).toBe(request.id)
297
+ expect(back.state).toBe('PENDING')
298
+ expect(await getRepository(AccessRequest).count()).toBe(1)
299
+ expect(first.notify).toHaveBeenCalledTimes(1)
300
+ expect(again.notify).not.toHaveBeenCalled()
301
+ })
302
+
303
+ it('★ a decision may name a parent domain\'s role - the scope the list screen offers', async () => {
304
+ const savedChild = await getRepository(Domain).save({ name: 'Plant A-2', subdomain: 'plant-a-2', parent: domain })
305
+ const child = await getRepository(Domain).findOneBy({ id: savedChild.id })
306
+ const found = await new AccessRequestQuery().assignableRolesNamed('inbound', { state: { domain: child, user: owner } } as any)
307
+ expect(found.map((r: any) => r.id)).toEqual([inbound.id])
308
+ expect(await new AccessRequestQuery().assignableRolesNamed('inbou', { state: { domain: child, user: owner } } as any)).toEqual([])
309
+ })
310
+
311
+ it('my requests carry their domain and requester - non-null fields are loaded', async () => {
312
+ await access.requestAccess(ASK, contextOf(kim))
313
+ const [mine] = await new AccessRequestQuery().myAccessRequests(contextOf(kim))
314
+ expect(mine.domain?.id).toBe(domain.id)
315
+ expect(mine.requester?.id).toBe(kim.id)
316
+ })
317
+ })
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The access request under real concurrency - postgres only (`AUTH_TEST_PG`). sqlite holds one connection and
3
+ * cannot show two transactions racing (feedback: verify concurrency on postgres).
4
+ *
5
+ * Each call runs in its own transaction, as the `@transaction` door gives it.
6
+ */
7
+ import { authDataSource, closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
8
+ import { loadCompiled } from './compiled'
9
+
10
+ const { Role } = loadCompiled('service/role/role')
11
+ const { Privilege } = loadCompiled('service/privilege/privilege')
12
+ const { User } = loadCompiled('service/user/user')
13
+ const { AccessRequest } = loadCompiled('service/access-request/access-request')
14
+ const access = loadCompiled('controllers/access-request')
15
+
16
+ const onPostgres = process.env.AUTH_TEST_PG ? describe : describe.skip
17
+
18
+ onPostgres('on postgres', () => {
19
+ let domain: any
20
+ let kim: any
21
+ let park: any
22
+ let owner: any
23
+ let inbound: any
24
+
25
+ const inTransaction = <T>(user: any, run: (context: any) => Promise<T>) =>
26
+ authDataSource().transaction(tx => run({ state: { domain, user, tx, afterCommit: [] }, t: (key: string) => key }))
27
+
28
+ beforeAll(async () => {
29
+ await openAuthDatabase()
30
+ })
31
+
32
+ afterAll(async () => {
33
+ await closeAuthDatabase()
34
+ })
35
+
36
+ beforeEach(async () => {
37
+ await resetAuthDatabase()
38
+ domain = await getRepository(Domain).findOneBy({ id: (await getRepository(Domain).save({ name: 'Plant A', subdomain: 'plant-a' })).id })
39
+ const mutation = await getRepository(Privilege).save({ name: 'mutation', category: 'warehouse' })
40
+ const grant = await getRepository(Privilege).save({ name: 'mutation', category: 'user' })
41
+ inbound = await getRepository(Role).save({ name: 'inbound', domain, privileges: [mutation] })
42
+ const admins = await getRepository(Role).save({ name: 'admins', domain, privileges: [grant] })
43
+ const make = (name: string, roles: any[] = []) =>
44
+ getRepository(User).save({ username: name, email: `${name}@example.com`, name, salt: User.generateSalt(), status: 'activated', domains: [domain], roles })
45
+ kim = await make('kim')
46
+ park = await make('park', [admins])
47
+ owner = await make('owner', [admins])
48
+ })
49
+
50
+ it('★ two people deciding at once, each in a transaction - exactly one wins, and the loser leaves nothing', async () => {
51
+ const request = await inTransaction(kim, context => access.requestAccess({ category: 'warehouse', privilege: 'mutation', reason: 'inbound' }, context))
52
+
53
+ const results = await Promise.allSettled([
54
+ inTransaction(owner, context => access.decideAccessRequest({ requestId: request.id, decision: 'DENY', reason: 'no' }, context)),
55
+ inTransaction(park, context => access.decideAccessRequest({ requestId: request.id, decision: 'GRANT', roleId: inbound.id }, context))
56
+ ])
57
+
58
+ expect(results.map(r => r.status).sort()).toEqual(['fulfilled', 'rejected'])
59
+ const stored = await getRepository(AccessRequest).findOne({ where: { id: request.id }, relations: ['grantedRole'] })
60
+ const holds = await User.hasPrivilege('mutation', 'warehouse', domain, kim)
61
+ /* The role is given exactly when the stored decision is a grant - a losing grant does not leave the role behind. */
62
+ expect(holds).toBe(stored.state === 'GRANTED')
63
+ })
64
+
65
+ it('★ a grant closes the requester\'s other requests it answers - read inside the transaction, not the default connection', async () => {
66
+ const query = await getRepository(Privilege).save({ name: 'query', category: 'warehouse' })
67
+ const both = await getRepository(Role).save({ name: 'both', domain, privileges: [query, await getRepository(Privilege).findOneBy({ name: 'mutation', category: 'warehouse' })] })
68
+ const first = await inTransaction(kim, context => access.requestAccess({ category: 'warehouse', privilege: 'mutation', reason: 'inbound' }, context))
69
+ const second = await inTransaction(kim, context => access.requestAccess({ category: 'warehouse', privilege: 'query', reason: 'stock' }, context))
70
+
71
+ await inTransaction(owner, context => access.decideAccessRequest({ requestId: first.id, decision: 'GRANT', roleId: both.id }, context))
72
+
73
+ expect((await getRepository(AccessRequest).findOneBy({ id: second.id })).state).toBe('GRANTED')
74
+ })
75
+
76
+ it('★ the same person asking twice at once - one pending request, not two', async () => {
77
+ await Promise.allSettled([
78
+ inTransaction(kim, context => access.requestAccess({ category: 'warehouse', privilege: 'mutation', reason: 'inbound' }, context)),
79
+ inTransaction(kim, context => access.requestAccess({ category: 'warehouse', privilege: 'mutation', reason: 'inbound' }, context))
80
+ ])
81
+ expect(await getRepository(AccessRequest).count({ where: { state: 'PENDING' } })).toBe(1)
82
+ })
83
+ })
@@ -153,3 +153,11 @@ describe('helpers', () => {
153
153
  expect(effectiveState('GRANTED', new Date(0), NOW)).toBe('GRANTED')
154
154
  })
155
155
  })
156
+
157
+ describe('the parent domain\'s role (ADR-0103 decision 8)', () => {
158
+ it('★ blocks when the decider does not decide in the parent, passes when they do', () => {
159
+ expect(codes(facts({ roleIsParents: true, deciderMayChangeParentRoles: false }))).toContain('⛔role-belongs-to-parent')
160
+ expect(codes(facts({ roleIsParents: true, deciderMayChangeParentRoles: true }))).not.toContain('⛔role-belongs-to-parent')
161
+ expect(codes(facts({ roleIsParents: false }))).not.toContain('⛔role-belongs-to-parent')
162
+ })
163
+ })
@@ -15,6 +15,8 @@ const { GrantedRole } = loadCompiled('service/granted-role/granted-role')
15
15
  const { User } = loadCompiled('service/user/user')
16
16
  const { UserMutation } = loadCompiled('service/user/user-mutation')
17
17
  const { assignRoles } = loadCompiled('utils/assignable-roles')
18
+ const { DomainOwner } = loadCompiled('service/domain-owner/domain-owner')
19
+ const { Privilege } = loadCompiled('service/privilege/privilege')
18
20
 
19
21
  let parent: any
20
22
  let child: any
@@ -80,7 +82,9 @@ describe('what a domain can put a person into', () => {
80
82
  await getRepository(GrantedRole).save({ role: granted, domain: child })
81
83
  const user = await makeUser([child], [])
82
84
 
83
- const { added } = await assignRoles(user, { add: [own.id, inherited.id, granted.id] }, child)
85
+ const parentOwner = await makeUser([parent], [])
86
+ await getRepository(DomainOwner).save({ domain: parent, user: parentOwner })
87
+ const { added } = await assignRoles(user, { add: [own.id, inherited.id, granted.id] }, child, undefined, parentOwner)
84
88
 
85
89
  expect(added.map((role: any) => role.name).sort()).toEqual(['operator', 'partner', 'viewer'])
86
90
  expect(await heldRoleNames(user)).toEqual(['operator', 'partner', 'viewer'])
@@ -139,3 +143,77 @@ describe('updateUserRoles goes through the same function', () => {
139
143
  expect(await heldRoleNames(user)).toEqual(['b'])
140
144
  })
141
145
  })
146
+
147
+ describe('the parent domain\'s roles - only someone who decides there (ADR-0103 decision 8)', () => {
148
+ let parentRole: any
149
+ let childAdmin: any
150
+ let user: any
151
+
152
+ beforeEach(async () => {
153
+ const grant = await getRepository(Privilege).save({ name: 'mutation', category: 'user' })
154
+ const childAdmins = await getRepository(Role).save({ name: 'child-admins', domain: child, privileges: [grant] })
155
+ parentRole = await getRepository(Role).save({ name: 'hq-approver', domain: parent })
156
+ childAdmin = await makeUser([child], [childAdmins])
157
+ user = await makeUser([child], [])
158
+ })
159
+
160
+ it('★ a child domain\'s decider cannot give the parent\'s role - refused by name', async () => {
161
+ await expect(assignRoles(user, { add: [parentRole.id] }, child, undefined, childAdmin)).rejects.toMatchObject({
162
+ code: 'role-belongs-to-parent',
163
+ params: { roles: 'hq-approver' }
164
+ })
165
+ expect(await heldRoleNames(user)).toEqual([])
166
+ })
167
+
168
+ it('★ nor take it away', async () => {
169
+ const holder = await makeUser([child], [parentRole])
170
+ await expect(assignRoles(holder, { remove: [parentRole.id] }, child, undefined, childAdmin)).rejects.toMatchObject({ code: 'role-belongs-to-parent' })
171
+ expect(await heldRoleNames(holder)).toEqual(['hq-approver'])
172
+ })
173
+
174
+ it('★ the parent\'s owner can - from the child', async () => {
175
+ const parentOwner = await makeUser([parent], [])
176
+ await getRepository(DomainOwner).save({ domain: parent, user: parentOwner })
177
+ await assignRoles(user, { add: [parentRole.id] }, child, undefined, parentOwner)
178
+ expect(await heldRoleNames(user)).toEqual(['hq-approver'])
179
+ })
180
+
181
+ it('★ a holder of the role-giving privilege in the parent can', async () => {
182
+ const grant = await getRepository(Privilege).findOneBy({ name: 'mutation', category: 'user' })
183
+ const hqAdmins = await getRepository(Role).save({ name: 'hq-admins', domain: parent, privileges: [grant] })
184
+ const hqAdmin = await makeUser([parent, child], [hqAdmins])
185
+ await assignRoles(user, { add: [parentRole.id] }, child, undefined, hqAdmin)
186
+ expect(await heldRoleNames(user)).toEqual(['hq-approver'])
187
+ })
188
+
189
+ it('a parent role left as it was is no change - the user screen sends every shown role, nothing is refused', async () => {
190
+ const own = await getRepository(Role).save({ name: 'operator', domain: child })
191
+ const holder = await makeUser([child], [parentRole])
192
+ await assignRoles(holder, { add: [parentRole.id, own.id], remove: [parentRole.id, own.id] }, child, undefined, childAdmin)
193
+ expect(await heldRoleNames(holder)).toEqual(['hq-approver', 'operator'])
194
+ })
195
+
196
+ it('★ updateUserRoles carries the caller - a child admin adding the parent\'s role is refused', async () => {
197
+ const mutation = new UserMutation()
198
+ await expect(
199
+ mutation.updateUserRoles(user.username, [{ id: parentRole.id }], [{ id: parentRole.id }], { state: { domain: child, user: childAdmin }, t: (k: string) => k } as any)
200
+ ).rejects.toMatchObject({ code: 'role-belongs-to-parent' })
201
+ })
202
+
203
+ it('★ updateUserRoles carries the caller - the parent\'s owner adding it from the child passes', async () => {
204
+ const parentOwner = await makeUser([parent, child], [])
205
+ await getRepository(DomainOwner).save({ domain: parent, user: parentOwner })
206
+ await new UserMutation().updateUserRoles(user.username, [{ id: parentRole.id }], [{ id: parentRole.id }], {
207
+ state: { domain: child, user: parentOwner },
208
+ t: (k: string) => k
209
+ } as any)
210
+ expect(await heldRoleNames(user)).toEqual(['hq-approver'])
211
+ })
212
+
213
+ it('a granted role is the granting domain\'s delegation - a child decider assigns it', async () => {
214
+ const partnerRole = await getRepository(Role).save({ name: 'partner', domain: stranger })
215
+ await getRepository(GrantedRole).save({ role: partnerRole, domain: child })
216
+ await assignRoles(user, { add: [partnerRole.id] }, child, undefined, childAdmin)
217
+ expect(await heldRoleNames(user)).toEqual(['partner'])
218
+ })
219
+ })
@@ -312,3 +312,21 @@ describe('거절이 이유를 싣는다 (ADR-0103 결정 1)', () => {
312
312
  expect((result.errors![0].extensions as any).because).toBe('address')
313
313
  })
314
314
  })
315
+
316
+ describe('거절이 무엇이 막혔는지도 싣는다 — 변경인가 조회인가 (질문지 Q9 · 4)', () => {
317
+ it('★ 조회가 막히면 operation: query', async () => {
318
+ const result = await run(buildSchema(), 'guardedBoard')
319
+ expect((result.errors![0].extensions as any).operation).toBe('query')
320
+ })
321
+
322
+ it('★ 변경이 막히면 operation: mutation — 사람이 누른 일이다', async () => {
323
+ const schema = privilegeDirectiveResolver(
324
+ makeExecutableSchema({
325
+ typeDefs: [privilegeDirectiveTypeDefs, 'type Query { ok: String } type Mutation { saveBoard: String @privilege(category: "board", privilege: "mutation") }'],
326
+ resolvers: { Query: { ok: () => 'ok' }, Mutation: { saveBoard: () => 'saved' } }
327
+ })
328
+ )
329
+ const result = await graphql({ schema, source: 'mutation { saveBoard }', contextValue: { state: { domain: DOMAIN, user: USER } } })
330
+ expect((result.errors![0].extensions as any)).toMatchObject({ because: 'role', operation: 'mutation' })
331
+ })
332
+ })
@@ -247,5 +247,8 @@
247
247
  "access-request.notify.granted.title": "access granted",
248
248
  "access-request.notify.granted.body": "you now have {privilege} (role: {role}).",
249
249
  "access-request.notify.denied.title": "access request denied",
250
- "access-request.notify.denied.body": "{privilege}: {reason}"
250
+ "access-request.notify.denied.body": "{privilege}: {reason}",
251
+ "error.access-request-needs-reason": "write why you need it.",
252
+ "error.access-request-same-reason-as-denied": "this was denied lately for the same reason ({reason}). write what has changed.",
253
+ "error.role-belongs-to-parent": "{roles}: a role of the parent domain. only someone who decides roles there can give or take it."
251
254
  }
@@ -247,5 +247,8 @@
247
247
  "access-request.notify.granted.title": "権限が付与されました",
248
248
  "access-request.notify.granted.body": "{privilege} 権限が付与されました(ロール: {role})。",
249
249
  "access-request.notify.denied.title": "権限リクエストが拒否されました",
250
- "access-request.notify.denied.body": "{privilege}: {reason}"
250
+ "access-request.notify.denied.body": "{privilege}: {reason}",
251
+ "error.access-request-needs-reason": "必要な理由を書いてください。",
252
+ "error.access-request-same-reason-as-denied": "同じ理由で最近拒否されました({reason})。何が変わったかを書いてください。",
253
+ "error.role-belongs-to-parent": "{roles}: 上位$t(term.domain)のロールです。その$t(term.domain)で権限を決める人だけが付与・解除できます。"
251
254
  }
@@ -247,5 +247,8 @@
247
247
  "access-request.notify.granted.title": "권한을 받았습니다",
248
248
  "access-request.notify.granted.body": "{privilege} 권한을 받았습니다(역할: {role}).",
249
249
  "access-request.notify.denied.title": "권한 요청이 거부되었습니다",
250
- "access-request.notify.denied.body": "{privilege}: {reason}"
250
+ "access-request.notify.denied.body": "{privilege}: {reason}",
251
+ "error.access-request-needs-reason": "필요한 까닭을 적어 주세요.",
252
+ "error.access-request-same-reason-as-denied": "같은 까닭으로 최근에 거부되었습니다({reason}). 무엇이 달라졌는지 적어 주세요.",
253
+ "error.role-belongs-to-parent": "{roles}: 상위 $t(term.domain)의 역할입니다. 그 $t(term.domain)에서 권한을 정하는 사람만 주거나 뺄 수 있습니다."
251
254
  }
@@ -247,5 +247,8 @@
247
247
  "access-request.notify.granted.title": "akses diberikan",
248
248
  "access-request.notify.granted.body": "anda kini mempunyai {privilege} (peranan: {role}).",
249
249
  "access-request.notify.denied.title": "permintaan akses ditolak",
250
- "access-request.notify.denied.body": "{privilege}: {reason}"
250
+ "access-request.notify.denied.body": "{privilege}: {reason}",
251
+ "error.access-request-needs-reason": "tulis sebab anda memerlukannya.",
252
+ "error.access-request-same-reason-as-denied": "ini ditolak baru-baru ini atas sebab yang sama ({reason}). tulis apa yang telah berubah.",
253
+ "error.role-belongs-to-parent": "{roles}: peranan $t(term.domain) induk. hanya mereka yang memutuskan peranan di sana boleh memberi atau menariknya."
251
254
  }
@@ -247,5 +247,8 @@
247
247
  "access-request.notify.granted.title": "权限已授予",
248
248
  "access-request.notify.granted.body": "您已获得 {privilege} 权限(角色: {role})。",
249
249
  "access-request.notify.denied.title": "权限请求被拒绝",
250
- "access-request.notify.denied.body": "{privilege}: {reason}"
250
+ "access-request.notify.denied.body": "{privilege}: {reason}",
251
+ "error.access-request-needs-reason": "请填写需要的理由。",
252
+ "error.access-request-same-reason-as-denied": "最近以相同理由被拒绝({reason})。请写明有何变化。",
253
+ "error.role-belongs-to-parent": "{roles}: 上级$t(term.domain)的角色。只有在该$t(term.domain)负责权限的人才能授予或移除。"
251
254
  }