@things-factory/auth-ui 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.
- package/dist-client/bootstrap.js +22 -1
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/components/access-decision-popup.d.ts +30 -0
- package/dist-client/components/access-decision-popup.js +228 -0
- package/dist-client/components/access-decision-popup.js.map +1 -0
- package/dist-client/components/access-request-popup.d.ts +32 -0
- package/dist-client/components/access-request-popup.js +211 -0
- package/dist-client/components/access-request-popup.js.map +1 -0
- package/dist-client/components/user-detail.d.ts +2 -0
- package/dist-client/components/user-detail.js +18 -0
- package/dist-client/components/user-detail.js.map +1 -1
- package/dist-client/pages/access-request/access-requests.d.ts +51 -0
- package/dist-client/pages/access-request/access-requests.js +220 -0
- package/dist-client/pages/access-request/access-requests.js.map +1 -0
- package/dist-client/pages/access-request/my-access-requests.d.ts +26 -0
- package/dist-client/pages/access-request/my-access-requests.js +151 -0
- package/dist-client/pages/access-request/my-access-requests.js.map +1 -0
- package/dist-client/route.js +6 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.layout-check.tsbuildinfo +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/access-request.d.ts +49 -0
- package/dist-client/utils/access-request.js +67 -0
- package/dist-client/utils/access-request.js.map +1 -0
- package/dist-client/utils/access-request.test.d.ts +1 -0
- package/dist-client/utils/access-request.test.js +70 -0
- package/dist-client/utils/access-request.test.js.map +1 -0
- package/dist-client/utils/role-origin.d.ts +8 -0
- package/dist-client/utils/role-origin.js +10 -0
- package/dist-client/utils/role-origin.js.map +1 -1
- package/dist-client/utils/role-origin.test.js +7 -1
- package/dist-client/utils/role-origin.test.js.map +1 -1
- package/package.json +5 -5
- package/things-factory.config.js +3 -1
- package/translations/en.json +63 -1
- package/translations/ja.json +63 -1
- package/translations/ko.json +63 -1
- package/translations/ms.json +63 -1
- package/translations/zh.json +63 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type PrivilegeRequestDetail = {
|
|
2
|
+
category: string;
|
|
3
|
+
privilege: string;
|
|
4
|
+
operationName?: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* What a refusal offers to ask for - only a refusal a role would open. Address, license, ownership and
|
|
8
|
+
* unknown refusals offer nothing: giving a role would not open them.
|
|
9
|
+
*/
|
|
10
|
+
export declare function requestableFrom(extensions: any, operationName?: string): PrivilegeRequestDetail | null;
|
|
11
|
+
type Translate = (key: string, options?: Record<string, unknown>) => string;
|
|
12
|
+
/**
|
|
13
|
+
* What a privilege lets one do, in the reader's words - the sentence the role screen shows
|
|
14
|
+
* (`privilege.allows.<category>.<privilege>`), or the name itself when none is written. Never made up.
|
|
15
|
+
*/
|
|
16
|
+
export declare function privilegeWords(t: Translate, category: string, privilege: string): string;
|
|
17
|
+
export type ReviewIssue = {
|
|
18
|
+
code: string;
|
|
19
|
+
blocking: boolean;
|
|
20
|
+
field?: string;
|
|
21
|
+
params?: Record<string, unknown>;
|
|
22
|
+
};
|
|
23
|
+
/** One review finding as a sentence - `access-request.issue.<code>`, the code itself when the key is missing. */
|
|
24
|
+
export declare function issueWords(t: Translate, issue: ReviewIssue): string;
|
|
25
|
+
export declare const blocks: (issues: ReviewIssue[]) => boolean;
|
|
26
|
+
/** Which buttons a decision popup offers, from the review the server gave. */
|
|
27
|
+
export declare function decisionButtons(grantIssues: ReviewIssue[] | null, denyIssues: ReviewIssue[] | null): {
|
|
28
|
+
grant: boolean;
|
|
29
|
+
deny: boolean;
|
|
30
|
+
};
|
|
31
|
+
/** The candidate a popup selects first - the smallest; ties by name, as the server sorts them. */
|
|
32
|
+
export declare function firstCandidate<T extends {
|
|
33
|
+
name: string;
|
|
34
|
+
privilegeCount: number;
|
|
35
|
+
}>(candidates: T[]): T | undefined;
|
|
36
|
+
/** How long a denial is shown back before asking again - the server holds the same words against it (auth-base ACCESS_REQUEST_DENIAL_DAYS). */
|
|
37
|
+
export declare const DENIAL_SHOWN_DAYS = 7;
|
|
38
|
+
/** The latest denial of this privilege among the person's own requests, within DENIAL_SHOWN_DAYS. */
|
|
39
|
+
export declare function recentDenial<T extends {
|
|
40
|
+
state: string;
|
|
41
|
+
category: string;
|
|
42
|
+
privilege: string;
|
|
43
|
+
decidedAt?: string | null;
|
|
44
|
+
}>(mine: T[], category: string, privilege: string, now?: number): T | undefined;
|
|
45
|
+
/** Send is offered only with a reason - and, after a denial, with words other than the denied ones. */
|
|
46
|
+
export declare function maySend(reason: string, denied?: {
|
|
47
|
+
reason?: string | null;
|
|
48
|
+
}): boolean;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The access request on the screen - pure, so the rules are tested without a browser (ADR-0103).
|
|
3
|
+
*
|
|
4
|
+
* A refusal's toast gets its button from this package through @operato/layout's registerNotifyAction
|
|
5
|
+
* (bootstrap.ts) - the toast and @operato/graphql do not know what a refusal means (ADR-0103 decision 5).
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* What a refusal offers to ask for - only a refusal a role would open. Address, license, ownership and
|
|
9
|
+
* unknown refusals offer nothing: giving a role would not open them.
|
|
10
|
+
*/
|
|
11
|
+
export function requestableFrom(extensions, operationName) {
|
|
12
|
+
if (!extensions || extensions.code !== 'FORBIDDEN' || extensions.because !== 'role')
|
|
13
|
+
return null;
|
|
14
|
+
/*
|
|
15
|
+
* Only a change the person made (questions Q9 · 4). A page fires several reads as it opens; offering a request on
|
|
16
|
+
* each would put the button in front of someone who did not choose to do anything yet.
|
|
17
|
+
*/
|
|
18
|
+
if (extensions.operation !== 'mutation')
|
|
19
|
+
return null;
|
|
20
|
+
const requires = extensions.requires;
|
|
21
|
+
if (!requires || typeof requires !== 'object' || !requires.category || !requires.privilege)
|
|
22
|
+
return null;
|
|
23
|
+
return { category: requires.category, privilege: requires.privilege, ...(operationName ? { operationName } : {}) };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* What a privilege lets one do, in the reader's words - the sentence the role screen shows
|
|
27
|
+
* (`privilege.allows.<category>.<privilege>`), or the name itself when none is written. Never made up.
|
|
28
|
+
*/
|
|
29
|
+
export function privilegeWords(t, category, privilege) {
|
|
30
|
+
return t(`privilege.allows.${category}.${privilege}`, { defaultValue: '' }) || `${category}:${privilege}`;
|
|
31
|
+
}
|
|
32
|
+
/** One review finding as a sentence - `access-request.issue.<code>`, the code itself when the key is missing. */
|
|
33
|
+
export function issueWords(t, issue) {
|
|
34
|
+
const params = { ...(issue.params || {}) };
|
|
35
|
+
if (typeof params.state === 'string')
|
|
36
|
+
params.state = t(`access-request.state.${String(params.state).toLowerCase()}`, { defaultValue: String(params.state) });
|
|
37
|
+
return t(`access-request.issue.${issue.code}`, { ...params, defaultValue: issue.code });
|
|
38
|
+
}
|
|
39
|
+
export const blocks = (issues) => issues.some(issue => issue.blocking);
|
|
40
|
+
/** Which buttons a decision popup offers, from the review the server gave. */
|
|
41
|
+
export function decisionButtons(grantIssues, denyIssues) {
|
|
42
|
+
return {
|
|
43
|
+
grant: !!grantIssues && !blocks(grantIssues),
|
|
44
|
+
deny: !!denyIssues && !blocks(denyIssues)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** The candidate a popup selects first - the smallest; ties by name, as the server sorts them. */
|
|
48
|
+
export function firstCandidate(candidates) {
|
|
49
|
+
return [...(candidates || [])].sort((a, b) => a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0];
|
|
50
|
+
}
|
|
51
|
+
/** How long a denial is shown back before asking again - the server holds the same words against it (auth-base ACCESS_REQUEST_DENIAL_DAYS). */
|
|
52
|
+
export const DENIAL_SHOWN_DAYS = 7;
|
|
53
|
+
/** The latest denial of this privilege among the person's own requests, within DENIAL_SHOWN_DAYS. */
|
|
54
|
+
export function recentDenial(mine, category, privilege, now = Date.now()) {
|
|
55
|
+
return (mine || [])
|
|
56
|
+
.filter(r => r.state === 'DENIED' && r.category === category && r.privilege === privilege && r.decidedAt)
|
|
57
|
+
.filter(r => now - new Date(r.decidedAt).getTime() <= DENIAL_SHOWN_DAYS * 24 * 60 * 60 * 1000)
|
|
58
|
+
.sort((a, b) => new Date(b.decidedAt).getTime() - new Date(a.decidedAt).getTime())[0];
|
|
59
|
+
}
|
|
60
|
+
/** Send is offered only with a reason - and, after a denial, with words other than the denied ones. */
|
|
61
|
+
export function maySend(reason, denied) {
|
|
62
|
+
const said = (reason || '').trim();
|
|
63
|
+
if (!said)
|
|
64
|
+
return false;
|
|
65
|
+
return !(denied && (denied.reason || '').trim() === said);
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=access-request.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access-request.js","sourceRoot":"","sources":["../../client/utils/access-request.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The access request on the screen - pure, so the rules are tested without a browser (ADR-0103).\n *\n * A refusal's toast gets its button from this package through @operato/layout's registerNotifyAction\n * (bootstrap.ts) - the toast and @operato/graphql do not know what a refusal means (ADR-0103 decision 5).\n */\n\nexport type PrivilegeRequestDetail = { category: string; privilege: string; operationName?: string }\n\n/**\n * What a refusal offers to ask for - only a refusal a role would open. Address, license, ownership and\n * unknown refusals offer nothing: giving a role would not open them.\n */\nexport function requestableFrom(extensions: any, operationName?: string): PrivilegeRequestDetail | null {\n if (!extensions || extensions.code !== 'FORBIDDEN' || extensions.because !== 'role') return null\n /*\n * Only a change the person made (questions Q9 · 4). A page fires several reads as it opens; offering a request on\n * each would put the button in front of someone who did not choose to do anything yet.\n */\n if (extensions.operation !== 'mutation') return null\n const requires = extensions.requires\n if (!requires || typeof requires !== 'object' || !requires.category || !requires.privilege) return null\n return { category: requires.category, privilege: requires.privilege, ...(operationName ? { operationName } : {}) }\n}\n\ntype Translate = (key: string, options?: Record<string, unknown>) => string\n\n/**\n * What a privilege lets one do, in the reader's words - the sentence the role screen shows\n * (`privilege.allows.<category>.<privilege>`), or the name itself when none is written. Never made up.\n */\nexport function privilegeWords(t: Translate, category: string, privilege: string): string {\n return t(`privilege.allows.${category}.${privilege}`, { defaultValue: '' }) || `${category}:${privilege}`\n}\n\nexport type ReviewIssue = { code: string; blocking: boolean; field?: string; params?: Record<string, unknown> }\n\n/** One review finding as a sentence - `access-request.issue.<code>`, the code itself when the key is missing. */\nexport function issueWords(t: Translate, issue: ReviewIssue): string {\n const params = { ...(issue.params || {}) }\n if (typeof params.state === 'string') params.state = t(`access-request.state.${String(params.state).toLowerCase()}`, { defaultValue: String(params.state) })\n return t(`access-request.issue.${issue.code}`, { ...params, defaultValue: issue.code })\n}\n\nexport const blocks = (issues: ReviewIssue[]) => issues.some(issue => issue.blocking)\n\n/** Which buttons a decision popup offers, from the review the server gave. */\nexport function decisionButtons(grantIssues: ReviewIssue[] | null, denyIssues: ReviewIssue[] | null) {\n return {\n grant: !!grantIssues && !blocks(grantIssues),\n deny: !!denyIssues && !blocks(denyIssues)\n }\n}\n\n/** The candidate a popup selects first - the smallest; ties by name, as the server sorts them. */\nexport function firstCandidate<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\n/** How long a denial is shown back before asking again - the server holds the same words against it (auth-base ACCESS_REQUEST_DENIAL_DAYS). */\nexport const DENIAL_SHOWN_DAYS = 7\n\n/** The latest denial of this privilege among the person's own requests, within DENIAL_SHOWN_DAYS. */\nexport function recentDenial<T extends { state: string; category: string; privilege: string; decidedAt?: string | null }>(\n mine: T[],\n category: string,\n privilege: string,\n now = Date.now()\n): T | undefined {\n return (mine || [])\n .filter(r => r.state === 'DENIED' && r.category === category && r.privilege === privilege && r.decidedAt)\n .filter(r => now - new Date(r.decidedAt as string).getTime() <= DENIAL_SHOWN_DAYS * 24 * 60 * 60 * 1000)\n .sort((a, b) => new Date(b.decidedAt as string).getTime() - new Date(a.decidedAt as string).getTime())[0]\n}\n\n/** Send is offered only with a reason - and, after a denial, with words other than the denied ones. */\nexport function maySend(reason: string, denied?: { reason?: string | null }): boolean {\n const said = (reason || '').trim()\n if (!said) return false\n return !(denied && (denied.reason || '').trim() === said)\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { blocks, decisionButtons, firstCandidate, issueWords, maySend, privilegeWords, recentDenial, requestableFrom } from './access-request';
|
|
2
|
+
/**
|
|
3
|
+
* The access request on the screen (ADR-0103). The refusal says why; only a missing role offers a request.
|
|
4
|
+
*/
|
|
5
|
+
const ROLE = { code: 'FORBIDDEN', requires: { category: 'warehouse', privilege: 'mutation' }, because: 'role', operation: 'mutation' };
|
|
6
|
+
describe('what a refusal offers to ask for', () => {
|
|
7
|
+
it('★ a refusal a role would open - the privilege it named', () => {
|
|
8
|
+
expect(requestableFrom(ROLE, 'updateInboundPlan')).toEqual({ category: 'warehouse', privilege: 'mutation', operationName: 'updateInboundPlan' });
|
|
9
|
+
});
|
|
10
|
+
it('★ address, license, ownership and unknown offer nothing - a role would not open them', () => {
|
|
11
|
+
for (const because of ['address', 'license', 'ownership', 'unknown']) {
|
|
12
|
+
expect(requestableFrom({ ...ROLE, because })).toBeNull();
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
it('★ a read the page fired offers nothing - only a change the person made (Q9)', () => {
|
|
16
|
+
expect(requestableFrom({ ...ROLE, operation: 'query' })).toBeNull();
|
|
17
|
+
expect(requestableFrom({ ...ROLE, operation: 'subscription' })).toBeNull();
|
|
18
|
+
expect(requestableFrom({ ...ROLE, operation: undefined })).toBeNull();
|
|
19
|
+
});
|
|
20
|
+
it('a refusal from before the cause was carried offers nothing - it is not guessed', () => {
|
|
21
|
+
expect(requestableFrom({ code: 'FORBIDDEN', requires: ROLE.requires })).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
it('ownership-only and other errors offer nothing', () => {
|
|
24
|
+
expect(requestableFrom({ ...ROLE, requires: 'ownership' })).toBeNull();
|
|
25
|
+
expect(requestableFrom({ code: 'BAD_USER_INPUT' })).toBeNull();
|
|
26
|
+
expect(requestableFrom(undefined)).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('words', () => {
|
|
30
|
+
const t = (key, options = {}) => (key === 'privilege.allows.warehouse.mutation' ? '창고 기록을 바꿀 수 있습니다.' : options.defaultValue);
|
|
31
|
+
it('★ the sentence the role screen writes, or the name - never a key', () => {
|
|
32
|
+
expect(privilegeWords(t, 'warehouse', 'mutation')).toBe('창고 기록을 바꿀 수 있습니다.');
|
|
33
|
+
expect(privilegeWords(t, 'board', 'query')).toBe('board:query');
|
|
34
|
+
});
|
|
35
|
+
it('an issue without a sentence shows its code, not the key', () => {
|
|
36
|
+
expect(issueWords(t, { code: 'own-request', blocking: true })).toBe('own-request');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('the decision buttons', () => {
|
|
40
|
+
it('★ a blocking finding takes the button away; a warning does not', () => {
|
|
41
|
+
expect(decisionButtons([{ code: 'role-grants-more', blocking: false }], [{ code: 'deny-needs-reason', blocking: true }])).toEqual({ grant: true, deny: false });
|
|
42
|
+
expect(decisionButtons([{ code: 'role-does-not-grant', blocking: true }], [])).toEqual({ grant: false, deny: true });
|
|
43
|
+
expect(decisionButtons(null, [])).toEqual({ grant: false, deny: true });
|
|
44
|
+
expect(blocks([])).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
it('the smallest role first, ties by name', () => {
|
|
47
|
+
expect(firstCandidate([{ name: 'b', privilegeCount: 1 }, { name: 'a', privilegeCount: 1 }, { name: 'c', privilegeCount: 0 }])?.name).toBe('c');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('asking is not a reflex (Q9)', () => {
|
|
51
|
+
const NOW = new Date('2026-10-02T09:00:00Z').getTime();
|
|
52
|
+
const mine = [
|
|
53
|
+
{ state: 'DENIED', category: 'warehouse', privilege: 'mutation', decidedAt: '2026-09-30T09:00:00Z', reason: 'inbound', decisionReason: 'IT only' },
|
|
54
|
+
{ state: 'DENIED', category: 'warehouse', privilege: 'mutation', decidedAt: '2026-09-01T09:00:00Z', reason: 'old', decisionReason: 'old' },
|
|
55
|
+
{ state: 'DENIED', category: 'board', privilege: 'mutation', decidedAt: '2026-10-01T09:00:00Z', reason: 'x', decisionReason: 'y' }
|
|
56
|
+
];
|
|
57
|
+
it('★ the latest denial of the same privilege within seven days is shown back', () => {
|
|
58
|
+
expect(recentDenial(mine, 'warehouse', 'mutation', NOW)?.decisionReason).toBe('IT only');
|
|
59
|
+
expect(recentDenial(mine, 'warehouse', 'query', NOW)).toBeUndefined();
|
|
60
|
+
expect(recentDenial(mine.slice(1, 2), 'warehouse', 'mutation', NOW)).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
it('★ send needs a reason, and after a denial other words than the denied ones', () => {
|
|
63
|
+
expect(maySend('')).toBe(false);
|
|
64
|
+
expect(maySend(' ')).toBe(false);
|
|
65
|
+
expect(maySend('inbound')).toBe(true);
|
|
66
|
+
expect(maySend('inbound', { reason: 'inbound' })).toBe(false);
|
|
67
|
+
expect(maySend('moved to inbound', { reason: 'inbound' })).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
//# sourceMappingURL=access-request.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access-request.test.js","sourceRoot":"","sources":["../../client/utils/access-request.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { blocks, decisionButtons, firstCandidate, issueWords, maySend, privilegeWords, recentDenial, requestableFrom } from './access-request'\n\n/**\n * The access request on the screen (ADR-0103). The refusal says why; only a missing role offers a request.\n */\n\nconst ROLE = { code: 'FORBIDDEN', requires: { category: 'warehouse', privilege: 'mutation' }, because: 'role', operation: 'mutation' }\n\ndescribe('what a refusal offers to ask for', () => {\n it('★ a refusal a role would open - the privilege it named', () => {\n expect(requestableFrom(ROLE, 'updateInboundPlan')).toEqual({ category: 'warehouse', privilege: 'mutation', operationName: 'updateInboundPlan' })\n })\n\n it('★ address, license, ownership and unknown offer nothing - a role would not open them', () => {\n for (const because of ['address', 'license', 'ownership', 'unknown']) {\n expect(requestableFrom({ ...ROLE, because })).toBeNull()\n }\n })\n\n it('★ a read the page fired offers nothing - only a change the person made (Q9)', () => {\n expect(requestableFrom({ ...ROLE, operation: 'query' })).toBeNull()\n expect(requestableFrom({ ...ROLE, operation: 'subscription' })).toBeNull()\n expect(requestableFrom({ ...ROLE, operation: undefined })).toBeNull()\n })\n\n it('a refusal from before the cause was carried offers nothing - it is not guessed', () => {\n expect(requestableFrom({ code: 'FORBIDDEN', requires: ROLE.requires })).toBeNull()\n })\n\n it('ownership-only and other errors offer nothing', () => {\n expect(requestableFrom({ ...ROLE, requires: 'ownership' })).toBeNull()\n expect(requestableFrom({ code: 'BAD_USER_INPUT' })).toBeNull()\n expect(requestableFrom(undefined)).toBeNull()\n })\n})\n\ndescribe('words', () => {\n const t = (key: string, options: any = {}) => (key === 'privilege.allows.warehouse.mutation' ? '창고 기록을 바꿀 수 있습니다.' : options.defaultValue)\n\n it('★ the sentence the role screen writes, or the name - never a key', () => {\n expect(privilegeWords(t, 'warehouse', 'mutation')).toBe('창고 기록을 바꿀 수 있습니다.')\n expect(privilegeWords(t, 'board', 'query')).toBe('board:query')\n })\n\n it('an issue without a sentence shows its code, not the key', () => {\n expect(issueWords(t, { code: 'own-request', blocking: true })).toBe('own-request')\n })\n})\n\ndescribe('the decision buttons', () => {\n it('★ a blocking finding takes the button away; a warning does not', () => {\n expect(decisionButtons([{ code: 'role-grants-more', blocking: false }], [{ code: 'deny-needs-reason', blocking: true }])).toEqual({ grant: true, deny: false })\n expect(decisionButtons([{ code: 'role-does-not-grant', blocking: true }], [])).toEqual({ grant: false, deny: true })\n expect(decisionButtons(null, [])).toEqual({ grant: false, deny: true })\n expect(blocks([])).toBe(false)\n })\n\n it('the smallest role first, ties by name', () => {\n expect(firstCandidate([{ name: 'b', privilegeCount: 1 }, { name: 'a', privilegeCount: 1 }, { name: 'c', privilegeCount: 0 }])?.name).toBe('c')\n })\n})\n\ndescribe('asking is not a reflex (Q9)', () => {\n const NOW = new Date('2026-10-02T09:00:00Z').getTime()\n const mine = [\n { state: 'DENIED', category: 'warehouse', privilege: 'mutation', decidedAt: '2026-09-30T09:00:00Z', reason: 'inbound', decisionReason: 'IT only' },\n { state: 'DENIED', category: 'warehouse', privilege: 'mutation', decidedAt: '2026-09-01T09:00:00Z', reason: 'old', decisionReason: 'old' },\n { state: 'DENIED', category: 'board', privilege: 'mutation', decidedAt: '2026-10-01T09:00:00Z', reason: 'x', decisionReason: 'y' }\n ]\n\n it('★ the latest denial of the same privilege within seven days is shown back', () => {\n expect(recentDenial(mine, 'warehouse', 'mutation', NOW)?.decisionReason).toBe('IT only')\n expect(recentDenial(mine, 'warehouse', 'query', NOW)).toBeUndefined()\n expect(recentDenial(mine.slice(1, 2), 'warehouse', 'mutation', NOW)).toBeUndefined()\n })\n\n it('★ send needs a reason, and after a denial other words than the denied ones', () => {\n expect(maySend('')).toBe(false)\n expect(maySend(' ')).toBe(false)\n expect(maySend('inbound')).toBe(true)\n expect(maySend('inbound', { reason: 'inbound' })).toBe(false)\n expect(maySend('moved to inbound', { reason: 'inbound' })).toBe(true)\n })\n})\n"]}
|
|
@@ -42,3 +42,11 @@ export declare function showsRoleOrigin(roles: RoleWithOrigin[] | null | undefin
|
|
|
42
42
|
* labelled, so an unlabelled row reads as "not said" instead of as a domain called something.
|
|
43
43
|
*/
|
|
44
44
|
export declare function roleOrigin(role: RoleWithOrigin): string;
|
|
45
|
+
/**
|
|
46
|
+
* The parent domain's roles a person here may not change (things-factory ADR-0103 decision 8).
|
|
47
|
+
*
|
|
48
|
+
* Holding a role is not tied to a domain: a parent's role given from a child counts in the parent and in its other
|
|
49
|
+
* children. So only someone who decides roles in the parent may give or take it - the server refuses everyone else
|
|
50
|
+
* (`role-belongs-to-parent`), and the screen locks those boxes rather than let a save fail.
|
|
51
|
+
*/
|
|
52
|
+
export declare function lockedParentRoles(parentRoleIds: Iterable<string>, mayChangeParentRoles: boolean): Set<string>;
|
|
@@ -44,4 +44,14 @@ export function showsRoleOrigin(roles) {
|
|
|
44
44
|
export function roleOrigin(role) {
|
|
45
45
|
return role?.domain?.name || '';
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* The parent domain's roles a person here may not change (things-factory ADR-0103 decision 8).
|
|
49
|
+
*
|
|
50
|
+
* Holding a role is not tied to a domain: a parent's role given from a child counts in the parent and in its other
|
|
51
|
+
* children. So only someone who decides roles in the parent may give or take it - the server refuses everyone else
|
|
52
|
+
* (`role-belongs-to-parent`), and the screen locks those boxes rather than let a save fail.
|
|
53
|
+
*/
|
|
54
|
+
export function lockedParentRoles(parentRoleIds, mayChangeParentRoles) {
|
|
55
|
+
return mayChangeParentRoles ? new Set() : new Set(parentRoleIds);
|
|
56
|
+
}
|
|
47
57
|
//# sourceMappingURL=role-origin.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role-origin.js","sourceRoot":"","sources":["../../client/utils/role-origin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,KAA0C;IACxE,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,OAAO,KAAK,CAAA;IACd,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC,CAAA;IAEpE,OAAO,OAAO,CAAC,IAAI,GAAG,CAAC,CAAA;AACzB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,IAAoB;IAC7C,OAAO,IAAI,EAAE,MAAM,EAAE,IAAI,IAAI,EAAE,CAAA;AACjC,CAAC","sourcesContent":["/**\n * When a list of roles has to say where each one came from. (Pure rule.)\n *\n * ## The situation it is for\n *\n * Roles are visible one level up: a domain sees its own and its parent's. Nothing stops a\n * domain having a role with the same name as one it inherits — the duplicate checks on the\n * server ask only about the domain itself — so a picker can offer two rows reading `approver`\n * with no way to tell them apart. Whichever gets ticked decides who a gate lets through, and\n * an approval line can end up pointing at the one nobody looks at (ADR-0062 decision 3).\n *\n * ## Why the rule is about the list and not about one role\n *\n * A role does not know whether it is inherited — that depends on which domain is being looked\n * at, and a list of roles from one domain is the ordinary case. So the question is asked of the\n * list: **does this set span more than one domain?** If it does, every row says which, and the\n * rows that shared a name stop being identical. If it does not, nothing is added and the\n * ordinary case stays quiet.\n *\n * That also answers correctly for a role whose name happens to be unique but which still comes\n * from somewhere else — granting one of those is a different act from granting your own, and\n * the operator should be able to see which they are doing.\n */\n\n/** The part of a role this rule reads. `domain` is absent on lists that never asked for it. */\nexport type RoleWithOrigin = {\n domain?: { name?: string | null } | null\n} | null | undefined\n\n/**\n * Should this list show where each role comes from?\n *\n * False when every role names the same domain, when none of them names one at all, and for an\n * empty list. A list that is only partly labelled counts as spanning — one row that cannot say\n * where it came from, beside rows that can, is the ambiguity this exists to remove.\n */\nexport function showsRoleOrigin(roles: RoleWithOrigin[] | null | undefined): boolean {\n if (!roles || roles.length < 2) {\n return false\n }\n\n const origins = new Set(roles.map(role => role?.domain?.name || ''))\n\n return origins.size > 1\n}\n\n/**\n * What to print beside a role, or an empty string.\n *\n * A role with no domain on it prints nothing rather than a placeholder: the list is already\n * labelled, so an unlabelled row reads as \"not said\" instead of as a domain called something.\n */\nexport function roleOrigin(role: RoleWithOrigin): string {\n return role?.domain?.name || ''\n}\n"]}
|
|
1
|
+
{"version":3,"file":"role-origin.js","sourceRoot":"","sources":["../../client/utils/role-origin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,KAA0C;IACxE,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,OAAO,KAAK,CAAA;IACd,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC,CAAA;IAEpE,OAAO,OAAO,CAAC,IAAI,GAAG,CAAC,CAAA;AACzB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,IAAoB;IAC7C,OAAO,IAAI,EAAE,MAAM,EAAE,IAAI,IAAI,EAAE,CAAA;AACjC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,aAA+B,EAAE,oBAA6B;IAC9F,OAAO,oBAAoB,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,aAAa,CAAC,CAAA;AAClE,CAAC","sourcesContent":["/**\n * When a list of roles has to say where each one came from. (Pure rule.)\n *\n * ## The situation it is for\n *\n * Roles are visible one level up: a domain sees its own and its parent's. Nothing stops a\n * domain having a role with the same name as one it inherits — the duplicate checks on the\n * server ask only about the domain itself — so a picker can offer two rows reading `approver`\n * with no way to tell them apart. Whichever gets ticked decides who a gate lets through, and\n * an approval line can end up pointing at the one nobody looks at (ADR-0062 decision 3).\n *\n * ## Why the rule is about the list and not about one role\n *\n * A role does not know whether it is inherited — that depends on which domain is being looked\n * at, and a list of roles from one domain is the ordinary case. So the question is asked of the\n * list: **does this set span more than one domain?** If it does, every row says which, and the\n * rows that shared a name stop being identical. If it does not, nothing is added and the\n * ordinary case stays quiet.\n *\n * That also answers correctly for a role whose name happens to be unique but which still comes\n * from somewhere else — granting one of those is a different act from granting your own, and\n * the operator should be able to see which they are doing.\n */\n\n/** The part of a role this rule reads. `domain` is absent on lists that never asked for it. */\nexport type RoleWithOrigin = {\n domain?: { name?: string | null } | null\n} | null | undefined\n\n/**\n * Should this list show where each role comes from?\n *\n * False when every role names the same domain, when none of them names one at all, and for an\n * empty list. A list that is only partly labelled counts as spanning — one row that cannot say\n * where it came from, beside rows that can, is the ambiguity this exists to remove.\n */\nexport function showsRoleOrigin(roles: RoleWithOrigin[] | null | undefined): boolean {\n if (!roles || roles.length < 2) {\n return false\n }\n\n const origins = new Set(roles.map(role => role?.domain?.name || ''))\n\n return origins.size > 1\n}\n\n/**\n * What to print beside a role, or an empty string.\n *\n * A role with no domain on it prints nothing rather than a placeholder: the list is already\n * labelled, so an unlabelled row reads as \"not said\" instead of as a domain called something.\n */\nexport function roleOrigin(role: RoleWithOrigin): string {\n return role?.domain?.name || ''\n}\n\n/**\n * The parent domain's roles a person here may not change (things-factory ADR-0103 decision 8).\n *\n * Holding a role is not tied to a domain: a parent's role given from a child counts in the parent and in its other\n * children. So only someone who decides roles in the parent may give or take it - the server refuses everyone else\n * (`role-belongs-to-parent`), and the screen locks those boxes rather than let a save fail.\n */\nexport function lockedParentRoles(parentRoleIds: Iterable<string>, mayChangeParentRoles: boolean): Set<string> {\n return mayChangeParentRoles ? new Set() : new Set(parentRoleIds)\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { roleOrigin, showsRoleOrigin } from './role-origin';
|
|
1
|
+
import { lockedParentRoles, roleOrigin, showsRoleOrigin } from './role-origin';
|
|
2
2
|
/**
|
|
3
3
|
* Whether a role picker has to say where each role came from.
|
|
4
4
|
*
|
|
@@ -57,4 +57,10 @@ describe('what is printed beside a role', () => {
|
|
|
57
57
|
expect(roleOrigin({ domain: { name: null } })).toEqual('');
|
|
58
58
|
});
|
|
59
59
|
});
|
|
60
|
+
describe('the parent domain\'s roles (ADR-0103 decision 8)', () => {
|
|
61
|
+
it('★ locked for someone who does not decide in the parent, open for someone who does', () => {
|
|
62
|
+
expect([...lockedParentRoles(['p1', 'p2'], false)].sort()).toEqual(['p1', 'p2']);
|
|
63
|
+
expect([...lockedParentRoles(['p1', 'p2'], true)]).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
60
66
|
//# sourceMappingURL=role-origin.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role-origin.test.js","sourceRoot":"","sources":["../../client/utils/role-origin.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"role-origin.test.js","sourceRoot":"","sources":["../../client/utils/role-origin.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { lockedParentRoles, roleOrigin, showsRoleOrigin } from './role-origin'\n\n/**\n * Whether a role picker has to say where each role came from.\n *\n * The cost of being wrong is not symmetric. Saying nothing when the list spans two domains\n * leaves two identical checkboxes, and ticking the wrong one points a gate — or an approval\n * line — at a role nobody looks at. Saying it when there is nothing to disambiguate only adds\n * a word.\n */\n\nconst OURS = { domain: { name: 'acme' } }\nconst THEIRS = { domain: { name: 'system' } }\n\ndescribe('a list that needs to say', () => {\n it('spans two domains', () => {\n /* ⚠ The case. Two rows can read the same name and only this separates them. */\n expect(showsRoleOrigin([OURS, THEIRS])).toBe(true)\n })\n\n it('says so even when the names do not collide', () => {\n /*\n * Granting an inherited role is a different act from granting your own, whether or not\n * some other row happens to share its name.\n */\n expect(showsRoleOrigin([{ domain: { name: 'acme' } }, { domain: { name: 'system' } }])).toBe(true)\n })\n\n it('counts a row that cannot say where it came from as a difference', () => {\n /*\n * ⚠ A half-labelled list is the ambiguity, not the cure. One blank row beside labelled ones\n * is exactly what the reader cannot resolve.\n */\n expect(showsRoleOrigin([OURS, {}])).toBe(true)\n expect(showsRoleOrigin([OURS, { domain: null }])).toBe(true)\n })\n})\n\ndescribe('a list that does not', () => {\n it('is all one domain', () => {\n expect(showsRoleOrigin([OURS, OURS, OURS])).toBe(false)\n })\n\n it('never asked for the domain at all', () => {\n /* Nothing to say. Labelling every row with nothing would be noise, not information. */\n expect(showsRoleOrigin([{}, {}])).toBe(false)\n })\n\n it('has one role, or none', () => {\n expect(showsRoleOrigin([OURS])).toBe(false)\n expect(showsRoleOrigin([])).toBe(false)\n expect(showsRoleOrigin(null)).toBe(false)\n expect(showsRoleOrigin(undefined)).toBe(false)\n })\n})\n\ndescribe('what is printed beside a role', () => {\n it('is the domain it came from', () => {\n expect(roleOrigin(OURS)).toEqual('acme')\n })\n\n it('is nothing when the role does not carry one', () => {\n /* Not a placeholder. An unlabelled row reads as \"not said\", not as a domain named that. */\n expect(roleOrigin({})).toEqual('')\n expect(roleOrigin({ domain: null })).toEqual('')\n expect(roleOrigin(null)).toEqual('')\n expect(roleOrigin({ domain: { name: null } })).toEqual('')\n })\n})\n\ndescribe('the parent domain\\'s roles (ADR-0103 decision 8)', () => {\n it('★ locked for someone who does not decide in the parent, open for someone who does', () => {\n expect([...lockedParentRoles(['p1', 'p2'], false)].sort()).toEqual(['p1', 'p2'])\n expect([...lockedParentRoles(['p1', 'p2'], true)]).toEqual([])\n })\n})\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/auth-ui",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.111",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"browser": "dist-client/index.js",
|
|
6
6
|
"things-factory": true,
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@operato/attribute": "^10.0.0",
|
|
32
32
|
"@operato/data-grist": "^10.19.0",
|
|
33
33
|
"@operato/i18n": "^10.0.0",
|
|
34
|
-
"@operato/layout": "^10.
|
|
34
|
+
"@operato/layout": "^10.29.5",
|
|
35
35
|
"@operato/list-state": "^10.15.0",
|
|
36
36
|
"@operato/lottie-player": "^10.0.0",
|
|
37
37
|
"@operato/moment-timezone-es": "^10.0.0",
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
"@operato/popup": "^10.15.0",
|
|
40
40
|
"@operato/shell": "^10.29.2",
|
|
41
41
|
"@operato/styles": "^10.0.0",
|
|
42
|
-
"@things-factory/auth-base": "^10.1.
|
|
43
|
-
"@things-factory/component-ui": "^10.1.
|
|
42
|
+
"@things-factory/auth-base": "^10.1.111",
|
|
43
|
+
"@things-factory/component-ui": "^10.1.111",
|
|
44
44
|
"@things-factory/more-base": "^10.1.106",
|
|
45
45
|
"base64url": "^3.0.1",
|
|
46
46
|
"clipboard": "^2.0.6"
|
|
47
47
|
},
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "f33122c15e5b3e45393129e9418e2c367a273ade"
|
|
49
49
|
}
|
package/things-factory.config.js
CHANGED
|
@@ -18,7 +18,9 @@ export default {
|
|
|
18
18
|
{ tagname: 'env-var-list-page', page: 'env-vars' },
|
|
19
19
|
{ tagname: 'attribute-set-management', page: 'attributes' },
|
|
20
20
|
{ tagname: 'auth-provider-management', page: 'auth-providers' },
|
|
21
|
-
{ tagname: 'domain-link-management', page: 'domain-links' }
|
|
21
|
+
{ tagname: 'domain-link-management', page: 'domain-links' },
|
|
22
|
+
{ tagname: 'access-requests-page', page: 'access-requests' },
|
|
23
|
+
{ tagname: 'my-access-requests-page', page: 'my-access-requests' }
|
|
22
24
|
],
|
|
23
25
|
bootstrap
|
|
24
26
|
}
|
package/translations/en.json
CHANGED
|
@@ -349,5 +349,67 @@
|
|
|
349
349
|
"title.secure-ip-list": "secured IP list",
|
|
350
350
|
"title.sign in": "sign in",
|
|
351
351
|
"title.sign up": "sign up",
|
|
352
|
-
"title.unlock account": "unlock account"
|
|
352
|
+
"title.unlock account": "unlock account",
|
|
353
|
+
"access-request.title.access requests": "access requests",
|
|
354
|
+
"access-request.title.my access requests": "my access requests",
|
|
355
|
+
"access-request.title.decide": "decide the access request",
|
|
356
|
+
"access-request.title.request access": "request access",
|
|
357
|
+
"access-request.text.refused at": "refused at: {operation}",
|
|
358
|
+
"access-request.text.already waiting": "a request is already waiting (last asked {at}). sending adds to it.",
|
|
359
|
+
"access-request.text.sent": "sent to the people who decide access.",
|
|
360
|
+
"access-request.text.added to waiting": "added to the request already waiting.",
|
|
361
|
+
"access-request.text.who asks": "{name} asks for",
|
|
362
|
+
"access-request.text.grant with role": "grant by putting them into a role",
|
|
363
|
+
"access-request.text.no role grants it": "no role in this domain grants it. add it to a role on the role screen first.",
|
|
364
|
+
"access-request.text.privilege count": "{count} privileges",
|
|
365
|
+
"access-request.field.reason": "why it is needed",
|
|
366
|
+
"access-request.field.deny reason": "reason to deny",
|
|
367
|
+
"access-request.field.requester": "requester",
|
|
368
|
+
"access-request.field.needed": "needed",
|
|
369
|
+
"access-request.field.count": "times",
|
|
370
|
+
"access-request.field.decided by": "decided by",
|
|
371
|
+
"access-request.field.granted role": "role given",
|
|
372
|
+
"access-request.field.decision reason": "reason",
|
|
373
|
+
"access-request.field.decided at": "decided at",
|
|
374
|
+
"access-request.button.send": "send",
|
|
375
|
+
"access-request.button.grant": "grant",
|
|
376
|
+
"access-request.button.deny": "deny",
|
|
377
|
+
"access-request.button.decide": "decide",
|
|
378
|
+
"access-request.button.withdraw": "withdraw the request",
|
|
379
|
+
"access-request.state.pending": "waiting",
|
|
380
|
+
"access-request.state.granted": "granted",
|
|
381
|
+
"access-request.state.denied": "denied",
|
|
382
|
+
"access-request.state.withdrawn": "withdrawn",
|
|
383
|
+
"access-request.state.expired": "expired",
|
|
384
|
+
"access-request.issue.access-request-not-found": "the request is not found in this domain.",
|
|
385
|
+
"access-request.issue.request-not-pending": "this request is already {state}.",
|
|
386
|
+
"access-request.issue.own-request": "you cannot decide your own request.",
|
|
387
|
+
"access-request.issue.requester-unavailable": "the requester is locked, inactive or no longer in this domain.",
|
|
388
|
+
"access-request.issue.deny-needs-reason": "write why it is denied.",
|
|
389
|
+
"access-request.issue.already-holds": "the requester already has it - through another role.",
|
|
390
|
+
"access-request.issue.role-required": "choose a role.",
|
|
391
|
+
"access-request.issue.role-not-assignable": "this domain cannot assign that role.",
|
|
392
|
+
"access-request.issue.role-does-not-grant": "{role} does not hold {privilege}.",
|
|
393
|
+
"access-request.issue.role-grants-more": "{role} also gives {count} more: {privileges}",
|
|
394
|
+
"access-request.issue.role-grants-admin": "{role} lets them give roles to others.",
|
|
395
|
+
"access-request.issue.smaller-role-exists": "a smaller role grants it too: {role} ({count} privileges)",
|
|
396
|
+
"access-request.issue.decider-lacks-it": "you are giving what you do not have yourself: {privileges}",
|
|
397
|
+
"access-request.issue.denied-before": "denied before by {by} ({at}): {reason}",
|
|
398
|
+
"access-request.title.decision draft": "access decision draft",
|
|
399
|
+
"access-request.title.request draft": "access request draft",
|
|
400
|
+
"access-request.field.request": "request",
|
|
401
|
+
"access-request.field.decision": "decision",
|
|
402
|
+
"access-request.field.category": "category",
|
|
403
|
+
"access-request.field.privilege": "privilege",
|
|
404
|
+
"access-request.decision.grant": "grant",
|
|
405
|
+
"access-request.decision.deny": "deny",
|
|
406
|
+
"access-request.issue.not-by-role": "a role would not open this here ({because}).",
|
|
407
|
+
"access-request.dock.mode": "access requests",
|
|
408
|
+
"access-request.dock.placeholder": "e.g. sort out today's access requests",
|
|
409
|
+
"access-request.text.recorded and told": "the request is recorded and {count} people who decide access are told.",
|
|
410
|
+
"access-request.text.nobody to tell": "nobody in this domain decides access requests, so it cannot be sent. ask your administrator directly.",
|
|
411
|
+
"access-request.text.denied lately": "denied on {at}: {reason}. to ask again, write what has changed.",
|
|
412
|
+
"access-request.button.request": "request access…",
|
|
413
|
+
"text.parent roles are changed by the parent domain": "roles of the parent domain can be changed only by someone who decides roles there.",
|
|
414
|
+
"access-request.issue.role-belongs-to-parent": "{role} belongs to the parent domain - only someone who decides roles there can give it."
|
|
353
415
|
}
|
package/translations/ja.json
CHANGED
|
@@ -349,5 +349,67 @@
|
|
|
349
349
|
"text.x on": "{x}件オン",
|
|
350
350
|
"text.x people hold this role": "このロールを持つ人 {x}名",
|
|
351
351
|
"title.add domain owner": "$t(term.domain)のオーナーを追加",
|
|
352
|
-
"title.role privileges": "ロールの権限"
|
|
352
|
+
"title.role privileges": "ロールの権限",
|
|
353
|
+
"access-request.title.access requests": "権限リクエスト",
|
|
354
|
+
"access-request.title.my access requests": "自分の権限リクエスト",
|
|
355
|
+
"access-request.title.decide": "権限リクエストの決定",
|
|
356
|
+
"access-request.title.request access": "権限をリクエスト",
|
|
357
|
+
"access-request.text.refused at": "止まった操作: {operation}",
|
|
358
|
+
"access-request.text.already waiting": "すでに送ったリクエストがあります(最終 {at})。送るとそのリクエストに追加されます。",
|
|
359
|
+
"access-request.text.sent": "権限を決める人に送りました。",
|
|
360
|
+
"access-request.text.added to waiting": "送信済みのリクエストに追加しました。",
|
|
361
|
+
"access-request.text.who asks": "{name} さんのリクエスト",
|
|
362
|
+
"access-request.text.grant with role": "ロールに追加して付与",
|
|
363
|
+
"access-request.text.no role grants it": "この権限を与えるロールがありません。先にロール画面でロールに追加してください。",
|
|
364
|
+
"access-request.text.privilege count": "権限 {count} 件",
|
|
365
|
+
"access-request.field.reason": "必要な理由",
|
|
366
|
+
"access-request.field.deny reason": "拒否の理由",
|
|
367
|
+
"access-request.field.requester": "リクエスト者",
|
|
368
|
+
"access-request.field.needed": "必要なもの",
|
|
369
|
+
"access-request.field.count": "回数",
|
|
370
|
+
"access-request.field.decided by": "決定者",
|
|
371
|
+
"access-request.field.granted role": "付与したロール",
|
|
372
|
+
"access-request.field.decision reason": "理由",
|
|
373
|
+
"access-request.field.decided at": "決定日時",
|
|
374
|
+
"access-request.button.send": "送信",
|
|
375
|
+
"access-request.button.grant": "付与",
|
|
376
|
+
"access-request.button.deny": "拒否",
|
|
377
|
+
"access-request.button.decide": "決定",
|
|
378
|
+
"access-request.button.withdraw": "リクエストを取り下げる",
|
|
379
|
+
"access-request.state.pending": "保留中",
|
|
380
|
+
"access-request.state.granted": "付与済み",
|
|
381
|
+
"access-request.state.denied": "拒否",
|
|
382
|
+
"access-request.state.withdrawn": "取り下げ",
|
|
383
|
+
"access-request.state.expired": "期限切れ",
|
|
384
|
+
"access-request.issue.access-request-not-found": "この$t(term.domain)にリクエストがありません。",
|
|
385
|
+
"access-request.issue.request-not-pending": "このリクエストはすでに{state}です。",
|
|
386
|
+
"access-request.issue.own-request": "自分のリクエストは決定できません。",
|
|
387
|
+
"access-request.issue.requester-unavailable": "リクエスト者がロック中、無効、またはこの$t(term.domain)にいません。",
|
|
388
|
+
"access-request.issue.deny-needs-reason": "拒否の理由を書いてください。",
|
|
389
|
+
"access-request.issue.already-holds": "リクエスト者はすでに別のロールでその権限を持っています。",
|
|
390
|
+
"access-request.issue.role-required": "ロールを選んでください。",
|
|
391
|
+
"access-request.issue.role-not-assignable": "この$t(term.domain)では付与できないロールです。",
|
|
392
|
+
"access-request.issue.role-does-not-grant": "{role} には {privilege} 権限がありません。",
|
|
393
|
+
"access-request.issue.role-grants-more": "{role} は要求以外に {count} 件を与えます: {privileges}",
|
|
394
|
+
"access-request.issue.role-grants-admin": "{role} を持つと他の人にロールを与えられるようになります。",
|
|
395
|
+
"access-request.issue.smaller-role-exists": "より小さいロールもこの権限を与えます: {role}(権限 {count} 件)",
|
|
396
|
+
"access-request.issue.decider-lacks-it": "自分にない権限を与えることになります: {privileges}",
|
|
397
|
+
"access-request.issue.denied-before": "以前に拒否されています — {by}、{at}: {reason}",
|
|
398
|
+
"access-request.title.decision draft": "権限リクエストの決定案",
|
|
399
|
+
"access-request.title.request draft": "権限リクエスト案",
|
|
400
|
+
"access-request.field.request": "リクエスト",
|
|
401
|
+
"access-request.field.decision": "決定",
|
|
402
|
+
"access-request.field.category": "カテゴリー",
|
|
403
|
+
"access-request.field.privilege": "権限",
|
|
404
|
+
"access-request.decision.grant": "付与",
|
|
405
|
+
"access-request.decision.deny": "拒否",
|
|
406
|
+
"access-request.issue.not-by-role": "ここではロールで開けない権限です({because})。",
|
|
407
|
+
"access-request.dock.mode": "権限リクエスト",
|
|
408
|
+
"access-request.dock.placeholder": "例: 今日届いた権限リクエストを整理して",
|
|
409
|
+
"access-request.text.recorded and told": "このリクエストは記録され、権限を決める管理者 {count} 名に通知されます。",
|
|
410
|
+
"access-request.text.nobody to tell": "この$t(term.domain)には権限を決める管理者がいないため送信できません。管理者に直接問い合わせてください。",
|
|
411
|
+
"access-request.text.denied lately": "{at} に拒否されました: {reason}。再度リクエストするには、何が変わったかを書いてください。",
|
|
412
|
+
"access-request.button.request": "権限をリクエスト…",
|
|
413
|
+
"text.parent roles are changed by the parent domain": "上位$t(term.domain)のロールは、その$t(term.domain)で権限を決める人だけが変更できます。",
|
|
414
|
+
"access-request.issue.role-belongs-to-parent": "{role} は上位$t(term.domain)のロールです。その$t(term.domain)で権限を決める人だけが付与できます。"
|
|
353
415
|
}
|
package/translations/ko.json
CHANGED
|
@@ -349,5 +349,67 @@
|
|
|
349
349
|
"title.secure-ip-list": "보안 IP 리스트",
|
|
350
350
|
"title.sign in": "로그인",
|
|
351
351
|
"title.sign up": "사용자 등록",
|
|
352
|
-
"title.unlock account": "계정 잠금 해제"
|
|
352
|
+
"title.unlock account": "계정 잠금 해제",
|
|
353
|
+
"access-request.title.access requests": "권한 요청",
|
|
354
|
+
"access-request.title.my access requests": "내 권한 요청",
|
|
355
|
+
"access-request.title.decide": "권한 요청 결정",
|
|
356
|
+
"access-request.title.request access": "권한 요청",
|
|
357
|
+
"access-request.text.refused at": "막힌 일: {operation}",
|
|
358
|
+
"access-request.text.already waiting": "이미 보낸 요청이 있습니다(마지막 {at}). 보내면 그 요청에 더해집니다.",
|
|
359
|
+
"access-request.text.sent": "권한을 정하는 사람에게 보냈습니다.",
|
|
360
|
+
"access-request.text.added to waiting": "이미 보낸 요청에 더했습니다.",
|
|
361
|
+
"access-request.text.who asks": "{name} 님의 요청",
|
|
362
|
+
"access-request.text.grant with role": "역할에 더해서 부여",
|
|
363
|
+
"access-request.text.no role grants it": "이 권한을 주는 역할이 없습니다. 역할 화면에서 먼저 역할에 더하세요.",
|
|
364
|
+
"access-request.text.privilege count": "권한 {count}개",
|
|
365
|
+
"access-request.field.reason": "필요한 까닭",
|
|
366
|
+
"access-request.field.deny reason": "거부 까닭",
|
|
367
|
+
"access-request.field.requester": "요청자",
|
|
368
|
+
"access-request.field.needed": "필요한 것",
|
|
369
|
+
"access-request.field.count": "횟수",
|
|
370
|
+
"access-request.field.decided by": "정한 사람",
|
|
371
|
+
"access-request.field.granted role": "준 역할",
|
|
372
|
+
"access-request.field.decision reason": "까닭",
|
|
373
|
+
"access-request.field.decided at": "정한 시각",
|
|
374
|
+
"access-request.button.send": "보내기",
|
|
375
|
+
"access-request.button.grant": "부여",
|
|
376
|
+
"access-request.button.deny": "거부",
|
|
377
|
+
"access-request.button.decide": "결정",
|
|
378
|
+
"access-request.button.withdraw": "요청 거두기",
|
|
379
|
+
"access-request.state.pending": "기다림",
|
|
380
|
+
"access-request.state.granted": "부여됨",
|
|
381
|
+
"access-request.state.denied": "거부됨",
|
|
382
|
+
"access-request.state.withdrawn": "거둠",
|
|
383
|
+
"access-request.state.expired": "만료됨",
|
|
384
|
+
"access-request.issue.access-request-not-found": "이 $t(term.domain)에서 요청을 찾을 수 없습니다.",
|
|
385
|
+
"access-request.issue.request-not-pending": "이 요청은 이미 {state} 상태입니다.",
|
|
386
|
+
"access-request.issue.own-request": "자기 요청은 정할 수 없습니다.",
|
|
387
|
+
"access-request.issue.requester-unavailable": "요청자가 잠겼거나, 비활성이거나, 이 $t(term.domain)에서 빠졌습니다.",
|
|
388
|
+
"access-request.issue.deny-needs-reason": "거부 까닭을 적으세요.",
|
|
389
|
+
"access-request.issue.already-holds": "요청자가 이미 그 권한을 가지고 있습니다(다른 역할로).",
|
|
390
|
+
"access-request.issue.role-required": "역할을 고르세요.",
|
|
391
|
+
"access-request.issue.role-not-assignable": "이 $t(term.domain)에서 줄 수 없는 역할입니다.",
|
|
392
|
+
"access-request.issue.role-does-not-grant": "{role} 역할에는 {privilege} 권한이 없습니다.",
|
|
393
|
+
"access-request.issue.role-grants-more": "{role} 역할은 요청한 것 말고도 {count}개를 더 줍니다: {privileges}",
|
|
394
|
+
"access-request.issue.role-grants-admin": "{role} 역할을 받으면 남에게 권한을 줄 수 있게 됩니다.",
|
|
395
|
+
"access-request.issue.smaller-role-exists": "더 작은 역할도 이 권한을 줍니다: {role}(권한 {count}개)",
|
|
396
|
+
"access-request.issue.decider-lacks-it": "내게 없는 권한을 주게 됩니다: {privileges}",
|
|
397
|
+
"access-request.issue.denied-before": "전에 거부된 요청입니다 — {by}, {at}: {reason}",
|
|
398
|
+
"access-request.title.decision draft": "권한 요청 결정안",
|
|
399
|
+
"access-request.title.request draft": "권한 요청안",
|
|
400
|
+
"access-request.field.request": "요청",
|
|
401
|
+
"access-request.field.decision": "결정",
|
|
402
|
+
"access-request.field.category": "범주",
|
|
403
|
+
"access-request.field.privilege": "권한",
|
|
404
|
+
"access-request.decision.grant": "부여",
|
|
405
|
+
"access-request.decision.deny": "거부",
|
|
406
|
+
"access-request.issue.not-by-role": "여기서는 역할로 열리지 않는 권한입니다({because}).",
|
|
407
|
+
"access-request.dock.mode": "권한 요청",
|
|
408
|
+
"access-request.dock.placeholder": "예: 오늘 들어온 권한 요청 정리해 줘",
|
|
409
|
+
"access-request.text.recorded and told": "이 요청은 기록되고, 권한을 정하는 관리자 {count}명에게 알림이 갑니다.",
|
|
410
|
+
"access-request.text.nobody to tell": "이 $t(term.domain)에는 권한을 정할 관리자가 없어 보낼 수 없습니다. 관리자에게 직접 문의하세요.",
|
|
411
|
+
"access-request.text.denied lately": "{at}에 거부되었습니다: {reason}. 다시 요청하려면 무엇이 달라졌는지 적어 주세요.",
|
|
412
|
+
"access-request.button.request": "권한 요청…",
|
|
413
|
+
"text.parent roles are changed by the parent domain": "상위 $t(term.domain)의 역할은 그 $t(term.domain)에서 권한을 정하는 사람만 바꿀 수 있습니다.",
|
|
414
|
+
"access-request.issue.role-belongs-to-parent": "{role} 역할은 상위 $t(term.domain)의 것입니다. 그 $t(term.domain)에서 권한을 정하는 사람만 줄 수 있습니다."
|
|
353
415
|
}
|