@things-factory/auth-base 10.1.20 → 10.1.24

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.
@@ -4,5 +4,6 @@ export declare class ApplianceMutation {
4
4
  createAppliance(appliance: NewAppliance, context: ResolverContext): Promise<Appliance>;
5
5
  deleteAppliance(id: string, context: ResolverContext): Promise<Boolean>;
6
6
  generateApplianceSecret(id: string, context: ResolverContext): Promise<Appliance>;
7
+ revokeApplianceCredential(id: string, context: ResolverContext): Promise<Appliance>;
7
8
  updateAppliance(id: string, patch: AppliancePatch, context: ResolverContext): Promise<Appliance>;
8
9
  }
@@ -39,6 +39,11 @@ let ApplianceMutation = class ApplianceMutation {
39
39
  context.throw(401, 'appliance is not allowed for this domain');
40
40
  }
41
41
  appuser.password = appliance_js_1.Appliance.generateAccessToken(domain, appuser, appliance);
42
+ /*
43
+ * Back to ACTIVATED. A revoked appliance is INACTIVE, and `checkAuth` refuses that whatever
44
+ * the token says — so without this line a freshly issued secret would be dead on arrival.
45
+ */
46
+ appuser.status = user_js_1.UserStatus.ACTIVATED;
42
47
  await (0, shell_1.getRepository)(user_js_1.User).save(appuser);
43
48
  return await (0, shell_1.getRepository)(appliance_js_1.Appliance, tx).save({
44
49
  ...appliance,
@@ -46,6 +51,65 @@ let ApplianceMutation = class ApplianceMutation {
46
51
  updater: user
47
52
  });
48
53
  }
54
+ /*
55
+ * Take an appliance's credential away without taking the appliance with it.
56
+ *
57
+ * ── Emptying the field is not revocation ────────────────────────────────
58
+ * The access token is a **signed JWT**, not a secret we compare against. Clearing
59
+ * `Appliance.accessToken` only drops our copy — the token already on the device keeps
60
+ * verifying against SECRET until its own `exp`, which is a year out.
61
+ *
62
+ * What stops it is `User.checkAuth`. It runs on every request once the signature verifies,
63
+ * looks the subject up by the id baked into the token, and refuses an INACTIVE one. So the
64
+ * shadow user's status is the switch, and every outstanding token is refused from the next
65
+ * request onward.
66
+ *
67
+ * ── Why INACTIVE and not LOCKED ─────────────────────────────────────────
68
+ * LOCKED is the failed-sign-in lockout, and `checkAuth` lets it through once
69
+ * `isAccountLocked` says the lock has aged out. A revocation that expires by itself would
70
+ * hand a lost device its access back. INACTIVE is refused unconditionally.
71
+ *
72
+ * BANNED would read well here and does not work: it is in `UserStatus` but `checkAuth`'s
73
+ * switch has no case for it, so a banned subject authenticates.
74
+ *
75
+ * Clearing `password` and `accessToken` is tidiness rather than a control: the JWT verifies
76
+ * on its signature and never against those columns. ⚠ So a hazard remains — set the status
77
+ * back to ACTIVATED without issuing a new secret and the old token authenticates again.
78
+ * Closing that needs the token to be refusable on its own (a jti denylist), which this is
79
+ * not.
80
+ *
81
+ * ── Why the shadow user survives ────────────────────────────────────────
82
+ * `deleteAppliance` deletes it, and that is right when the appliance goes too. Here the
83
+ * appliance stays, so the identity has to stay with it — it is what past rows point at as
84
+ * creator and updater, and re-issuing later keeps the same id rather than orphaning them.
85
+ *
86
+ * ── What this is for ────────────────────────────────────────────────────
87
+ * A device that is lost, decommissioned or suspect. Before this the only ways to stop one
88
+ * were to delete the row, which takes the history with it, or to issue a new secret, which
89
+ * leaves a working credential in the database and on the screen. Neither is revocation.
90
+ */
91
+ async revokeApplianceCredential(id, context) {
92
+ const { domain, user, tx } = context.state;
93
+ const applianceRepository = (0, shell_1.getRepository)(appliance_js_1.Appliance, tx);
94
+ const appliance = await applianceRepository.findOneBy({ domain: { id: domain.id }, id });
95
+ if (!appliance) {
96
+ context.throw(404, 'appliance not found');
97
+ }
98
+ const appuser = await (0, shell_1.getRepository)(user_js_1.User, tx).findOneBy({ reference: id, userType: 'appliance' });
99
+ if (appuser) {
100
+ await (0, shell_1.getRepository)(user_js_1.User, tx).save({
101
+ ...appuser,
102
+ status: user_js_1.UserStatus.INACTIVE,
103
+ password: null,
104
+ updater: user
105
+ });
106
+ }
107
+ return await applianceRepository.save({
108
+ ...appliance,
109
+ accessToken: null,
110
+ updater: user
111
+ });
112
+ }
49
113
  async updateAppliance(id, patch, context) {
50
114
  const { domain, user, tx } = context.state;
51
115
  const applianceRepository = (0, shell_1.getRepository)(appliance_js_1.Appliance, tx);
@@ -94,6 +158,16 @@ tslib_1.__decorate([
94
158
  tslib_1.__metadata("design:paramtypes", [String, Object]),
95
159
  tslib_1.__metadata("design:returntype", Promise)
96
160
  ], ApplianceMutation.prototype, "generateApplianceSecret", null);
161
+ tslib_1.__decorate([
162
+ (0, type_graphql_1.Directive)('@privilege(category: "security", privilege: "mutation", domainOwnerGranted: true)'),
163
+ (0, type_graphql_1.Directive)('@transaction'),
164
+ (0, type_graphql_1.Mutation)(returns => appliance_js_1.Appliance, { description: "To revoke an appliance's access credential" }),
165
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('id')),
166
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
167
+ tslib_1.__metadata("design:type", Function),
168
+ tslib_1.__metadata("design:paramtypes", [String, Object]),
169
+ tslib_1.__metadata("design:returntype", Promise)
170
+ ], ApplianceMutation.prototype, "revokeApplianceCredential", null);
97
171
  tslib_1.__decorate([
98
172
  (0, type_graphql_1.Directive)('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)'),
99
173
  (0, type_graphql_1.Directive)('@transaction'),
@@ -1 +1 @@
1
- {"version":3,"file":"appliance-mutation.js","sourceRoot":"","sources":["../../../server/service/appliance/appliance-mutation.ts"],"names":[],"mappings":";;;;AAAA,+CAAsE;AAEtE,iDAAqD;AAErD,6CAAkD;AAClD,iDAA0C;AAC1C,6DAAmE;AAEnE,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAGzB,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAItB,AAAN,KAAK,CAAC,eAAe,CACD,SAAuB,EAClC,OAAwB;QAE/B,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAE1C,MAAM,YAAY,GAAG,MAAM,IAAA,qBAAa,EAAC,wBAAS,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC;YAC3D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,IAAI;YACb,GAAG,SAAS;SACb,CAAC,CAAA;QAEF,MAAM,wBAAS,CAAC,mBAAmB,CAAC,YAAY,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QAE3D,OAAO,YAAY,CAAA;IACrB,CAAC;IAKK,AAAN,KAAK,CAAC,eAAe,CAAY,EAAU,EAAS,OAAwB;QAC1E,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QACpC,qCAAqC;QACrC,iEAAiE;QACjE,MAAM,IAAA,qBAAa,EAAC,cAAI,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC;YACnC,SAAS,EAAE,EAAE;YACb,QAAQ,EAAE,WAAW;SACtB,CAAC,CAAA;QAEF,MAAM,IAAA,qBAAa,EAAC,wBAAS,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;QAE5E,OAAO,IAAI,CAAA;IACb,CAAC;IAKK,AAAN,KAAK,CAAC,uBAAuB,CAAY,EAAU,EAAS,OAAwB;QAClF,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAE1C,MAAM,SAAS,GAAc,MAAM,IAAA,qBAAa,EAAC,wBAAS,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;QAC5G,MAAM,OAAO,GAAG,MAAM,wBAAS,CAAC,mBAAmB,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QAExE,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;YACnD,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,0CAA0C,CAAC,CAAA;QAChE,CAAC;QAED,OAAO,CAAC,QAAQ,GAAG,wBAAS,CAAC,mBAAmB,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,CAAA;QAE5E,MAAM,IAAA,qBAAa,EAAC,cAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAEvC,OAAO,MAAM,IAAA,qBAAa,EAAC,wBAAS,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC;YAC7C,GAAG,SAAS;YACZ,WAAW,EAAE,OAAO,CAAC,QAAQ;YAC7B,OAAO,EAAE,IAAI;SACd,CAAC,CAAA;IACJ,CAAC;IAKK,AAAN,KAAK,CAAC,eAAe,CACR,EAAU,EACP,KAAqB,EAC5B,OAAwB;QAE/B,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAE1C,MAAM,mBAAmB,GAAG,IAAA,qBAAa,EAAC,wBAAS,EAAE,EAAE,CAAC,CAAA;QACxD,MAAM,cAAc,GAAG,IAAA,qBAAa,EAAC,cAAI,EAAE,EAAE,CAAC,CAAA;QAE9C,MAAM,SAAS,GAAG,MAAM,mBAAmB,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;QACjG,MAAM,OAAO,GAAG,MAAM,wBAAS,CAAC,mBAAmB,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QAExE,MAAM,cAAc,CAAC,IAAI,CAAC;YACxB,GAAG,OAAO;YACV,IAAI,EAAE,KAAK,EAAE,IAAI,IAAI,IAAI,CAAC,IAAI;SAC/B,CAAC,CAAA;QAEF,OAAO,MAAM,mBAAmB,CAAC,IAAI,CAAC;YACpC,GAAG,SAAS;YACZ,GAAG,KAAK;YACR,OAAO,EAAE,IAAI;SACd,CAAC,CAAA;IACJ,CAAC;CACF,CAAA;AA1FY,8CAAiB;AAItB;IAHL,IAAA,wBAAS,EAAC,+EAA+E,CAAC;IAC1F,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,wBAAS,EAAE,EAAE,WAAW,EAAE,yBAAyB,EAAE,CAAC;IAExE,mBAAA,IAAA,kBAAG,EAAC,WAAW,CAAC,CAAA;IAChB,mBAAA,IAAA,kBAAG,GAAE,CAAA;;6CADuB,iCAAY;;wDAe1C;AAKK;IAHL,IAAA,wBAAS,EAAC,+EAA+E,CAAC;IAC1F,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,qBAAqB,EAAE,CAAC;IAC9C,mBAAA,IAAA,kBAAG,EAAC,IAAI,CAAC,CAAA;IAAc,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;wDAYlD;AAKK;IAHL,IAAA,wBAAS,EAAC,mFAAmF,CAAC;IAC9F,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,wBAAS,CAAC;IACA,mBAAA,IAAA,kBAAG,EAAC,IAAI,CAAC,CAAA;IAAc,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;gEAmB1D;AAKK;IAHL,IAAA,wBAAS,EAAC,+EAA+E,CAAC;IAC1F,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,wBAAS,CAAC;IAE5B,mBAAA,IAAA,kBAAG,EAAC,IAAI,CAAC,CAAA;IACT,mBAAA,IAAA,kBAAG,EAAC,OAAO,CAAC,CAAA;IACZ,mBAAA,IAAA,kBAAG,GAAE,CAAA;;qDADe,mCAAc;;wDAqBpC;4BAzFU,iBAAiB;IAD7B,IAAA,uBAAQ,EAAC,wBAAS,CAAC;GACP,iBAAiB,CA0F7B","sourcesContent":["import { Directive, Arg, Ctx, Mutation, Resolver } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { User, UserStatus } from '../user/user.js'\nimport { Appliance } from './appliance.js'\nimport { AppliancePatch, NewAppliance } from './appliance-types.js'\n\nconst crypto = require('crypto')\n\n@Resolver(Appliance)\nexport class ApplianceMutation {\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Appliance, { description: 'To create new appliance' })\n async createAppliance(\n @Arg('appliance') appliance: NewAppliance,\n @Ctx() context: ResolverContext\n ): Promise<Appliance> {\n const { domain, user, tx } = context.state\n\n const newAppliance = await getRepository(Appliance, tx).save({\n domain: domain,\n creator: user,\n updater: user,\n ...appliance\n })\n\n await Appliance.ensureApplianceUser(newAppliance, user, tx)\n\n return newAppliance\n }\n\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Boolean, { description: 'To delete appliance' })\n async deleteAppliance(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Boolean> {\n const { domain, tx } = context.state\n // TODO 이 사용자가 이 도메인에 속한 사용자인지 확인해야함.\n // TODO 다른 도메인에도 포함되어있다면, domains-users 관게와 해당 도메인 관련 정보만 삭제해야 함.\n await getRepository(User, tx).delete({\n reference: id,\n userType: 'appliance'\n })\n\n await getRepository(Appliance, tx).delete({ domain: { id: domain.id }, id })\n\n return true\n }\n\n @Directive('@privilege(category: \"security\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Appliance)\n async generateApplianceSecret(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Appliance> {\n const { domain, user, tx } = context.state\n\n const appliance: Appliance = await getRepository(Appliance, tx).findOneBy({ domain: { id: domain.id }, id })\n const appuser = await Appliance.ensureApplianceUser(appliance, user, tx)\n\n if (!appuser.domains.find(d => d.id === domain.id)) {\n context.throw(401, 'appliance is not allowed for this domain')\n }\n\n appuser.password = Appliance.generateAccessToken(domain, appuser, appliance)\n\n await getRepository(User).save(appuser)\n\n return await getRepository(Appliance, tx).save({\n ...appliance,\n accessToken: appuser.password,\n updater: user\n })\n }\n\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Appliance)\n async updateAppliance(\n @Arg('id') id: string,\n @Arg('patch') patch: AppliancePatch,\n @Ctx() context: ResolverContext\n ): Promise<Appliance> {\n const { domain, user, tx } = context.state\n\n const applianceRepository = getRepository(Appliance, tx)\n const userRepository = getRepository(User, tx)\n\n const appliance = await applianceRepository.findOne({ where: { domain: { id: domain.id }, id } })\n const appuser = await Appliance.ensureApplianceUser(appliance, user, tx)\n\n await userRepository.save({\n ...appuser,\n name: patch?.name || user.name\n })\n\n return await applianceRepository.save({\n ...appliance,\n ...patch,\n updater: user\n })\n }\n}\n"]}
1
+ {"version":3,"file":"appliance-mutation.js","sourceRoot":"","sources":["../../../server/service/appliance/appliance-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Directive, Arg, Ctx, Mutation, Resolver } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { User, UserStatus } from '../user/user.js'\nimport { Appliance } from './appliance.js'\nimport { AppliancePatch, NewAppliance } from './appliance-types.js'\n\nconst crypto = require('crypto')\n\n@Resolver(Appliance)\nexport class ApplianceMutation {\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Appliance, { description: 'To create new appliance' })\n async createAppliance(\n @Arg('appliance') appliance: NewAppliance,\n @Ctx() context: ResolverContext\n ): Promise<Appliance> {\n const { domain, user, tx } = context.state\n\n const newAppliance = await getRepository(Appliance, tx).save({\n domain: domain,\n creator: user,\n updater: user,\n ...appliance\n })\n\n await Appliance.ensureApplianceUser(newAppliance, user, tx)\n\n return newAppliance\n }\n\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Boolean, { description: 'To delete appliance' })\n async deleteAppliance(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Boolean> {\n const { domain, tx } = context.state\n // TODO 이 사용자가 이 도메인에 속한 사용자인지 확인해야함.\n // TODO 다른 도메인에도 포함되어있다면, domains-users 관게와 해당 도메인 관련 정보만 삭제해야 함.\n await getRepository(User, tx).delete({\n reference: id,\n userType: 'appliance'\n })\n\n await getRepository(Appliance, tx).delete({ domain: { id: domain.id }, id })\n\n return true\n }\n\n @Directive('@privilege(category: \"security\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Appliance)\n async generateApplianceSecret(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Appliance> {\n const { domain, user, tx } = context.state\n\n const appliance: Appliance = await getRepository(Appliance, tx).findOneBy({ domain: { id: domain.id }, id })\n const appuser = await Appliance.ensureApplianceUser(appliance, user, tx)\n\n if (!appuser.domains.find(d => d.id === domain.id)) {\n context.throw(401, 'appliance is not allowed for this domain')\n }\n\n appuser.password = Appliance.generateAccessToken(domain, appuser, appliance)\n\n /*\n * Back to ACTIVATED. A revoked appliance is INACTIVE, and `checkAuth` refuses that whatever\n * the token says — so without this line a freshly issued secret would be dead on arrival.\n */\n appuser.status = UserStatus.ACTIVATED\n\n await getRepository(User).save(appuser)\n\n return await getRepository(Appliance, tx).save({\n ...appliance,\n accessToken: appuser.password,\n updater: user\n })\n }\n\n /*\n * Take an appliance's credential away without taking the appliance with it.\n *\n * ── Emptying the field is not revocation ────────────────────────────────\n * The access token is a **signed JWT**, not a secret we compare against. Clearing\n * `Appliance.accessToken` only drops our copy — the token already on the device keeps\n * verifying against SECRET until its own `exp`, which is a year out.\n *\n * What stops it is `User.checkAuth`. It runs on every request once the signature verifies,\n * looks the subject up by the id baked into the token, and refuses an INACTIVE one. So the\n * shadow user's status is the switch, and every outstanding token is refused from the next\n * request onward.\n *\n * ── Why INACTIVE and not LOCKED ─────────────────────────────────────────\n * LOCKED is the failed-sign-in lockout, and `checkAuth` lets it through once\n * `isAccountLocked` says the lock has aged out. A revocation that expires by itself would\n * hand a lost device its access back. INACTIVE is refused unconditionally.\n *\n * BANNED would read well here and does not work: it is in `UserStatus` but `checkAuth`'s\n * switch has no case for it, so a banned subject authenticates.\n *\n * Clearing `password` and `accessToken` is tidiness rather than a control: the JWT verifies\n * on its signature and never against those columns. ⚠ So a hazard remains — set the status\n * back to ACTIVATED without issuing a new secret and the old token authenticates again.\n * Closing that needs the token to be refusable on its own (a jti denylist), which this is\n * not.\n *\n * ── Why the shadow user survives ────────────────────────────────────────\n * `deleteAppliance` deletes it, and that is right when the appliance goes too. Here the\n * appliance stays, so the identity has to stay with it — it is what past rows point at as\n * creator and updater, and re-issuing later keeps the same id rather than orphaning them.\n *\n * ── What this is for ────────────────────────────────────────────────────\n * A device that is lost, decommissioned or suspect. Before this the only ways to stop one\n * were to delete the row, which takes the history with it, or to issue a new secret, which\n * leaves a working credential in the database and on the screen. Neither is revocation.\n */\n @Directive('@privilege(category: \"security\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Appliance, { description: \"To revoke an appliance's access credential\" })\n async revokeApplianceCredential(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Appliance> {\n const { domain, user, tx } = context.state\n\n const applianceRepository = getRepository(Appliance, tx)\n const appliance = await applianceRepository.findOneBy({ domain: { id: domain.id }, id })\n\n if (!appliance) {\n context.throw(404, 'appliance not found')\n }\n\n const appuser = await getRepository(User, tx).findOneBy({ reference: id, userType: 'appliance' })\n\n if (appuser) {\n await getRepository(User, tx).save({\n ...appuser,\n status: UserStatus.INACTIVE,\n password: null,\n updater: user\n })\n }\n\n return await applianceRepository.save({\n ...appliance,\n accessToken: null,\n updater: user\n })\n }\n\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Appliance)\n async updateAppliance(\n @Arg('id') id: string,\n @Arg('patch') patch: AppliancePatch,\n @Ctx() context: ResolverContext\n ): Promise<Appliance> {\n const { domain, user, tx } = context.state\n\n const applianceRepository = getRepository(Appliance, tx)\n const userRepository = getRepository(User, tx)\n\n const appliance = await applianceRepository.findOne({ where: { domain: { id: domain.id }, id } })\n const appuser = await Appliance.ensureApplianceUser(appliance, user, tx)\n\n await userRepository.save({\n ...appuser,\n name: patch?.name || user.name\n })\n\n return await applianceRepository.save({\n ...appliance,\n ...patch,\n updater: user\n })\n }\n}\n"]}