@things-factory/auth-base 10.0.0-beta.53 → 10.0.0-beta.57
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-server/middlewares/domain-authenticate-middleware.js +35 -1
- package/dist-server/middlewares/domain-authenticate-middleware.js.map +1 -1
- package/dist-server/migrations/1745000000000-MigrateDomainOwnersToTable.d.ts +14 -0
- package/dist-server/migrations/1745000000000-MigrateDomainOwnersToTable.js +60 -0
- package/dist-server/migrations/1745000000000-MigrateDomainOwnersToTable.js.map +1 -0
- package/dist-server/service/domain-owner/domain-owner-mutation.d.ts +16 -0
- package/dist-server/service/domain-owner/domain-owner-mutation.js +151 -0
- package/dist-server/service/domain-owner/domain-owner-mutation.js.map +1 -0
- package/dist-server/service/domain-owner/domain-owner-query.d.ts +12 -0
- package/dist-server/service/domain-owner/domain-owner-query.js +70 -0
- package/dist-server/service/domain-owner/domain-owner-query.js.map +1 -0
- package/dist-server/service/domain-owner/domain-owner.d.ts +29 -0
- package/dist-server/service/domain-owner/domain-owner.js +77 -0
- package/dist-server/service/domain-owner/domain-owner.js.map +1 -0
- package/dist-server/service/domain-owner/index.d.ts +5 -0
- package/dist-server/service/domain-owner/index.js +9 -0
- package/dist-server/service/domain-owner/index.js.map +1 -0
- package/dist-server/service/index.d.ts +2 -1
- package/dist-server/service/index.js +6 -2
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/spec/unit/domain-owner-entity.spec.ts +179 -0
- package/spec/unit/domain-owner-granted.spec.ts +215 -0
- package/spec/unit/domain-owner-migration.spec.ts +236 -0
- package/spec/unit/domain-owner-mutation.spec.ts +337 -0
|
@@ -3,10 +3,44 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.domainAuthenticateMiddleware = domainAuthenticateMiddleware;
|
|
4
4
|
const shell_1 = require("@things-factory/shell");
|
|
5
5
|
const auth_error_js_1 = require("../errors/auth-error.js");
|
|
6
|
+
const domain_owner_js_1 = require("../service/domain-owner/domain-owner.js");
|
|
6
7
|
const user_js_1 = require("../service/user/user.js");
|
|
7
8
|
const get_user_domains_js_1 = require("../utils/get-user-domains.js");
|
|
9
|
+
/**
|
|
10
|
+
* 도메인 소유권 확인.
|
|
11
|
+
*
|
|
12
|
+
* 소유권은 두 가지 경로 중 하나로 인식된다:
|
|
13
|
+
* 1) 하위 호환: `Domain.owner` 컬럼에 user.id 일치 (마이그레이션 이전 데이터
|
|
14
|
+
* 또는 "대표 owner 표시용 캐시")
|
|
15
|
+
* 2) `DomainOwner` 테이블에 (domainId, userId) 엔트리 존재 (정식 멀티 owner)
|
|
16
|
+
*
|
|
17
|
+
* 둘 중 하나라도 만족하면 owner로 인정 — 마이그레이션 기간/기존 API 호환을
|
|
18
|
+
* 끊어뜨리지 않기 위함.
|
|
19
|
+
*/
|
|
8
20
|
process.domainOwnerGranted = async (domain, user) => {
|
|
9
|
-
|
|
21
|
+
if (!user || !domain)
|
|
22
|
+
return false;
|
|
23
|
+
// (1) 하위 호환: Domain.owner 캐시 필드
|
|
24
|
+
if (domain.owner === user.id)
|
|
25
|
+
return true;
|
|
26
|
+
// Request-level 메모이제이션 — 한 요청 안에서 @privilege directive 가
|
|
27
|
+
// 걸린 field 마다 이 함수가 호출될 수 있는데 (list 쿼리의 경우 N 번),
|
|
28
|
+
// 같은 (domain, user) 조합은 DB 조회를 1 번만 수행.
|
|
29
|
+
// user 객체는 request-scoped 이므로 request 종료 시 자연스럽게 소멸.
|
|
30
|
+
const cacheKey = `__domainOwnerGranted:${domain.id}`;
|
|
31
|
+
const cached = user[cacheKey];
|
|
32
|
+
if (cached !== undefined)
|
|
33
|
+
return cached;
|
|
34
|
+
// (2) DomainOwner join table
|
|
35
|
+
const exists = await (0, shell_1.getRepository)(domain_owner_js_1.DomainOwner)
|
|
36
|
+
.createQueryBuilder('ow')
|
|
37
|
+
.where('ow.domain_id = :domainId AND ow.user_id = :userId', {
|
|
38
|
+
domainId: domain.id,
|
|
39
|
+
userId: user.id
|
|
40
|
+
})
|
|
41
|
+
.getExists();
|
|
42
|
+
user[cacheKey] = exists;
|
|
43
|
+
return exists;
|
|
10
44
|
};
|
|
11
45
|
process.superUserGranted = async (domain, user) => {
|
|
12
46
|
if (!user) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domain-authenticate-middleware.js","sourceRoot":"","sources":["../../server/middlewares/domain-authenticate-middleware.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"domain-authenticate-middleware.js","sourceRoot":"","sources":["../../server/middlewares/domain-authenticate-middleware.ts"],"names":[],"mappings":"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","sourcesContent":["import { Domain, getRepository } from '@things-factory/shell'\n\nimport { AuthError } from '../errors/auth-error.js'\nimport { DomainOwner } from '../service/domain-owner/domain-owner.js'\nimport { User } from '../service/user/user.js'\nimport { getUserDomains } from '../utils/get-user-domains.js'\n\ndeclare global {\n namespace NodeJS {\n interface Process {\n domainOwnerGranted: (domain: Domain, user: User) => Promise<boolean>\n superUserGranted: (domain: Domain, user: User) => Promise<boolean>\n }\n }\n}\n\n/**\n * 도메인 소유권 확인.\n *\n * 소유권은 두 가지 경로 중 하나로 인식된다:\n * 1) 하위 호환: `Domain.owner` 컬럼에 user.id 일치 (마이그레이션 이전 데이터\n * 또는 \"대표 owner 표시용 캐시\")\n * 2) `DomainOwner` 테이블에 (domainId, userId) 엔트리 존재 (정식 멀티 owner)\n *\n * 둘 중 하나라도 만족하면 owner로 인정 — 마이그레이션 기간/기존 API 호환을\n * 끊어뜨리지 않기 위함.\n */\nprocess.domainOwnerGranted = async (domain: Domain, user: User): Promise<boolean> => {\n if (!user || !domain) return false\n\n // (1) 하위 호환: Domain.owner 캐시 필드\n if (domain.owner === user.id) return true\n\n // Request-level 메모이제이션 — 한 요청 안에서 @privilege directive 가\n // 걸린 field 마다 이 함수가 호출될 수 있는데 (list 쿼리의 경우 N 번),\n // 같은 (domain, user) 조합은 DB 조회를 1 번만 수행.\n // user 객체는 request-scoped 이므로 request 종료 시 자연스럽게 소멸.\n const cacheKey = `__domainOwnerGranted:${domain.id}`\n const cached = (user as any)[cacheKey]\n if (cached !== undefined) return cached\n\n // (2) DomainOwner join table\n const exists = await getRepository(DomainOwner)\n .createQueryBuilder('ow')\n .where('ow.domain_id = :domainId AND ow.user_id = :userId', {\n domainId: domain.id,\n userId: user.id\n })\n .getExists()\n ;(user as any)[cacheKey] = exists\n return exists\n}\n\nprocess.superUserGranted = async (domain: Domain, user: User): Promise<boolean> => {\n if (!user) {\n return false\n }\n\n if (!user.domains.length) {\n user = await getRepository(User).findOne({\n where: { id: user.id },\n relations: ['domains']\n })\n }\n\n const systemDomain: Domain = user.domains.find((domain: Domain) => domain.subdomain === 'system')\n if (!systemDomain) {\n return false\n }\n\n return systemDomain.owner === user.id\n}\n\n/*\n * 현재 subdomain 과 user의 domain list와의 비교를 통해서,\n * 인증 성공 또는 인증 에러를 발생시킬 것인지를 결정한다.\n * 1. 현재 subdomain 이 결정되지 않은 경우.\n * - checkin로 이동한다.\n * 2. superUser 판단\n * 3. 현재 subdomain 이 결정된 경우.\n * - user의 domains 리스트에 해당 subdomain이 없다면, 인증 오류를 발생한다.\n */\n\nexport async function domainAuthenticateMiddleware(context: any, next: any) {\n const { t } = context\n const { domain, user } = context.state\n\n const subdomain: string = domain?.subdomain\n\n // 1. 현재 subdomain 이 결정되지 않은 경우.\n // - checkin로 이동한다.\n if (!subdomain) {\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.SUBDOMAIN_NOTFOUND\n })\n }\n\n // 2. 현재 subdomain 이 결정된 경우.\n const userDomains: Partial<Domain>[] = await getUserDomains(user)\n if (userDomains.find(domain => domain.subdomain == subdomain) || (await process.superUserGranted(domain, user))) {\n return await next()\n }\n\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.SUBDOMAIN_NOTFOUND\n })\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
2
|
+
/**
|
|
3
|
+
* 기존 `Domain.owner` 단일 소유자 데이터를 신규 `DomainOwner` 테이블로 이관한다.
|
|
4
|
+
*
|
|
5
|
+
* `Domain.owner` 컬럼은 제거하지 않는다 — "대표 owner 표시용 캐시"로 유지되며
|
|
6
|
+
* 하위 호환 API에서 계속 사용된다. 권위 있는 소유권 체크는 `DomainOwner` 테이블
|
|
7
|
+
* 또는 `Domain.owner` 캐시 중 하나라도 일치하면 통과 (domain-authenticate-middleware).
|
|
8
|
+
*
|
|
9
|
+
* 이 마이그레이션은 idempotent — 이미 존재하는 (domain, user) 쌍은 skip.
|
|
10
|
+
*/
|
|
11
|
+
export declare class MigrateDomainOwnersToTable1745000000000 implements MigrationInterface {
|
|
12
|
+
up(_queryRunner: QueryRunner): Promise<void>;
|
|
13
|
+
down(_queryRunner: QueryRunner): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MigrateDomainOwnersToTable1745000000000 = void 0;
|
|
4
|
+
const shell_1 = require("@things-factory/shell");
|
|
5
|
+
const domain_owner_js_1 = require("../service/domain-owner/domain-owner.js");
|
|
6
|
+
/**
|
|
7
|
+
* 기존 `Domain.owner` 단일 소유자 데이터를 신규 `DomainOwner` 테이블로 이관한다.
|
|
8
|
+
*
|
|
9
|
+
* `Domain.owner` 컬럼은 제거하지 않는다 — "대표 owner 표시용 캐시"로 유지되며
|
|
10
|
+
* 하위 호환 API에서 계속 사용된다. 권위 있는 소유권 체크는 `DomainOwner` 테이블
|
|
11
|
+
* 또는 `Domain.owner` 캐시 중 하나라도 일치하면 통과 (domain-authenticate-middleware).
|
|
12
|
+
*
|
|
13
|
+
* 이 마이그레이션은 idempotent — 이미 존재하는 (domain, user) 쌍은 skip.
|
|
14
|
+
*/
|
|
15
|
+
class MigrateDomainOwnersToTable1745000000000 {
|
|
16
|
+
async up(_queryRunner) {
|
|
17
|
+
const domainRepository = (0, shell_1.getRepository)(shell_1.Domain);
|
|
18
|
+
const ownerRepository = (0, shell_1.getRepository)(domain_owner_js_1.DomainOwner);
|
|
19
|
+
const domains = await domainRepository
|
|
20
|
+
.createQueryBuilder('d')
|
|
21
|
+
.where('d.owner IS NOT NULL')
|
|
22
|
+
.getMany();
|
|
23
|
+
let migrated = 0;
|
|
24
|
+
let skipped = 0;
|
|
25
|
+
for (const domain of domains) {
|
|
26
|
+
if (!domain.owner)
|
|
27
|
+
continue;
|
|
28
|
+
const existing = await ownerRepository
|
|
29
|
+
.createQueryBuilder('ow')
|
|
30
|
+
.where('ow.domain_id = :d AND ow.user_id = :u', { d: domain.id, u: domain.owner })
|
|
31
|
+
.getOne();
|
|
32
|
+
if (existing) {
|
|
33
|
+
skipped++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const entry = ownerRepository.create({
|
|
37
|
+
domain: { id: domain.id },
|
|
38
|
+
user: { id: domain.owner },
|
|
39
|
+
reason: 'migrated from legacy single-owner field'
|
|
40
|
+
});
|
|
41
|
+
await ownerRepository.save(entry);
|
|
42
|
+
migrated++;
|
|
43
|
+
}
|
|
44
|
+
// eslint-disable-next-line no-console
|
|
45
|
+
console.log(`[MigrateDomainOwnersToTable] migrated ${migrated} ownership entries, skipped ${skipped} (already exist)`);
|
|
46
|
+
}
|
|
47
|
+
async down(_queryRunner) {
|
|
48
|
+
// rollback: domain_owners 중 'migrated from legacy...' reason만 제거
|
|
49
|
+
// Domain.owner 필드는 그대로이므로 데이터 유실 없음
|
|
50
|
+
const ownerRepository = (0, shell_1.getRepository)(domain_owner_js_1.DomainOwner);
|
|
51
|
+
await ownerRepository
|
|
52
|
+
.createQueryBuilder()
|
|
53
|
+
.delete()
|
|
54
|
+
.from(domain_owner_js_1.DomainOwner)
|
|
55
|
+
.where('reason = :reason', { reason: 'migrated from legacy single-owner field' })
|
|
56
|
+
.execute();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.MigrateDomainOwnersToTable1745000000000 = MigrateDomainOwnersToTable1745000000000;
|
|
60
|
+
//# sourceMappingURL=1745000000000-MigrateDomainOwnersToTable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"1745000000000-MigrateDomainOwnersToTable.js","sourceRoot":"","sources":["../../server/migrations/1745000000000-MigrateDomainOwnersToTable.ts"],"names":[],"mappings":";;;AAEA,iDAA6D;AAE7D,6EAAqE;AAErE;;;;;;;;GAQG;AACH,MAAa,uCAAuC;IAC3C,KAAK,CAAC,EAAE,CAAC,YAAyB;QACvC,MAAM,gBAAgB,GAAG,IAAA,qBAAa,EAAC,cAAM,CAAC,CAAA;QAC9C,MAAM,eAAe,GAAG,IAAA,qBAAa,EAAC,6BAAW,CAAC,CAAA;QAElD,MAAM,OAAO,GAAG,MAAM,gBAAgB;aACnC,kBAAkB,CAAC,GAAG,CAAC;aACvB,KAAK,CAAC,qBAAqB,CAAC;aAC5B,OAAO,EAAE,CAAA;QAEZ,IAAI,QAAQ,GAAG,CAAC,CAAA;QAChB,IAAI,OAAO,GAAG,CAAC,CAAA;QAEf,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,IAAI,CAAC,MAAM,CAAC,KAAK;gBAAE,SAAQ;YAE3B,MAAM,QAAQ,GAAG,MAAM,eAAe;iBACnC,kBAAkB,CAAC,IAAI,CAAC;iBACxB,KAAK,CAAC,uCAAuC,EAAE,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC;iBACjF,MAAM,EAAE,CAAA;YAEX,IAAI,QAAQ,EAAE,CAAC;gBACb,OAAO,EAAE,CAAA;gBACT,SAAQ;YACV,CAAC;YAED,MAAM,KAAK,GAAG,eAAe,CAAC,MAAM,CAAC;gBACnC,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAY;gBACnC,IAAI,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,KAAK,EAAS;gBACjC,MAAM,EAAE,yCAAyC;aAClD,CAAC,CAAA;YACF,MAAM,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YACjC,QAAQ,EAAE,CAAA;QACZ,CAAC;QAED,sCAAsC;QACtC,OAAO,CAAC,GAAG,CACT,yCAAyC,QAAQ,+BAA+B,OAAO,kBAAkB,CAC1G,CAAA;IACH,CAAC;IAEM,KAAK,CAAC,IAAI,CAAC,YAAyB;QACzC,iEAAiE;QACjE,oCAAoC;QACpC,MAAM,eAAe,GAAG,IAAA,qBAAa,EAAC,6BAAW,CAAC,CAAA;QAClD,MAAM,eAAe;aAClB,kBAAkB,EAAE;aACpB,MAAM,EAAE;aACR,IAAI,CAAC,6BAAW,CAAC;aACjB,KAAK,CAAC,kBAAkB,EAAE,EAAE,MAAM,EAAE,yCAAyC,EAAE,CAAC;aAChF,OAAO,EAAE,CAAA;IACd,CAAC;CACF;AApDD,0FAoDC","sourcesContent":["import { MigrationInterface, QueryRunner } from 'typeorm'\n\nimport { Domain, getRepository } from '@things-factory/shell'\n\nimport { DomainOwner } from '../service/domain-owner/domain-owner.js'\n\n/**\n * 기존 `Domain.owner` 단일 소유자 데이터를 신규 `DomainOwner` 테이블로 이관한다.\n *\n * `Domain.owner` 컬럼은 제거하지 않는다 — \"대표 owner 표시용 캐시\"로 유지되며\n * 하위 호환 API에서 계속 사용된다. 권위 있는 소유권 체크는 `DomainOwner` 테이블\n * 또는 `Domain.owner` 캐시 중 하나라도 일치하면 통과 (domain-authenticate-middleware).\n *\n * 이 마이그레이션은 idempotent — 이미 존재하는 (domain, user) 쌍은 skip.\n */\nexport class MigrateDomainOwnersToTable1745000000000 implements MigrationInterface {\n public async up(_queryRunner: QueryRunner): Promise<void> {\n const domainRepository = getRepository(Domain)\n const ownerRepository = getRepository(DomainOwner)\n\n const domains = await domainRepository\n .createQueryBuilder('d')\n .where('d.owner IS NOT NULL')\n .getMany()\n\n let migrated = 0\n let skipped = 0\n\n for (const domain of domains) {\n if (!domain.owner) continue\n\n const existing = await ownerRepository\n .createQueryBuilder('ow')\n .where('ow.domain_id = :d AND ow.user_id = :u', { d: domain.id, u: domain.owner })\n .getOne()\n\n if (existing) {\n skipped++\n continue\n }\n\n const entry = ownerRepository.create({\n domain: { id: domain.id } as Domain,\n user: { id: domain.owner } as any,\n reason: 'migrated from legacy single-owner field'\n })\n await ownerRepository.save(entry)\n migrated++\n }\n\n // eslint-disable-next-line no-console\n console.log(\n `[MigrateDomainOwnersToTable] migrated ${migrated} ownership entries, skipped ${skipped} (already exist)`\n )\n }\n\n public async down(_queryRunner: QueryRunner): Promise<void> {\n // rollback: domain_owners 중 'migrated from legacy...' reason만 제거\n // Domain.owner 필드는 그대로이므로 데이터 유실 없음\n const ownerRepository = getRepository(DomainOwner)\n await ownerRepository\n .createQueryBuilder()\n .delete()\n .from(DomainOwner)\n .where('reason = :reason', { reason: 'migrated from legacy single-owner field' })\n .execute()\n }\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DomainOwner } from './domain-owner.js';
|
|
2
|
+
export declare class DomainOwnerMutation {
|
|
3
|
+
/**
|
|
4
|
+
* 현재 도메인에 새 owner를 추가한다. 기존 도메인 오너 또는 수퍼유저만 호출 가능.
|
|
5
|
+
*
|
|
6
|
+
* 하위 호환: `Domain.owner` 캐시 필드가 비어 있으면 추가된 user를 세팅한다.
|
|
7
|
+
*/
|
|
8
|
+
addDomainOwner(username: string, context: any, reason?: string): Promise<DomainOwner>;
|
|
9
|
+
/**
|
|
10
|
+
* 현재 도메인의 owner를 제거한다.
|
|
11
|
+
* - 기존 도메인 오너 또는 수퍼유저만 호출 가능
|
|
12
|
+
* - 마지막 남은 owner는 제거 불가 (최소 1명 유지)
|
|
13
|
+
* - `Domain.owner` 캐시가 제거되는 user였다면, 남은 owner 중 하나로 재설정
|
|
14
|
+
*/
|
|
15
|
+
removeDomainOwner(username: string, context: any, _reason?: string): Promise<boolean>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DomainOwnerMutation = 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 domain_owner_js_1 = require("./domain-owner.js");
|
|
8
|
+
const user_js_1 = require("../user/user.js");
|
|
9
|
+
let DomainOwnerMutation = class DomainOwnerMutation {
|
|
10
|
+
/**
|
|
11
|
+
* 현재 도메인에 새 owner를 추가한다. 기존 도메인 오너 또는 수퍼유저만 호출 가능.
|
|
12
|
+
*
|
|
13
|
+
* 하위 호환: `Domain.owner` 캐시 필드가 비어 있으면 추가된 user를 세팅한다.
|
|
14
|
+
*/
|
|
15
|
+
async addDomainOwner(username, context, reason) {
|
|
16
|
+
const { domain, user: actor } = context.state;
|
|
17
|
+
return (0, shell_1.getRepository)(shell_1.Domain).manager.transaction(async (tx) => {
|
|
18
|
+
const userRepo = tx.getRepository(user_js_1.User);
|
|
19
|
+
const ownerRepo = tx.getRepository(domain_owner_js_1.DomainOwner);
|
|
20
|
+
const domainRepo = tx.getRepository(shell_1.Domain);
|
|
21
|
+
// username 컬럼이 nullable — 사용자는 username 을 가지지 않고
|
|
22
|
+
// email 만 갖는 경우가 많음. User.get username() 도 `username || email`
|
|
23
|
+
// 을 리턴하므로 UI 가 email 을 보낼 수 있음. 둘 다 매칭한다.
|
|
24
|
+
const targetUser = await userRepo
|
|
25
|
+
.createQueryBuilder('u')
|
|
26
|
+
.innerJoin('u.domains', 'd')
|
|
27
|
+
.where('(u.username = :key OR u.email = :key) AND d.id = :domainId', {
|
|
28
|
+
key: username,
|
|
29
|
+
domainId: domain.id
|
|
30
|
+
})
|
|
31
|
+
.getOne();
|
|
32
|
+
if (!targetUser) {
|
|
33
|
+
throw new Error(`User '${username}' is not a member of this domain.`);
|
|
34
|
+
}
|
|
35
|
+
const existing = await ownerRepo.findOne({
|
|
36
|
+
where: { domain: { id: domain.id }, user: { id: targetUser.id } }
|
|
37
|
+
});
|
|
38
|
+
if (existing) {
|
|
39
|
+
throw new Error(`User '${username}' is already an owner of this domain.`);
|
|
40
|
+
}
|
|
41
|
+
const entry = ownerRepo.create({
|
|
42
|
+
domain: { id: domain.id },
|
|
43
|
+
user: { id: targetUser.id },
|
|
44
|
+
grantedBy: actor,
|
|
45
|
+
reason
|
|
46
|
+
});
|
|
47
|
+
const saved = await ownerRepo.save(entry);
|
|
48
|
+
// 하위 호환 캐시: domain.owner가 비어 있으면 세팅
|
|
49
|
+
if (!domain.owner) {
|
|
50
|
+
await domainRepo.update(domain.id, { owner: targetUser.id });
|
|
51
|
+
}
|
|
52
|
+
// GraphQL 응답을 위해 relations 포함해 재조회.
|
|
53
|
+
// (save() 반환값은 { user: {id}, grantedBy: actor } 인데 user/grantedBy 의
|
|
54
|
+
// non-nullable 필드 email 등이 채워져 있지 않아 직렬화 에러 발생)
|
|
55
|
+
return ownerRepo.findOneOrFail({
|
|
56
|
+
where: { id: saved.id },
|
|
57
|
+
relations: ['user', 'grantedBy', 'domain']
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 현재 도메인의 owner를 제거한다.
|
|
63
|
+
* - 기존 도메인 오너 또는 수퍼유저만 호출 가능
|
|
64
|
+
* - 마지막 남은 owner는 제거 불가 (최소 1명 유지)
|
|
65
|
+
* - `Domain.owner` 캐시가 제거되는 user였다면, 남은 owner 중 하나로 재설정
|
|
66
|
+
*/
|
|
67
|
+
async removeDomainOwner(username, context, _reason) {
|
|
68
|
+
const { domain } = context.state;
|
|
69
|
+
return (0, shell_1.getRepository)(shell_1.Domain).manager.transaction(async (tx) => {
|
|
70
|
+
const userRepo = tx.getRepository(user_js_1.User);
|
|
71
|
+
const ownerRepo = tx.getRepository(domain_owner_js_1.DomainOwner);
|
|
72
|
+
const domainRepo = tx.getRepository(shell_1.Domain);
|
|
73
|
+
// username 또는 email 둘 다 허용 — addDomainOwner 와 대칭.
|
|
74
|
+
const targetUser = await userRepo.findOne({
|
|
75
|
+
where: [{ username }, { email: username }]
|
|
76
|
+
});
|
|
77
|
+
if (!targetUser) {
|
|
78
|
+
throw new Error(`User '${username}' not found.`);
|
|
79
|
+
}
|
|
80
|
+
const entry = await ownerRepo.findOne({
|
|
81
|
+
where: { domain: { id: domain.id }, user: { id: targetUser.id } }
|
|
82
|
+
});
|
|
83
|
+
const hasLegacyOwner = domain.owner === targetUser.id;
|
|
84
|
+
if (!entry && !hasLegacyOwner) {
|
|
85
|
+
throw new Error(`User '${username}' is not an owner of this domain.`);
|
|
86
|
+
}
|
|
87
|
+
// 마지막 1인 보호: DomainOwner 테이블 기준 count + legacy owner 포함 여부
|
|
88
|
+
const ownerCount = await ownerRepo
|
|
89
|
+
.createQueryBuilder('ow')
|
|
90
|
+
.where('ow.domain_id = :d', { d: domain.id })
|
|
91
|
+
.getCount();
|
|
92
|
+
// 남을 owner 수 계산:
|
|
93
|
+
// - DomainOwner 테이블에서 제거 대상이 있으면 ownerCount - 1
|
|
94
|
+
// - legacy owner가 별도로 있고 DomainOwner에 없다면 +1
|
|
95
|
+
let remaining = ownerCount;
|
|
96
|
+
if (entry)
|
|
97
|
+
remaining -= 1;
|
|
98
|
+
// legacy owner가 지금 제거 대상과 다른 user라면, 그 user도 하나의 owner로 카운트
|
|
99
|
+
if (domain.owner && domain.owner !== targetUser.id) {
|
|
100
|
+
// legacy owner가 DomainOwner 테이블에도 이미 있는지
|
|
101
|
+
const legacyInTable = await ownerRepo
|
|
102
|
+
.createQueryBuilder('ow')
|
|
103
|
+
.where('ow.domain_id = :d AND ow.user_id = :u', { d: domain.id, u: domain.owner })
|
|
104
|
+
.getExists();
|
|
105
|
+
if (!legacyInTable)
|
|
106
|
+
remaining += 1;
|
|
107
|
+
}
|
|
108
|
+
if (remaining < 1) {
|
|
109
|
+
throw new Error('Cannot remove the last owner of a domain. Add another owner first.');
|
|
110
|
+
}
|
|
111
|
+
if (entry) {
|
|
112
|
+
await ownerRepo.delete(entry.id);
|
|
113
|
+
}
|
|
114
|
+
if (hasLegacyOwner) {
|
|
115
|
+
// 캐시 재설정: 남은 DomainOwner 중 가장 먼저 추가된 사람
|
|
116
|
+
const next = await ownerRepo
|
|
117
|
+
.createQueryBuilder('ow')
|
|
118
|
+
.where('ow.domain_id = :d', { d: domain.id })
|
|
119
|
+
.orderBy('ow.grantedAt', 'ASC')
|
|
120
|
+
.getOne();
|
|
121
|
+
await domainRepo.update(domain.id, { owner: next?.userId ?? null });
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
exports.DomainOwnerMutation = DomainOwnerMutation;
|
|
128
|
+
tslib_1.__decorate([
|
|
129
|
+
(0, type_graphql_1.Directive)('@privilege(domainOwnerGranted: true, superUserGranted: true)'),
|
|
130
|
+
(0, type_graphql_1.Mutation)(returns => domain_owner_js_1.DomainOwner, { description: 'Add a user as owner of the current domain.' }),
|
|
131
|
+
tslib_1.__param(0, (0, type_graphql_1.Arg)('username')),
|
|
132
|
+
tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
|
|
133
|
+
tslib_1.__param(2, (0, type_graphql_1.Arg)('reason', { nullable: true })),
|
|
134
|
+
tslib_1.__metadata("design:type", Function),
|
|
135
|
+
tslib_1.__metadata("design:paramtypes", [String, Object, String]),
|
|
136
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
137
|
+
], DomainOwnerMutation.prototype, "addDomainOwner", null);
|
|
138
|
+
tslib_1.__decorate([
|
|
139
|
+
(0, type_graphql_1.Directive)('@privilege(domainOwnerGranted: true, superUserGranted: true)'),
|
|
140
|
+
(0, type_graphql_1.Mutation)(returns => Boolean, { description: 'Remove a user from the owners of the current domain.' }),
|
|
141
|
+
tslib_1.__param(0, (0, type_graphql_1.Arg)('username')),
|
|
142
|
+
tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
|
|
143
|
+
tslib_1.__param(2, (0, type_graphql_1.Arg)('reason', { nullable: true })),
|
|
144
|
+
tslib_1.__metadata("design:type", Function),
|
|
145
|
+
tslib_1.__metadata("design:paramtypes", [String, Object, String]),
|
|
146
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
147
|
+
], DomainOwnerMutation.prototype, "removeDomainOwner", null);
|
|
148
|
+
exports.DomainOwnerMutation = DomainOwnerMutation = tslib_1.__decorate([
|
|
149
|
+
(0, type_graphql_1.Resolver)(of => domain_owner_js_1.DomainOwner)
|
|
150
|
+
], DomainOwnerMutation);
|
|
151
|
+
//# sourceMappingURL=domain-owner-mutation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-owner-mutation.js","sourceRoot":"","sources":["../../../server/service/domain-owner/domain-owner-mutation.ts"],"names":[],"mappings":";;;;AAAA,+CAAsE;AACtE,iDAA6D;AAE7D,uDAA+C;AAC/C,6CAAsC;AAG/B,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAC9B;;;;OAIG;IAGG,AAAN,KAAK,CAAC,cAAc,CACD,QAAgB,EAC1B,OAAY,EACgB,MAAe;QAElD,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAE7C,OAAQ,IAAA,qBAAa,EAAC,cAAM,CAAS,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,EAAE,EAAO,EAAE,EAAE;YAC1E,MAAM,QAAQ,GAAG,EAAE,CAAC,aAAa,CAAC,cAAI,CAAC,CAAA;YACvC,MAAM,SAAS,GAAG,EAAE,CAAC,aAAa,CAAC,6BAAW,CAAC,CAAA;YAC/C,MAAM,UAAU,GAAG,EAAE,CAAC,aAAa,CAAC,cAAM,CAAC,CAAA;YAE3C,iDAAiD;YACjD,+DAA+D;YAC/D,0CAA0C;YAC1C,MAAM,UAAU,GAAG,MAAM,QAAQ;iBAC9B,kBAAkB,CAAC,GAAG,CAAC;iBACvB,SAAS,CAAC,WAAW,EAAE,GAAG,CAAC;iBAC3B,KAAK,CAAC,4DAA4D,EAAE;gBACnE,GAAG,EAAE,QAAQ;gBACb,QAAQ,EAAE,MAAM,CAAC,EAAE;aACpB,CAAC;iBACD,MAAM,EAAE,CAAA;YACX,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,MAAM,IAAI,KAAK,CAAC,SAAS,QAAQ,mCAAmC,CAAC,CAAA;YACvE,CAAC;YAED,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC;gBACvC,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,UAAU,CAAC,EAAE,EAAE,EAAE;aAClE,CAAC,CAAA;YACF,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,IAAI,KAAK,CAAC,SAAS,QAAQ,uCAAuC,CAAC,CAAA;YAC3E,CAAC;YAED,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC;gBAC7B,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAY;gBACnC,IAAI,EAAE,EAAE,EAAE,EAAE,UAAU,CAAC,EAAE,EAAU;gBACnC,SAAS,EAAE,KAAK;gBAChB,MAAM;aACP,CAAC,CAAA;YACF,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YAEzC,oCAAoC;YACpC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;gBAClB,MAAM,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,CAAA;YAC9D,CAAC;YAED,oCAAoC;YACpC,oEAAoE;YACpE,iDAAiD;YACjD,OAAO,SAAS,CAAC,aAAa,CAAC;gBAC7B,KAAK,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE;gBACvB,SAAS,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,CAAC;aAC3C,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;IACJ,CAAC;IAED;;;;;OAKG;IAGG,AAAN,KAAK,CAAC,iBAAiB,CACJ,QAAgB,EAC1B,OAAY,EACgB,OAAgB;QAEnD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAEhC,OAAQ,IAAA,qBAAa,EAAC,cAAM,CAAS,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,EAAE,EAAO,EAAE,EAAE;YAC1E,MAAM,QAAQ,GAAG,EAAE,CAAC,aAAa,CAAC,cAAI,CAAC,CAAA;YACvC,MAAM,SAAS,GAAG,EAAE,CAAC,aAAa,CAAC,6BAAW,CAAC,CAAA;YAC/C,MAAM,UAAU,GAAG,EAAE,CAAC,aAAa,CAAC,cAAM,CAAC,CAAA;YAE3C,kDAAkD;YAClD,MAAM,UAAU,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC;gBACxC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;aAC3C,CAAC,CAAA;YACF,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,MAAM,IAAI,KAAK,CAAC,SAAS,QAAQ,cAAc,CAAC,CAAA;YAClD,CAAC;YAED,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC;gBACpC,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,UAAU,CAAC,EAAE,EAAE,EAAE;aAClE,CAAC,CAAA;YACF,MAAM,cAAc,GAAG,MAAM,CAAC,KAAK,KAAK,UAAU,CAAC,EAAE,CAAA;YAErD,IAAI,CAAC,KAAK,IAAI,CAAC,cAAc,EAAE,CAAC;gBAC9B,MAAM,IAAI,KAAK,CAAC,SAAS,QAAQ,mCAAmC,CAAC,CAAA;YACvE,CAAC;YAED,2DAA2D;YAC3D,MAAM,UAAU,GAAG,MAAM,SAAS;iBAC/B,kBAAkB,CAAC,IAAI,CAAC;iBACxB,KAAK,CAAC,mBAAmB,EAAE,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;iBAC5C,QAAQ,EAAE,CAAA;YAEb,iBAAiB;YACjB,kDAAkD;YAClD,+CAA+C;YAC/C,IAAI,SAAS,GAAG,UAAU,CAAA;YAC1B,IAAI,KAAK;gBAAE,SAAS,IAAI,CAAC,CAAA;YACzB,4DAA4D;YAC5D,IAAI,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,KAAK,UAAU,CAAC,EAAE,EAAE,CAAC;gBACnD,yCAAyC;gBACzC,MAAM,aAAa,GAAG,MAAM,SAAS;qBAClC,kBAAkB,CAAC,IAAI,CAAC;qBACxB,KAAK,CAAC,uCAAuC,EAAE,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC;qBACjF,SAAS,EAAE,CAAA;gBACd,IAAI,CAAC,aAAa;oBAAE,SAAS,IAAI,CAAC,CAAA;YACpC,CAAC;YAED,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;gBAClB,MAAM,IAAI,KAAK,CACb,oEAAoE,CACrE,CAAA;YACH,CAAC;YAED,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;YAClC,CAAC;YAED,IAAI,cAAc,EAAE,CAAC;gBACnB,wCAAwC;gBACxC,MAAM,IAAI,GAAG,MAAM,SAAS;qBACzB,kBAAkB,CAAC,IAAI,CAAC;qBACxB,KAAK,CAAC,mBAAmB,EAAE,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;qBAC5C,OAAO,CAAC,cAAc,EAAE,KAAK,CAAC;qBAC9B,MAAM,EAAE,CAAA;gBACX,MAAM,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,IAAI,EAAE,CAAC,CAAA;YACrE,CAAC;YAED,OAAO,IAAI,CAAA;QACb,CAAC,CAAC,CAAA;IACJ,CAAC;CACF,CAAA;AAlJY,kDAAmB;AAQxB;IAFL,IAAA,wBAAS,EAAC,8DAA8D,CAAC;IACzE,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,6BAAW,EAAE,EAAE,WAAW,EAAE,4CAA4C,EAAE,CAAC;IAE7F,mBAAA,IAAA,kBAAG,EAAC,UAAU,CAAC,CAAA;IACf,mBAAA,IAAA,kBAAG,GAAE,CAAA;IACL,mBAAA,IAAA,kBAAG,EAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;;;;yDAoDnC;AAUK;IAFL,IAAA,wBAAS,EAAC,8DAA8D,CAAC;IACzE,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,sDAAsD,EAAE,CAAC;IAEnG,mBAAA,IAAA,kBAAG,EAAC,UAAU,CAAC,CAAA;IACf,mBAAA,IAAA,kBAAG,GAAE,CAAA;IACL,mBAAA,IAAA,kBAAG,EAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;;;;4DAqEnC;8BAjJU,mBAAmB;IAD/B,IAAA,uBAAQ,EAAC,EAAE,CAAC,EAAE,CAAC,6BAAW,CAAC;GACf,mBAAmB,CAkJ/B","sourcesContent":["import { Resolver, Mutation, Arg, Ctx, Directive } from 'type-graphql'\nimport { Domain, getRepository } from '@things-factory/shell'\n\nimport { DomainOwner } from './domain-owner.js'\nimport { User } from '../user/user.js'\n\n@Resolver(of => DomainOwner)\nexport class DomainOwnerMutation {\n /**\n * 현재 도메인에 새 owner를 추가한다. 기존 도메인 오너 또는 수퍼유저만 호출 가능.\n *\n * 하위 호환: `Domain.owner` 캐시 필드가 비어 있으면 추가된 user를 세팅한다.\n */\n @Directive('@privilege(domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => DomainOwner, { description: 'Add a user as owner of the current domain.' })\n async addDomainOwner(\n @Arg('username') username: string,\n @Ctx() context: any,\n @Arg('reason', { nullable: true }) reason?: string\n ): Promise<DomainOwner> {\n const { domain, user: actor } = context.state\n\n return (getRepository(Domain) as any).manager.transaction(async (tx: any) => {\n const userRepo = tx.getRepository(User)\n const ownerRepo = tx.getRepository(DomainOwner)\n const domainRepo = tx.getRepository(Domain)\n\n // username 컬럼이 nullable — 사용자는 username 을 가지지 않고\n // email 만 갖는 경우가 많음. User.get username() 도 `username || email`\n // 을 리턴하므로 UI 가 email 을 보낼 수 있음. 둘 다 매칭한다.\n const targetUser = await userRepo\n .createQueryBuilder('u')\n .innerJoin('u.domains', 'd')\n .where('(u.username = :key OR u.email = :key) AND d.id = :domainId', {\n key: username,\n domainId: domain.id\n })\n .getOne()\n if (!targetUser) {\n throw new Error(`User '${username}' is not a member of this domain.`)\n }\n\n const existing = await ownerRepo.findOne({\n where: { domain: { id: domain.id }, user: { id: targetUser.id } }\n })\n if (existing) {\n throw new Error(`User '${username}' is already an owner of this domain.`)\n }\n\n const entry = ownerRepo.create({\n domain: { id: domain.id } as Domain,\n user: { id: targetUser.id } as User,\n grantedBy: actor,\n reason\n })\n const saved = await ownerRepo.save(entry)\n\n // 하위 호환 캐시: domain.owner가 비어 있으면 세팅\n if (!domain.owner) {\n await domainRepo.update(domain.id, { owner: targetUser.id })\n }\n\n // GraphQL 응답을 위해 relations 포함해 재조회.\n // (save() 반환값은 { user: {id}, grantedBy: actor } 인데 user/grantedBy 의\n // non-nullable 필드 email 등이 채워져 있지 않아 직렬화 에러 발생)\n return ownerRepo.findOneOrFail({\n where: { id: saved.id },\n relations: ['user', 'grantedBy', 'domain']\n })\n })\n }\n\n /**\n * 현재 도메인의 owner를 제거한다.\n * - 기존 도메인 오너 또는 수퍼유저만 호출 가능\n * - 마지막 남은 owner는 제거 불가 (최소 1명 유지)\n * - `Domain.owner` 캐시가 제거되는 user였다면, 남은 owner 중 하나로 재설정\n */\n @Directive('@privilege(domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, { description: 'Remove a user from the owners of the current domain.' })\n async removeDomainOwner(\n @Arg('username') username: string,\n @Ctx() context: any,\n @Arg('reason', { nullable: true }) _reason?: string\n ): Promise<boolean> {\n const { domain } = context.state\n\n return (getRepository(Domain) as any).manager.transaction(async (tx: any) => {\n const userRepo = tx.getRepository(User)\n const ownerRepo = tx.getRepository(DomainOwner)\n const domainRepo = tx.getRepository(Domain)\n\n // username 또는 email 둘 다 허용 — addDomainOwner 와 대칭.\n const targetUser = await userRepo.findOne({\n where: [{ username }, { email: username }]\n })\n if (!targetUser) {\n throw new Error(`User '${username}' not found.`)\n }\n\n const entry = await ownerRepo.findOne({\n where: { domain: { id: domain.id }, user: { id: targetUser.id } }\n })\n const hasLegacyOwner = domain.owner === targetUser.id\n\n if (!entry && !hasLegacyOwner) {\n throw new Error(`User '${username}' is not an owner of this domain.`)\n }\n\n // 마지막 1인 보호: DomainOwner 테이블 기준 count + legacy owner 포함 여부\n const ownerCount = await ownerRepo\n .createQueryBuilder('ow')\n .where('ow.domain_id = :d', { d: domain.id })\n .getCount()\n\n // 남을 owner 수 계산:\n // - DomainOwner 테이블에서 제거 대상이 있으면 ownerCount - 1\n // - legacy owner가 별도로 있고 DomainOwner에 없다면 +1\n let remaining = ownerCount\n if (entry) remaining -= 1\n // legacy owner가 지금 제거 대상과 다른 user라면, 그 user도 하나의 owner로 카운트\n if (domain.owner && domain.owner !== targetUser.id) {\n // legacy owner가 DomainOwner 테이블에도 이미 있는지\n const legacyInTable = await ownerRepo\n .createQueryBuilder('ow')\n .where('ow.domain_id = :d AND ow.user_id = :u', { d: domain.id, u: domain.owner })\n .getExists()\n if (!legacyInTable) remaining += 1\n }\n\n if (remaining < 1) {\n throw new Error(\n 'Cannot remove the last owner of a domain. Add another owner first.'\n )\n }\n\n if (entry) {\n await ownerRepo.delete(entry.id)\n }\n\n if (hasLegacyOwner) {\n // 캐시 재설정: 남은 DomainOwner 중 가장 먼저 추가된 사람\n const next = await ownerRepo\n .createQueryBuilder('ow')\n .where('ow.domain_id = :d', { d: domain.id })\n .orderBy('ow.grantedAt', 'ASC')\n .getOne()\n await domainRepo.update(domain.id, { owner: next?.userId ?? null })\n }\n\n return true\n })\n }\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DomainOwner } from './domain-owner.js';
|
|
2
|
+
export declare class DomainOwnerQuery {
|
|
3
|
+
/**
|
|
4
|
+
* 현재 도메인의 owner 목록 조회 — **도메인 오너 또는 수퍼유저만** 가능.
|
|
5
|
+
* 오너 구성은 관리 정보라 비공개가 기본.
|
|
6
|
+
*/
|
|
7
|
+
domainOwners(context: any): Promise<DomainOwner[]>;
|
|
8
|
+
/**
|
|
9
|
+
* 특정 user가 현재 도메인의 owner인지 확인 — 도메인 오너 또는 수퍼유저만.
|
|
10
|
+
*/
|
|
11
|
+
isDomainOwner(username: string, context: any): Promise<boolean>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DomainOwnerQuery = 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 domain_owner_js_1 = require("./domain-owner.js");
|
|
8
|
+
const user_js_1 = require("../user/user.js");
|
|
9
|
+
let DomainOwnerQuery = class DomainOwnerQuery {
|
|
10
|
+
/**
|
|
11
|
+
* 현재 도메인의 owner 목록 조회 — **도메인 오너 또는 수퍼유저만** 가능.
|
|
12
|
+
* 오너 구성은 관리 정보라 비공개가 기본.
|
|
13
|
+
*/
|
|
14
|
+
async domainOwners(context) {
|
|
15
|
+
const { domain } = context.state;
|
|
16
|
+
return (0, shell_1.getRepository)(domain_owner_js_1.DomainOwner)
|
|
17
|
+
.createQueryBuilder('ow')
|
|
18
|
+
.leftJoinAndSelect('ow.user', 'user')
|
|
19
|
+
.leftJoinAndSelect('ow.grantedBy', 'grantedBy')
|
|
20
|
+
.where('ow.domain_id = :domainId', { domainId: domain.id })
|
|
21
|
+
.orderBy('ow.grantedAt', 'ASC')
|
|
22
|
+
.getMany();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 특정 user가 현재 도메인의 owner인지 확인 — 도메인 오너 또는 수퍼유저만.
|
|
26
|
+
*/
|
|
27
|
+
async isDomainOwner(username, context) {
|
|
28
|
+
const { domain } = context.state;
|
|
29
|
+
// username 또는 email 둘 다 허용 (username 컬럼이 nullable 이라 email 만
|
|
30
|
+
// 세팅된 사용자도 있음).
|
|
31
|
+
const user = await (0, shell_1.getRepository)(user_js_1.User).findOne({
|
|
32
|
+
where: [
|
|
33
|
+
{ username, domains: { id: domain.id } },
|
|
34
|
+
{ email: username, domains: { id: domain.id } }
|
|
35
|
+
]
|
|
36
|
+
});
|
|
37
|
+
if (!user)
|
|
38
|
+
return false;
|
|
39
|
+
// Domain.owner 캐시 또는 DomainOwner 테이블 둘 다 확인
|
|
40
|
+
if (domain.owner === user.id)
|
|
41
|
+
return true;
|
|
42
|
+
const exists = await (0, shell_1.getRepository)(domain_owner_js_1.DomainOwner)
|
|
43
|
+
.createQueryBuilder('ow')
|
|
44
|
+
.where('ow.domain_id = :d AND ow.user_id = :u', { d: domain.id, u: user.id })
|
|
45
|
+
.getExists();
|
|
46
|
+
return exists;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
exports.DomainOwnerQuery = DomainOwnerQuery;
|
|
50
|
+
tslib_1.__decorate([
|
|
51
|
+
(0, type_graphql_1.Directive)('@privilege(domainOwnerGranted: true, superUserGranted: true)'),
|
|
52
|
+
(0, type_graphql_1.Query)(returns => [domain_owner_js_1.DomainOwner], { description: 'List owners of the current domain.' }),
|
|
53
|
+
tslib_1.__param(0, (0, type_graphql_1.Ctx)()),
|
|
54
|
+
tslib_1.__metadata("design:type", Function),
|
|
55
|
+
tslib_1.__metadata("design:paramtypes", [Object]),
|
|
56
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
57
|
+
], DomainOwnerQuery.prototype, "domainOwners", null);
|
|
58
|
+
tslib_1.__decorate([
|
|
59
|
+
(0, type_graphql_1.Directive)('@privilege(domainOwnerGranted: true, superUserGranted: true)'),
|
|
60
|
+
(0, type_graphql_1.Query)(returns => Boolean, { description: 'Check if a user is an owner of the current domain.' }),
|
|
61
|
+
tslib_1.__param(0, (0, type_graphql_1.Arg)('username')),
|
|
62
|
+
tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
|
|
63
|
+
tslib_1.__metadata("design:type", Function),
|
|
64
|
+
tslib_1.__metadata("design:paramtypes", [String, Object]),
|
|
65
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
66
|
+
], DomainOwnerQuery.prototype, "isDomainOwner", null);
|
|
67
|
+
exports.DomainOwnerQuery = DomainOwnerQuery = tslib_1.__decorate([
|
|
68
|
+
(0, type_graphql_1.Resolver)(of => domain_owner_js_1.DomainOwner)
|
|
69
|
+
], DomainOwnerQuery);
|
|
70
|
+
//# sourceMappingURL=domain-owner-query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-owner-query.js","sourceRoot":"","sources":["../../../server/service/domain-owner/domain-owner-query.ts"],"names":[],"mappings":";;;;AAAA,+CAAmE;AACnE,iDAA6D;AAE7D,uDAA+C;AAC/C,6CAAsC;AAG/B,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;IAC3B;;;OAGG;IAGG,AAAN,KAAK,CAAC,YAAY,CAAQ,OAAY;QACpC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAEhC,OAAO,IAAA,qBAAa,EAAC,6BAAW,CAAC;aAC9B,kBAAkB,CAAC,IAAI,CAAC;aACxB,iBAAiB,CAAC,SAAS,EAAE,MAAM,CAAC;aACpC,iBAAiB,CAAC,cAAc,EAAE,WAAW,CAAC;aAC9C,KAAK,CAAC,0BAA0B,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;aAC1D,OAAO,CAAC,cAAc,EAAE,KAAK,CAAC;aAC9B,OAAO,EAAE,CAAA;IACd,CAAC;IAED;;OAEG;IAGG,AAAN,KAAK,CAAC,aAAa,CACA,QAAgB,EAC1B,OAAY;QAEnB,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAEhC,6DAA6D;QAC7D,gBAAgB;QAChB,MAAM,IAAI,GAAG,MAAM,IAAA,qBAAa,EAAC,cAAI,CAAC,CAAC,OAAO,CAAC;YAC7C,KAAK,EAAE;gBACL,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE;gBACxC,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE;aAChD;SACF,CAAC,CAAA;QACF,IAAI,CAAC,IAAI;YAAE,OAAO,KAAK,CAAA;QAEvB,4CAA4C;QAC5C,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,EAAE;YAAE,OAAO,IAAI,CAAA;QAEzC,MAAM,MAAM,GAAG,MAAM,IAAA,qBAAa,EAAC,6BAAW,CAAC;aAC5C,kBAAkB,CAAC,IAAI,CAAC;aACxB,KAAK,CAAC,uCAAuC,EAAE,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;aAC5E,SAAS,EAAE,CAAA;QACd,OAAO,MAAM,CAAA;IACf,CAAC;CACF,CAAA;AAjDY,4CAAgB;AAOrB;IAFL,IAAA,wBAAS,EAAC,8DAA8D,CAAC;IACzE,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,6BAAW,CAAC,EAAE,EAAE,WAAW,EAAE,oCAAoC,EAAE,CAAC;IACnE,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;oDAUxB;AAOK;IAFL,IAAA,wBAAS,EAAC,8DAA8D,CAAC;IACzE,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,oDAAoD,EAAE,CAAC;IAE9F,mBAAA,IAAA,kBAAG,EAAC,UAAU,CAAC,CAAA;IACf,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;qDAsBP;2BAhDU,gBAAgB;IAD5B,IAAA,uBAAQ,EAAC,EAAE,CAAC,EAAE,CAAC,6BAAW,CAAC;GACf,gBAAgB,CAiD5B","sourcesContent":["import { Resolver, Query, Arg, Ctx, Directive } from 'type-graphql'\nimport { Domain, getRepository } from '@things-factory/shell'\n\nimport { DomainOwner } from './domain-owner.js'\nimport { User } from '../user/user.js'\n\n@Resolver(of => DomainOwner)\nexport class DomainOwnerQuery {\n /**\n * 현재 도메인의 owner 목록 조회 — **도메인 오너 또는 수퍼유저만** 가능.\n * 오너 구성은 관리 정보라 비공개가 기본.\n */\n @Directive('@privilege(domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => [DomainOwner], { description: 'List owners of the current domain.' })\n async domainOwners(@Ctx() context: any): Promise<DomainOwner[]> {\n const { domain } = context.state\n\n return getRepository(DomainOwner)\n .createQueryBuilder('ow')\n .leftJoinAndSelect('ow.user', 'user')\n .leftJoinAndSelect('ow.grantedBy', 'grantedBy')\n .where('ow.domain_id = :domainId', { domainId: domain.id })\n .orderBy('ow.grantedAt', 'ASC')\n .getMany()\n }\n\n /**\n * 특정 user가 현재 도메인의 owner인지 확인 — 도메인 오너 또는 수퍼유저만.\n */\n @Directive('@privilege(domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => Boolean, { description: 'Check if a user is an owner of the current domain.' })\n async isDomainOwner(\n @Arg('username') username: string,\n @Ctx() context: any\n ): Promise<boolean> {\n const { domain } = context.state\n\n // username 또는 email 둘 다 허용 (username 컬럼이 nullable 이라 email 만\n // 세팅된 사용자도 있음).\n const user = await getRepository(User).findOne({\n where: [\n { username, domains: { id: domain.id } },\n { email: username, domains: { id: domain.id } }\n ]\n })\n if (!user) return false\n\n // Domain.owner 캐시 또는 DomainOwner 테이블 둘 다 확인\n if (domain.owner === user.id) return true\n\n const exists = await getRepository(DomainOwner)\n .createQueryBuilder('ow')\n .where('ow.domain_id = :d AND ow.user_id = :u', { d: domain.id, u: user.id })\n .getExists()\n return exists\n }\n}\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Domain } from '@things-factory/shell';
|
|
2
|
+
import { User } from '../user/user.js';
|
|
3
|
+
/**
|
|
4
|
+
* 도메인의 소유자(Owner)를 여러 명 허용하기 위한 join entity.
|
|
5
|
+
*
|
|
6
|
+
* 기존 `Domain.owner`(단일 UUID 문자열) 모델의 한계를 해소하고,
|
|
7
|
+
* 여러 User를 같은 Domain의 owner로 등록 가능하게 한다. 모든 owner는 동등한
|
|
8
|
+
* 권한을 가진다 (primary 개념 없음).
|
|
9
|
+
*
|
|
10
|
+
* `Domain.owner` 컬럼은 하위 호환을 위해 유지되며 "대표 owner 표시용 캐시"로
|
|
11
|
+
* 활용된다:
|
|
12
|
+
* - owner 추가 시, 기존 owner 컬럼이 비어있으면 자동 세팅
|
|
13
|
+
* - owner 제거 시, 그 사람이 owner 컬럼이었다면 남은 owners 중 첫 사람으로 재설정
|
|
14
|
+
* 권위 있는 소유권 정보는 이 테이블이 가지며, `domainOwnerGranted` 권한 체크는
|
|
15
|
+
* 양쪽을 OR로 확인해 마이그레이션 기간 동안의 안전성을 보장한다.
|
|
16
|
+
*
|
|
17
|
+
* User 삭제 시 CASCADE로 소유권 엔트리 자동 해제.
|
|
18
|
+
*/
|
|
19
|
+
export declare class DomainOwner {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
domain: Domain;
|
|
22
|
+
domainId: string;
|
|
23
|
+
user: User;
|
|
24
|
+
userId: string;
|
|
25
|
+
grantedBy: User;
|
|
26
|
+
grantedById: string;
|
|
27
|
+
grantedAt: Date;
|
|
28
|
+
reason: string;
|
|
29
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DomainOwner = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const shell_1 = require("@things-factory/shell");
|
|
6
|
+
const typeorm_1 = require("typeorm");
|
|
7
|
+
const type_graphql_1 = require("type-graphql");
|
|
8
|
+
const user_js_1 = require("../user/user.js");
|
|
9
|
+
/**
|
|
10
|
+
* 도메인의 소유자(Owner)를 여러 명 허용하기 위한 join entity.
|
|
11
|
+
*
|
|
12
|
+
* 기존 `Domain.owner`(단일 UUID 문자열) 모델의 한계를 해소하고,
|
|
13
|
+
* 여러 User를 같은 Domain의 owner로 등록 가능하게 한다. 모든 owner는 동등한
|
|
14
|
+
* 권한을 가진다 (primary 개념 없음).
|
|
15
|
+
*
|
|
16
|
+
* `Domain.owner` 컬럼은 하위 호환을 위해 유지되며 "대표 owner 표시용 캐시"로
|
|
17
|
+
* 활용된다:
|
|
18
|
+
* - owner 추가 시, 기존 owner 컬럼이 비어있으면 자동 세팅
|
|
19
|
+
* - owner 제거 시, 그 사람이 owner 컬럼이었다면 남은 owners 중 첫 사람으로 재설정
|
|
20
|
+
* 권위 있는 소유권 정보는 이 테이블이 가지며, `domainOwnerGranted` 권한 체크는
|
|
21
|
+
* 양쪽을 OR로 확인해 마이그레이션 기간 동안의 안전성을 보장한다.
|
|
22
|
+
*
|
|
23
|
+
* User 삭제 시 CASCADE로 소유권 엔트리 자동 해제.
|
|
24
|
+
*/
|
|
25
|
+
let DomainOwner = class DomainOwner {
|
|
26
|
+
};
|
|
27
|
+
exports.DomainOwner = DomainOwner;
|
|
28
|
+
tslib_1.__decorate([
|
|
29
|
+
(0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
|
|
30
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.ID, { description: 'Unique identifier.' }),
|
|
31
|
+
tslib_1.__metadata("design:type", String)
|
|
32
|
+
], DomainOwner.prototype, "id", void 0);
|
|
33
|
+
tslib_1.__decorate([
|
|
34
|
+
(0, typeorm_1.ManyToOne)(type => shell_1.Domain, { onDelete: 'CASCADE' }),
|
|
35
|
+
(0, typeorm_1.JoinColumn)(),
|
|
36
|
+
(0, type_graphql_1.Field)(type => shell_1.Domain, { description: 'Domain that the user owns.' }),
|
|
37
|
+
tslib_1.__metadata("design:type", shell_1.Domain)
|
|
38
|
+
], DomainOwner.prototype, "domain", void 0);
|
|
39
|
+
tslib_1.__decorate([
|
|
40
|
+
(0, typeorm_1.RelationId)((ow) => ow.domain),
|
|
41
|
+
tslib_1.__metadata("design:type", String)
|
|
42
|
+
], DomainOwner.prototype, "domainId", void 0);
|
|
43
|
+
tslib_1.__decorate([
|
|
44
|
+
(0, typeorm_1.ManyToOne)(type => user_js_1.User, { onDelete: 'CASCADE' }),
|
|
45
|
+
(0, typeorm_1.JoinColumn)(),
|
|
46
|
+
(0, type_graphql_1.Field)(type => user_js_1.User, { description: 'User who owns the domain.' }),
|
|
47
|
+
tslib_1.__metadata("design:type", user_js_1.User)
|
|
48
|
+
], DomainOwner.prototype, "user", void 0);
|
|
49
|
+
tslib_1.__decorate([
|
|
50
|
+
(0, typeorm_1.RelationId)((ow) => ow.user),
|
|
51
|
+
tslib_1.__metadata("design:type", String)
|
|
52
|
+
], DomainOwner.prototype, "userId", void 0);
|
|
53
|
+
tslib_1.__decorate([
|
|
54
|
+
(0, typeorm_1.ManyToOne)(type => user_js_1.User, { nullable: true }),
|
|
55
|
+
(0, type_graphql_1.Field)(type => user_js_1.User, { nullable: true, description: 'User who granted this ownership (audit).' }),
|
|
56
|
+
tslib_1.__metadata("design:type", user_js_1.User)
|
|
57
|
+
], DomainOwner.prototype, "grantedBy", void 0);
|
|
58
|
+
tslib_1.__decorate([
|
|
59
|
+
(0, typeorm_1.RelationId)((ow) => ow.grantedBy),
|
|
60
|
+
tslib_1.__metadata("design:type", String)
|
|
61
|
+
], DomainOwner.prototype, "grantedById", void 0);
|
|
62
|
+
tslib_1.__decorate([
|
|
63
|
+
(0, typeorm_1.CreateDateColumn)(),
|
|
64
|
+
(0, type_graphql_1.Field)({ description: 'When the ownership was granted.' }),
|
|
65
|
+
tslib_1.__metadata("design:type", Date)
|
|
66
|
+
], DomainOwner.prototype, "grantedAt", void 0);
|
|
67
|
+
tslib_1.__decorate([
|
|
68
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
69
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'Optional reason/memo for granting ownership.' }),
|
|
70
|
+
tslib_1.__metadata("design:type", String)
|
|
71
|
+
], DomainOwner.prototype, "reason", void 0);
|
|
72
|
+
exports.DomainOwner = DomainOwner = tslib_1.__decorate([
|
|
73
|
+
(0, typeorm_1.Entity)(),
|
|
74
|
+
(0, typeorm_1.Index)('ix_domain_owner_unique', (ow) => [ow.domain, ow.user], { unique: true }),
|
|
75
|
+
(0, type_graphql_1.ObjectType)({ description: 'An ownership record binding a User to a Domain (multi-owner support).' })
|
|
76
|
+
], DomainOwner);
|
|
77
|
+
//# sourceMappingURL=domain-owner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-owner.js","sourceRoot":"","sources":["../../../server/service/domain-owner/domain-owner.ts"],"names":[],"mappings":";;;;AAAA,iDAA8C;AAC9C,qCASgB;AAChB,+CAAoD;AAEpD,6CAAsC;AAEtC;;;;;;;;;;;;;;;GAeG;AAII,IAAM,WAAW,GAAjB,MAAM,WAAW;CAmCvB,CAAA;AAnCY,kCAAW;AAGb;IAFR,IAAA,gCAAsB,EAAC,MAAM,CAAC;IAC9B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAE,EAAE,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAC;;uCACtC;AAKnB;IAHC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;IAClD,IAAA,oBAAU,GAAE;IACZ,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,EAAE,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC;sCAC7D,cAAM;2CAAA;AAGd;IADC,IAAA,oBAAU,EAAC,CAAC,EAAe,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC;;6CAC3B;AAKhB;IAHC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,cAAI,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;IAChD,IAAA,oBAAU,GAAE;IACZ,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,cAAI,EAAE,EAAE,WAAW,EAAE,2BAA2B,EAAE,CAAC;sCAC5D,cAAI;yCAAA;AAGV;IADC,IAAA,oBAAU,EAAC,CAAC,EAAe,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC;;2CAC3B;AAId;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,cAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC3C,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,cAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,0CAA0C,EAAE,CAAC;sCACtF,cAAI;8CAAA;AAGf;IADC,IAAA,oBAAU,EAAC,CAAC,EAAe,EAAE,EAAE,CAAC,EAAE,CAAC,SAAS,CAAC;;gDAC3B;AAInB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,iCAAiC,EAAE,CAAC;sCAC/C,IAAI;8CAAA;AAIf;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,8CAA8C,EAAE,CAAC;;2CACzE;sBAlCH,WAAW;IAHvB,IAAA,gBAAM,GAAE;IACR,IAAA,eAAK,EAAC,wBAAwB,EAAE,CAAC,EAAe,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC5F,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,uEAAuE,EAAE,CAAC;GACxF,WAAW,CAmCvB","sourcesContent":["import { Domain } from '@things-factory/shell'\nimport {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n JoinColumn,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId\n} from 'typeorm'\nimport { ObjectType, Field, ID } from 'type-graphql'\n\nimport { User } from '../user/user.js'\n\n/**\n * 도메인의 소유자(Owner)를 여러 명 허용하기 위한 join entity.\n *\n * 기존 `Domain.owner`(단일 UUID 문자열) 모델의 한계를 해소하고,\n * 여러 User를 같은 Domain의 owner로 등록 가능하게 한다. 모든 owner는 동등한\n * 권한을 가진다 (primary 개념 없음).\n *\n * `Domain.owner` 컬럼은 하위 호환을 위해 유지되며 \"대표 owner 표시용 캐시\"로\n * 활용된다:\n * - owner 추가 시, 기존 owner 컬럼이 비어있으면 자동 세팅\n * - owner 제거 시, 그 사람이 owner 컬럼이었다면 남은 owners 중 첫 사람으로 재설정\n * 권위 있는 소유권 정보는 이 테이블이 가지며, `domainOwnerGranted` 권한 체크는\n * 양쪽을 OR로 확인해 마이그레이션 기간 동안의 안전성을 보장한다.\n *\n * User 삭제 시 CASCADE로 소유권 엔트리 자동 해제.\n */\n@Entity()\n@Index('ix_domain_owner_unique', (ow: DomainOwner) => [ow.domain, ow.user], { unique: true })\n@ObjectType({ description: 'An ownership record binding a User to a Domain (multi-owner support).' })\nexport class DomainOwner {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier.' })\n readonly id: string\n\n @ManyToOne(type => Domain, { onDelete: 'CASCADE' })\n @JoinColumn()\n @Field(type => Domain, { description: 'Domain that the user owns.' })\n domain: Domain\n\n @RelationId((ow: DomainOwner) => ow.domain)\n domainId: string\n\n @ManyToOne(type => User, { onDelete: 'CASCADE' })\n @JoinColumn()\n @Field(type => User, { description: 'User who owns the domain.' })\n user: User\n\n @RelationId((ow: DomainOwner) => ow.user)\n userId: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'User who granted this ownership (audit).' })\n grantedBy: User\n\n @RelationId((ow: DomainOwner) => ow.grantedBy)\n grantedById: string\n\n @CreateDateColumn()\n @Field({ description: 'When the ownership was granted.' })\n grantedAt: Date\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Optional reason/memo for granting ownership.' })\n reason: string\n}\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { DomainOwner } from './domain-owner.js';
|
|
2
|
+
import { DomainOwnerQuery } from './domain-owner-query.js';
|
|
3
|
+
import { DomainOwnerMutation } from './domain-owner-mutation.js';
|
|
4
|
+
export declare const entities: (typeof DomainOwner)[];
|
|
5
|
+
export declare const resolvers: (typeof DomainOwnerQuery | typeof DomainOwnerMutation)[];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolvers = exports.entities = void 0;
|
|
4
|
+
const domain_owner_js_1 = require("./domain-owner.js");
|
|
5
|
+
const domain_owner_query_js_1 = require("./domain-owner-query.js");
|
|
6
|
+
const domain_owner_mutation_js_1 = require("./domain-owner-mutation.js");
|
|
7
|
+
exports.entities = [domain_owner_js_1.DomainOwner];
|
|
8
|
+
exports.resolvers = [domain_owner_query_js_1.DomainOwnerQuery, domain_owner_mutation_js_1.DomainOwnerMutation];
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/domain-owner/index.ts"],"names":[],"mappings":";;;AAAA,uDAA+C;AAC/C,mEAA0D;AAC1D,yEAAgE;AAEnD,QAAA,QAAQ,GAAG,CAAC,6BAAW,CAAC,CAAA;AACxB,QAAA,SAAS,GAAG,CAAC,wCAAgB,EAAE,8CAAmB,CAAC,CAAA","sourcesContent":["import { DomainOwner } from './domain-owner.js'\nimport { DomainOwnerQuery } from './domain-owner-query.js'\nimport { DomainOwnerMutation } from './domain-owner-mutation.js'\n\nexport const entities = [DomainOwner]\nexport const resolvers = [DomainOwnerQuery, DomainOwnerMutation]\n"]}
|
|
@@ -14,6 +14,7 @@ export * from './verification-token/verification-token.js';
|
|
|
14
14
|
export * from './login-history/login-history.js';
|
|
15
15
|
export * from './web-auth-credential/web-auth-credential.js';
|
|
16
16
|
export * from './domain-link/domain-link.js';
|
|
17
|
+
export * from './domain-owner/domain-owner.js';
|
|
17
18
|
export * from './app-binding/app-binding-types.js';
|
|
18
19
|
export * from './appliance/appliance-types.js';
|
|
19
20
|
export * from './application/application-types.js';
|
|
@@ -24,7 +25,7 @@ export * from './privilege/privilege-types.js';
|
|
|
24
25
|
export * from './role/role-types.js';
|
|
25
26
|
export * from './user/user-types.js';
|
|
26
27
|
export * from './domain-link/domain-link-types.js';
|
|
27
|
-
export declare const entities: (typeof import("./
|
|
28
|
+
export declare const entities: (typeof import("./domain-owner/domain-owner.js").DomainOwner | typeof import("./user/user.js").User | typeof import("./users-auth-providers/users-auth-providers.js").UsersAuthProviders | typeof import("./auth-provider/auth-provider.js").AuthProvider | typeof import("./application/application.js").Application | typeof import("./appliance/appliance.js").Appliance | typeof import("./privilege/privilege.js").Privilege | typeof import("./role/role.js").Role | typeof import("./partner/partner.js").Partner | typeof import("./granted-role/granted-role.js").GrantedRole | typeof import("./invitation/invitation.js").Invitation | typeof import("./password-history/password-history.js").PasswordHistory | typeof import("./verification-token/verification-token.js").VerificationToken | typeof import("./verification-token/verification-token.js").VerificationTokenType | typeof import("./login-history/login-history.js").LoginHistory | typeof import("./web-auth-credential/web-auth-credential.js").WebAuthCredential | typeof import("./domain-link/domain-link.js").DomainLink)[];
|
|
28
29
|
export declare const schema: {
|
|
29
30
|
typeDefs: {
|
|
30
31
|
privilegeDirectiveTypeDefs: import("graphql").DocumentNode;
|