@things-factory/auth-base 10.1.215 → 10.1.218
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/config/config.development.js +2 -4
- package/config/config.production.js +1 -2
- package/dist-client/actions/auth.d.ts +1 -0
- package/dist-client/actions/auth.js +6 -0
- package/dist-client/actions/auth.js.map +1 -1
- package/dist-client/reducers/auth.d.ts +8 -0
- package/dist-client/reducers/auth.js +8 -2
- package/dist-client/reducers/auth.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/constants/error-code.d.ts +1 -0
- package/dist-server/constants/error-code.js +2 -1
- package/dist-server/constants/error-code.js.map +1 -1
- package/dist-server/controllers/auth.d.ts +0 -1
- package/dist-server/controllers/auth.js +0 -1
- package/dist-server/controllers/auth.js.map +1 -1
- package/dist-server/controllers/password-setup.d.ts +33 -0
- package/dist-server/controllers/password-setup.js +77 -0
- package/dist-server/controllers/password-setup.js.map +1 -0
- package/dist-server/controllers/reset-password.d.ts +8 -1
- package/dist-server/controllers/reset-password.js +21 -11
- package/dist-server/controllers/reset-password.js.map +1 -1
- package/dist-server/controllers/signin.js +10 -0
- package/dist-server/controllers/signin.js.map +1 -1
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +2 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/middlewares/jwt-authenticate-middleware.js +8 -25
- package/dist-server/middlewares/jwt-authenticate-middleware.js.map +1 -1
- package/dist-server/router/auth-public-process-router.js +3 -41
- package/dist-server/router/auth-public-process-router.js.map +1 -1
- package/dist-server/service/access-request/access-request.js +2 -2
- package/dist-server/service/access-request/access-request.js.map +1 -1
- package/dist-server/service/audit/audit-kinds.js +1 -0
- package/dist-server/service/audit/audit-kinds.js.map +1 -1
- package/dist-server/service/audit/audit-subscriber.d.ts +1 -7
- package/dist-server/service/audit/audit-subscriber.js +2 -33
- package/dist-server/service/audit/audit-subscriber.js.map +1 -1
- package/dist-server/service/audit/index.d.ts +1 -1
- package/dist-server/service/index.d.ts +2 -4
- package/dist-server/service/index.js +22 -27
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/open-fields.js +0 -2
- package/dist-server/service/open-fields.js.map +1 -1
- package/dist-server/service/user/user-mutation.d.ts +27 -6
- package/dist-server/service/user/user-mutation.js +26 -114
- package/dist-server/service/user/user-mutation.js.map +1 -1
- package/dist-server/service/user/user-query.d.ts +3 -4
- package/dist-server/service/user/user-query.js +7 -32
- package/dist-server/service/user/user-query.js.map +1 -1
- package/dist-server/service/user/user-types.d.ts +6 -2
- package/dist-server/service/user/user-types.js +20 -9
- package/dist-server/service/user/user-types.js.map +1 -1
- package/dist-server/service/user/user.js +17 -8
- package/dist-server/service/user/user.js.map +1 -1
- package/dist-server/service/verification-token/verification-token.d.ts +1 -0
- package/dist-server/service/verification-token/verification-token.js +1 -0
- package/dist-server/service/verification-token/verification-token.js.map +1 -1
- package/dist-server/templates/reset-password-email.d.ts +10 -3
- package/dist-server/templates/reset-password-email.js +8 -4
- package/dist-server/templates/reset-password-email.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-server/utils/verification-token-rule.js +3 -1
- package/dist-server/utils/verification-token-rule.js.map +1 -1
- package/package.json +2 -2
- package/tests/add-user-gates.test.ts +34 -0
- package/tests/list-inheritance-db.test.ts +0 -15
- package/tests/lock-recovery.test.ts +2 -2
- package/tests/open-doors.test.ts +2 -5
- package/tests/password-reset-rule-db.test.ts +17 -16
- package/tests/password-setup-db.test.ts +213 -0
- package/tests/picture-reducer.test.ts +25 -0
- package/tests/verification-token-db.test.ts +12 -7
- package/translations/en.json +2 -2
- package/translations/es.json +2 -2
- package/translations/ja.json +2 -2
- package/translations/ko.json +2 -2
- package/translations/ms.json +2 -2
- package/translations/zh.json +2 -2
- package/dist-server/controllers/invitation.d.ts +0 -75
- package/dist-server/controllers/invitation.js +0 -195
- package/dist-server/controllers/invitation.js.map +0 -1
- package/dist-server/controllers/utils/make-invitation-token.d.ts +0 -1
- package/dist-server/controllers/utils/make-invitation-token.js +0 -9
- package/dist-server/controllers/utils/make-invitation-token.js.map +0 -1
- package/dist-server/controllers/utils/save-invitation-token.d.ts +0 -5
- package/dist-server/controllers/utils/save-invitation-token.js +0 -12
- package/dist-server/controllers/utils/save-invitation-token.js.map +0 -1
- package/dist-server/service/invitation/index.d.ts +0 -5
- package/dist-server/service/invitation/index.js +0 -9
- package/dist-server/service/invitation/index.js.map +0 -1
- package/dist-server/service/invitation/invitation-mutation.d.ts +0 -20
- package/dist-server/service/invitation/invitation-mutation.js +0 -109
- package/dist-server/service/invitation/invitation-mutation.js.map +0 -1
- package/dist-server/service/invitation/invitation-query.d.ts +0 -23
- package/dist-server/service/invitation/invitation-query.js +0 -89
- package/dist-server/service/invitation/invitation-query.js.map +0 -1
- package/dist-server/service/invitation/invitation-types.d.ts +0 -5
- package/dist-server/service/invitation/invitation-types.js +0 -21
- package/dist-server/service/invitation/invitation-types.js.map +0 -1
- package/dist-server/service/invitation/invitation.d.ts +0 -26
- package/dist-server/service/invitation/invitation.js +0 -142
- package/dist-server/service/invitation/invitation.js.map +0 -1
- package/dist-server/templates/invitation-email.d.ts +0 -6
- package/dist-server/templates/invitation-email.js +0 -82
- package/dist-server/templates/invitation-email.js.map +0 -1
- package/dist-server/utils/invitation-state.d.ts +0 -52
- package/dist-server/utils/invitation-state.js +0 -76
- package/dist-server/utils/invitation-state.js.map +0 -1
- package/tests/invitation-db.test.ts +0 -416
- package/tests/invitation-route.test.ts +0 -116
- package/tests/invitation-state.test.ts +0 -92
package/translations/ko.json
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"error.invalid username": "유효하지 않은 사용자 이름: {username}",
|
|
10
10
|
"error.mail could not be sent": "메일을 보내지 못했습니다. 잠시 후 다시 시도하십시오.",
|
|
11
11
|
"error.ownership required": "권한이 없습니다. $t(term.domain) 또는 시스템 소유자 권한이 필요합니다.",
|
|
12
|
-
"error.password should be supported": "초기 비밀번호나 디폴트 비밀번호가 제공되어야 합니다.",
|
|
13
12
|
"error.password should match the rule": "비밀번호는 다음 규칙을 지켜야 합니다. {rule}",
|
|
14
13
|
"error.password used in the past": "과거에 사용된 비밀번호입니다.",
|
|
15
14
|
"error.role-name-stands-inherited": "{name} 역할은 {domain}에 정의되어 현재 도메인에 상속되어 있습니다. 상속된 역할을 대체하려는 경우에만 이 $t(term.domain)에 같은 이름의 역할을 생성해 주세요.",
|
|
@@ -29,6 +28,8 @@
|
|
|
29
28
|
"error.x is not a member of y": "{x}은(는) {y}의 멤버가 아닙니다.",
|
|
30
29
|
"error.x privilege required": "권한이 없습니다. {x} 권한이 필요합니다.",
|
|
31
30
|
"error.role-not-assignable-here": "이 $t(term.domain)에서 줄 수 없는 역할입니다: {roles}",
|
|
31
|
+
"error.password-setup-pending": "비밀번호 설정을 기다리는 계정입니다. 받은 비밀번호 설정 링크에서 비밀번호를 정한 뒤 로그인하세요.",
|
|
32
|
+
"error.password-setup-mail-not-sent": "{email}(으)로 메일을 보내지 못했습니다. 바뀐 것은 없습니다 — 링크 복사로 전하세요.",
|
|
32
33
|
"error.password-reset-owner-account": "$t(term.domain) 오너나 관리자의 비밀번호는 기본값으로 초기화하지 않습니다.",
|
|
33
34
|
"error.password-reset-reaches-other-domains": "{domains}에서도 일하는 사람이라 이 $t(term.domain)에서 비밀번호를 초기화할 수 없습니다.",
|
|
34
35
|
"error.x not allowed from this address": "권한이 없습니다. 이 주소에서는 {x} 권한을 사용할 수 없습니다.",
|
|
@@ -212,7 +213,6 @@
|
|
|
212
213
|
"text.delete account succeed": "계정이 삭제되었습니다.",
|
|
213
214
|
"text.inactive user": "비활성 사용자입니다.",
|
|
214
215
|
"text.invalid verification token": "확인 토큰이 유효하지 않습니다.",
|
|
215
|
-
"text.invitation email sent": "초대 이메일을 보냈습니다.",
|
|
216
216
|
"text.password changed successfully": "비밀번호가 성공적으로 변경되었습니다.",
|
|
217
217
|
"text.password reset email sent": "가입된 주소라면 비밀번호 재설정 메일을 보냈습니다.",
|
|
218
218
|
"text.password reset succeed": "비밀번호 성공적으로 재설정되었습니다.",
|
package/translations/ms.json
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"error.invalid username": "Nama pengguna tidak sah: {username}",
|
|
10
10
|
"error.mail could not be sent": "E-mel tidak dapat dihantar. Cuba lagi sebentar lagi.",
|
|
11
11
|
"error.ownership required": "tidak dibenarkan! kebenaran pemilik domain atau sistem diperlukan",
|
|
12
|
-
"error.password should be supported": "kata laluan awal atau kata laluan lalai harus disokong",
|
|
13
12
|
"error.password should match the rule": "Kata laluan harus mematuhi peraturan berikut. ${rule}",
|
|
14
13
|
"error.password used in the past": "Kata laluan telah digunakan dalam masa lampau",
|
|
15
14
|
"error.role-name-stands-inherited": "peranan bernama {name} ditakrifkan dalam {domain} dan kelihatan di sini juga. Bina milik domain ini dengan nama yang sama hanya jika itu memang yang dimaksudkan.",
|
|
@@ -29,6 +28,8 @@
|
|
|
29
28
|
"error.x is not a member of y": "{x} bukan ahli {y}",
|
|
30
29
|
"error.x privilege required": "tidak dibenarkan! kebenaran {x} diperlukan",
|
|
31
30
|
"error.role-not-assignable-here": "$t(term.domain) ini tidak boleh memberikan peranan: {roles}",
|
|
31
|
+
"error.password-setup-pending": "Akaun ini menunggu kata laluannya. Tetapkannya melalui pautan tetapan kata laluan yang diterima, kemudian log masuk.",
|
|
32
|
+
"error.password-setup-mail-not-sent": "E-mel kepada {email} tidak dihantar. Tiada apa yang diubah - salin pautan sebagai ganti.",
|
|
32
33
|
"error.password-reset-owner-account": "Kata laluan pemilik $t(term.domain) atau pentadbir tidak ditetapkan semula kepada lalai.",
|
|
33
34
|
"error.password-reset-reaches-other-domains": "Pengguna ini juga bekerja di {domains}, jadi $t(term.domain) ini tidak boleh menetapkan semula kata laluannya.",
|
|
34
35
|
"error.x not allowed from this address": "tidak dibenarkan! kebenaran {x} tidak dibenarkan dari alamat ini",
|
|
@@ -52,7 +53,6 @@
|
|
|
52
53
|
"text.delete account succeed": "Berjaya memadam akaun",
|
|
53
54
|
"text.inactive user": "Pengguna tidak aktif",
|
|
54
55
|
"text.invalid verification token": "Token pengesahan tidak sah",
|
|
55
|
-
"text.invitation email sent": "E-mel jemputan telah dihantar",
|
|
56
56
|
"text.password changed successfully": "Kata laluan berjaya diubah",
|
|
57
57
|
"text.password reset email sent": "Jika alamat ini didaftarkan, e-mel reset kata laluan telah dihantar",
|
|
58
58
|
"text.password reset succeed": "Reset kata laluan berjaya",
|
package/translations/zh.json
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"error.invalid username": "无效的用户名: {username}",
|
|
10
10
|
"error.mail could not be sent": "邮件未能发送。请稍后再试。",
|
|
11
11
|
"error.ownership required": "没有权限。需要$t(term.domain)或系统所有者权限。",
|
|
12
|
-
"error.password should be supported": "应支持初始密码或默认密码",
|
|
13
12
|
"error.password should match the rule": "密码应符合以下规则。${rule}",
|
|
14
13
|
"error.password used in the past": "使用过的密码!",
|
|
15
14
|
"error.role-name-stands-inherited": "名为 {name} 的角色定义在 {domain},在此处也可见。只有确实要在该$t(term.domain)中另行建立同名角色时才创建。",
|
|
@@ -29,6 +28,8 @@
|
|
|
29
28
|
"error.x is not a member of y": "{x}不是{y}的成员",
|
|
30
29
|
"error.x privilege required": "没有权限。需要 {x} 权限。",
|
|
31
30
|
"error.role-not-assignable-here": "此$t(term.domain)不能分配的角色: {roles}",
|
|
31
|
+
"error.password-setup-pending": "该账户正在等待设置密码。请通过收到的密码设置链接设置密码后登录。",
|
|
32
|
+
"error.password-setup-mail-not-sent": "未能向 {email} 发送邮件。没有任何更改,请复制链接转交。",
|
|
32
33
|
"error.password-reset-owner-account": "不会将$t(term.domain)所有者或管理员的密码重置为默认值。",
|
|
33
34
|
"error.password-reset-reaches-other-domains": "该用户也在{domains}中工作,此$t(term.domain)不能重置其密码。",
|
|
34
35
|
"error.x not allowed from this address": "没有权限。从此地址不能使用 {x} 权限。",
|
|
@@ -52,7 +53,6 @@
|
|
|
52
53
|
"text.delete account succeed": "已成功删除账号。",
|
|
53
54
|
"text.inactive user": "未激活用账号。",
|
|
54
55
|
"text.invalid verification token": "令牌无效。",
|
|
55
|
-
"text.invitation email sent": "成功发送邀请电子邮件。",
|
|
56
56
|
"text.password changed successfully": "密码更改成功。",
|
|
57
57
|
"text.password reset email sent": "如果该地址已注册,我们已发送密码重置邮件。",
|
|
58
58
|
"text.password reset succeed": "密码重置成功。",
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { EntityManager } from 'typeorm';
|
|
2
|
-
import { Domain } from '@things-factory/shell';
|
|
3
|
-
import { Invitation } from '../service/invitation/invitation.js';
|
|
4
|
-
import { Role } from '../service/role/role.js';
|
|
5
|
-
import { User } from '../service/user/user.js';
|
|
6
|
-
/**
|
|
7
|
-
* Inviting somebody into a domain, and their accepting it.
|
|
8
|
-
*
|
|
9
|
-
* ── What this is for, and what it is not ────────────────────────────────────
|
|
10
|
-
* `inviteUser` already puts a person who **has an account** into a domain, at once, with no mail
|
|
11
|
-
* and no say from them. That is the right tool when the administrator has the authority and the
|
|
12
|
-
* person already exists.
|
|
13
|
-
*
|
|
14
|
-
* This is for the case the `user-invite` screen records as having no path at all: **somebody with
|
|
15
|
-
* no account, in an installation with no default password.** Nobody can set their password for
|
|
16
|
-
* them, so the only way in is a mail they can prove they received. That is what the token is.
|
|
17
|
-
*
|
|
18
|
-
* ── Where the token sits ────────────────────────────────────────────────────
|
|
19
|
-
* In the link's **fragment** — `…/accept-invitation#<token>` — which browsers do not send to the
|
|
20
|
-
* server. So it stays out of access logs, proxy logs and `Referer`. The page strips it from the
|
|
21
|
-
* address bar as soon as it has read it.
|
|
22
|
-
*
|
|
23
|
-
* That leaves one way it can still travel: the mail itself being forwarded. Nothing about where
|
|
24
|
-
* the token sits can close that, so accepting asks for the invited address as well. Holding the
|
|
25
|
-
* link is then not enough on its own.
|
|
26
|
-
*
|
|
27
|
-
* ⚠ `/verify/:token`, `/reset-password?token=` and `/unlock-user?token=` still put theirs in the
|
|
28
|
-
* URL. A password reset token is not lighter than this one. Moving those three is its own job.
|
|
29
|
-
*/
|
|
30
|
-
/** Where the accept page answers. The mail, the page and the router all read this one spelling. */
|
|
31
|
-
export declare const ACCEPT_INVITATION_PATH = "/auth/accept-invitation";
|
|
32
|
-
/**
|
|
33
|
-
* Create a pending invitation and mail it.
|
|
34
|
-
*
|
|
35
|
-
* ⚠ One pending invitation per person and domain. The history keeps every row, so this cannot be
|
|
36
|
-
* a unique index without forbidding "invited, left, invited again" — see the note on the entity.
|
|
37
|
-
* An existing pending one is re-issued rather than duplicated: a new token, a new expiry, and the
|
|
38
|
-
* same row, so the list shows one line per invitation rather than one per time somebody pressed
|
|
39
|
-
* the button.
|
|
40
|
-
*/
|
|
41
|
-
export declare function inviteByEmail({ email, domain, roles, user, context, tx }: {
|
|
42
|
-
email: string;
|
|
43
|
-
domain: Domain;
|
|
44
|
-
roles?: Role[];
|
|
45
|
-
user?: User;
|
|
46
|
-
context: any;
|
|
47
|
-
tx?: EntityManager;
|
|
48
|
-
}): Promise<Invitation>;
|
|
49
|
-
/** Mail the link. Kept apart so re-sending does not have to go through creating one. */
|
|
50
|
-
export declare function mailInvitation(invitation: Invitation, context: any): Promise<void>;
|
|
51
|
-
/**
|
|
52
|
-
* Accept an invitation.
|
|
53
|
-
*
|
|
54
|
-
* The invited address has to be given as well as the token — see the note at the top. Everything
|
|
55
|
-
* that refuses here refuses the same way on purpose: a wrong token, a wrong address, an expired
|
|
56
|
-
* one and a cancelled one are one answer, so that trying them apart tells the caller nothing.
|
|
57
|
-
*/
|
|
58
|
-
export declare function acceptInvitation({ token, email, name, password, context, tx }: {
|
|
59
|
-
token: string;
|
|
60
|
-
email: string;
|
|
61
|
-
name?: string;
|
|
62
|
-
password?: string;
|
|
63
|
-
context: any;
|
|
64
|
-
tx?: EntityManager;
|
|
65
|
-
}): Promise<{
|
|
66
|
-
domain: Domain;
|
|
67
|
-
created: boolean;
|
|
68
|
-
}>;
|
|
69
|
-
/**
|
|
70
|
-
* Withdraw a pending invitation.
|
|
71
|
-
*
|
|
72
|
-
* The row stays and says it was cancelled. Deleting it would take with it the fact that somebody
|
|
73
|
-
* was invited at all, which is the thing the list exists to show.
|
|
74
|
-
*/
|
|
75
|
-
export declare function cancelInvitation(id: string, domain: Domain, user: User, tx?: EntityManager): Promise<boolean>;
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ACCEPT_INVITATION_PATH = void 0;
|
|
4
|
-
exports.inviteByEmail = inviteByEmail;
|
|
5
|
-
exports.mailInvitation = mailInvitation;
|
|
6
|
-
exports.acceptInvitation = acceptInvitation;
|
|
7
|
-
exports.cancelInvitation = cancelInvitation;
|
|
8
|
-
const typeorm_1 = require("typeorm");
|
|
9
|
-
const email_base_1 = require("@things-factory/email-base");
|
|
10
|
-
const env_1 = require("@things-factory/env");
|
|
11
|
-
const shell_1 = require("@things-factory/shell");
|
|
12
|
-
const invitation_js_1 = require("../service/invitation/invitation.js");
|
|
13
|
-
const user_js_1 = require("../service/user/user.js");
|
|
14
|
-
const invitation_email_js_1 = require("../templates/invitation-email.js");
|
|
15
|
-
const build_auth_url_js_1 = require("../utils/build-auth-url.js");
|
|
16
|
-
const invitation_state_js_1 = require("../utils/invitation-state.js");
|
|
17
|
-
const make_invitation_token_js_1 = require("./utils/make-invitation-token.js");
|
|
18
|
-
/**
|
|
19
|
-
* Inviting somebody into a domain, and their accepting it.
|
|
20
|
-
*
|
|
21
|
-
* ── What this is for, and what it is not ────────────────────────────────────
|
|
22
|
-
* `inviteUser` already puts a person who **has an account** into a domain, at once, with no mail
|
|
23
|
-
* and no say from them. That is the right tool when the administrator has the authority and the
|
|
24
|
-
* person already exists.
|
|
25
|
-
*
|
|
26
|
-
* This is for the case the `user-invite` screen records as having no path at all: **somebody with
|
|
27
|
-
* no account, in an installation with no default password.** Nobody can set their password for
|
|
28
|
-
* them, so the only way in is a mail they can prove they received. That is what the token is.
|
|
29
|
-
*
|
|
30
|
-
* ── Where the token sits ────────────────────────────────────────────────────
|
|
31
|
-
* In the link's **fragment** — `…/accept-invitation#<token>` — which browsers do not send to the
|
|
32
|
-
* server. So it stays out of access logs, proxy logs and `Referer`. The page strips it from the
|
|
33
|
-
* address bar as soon as it has read it.
|
|
34
|
-
*
|
|
35
|
-
* That leaves one way it can still travel: the mail itself being forwarded. Nothing about where
|
|
36
|
-
* the token sits can close that, so accepting asks for the invited address as well. Holding the
|
|
37
|
-
* link is then not enough on its own.
|
|
38
|
-
*
|
|
39
|
-
* ⚠ `/verify/:token`, `/reset-password?token=` and `/unlock-user?token=` still put theirs in the
|
|
40
|
-
* URL. A password reset token is not lighter than this one. Moving those three is its own job.
|
|
41
|
-
*/
|
|
42
|
-
/** Where the accept page answers. The mail, the page and the router all read this one spelling. */
|
|
43
|
-
exports.ACCEPT_INVITATION_PATH = '/auth/accept-invitation';
|
|
44
|
-
/** How long an invitation stands. Short, because it is a credential someone is holding. */
|
|
45
|
-
const EXPIRES_IN_HOURS = Number(env_1.config.get('invitation/expiresInHours', 48)) || 48;
|
|
46
|
-
function expiryFrom(now) {
|
|
47
|
-
return new Date(now.getTime() + EXPIRES_IN_HOURS * 60 * 60 * 1000);
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Create a pending invitation and mail it.
|
|
51
|
-
*
|
|
52
|
-
* ⚠ One pending invitation per person and domain. The history keeps every row, so this cannot be
|
|
53
|
-
* a unique index without forbidding "invited, left, invited again" — see the note on the entity.
|
|
54
|
-
* An existing pending one is re-issued rather than duplicated: a new token, a new expiry, and the
|
|
55
|
-
* same row, so the list shows one line per invitation rather than one per time somebody pressed
|
|
56
|
-
* the button.
|
|
57
|
-
*/
|
|
58
|
-
async function inviteByEmail({ email, domain, roles, user, context, tx }) {
|
|
59
|
-
const repository = (0, shell_1.getRepository)(invitation_js_1.Invitation, tx);
|
|
60
|
-
const now = new Date();
|
|
61
|
-
const alreadyIn = await (0, shell_1.getRepository)(user_js_1.User, tx).findOne({
|
|
62
|
-
where: { email: (0, typeorm_1.ILike)(email) },
|
|
63
|
-
relations: ['domains']
|
|
64
|
-
});
|
|
65
|
-
if (alreadyIn?.domains?.some(d => d.id === domain.id)) {
|
|
66
|
-
throw new shell_1.Refusal('invitation-already-a-member', { email, domain: domain.name }, `${email} is already in ${domain.name}.`);
|
|
67
|
-
}
|
|
68
|
-
const standing = await repository.findOne({
|
|
69
|
-
where: { email: (0, typeorm_1.ILike)(email), domain: { id: domain.id }, status: invitation_state_js_1.InvitationStatus.PENDING },
|
|
70
|
-
relations: ['domain']
|
|
71
|
-
});
|
|
72
|
-
const invitation = await repository.save({
|
|
73
|
-
...(standing || {}),
|
|
74
|
-
email,
|
|
75
|
-
domain,
|
|
76
|
-
roles: roles || [],
|
|
77
|
-
status: invitation_state_js_1.InvitationStatus.PENDING,
|
|
78
|
-
token: (0, make_invitation_token_js_1.makeInvitationToken)(),
|
|
79
|
-
expiresAt: expiryFrom(now),
|
|
80
|
-
acceptedAt: null,
|
|
81
|
-
creator: standing ? standing['creator'] : user,
|
|
82
|
-
updater: user
|
|
83
|
-
});
|
|
84
|
-
await mailInvitation(invitation, context);
|
|
85
|
-
return invitation;
|
|
86
|
-
}
|
|
87
|
-
/** Mail the link. Kept apart so re-sending does not have to go through creating one. */
|
|
88
|
-
async function mailInvitation(invitation, context) {
|
|
89
|
-
/*
|
|
90
|
-
* `buildAuthURL` reads the address this request arrived on, and falls back the way sign-in and
|
|
91
|
-
* OAuth do. The four mail controllers here each built theirs from `context.header.referer`
|
|
92
|
-
* instead, which **throws when the header is absent** — and the whole send sits in a
|
|
93
|
-
* `try { … } catch { return false }`, so it failed silently. This is that helper's first caller.
|
|
94
|
-
*/
|
|
95
|
-
/*
|
|
96
|
-
* ⚠ `/auth/…`. The route is declared as `/accept-invitation` on `authPublicProcessRouter`, and that
|
|
97
|
-
* router carries `prefix: '/auth'`. This line said `/accept-invitation` from 2026-09-20 until
|
|
98
|
-
* 09-24 — the link led to no route at all, fell through to the private history fallback, and the
|
|
99
|
-
* domain gate there turned the invitee away. It was the same defect as the `/auth/accept/<token>`
|
|
100
|
-
* link this replaced, made again. `tests/invitation-route.test.ts` now checks the path against
|
|
101
|
-
* the router's own table.
|
|
102
|
-
*/
|
|
103
|
-
const acceptUrl = `${(0, build_auth_url_js_1.buildAuthURL)(exports.ACCEPT_INVITATION_PATH, context)}#${invitation.token}`;
|
|
104
|
-
await (0, email_base_1.sendEmail)({
|
|
105
|
-
receiver: invitation.email,
|
|
106
|
-
subject: 'Invitation',
|
|
107
|
-
content: (0, invitation_email_js_1.getInvitationEmailForm)({
|
|
108
|
-
username: invitation.email,
|
|
109
|
-
email: invitation.email,
|
|
110
|
-
acceptUrl,
|
|
111
|
-
/* Printed as text too: the link's fragment needs JavaScript, and this does not. */
|
|
112
|
-
code: invitation.token
|
|
113
|
-
})
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Accept an invitation.
|
|
118
|
-
*
|
|
119
|
-
* The invited address has to be given as well as the token — see the note at the top. Everything
|
|
120
|
-
* that refuses here refuses the same way on purpose: a wrong token, a wrong address, an expired
|
|
121
|
-
* one and a cancelled one are one answer, so that trying them apart tells the caller nothing.
|
|
122
|
-
*/
|
|
123
|
-
async function acceptInvitation({ token, email, name, password, context, tx }) {
|
|
124
|
-
const repository = (0, shell_1.getRepository)(invitation_js_1.Invitation, tx);
|
|
125
|
-
const invitation = token && email
|
|
126
|
-
? await repository.findOne({ where: { token }, relations: ['domain', 'roles'] })
|
|
127
|
-
: null;
|
|
128
|
-
const addressMatches = invitation?.email && email && invitation.email.toLowerCase() === email.trim().toLowerCase();
|
|
129
|
-
if (!invitation || !addressMatches || !(0, invitation_state_js_1.mayAccept)(invitation)) {
|
|
130
|
-
throw new shell_1.Refusal('invitation-not-open', {}, 'This invitation cannot be used.');
|
|
131
|
-
}
|
|
132
|
-
const userRepository = (0, shell_1.getRepository)(user_js_1.User, tx);
|
|
133
|
-
let person = await userRepository.findOne({ where: { email: (0, typeorm_1.ILike)(invitation.email) }, relations: ['domains'] });
|
|
134
|
-
const created = !person;
|
|
135
|
-
if (!person) {
|
|
136
|
-
if (!password) {
|
|
137
|
-
throw new shell_1.Refusal('invitation-needs-a-password', {}, 'Choose a password to finish joining.');
|
|
138
|
-
}
|
|
139
|
-
/* The same rule the sign-up path applies; it throws its own message in the caller's language. */
|
|
140
|
-
user_js_1.User.validatePasswordByRule(password, context?.lng);
|
|
141
|
-
const salt = user_js_1.User.generateSalt();
|
|
142
|
-
person = await userRepository.save({
|
|
143
|
-
userType: 'user',
|
|
144
|
-
name: name || invitation.email,
|
|
145
|
-
username: invitation.email,
|
|
146
|
-
email: invitation.email,
|
|
147
|
-
salt,
|
|
148
|
-
password: user_js_1.User.encode(password, salt),
|
|
149
|
-
passwordUpdatedAt: new Date(),
|
|
150
|
-
/* Arriving through a mail they received is the verification, so they arrive usable. */
|
|
151
|
-
status: user_js_1.UserStatus.ACTIVATED,
|
|
152
|
-
domains: [invitation.domain]
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
else if (!person.domains?.some(d => d.id === invitation.domain.id)) {
|
|
156
|
-
person.domains = [...(person.domains || []), invitation.domain];
|
|
157
|
-
await userRepository.save(person);
|
|
158
|
-
}
|
|
159
|
-
/*
|
|
160
|
-
* Roles are given on arrival. Without them the person signs in and has no screens at all —
|
|
161
|
-
* which is why the invitation carries them, rather than leaving the administrator to notice
|
|
162
|
-
* afterwards.
|
|
163
|
-
*/
|
|
164
|
-
if (invitation.roles?.length) {
|
|
165
|
-
const loaded = await userRepository.findOne({ where: { id: person.id }, relations: ['roles'] });
|
|
166
|
-
const held = new Set((loaded?.roles || []).map(role => role.id));
|
|
167
|
-
await userRepository.save({
|
|
168
|
-
...loaded,
|
|
169
|
-
roles: [...(loaded?.roles || []), ...invitation.roles.filter(role => !held.has(role.id))]
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
await repository.save({
|
|
173
|
-
...invitation,
|
|
174
|
-
status: invitation_state_js_1.InvitationStatus.ACCEPTED,
|
|
175
|
-
acceptedAt: new Date()
|
|
176
|
-
});
|
|
177
|
-
return { domain: invitation.domain, created };
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Withdraw a pending invitation.
|
|
181
|
-
*
|
|
182
|
-
* The row stays and says it was cancelled. Deleting it would take with it the fact that somebody
|
|
183
|
-
* was invited at all, which is the thing the list exists to show.
|
|
184
|
-
*/
|
|
185
|
-
async function cancelInvitation(id, domain, user, tx) {
|
|
186
|
-
const repository = (0, shell_1.getRepository)(invitation_js_1.Invitation, tx);
|
|
187
|
-
const invitation = await repository.findOne({ where: { id, domain: { id: domain.id } }, relations: ['domain'] });
|
|
188
|
-
if (!invitation || !(0, invitation_state_js_1.mayAccept)(invitation)) {
|
|
189
|
-
/* Nothing to withdraw: already accepted, already cancelled, or already run out. */
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
192
|
-
await repository.save({ ...invitation, status: invitation_state_js_1.InvitationStatus.CANCELLED, updater: user });
|
|
193
|
-
return true;
|
|
194
|
-
}
|
|
195
|
-
//# sourceMappingURL=invitation.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"invitation.js","sourceRoot":"","sources":["../../server/controllers/invitation.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager, ILike } from 'typeorm'\n\nimport { sendEmail } from '@things-factory/email-base'\nimport { config } from '@things-factory/env'\nimport { Domain, getRepository, Refusal } from '@things-factory/shell'\n\nimport { Invitation } from '../service/invitation/invitation.js'\nimport { Role } from '../service/role/role.js'\nimport { User, UserStatus } from '../service/user/user.js'\nimport { getInvitationEmailForm } from '../templates/invitation-email.js'\nimport { buildAuthURL } from '../utils/build-auth-url.js'\nimport { InvitationStatus, mayAccept } from '../utils/invitation-state.js'\nimport { makeInvitationToken } from './utils/make-invitation-token.js'\n\n/**\n * Inviting somebody into a domain, and their accepting it.\n *\n * ── What this is for, and what it is not ────────────────────────────────────\n * `inviteUser` already puts a person who **has an account** into a domain, at once, with no mail\n * and no say from them. That is the right tool when the administrator has the authority and the\n * person already exists.\n *\n * This is for the case the `user-invite` screen records as having no path at all: **somebody with\n * no account, in an installation with no default password.** Nobody can set their password for\n * them, so the only way in is a mail they can prove they received. That is what the token is.\n *\n * ── Where the token sits ────────────────────────────────────────────────────\n * In the link's **fragment** — `…/accept-invitation#<token>` — which browsers do not send to the\n * server. So it stays out of access logs, proxy logs and `Referer`. The page strips it from the\n * address bar as soon as it has read it.\n *\n * That leaves one way it can still travel: the mail itself being forwarded. Nothing about where\n * the token sits can close that, so accepting asks for the invited address as well. Holding the\n * link is then not enough on its own.\n *\n * ⚠ `/verify/:token`, `/reset-password?token=` and `/unlock-user?token=` still put theirs in the\n * URL. A password reset token is not lighter than this one. Moving those three is its own job.\n */\n\n/** Where the accept page answers. The mail, the page and the router all read this one spelling. */\nexport const ACCEPT_INVITATION_PATH = '/auth/accept-invitation'\n\n/** How long an invitation stands. Short, because it is a credential someone is holding. */\nconst EXPIRES_IN_HOURS = Number(config.get('invitation/expiresInHours', 48)) || 48\n\nfunction expiryFrom(now: Date): Date {\n return new Date(now.getTime() + EXPIRES_IN_HOURS * 60 * 60 * 1000)\n}\n\n/**\n * Create a pending invitation and mail it.\n *\n * ⚠ One pending invitation per person and domain. The history keeps every row, so this cannot be\n * a unique index without forbidding \"invited, left, invited again\" — see the note on the entity.\n * An existing pending one is re-issued rather than duplicated: a new token, a new expiry, and the\n * same row, so the list shows one line per invitation rather than one per time somebody pressed\n * the button.\n */\nexport async function inviteByEmail({\n email,\n domain,\n roles,\n user,\n context,\n tx\n}: {\n email: string\n domain: Domain\n roles?: Role[]\n user?: User\n context: any\n tx?: EntityManager\n}): Promise<Invitation> {\n const repository = getRepository(Invitation, tx)\n const now = new Date()\n\n const alreadyIn = await getRepository(User, tx).findOne({\n where: { email: ILike(email) },\n relations: ['domains']\n })\n\n if (alreadyIn?.domains?.some(d => d.id === domain.id)) {\n throw new Refusal(\n 'invitation-already-a-member',\n { email, domain: domain.name },\n `${email} is already in ${domain.name}.`\n )\n }\n\n const standing = await repository.findOne({\n where: { email: ILike(email), domain: { id: domain.id }, status: InvitationStatus.PENDING },\n relations: ['domain']\n })\n\n const invitation = await repository.save({\n ...(standing || {}),\n email,\n domain,\n roles: roles || [],\n status: InvitationStatus.PENDING,\n token: makeInvitationToken(),\n expiresAt: expiryFrom(now),\n acceptedAt: null,\n creator: standing ? standing['creator'] : user,\n updater: user\n } as any)\n\n await mailInvitation(invitation, context)\n\n return invitation\n}\n\n/** Mail the link. Kept apart so re-sending does not have to go through creating one. */\nexport async function mailInvitation(invitation: Invitation, context: any): Promise<void> {\n /*\n * `buildAuthURL` reads the address this request arrived on, and falls back the way sign-in and\n * OAuth do. The four mail controllers here each built theirs from `context.header.referer`\n * instead, which **throws when the header is absent** — and the whole send sits in a\n * `try { … } catch { return false }`, so it failed silently. This is that helper's first caller.\n */\n /*\n * ⚠ `/auth/…`. The route is declared as `/accept-invitation` on `authPublicProcessRouter`, and that\n * router carries `prefix: '/auth'`. This line said `/accept-invitation` from 2026-09-20 until\n * 09-24 — the link led to no route at all, fell through to the private history fallback, and the\n * domain gate there turned the invitee away. It was the same defect as the `/auth/accept/<token>`\n * link this replaced, made again. `tests/invitation-route.test.ts` now checks the path against\n * the router's own table.\n */\n const acceptUrl = `${buildAuthURL(ACCEPT_INVITATION_PATH, context)}#${invitation.token}`\n\n await sendEmail({\n receiver: invitation.email,\n subject: 'Invitation',\n content: getInvitationEmailForm({\n username: invitation.email,\n email: invitation.email,\n acceptUrl,\n /* Printed as text too: the link's fragment needs JavaScript, and this does not. */\n code: invitation.token\n })\n })\n}\n\n/**\n * Accept an invitation.\n *\n * The invited address has to be given as well as the token — see the note at the top. Everything\n * that refuses here refuses the same way on purpose: a wrong token, a wrong address, an expired\n * one and a cancelled one are one answer, so that trying them apart tells the caller nothing.\n */\nexport async function acceptInvitation({\n token,\n email,\n name,\n password,\n context,\n tx\n}: {\n token: string\n email: string\n name?: string\n password?: string\n context: any\n tx?: EntityManager\n}): Promise<{ domain: Domain; created: boolean }> {\n const repository = getRepository(Invitation, tx)\n\n const invitation =\n token && email\n ? await repository.findOne({ where: { token }, relations: ['domain', 'roles'] })\n : null\n\n const addressMatches = invitation?.email && email && invitation.email.toLowerCase() === email.trim().toLowerCase()\n\n if (!invitation || !addressMatches || !mayAccept(invitation)) {\n throw new Refusal('invitation-not-open', {}, 'This invitation cannot be used.')\n }\n\n const userRepository = getRepository(User, tx)\n\n let person = await userRepository.findOne({ where: { email: ILike(invitation.email) }, relations: ['domains'] })\n const created = !person\n\n if (!person) {\n if (!password) {\n throw new Refusal('invitation-needs-a-password', {}, 'Choose a password to finish joining.')\n }\n\n /* The same rule the sign-up path applies; it throws its own message in the caller's language. */\n User.validatePasswordByRule(password, context?.lng)\n\n const salt = User.generateSalt()\n\n person = await userRepository.save({\n userType: 'user',\n name: name || invitation.email,\n username: invitation.email,\n email: invitation.email,\n salt,\n password: User.encode(password, salt),\n passwordUpdatedAt: new Date(),\n /* Arriving through a mail they received is the verification, so they arrive usable. */\n status: UserStatus.ACTIVATED,\n domains: [invitation.domain]\n } as any)\n } else if (!person.domains?.some(d => d.id === invitation.domain.id)) {\n person.domains = [...(person.domains || []), invitation.domain]\n await userRepository.save(person)\n }\n\n /*\n * Roles are given on arrival. Without them the person signs in and has no screens at all —\n * which is why the invitation carries them, rather than leaving the administrator to notice\n * afterwards.\n */\n if (invitation.roles?.length) {\n const loaded = await userRepository.findOne({ where: { id: person.id }, relations: ['roles'] })\n const held = new Set((loaded?.roles || []).map(role => role.id))\n\n await userRepository.save({\n ...loaded,\n roles: [...(loaded?.roles || []), ...invitation.roles.filter(role => !held.has(role.id))]\n } as any)\n }\n\n await repository.save({\n ...invitation,\n status: InvitationStatus.ACCEPTED,\n acceptedAt: new Date()\n } as any)\n\n return { domain: invitation.domain, created }\n}\n\n/**\n * Withdraw a pending invitation.\n *\n * The row stays and says it was cancelled. Deleting it would take with it the fact that somebody\n * was invited at all, which is the thing the list exists to show.\n */\nexport async function cancelInvitation(id: string, domain: Domain, user: User, tx?: EntityManager): Promise<boolean> {\n const repository = getRepository(Invitation, tx)\n\n const invitation = await repository.findOne({ where: { id, domain: { id: domain.id } }, relations: ['domain'] })\n\n if (!invitation || !mayAccept(invitation)) {\n /* Nothing to withdraw: already accepted, already cancelled, or already run out. */\n return false\n }\n\n await repository.save({ ...invitation, status: InvitationStatus.CANCELLED, updater: user } as any)\n\n return true\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function makeInvitationToken(): string;
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.makeInvitationToken = makeInvitationToken;
|
|
4
|
-
const tslib_1 = require("tslib");
|
|
5
|
-
const crypto_1 = tslib_1.__importDefault(require("crypto"));
|
|
6
|
-
function makeInvitationToken() {
|
|
7
|
-
return crypto_1.default.randomBytes(16).toString('hex');
|
|
8
|
-
}
|
|
9
|
-
//# sourceMappingURL=make-invitation-token.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"make-invitation-token.js","sourceRoot":"","sources":["../../../server/controllers/utils/make-invitation-token.ts"],"names":[],"mappings":";;AAEA,kDAEC;;AAJD,4DAA2B;AAE3B,SAAgB,mBAAmB;IACjC,OAAO,gBAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AAC/C,CAAC","sourcesContent":["import crypto from 'crypto'\n\nexport function makeInvitationToken() {\n return crypto.randomBytes(16).toString('hex')\n}\n"]}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.saveInvitationToken = saveInvitationToken;
|
|
4
|
-
const shell_1 = require("@things-factory/shell");
|
|
5
|
-
const invitation_js_1 = require("../../service/invitation/invitation.js");
|
|
6
|
-
async function saveInvitationToken(id, token) {
|
|
7
|
-
return await (0, shell_1.getRepository)(invitation_js_1.Invitation).save({
|
|
8
|
-
id,
|
|
9
|
-
token
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
//# sourceMappingURL=save-invitation-token.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"save-invitation-token.js","sourceRoot":"","sources":["../../../server/controllers/utils/save-invitation-token.ts"],"names":[],"mappings":";;AAIA,kDAKC;AATD,iDAAqD;AAErD,0EAAmE;AAE5D,KAAK,UAAU,mBAAmB,CAAC,EAAE,EAAE,KAAK;IACjD,OAAO,MAAM,IAAA,qBAAa,EAAC,0BAAU,CAAC,CAAC,IAAI,CAAC;QAC1C,EAAE;QACF,KAAK;KACN,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import { getRepository } from '@things-factory/shell'\n\nimport { Invitation } from '../../service/invitation/invitation.js'\n\nexport async function saveInvitationToken(id, token) {\n return await getRepository(Invitation).save({\n id,\n token\n })\n}\n"]}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { Invitation } from './invitation.js';
|
|
2
|
-
import { InvitationQuery } from './invitation-query.js';
|
|
3
|
-
import { InvitationMutation } from './invitation-mutation.js';
|
|
4
|
-
export declare const entities: (typeof Invitation)[];
|
|
5
|
-
export declare const resolvers: (typeof InvitationQuery | typeof InvitationMutation)[];
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolvers = exports.entities = void 0;
|
|
4
|
-
const invitation_js_1 = require("./invitation.js");
|
|
5
|
-
const invitation_query_js_1 = require("./invitation-query.js");
|
|
6
|
-
const invitation_mutation_js_1 = require("./invitation-mutation.js");
|
|
7
|
-
exports.entities = [invitation_js_1.Invitation];
|
|
8
|
-
exports.resolvers = [invitation_query_js_1.InvitationQuery, invitation_mutation_js_1.InvitationMutation];
|
|
9
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/invitation/index.ts"],"names":[],"mappings":";;;AAAA,mDAA4C;AAC5C,+DAAuD;AACvD,qEAA6D;AAEhD,QAAA,QAAQ,GAAG,CAAC,0BAAU,CAAC,CAAA;AACvB,QAAA,SAAS,GAAG,CAAC,qCAAe,EAAE,2CAAkB,CAAC,CAAA","sourcesContent":["import { Invitation } from './invitation.js'\nimport { InvitationQuery } from './invitation-query.js'\nimport { InvitationMutation } from './invitation-mutation.js'\n\nexport const entities = [Invitation]\nexport const resolvers = [InvitationQuery, InvitationMutation]\n"]}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { Invitation } from './invitation.js';
|
|
2
|
-
export declare class InvitationMutation {
|
|
3
|
-
/**
|
|
4
|
-
* Invite somebody into this domain by email.
|
|
5
|
-
*
|
|
6
|
-
* ⚠ The domain is the caller's own, never an argument. It used to be `reference`, a string the
|
|
7
|
-
* caller passed, which is how a door that reads by `{ email, reference, type }` ends up
|
|
8
|
-
* answering about other tenants. The domain comes from the request now.
|
|
9
|
-
*/
|
|
10
|
-
sendInvitation(email: string, context: ResolverContext, roleIds?: string[]): Promise<Invitation>;
|
|
11
|
-
/** Send the mail again for an invitation that is still open. A new token, a new expiry. */
|
|
12
|
-
resendInvitation(id: string, context: ResolverContext): Promise<boolean>;
|
|
13
|
-
/**
|
|
14
|
-
* Withdraw a pending invitation.
|
|
15
|
-
*
|
|
16
|
-
* The row stays and says it was cancelled. It used to be deleted by `{ email, reference, type }`
|
|
17
|
-
* — which erased the fact that anybody had been invited, and took an id from any tenant.
|
|
18
|
-
*/
|
|
19
|
-
cancelInvitation(id: string, context: ResolverContext): Promise<boolean>;
|
|
20
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.InvitationMutation = void 0;
|
|
4
|
-
const tslib_1 = require("tslib");
|
|
5
|
-
const type_graphql_1 = require("type-graphql");
|
|
6
|
-
const typeorm_1 = require("typeorm");
|
|
7
|
-
const shell_1 = require("@things-factory/shell");
|
|
8
|
-
const invitation_js_1 = require("../../controllers/invitation.js");
|
|
9
|
-
const invitation_state_js_1 = require("../../utils/invitation-state.js");
|
|
10
|
-
const role_js_1 = require("../role/role.js");
|
|
11
|
-
const invitation_js_2 = require("./invitation.js");
|
|
12
|
-
/* Both doors carry what the user screens ask for — inviting somebody is changing who is here. */
|
|
13
|
-
const ADMIN = '@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true)';
|
|
14
|
-
let InvitationMutation = class InvitationMutation {
|
|
15
|
-
/**
|
|
16
|
-
* Invite somebody into this domain by email.
|
|
17
|
-
*
|
|
18
|
-
* ⚠ The domain is the caller's own, never an argument. It used to be `reference`, a string the
|
|
19
|
-
* caller passed, which is how a door that reads by `{ email, reference, type }` ends up
|
|
20
|
-
* answering about other tenants. The domain comes from the request now.
|
|
21
|
-
*/
|
|
22
|
-
async sendInvitation(email, context, roleIds) {
|
|
23
|
-
const { domain, user, tx } = context.state;
|
|
24
|
-
/*
|
|
25
|
-
* Roles are looked up inside this domain and its parent — the same reach a role picker
|
|
26
|
-
* offers. An id from anywhere else simply does not match, so a role cannot be smuggled in.
|
|
27
|
-
*/
|
|
28
|
-
const roles = roleIds?.length
|
|
29
|
-
? await (0, shell_1.getRepository)(role_js_1.Role, tx).find({
|
|
30
|
-
where: {
|
|
31
|
-
id: (0, typeorm_1.In)(roleIds),
|
|
32
|
-
domain: { id: (0, typeorm_1.In)([domain.id, domain.parentId].filter(Boolean)) }
|
|
33
|
-
}
|
|
34
|
-
})
|
|
35
|
-
: [];
|
|
36
|
-
return await (0, invitation_js_1.inviteByEmail)({ email, domain, roles, user, context, tx });
|
|
37
|
-
}
|
|
38
|
-
/** Send the mail again for an invitation that is still open. A new token, a new expiry. */
|
|
39
|
-
async resendInvitation(id, context) {
|
|
40
|
-
const { domain, user, tx } = context.state;
|
|
41
|
-
const invitation = await (0, shell_1.getRepository)(invitation_js_2.Invitation, tx).findOne({
|
|
42
|
-
where: { id, domain: { id: domain.id } },
|
|
43
|
-
relations: ['domain']
|
|
44
|
-
});
|
|
45
|
-
if (!invitation || !(0, invitation_state_js_1.mayAccept)(invitation)) {
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
/*
|
|
49
|
-
* A fresh token. Re-mailing the old one would leave two live links for one invitation, and
|
|
50
|
-
* the older mail is the one more likely to have been forwarded.
|
|
51
|
-
*/
|
|
52
|
-
const reissued = await (0, invitation_js_1.inviteByEmail)({
|
|
53
|
-
email: invitation.email,
|
|
54
|
-
domain: invitation.domain,
|
|
55
|
-
roles: invitation.roles,
|
|
56
|
-
user,
|
|
57
|
-
context,
|
|
58
|
-
tx
|
|
59
|
-
});
|
|
60
|
-
return !!reissued;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Withdraw a pending invitation.
|
|
64
|
-
*
|
|
65
|
-
* The row stays and says it was cancelled. It used to be deleted by `{ email, reference, type }`
|
|
66
|
-
* — which erased the fact that anybody had been invited, and took an id from any tenant.
|
|
67
|
-
*/
|
|
68
|
-
async cancelInvitation(id, context) {
|
|
69
|
-
const { domain, user, tx } = context.state;
|
|
70
|
-
return await (0, invitation_js_1.cancelInvitation)(id, domain, user, tx);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
exports.InvitationMutation = InvitationMutation;
|
|
74
|
-
tslib_1.__decorate([
|
|
75
|
-
(0, type_graphql_1.Directive)(ADMIN),
|
|
76
|
-
(0, type_graphql_1.Directive)('@transaction'),
|
|
77
|
-
(0, type_graphql_1.Mutation)(returns => invitation_js_2.Invitation, { description: "To invite someone into this domain by email" }),
|
|
78
|
-
tslib_1.__param(0, (0, type_graphql_1.Arg)('email')),
|
|
79
|
-
tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
|
|
80
|
-
tslib_1.__param(2, (0, type_graphql_1.Arg)('roleIds', type => [String], { nullable: true })),
|
|
81
|
-
tslib_1.__metadata("design:type", Function),
|
|
82
|
-
tslib_1.__metadata("design:paramtypes", [String, Object, Array]),
|
|
83
|
-
tslib_1.__metadata("design:returntype", Promise)
|
|
84
|
-
], InvitationMutation.prototype, "sendInvitation", null);
|
|
85
|
-
tslib_1.__decorate([
|
|
86
|
-
(0, type_graphql_1.Directive)(ADMIN),
|
|
87
|
-
(0, type_graphql_1.Directive)('@transaction'),
|
|
88
|
-
(0, type_graphql_1.Mutation)(returns => Boolean, { description: 'To send a pending invitation again' }),
|
|
89
|
-
tslib_1.__param(0, (0, type_graphql_1.Arg)('id')),
|
|
90
|
-
tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
|
|
91
|
-
tslib_1.__metadata("design:type", Function),
|
|
92
|
-
tslib_1.__metadata("design:paramtypes", [String, Object]),
|
|
93
|
-
tslib_1.__metadata("design:returntype", Promise)
|
|
94
|
-
], InvitationMutation.prototype, "resendInvitation", null);
|
|
95
|
-
tslib_1.__decorate([
|
|
96
|
-
(0, type_graphql_1.Directive)(ADMIN),
|
|
97
|
-
(0, type_graphql_1.Directive)('@transaction'),
|
|
98
|
-
(0, type_graphql_1.Mutation)(returns => Boolean, { description: 'To withdraw a pending invitation' }),
|
|
99
|
-
tslib_1.__param(0, (0, type_graphql_1.Arg)('id')),
|
|
100
|
-
tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
|
|
101
|
-
tslib_1.__metadata("design:type", Function),
|
|
102
|
-
tslib_1.__metadata("design:paramtypes", [String, Object]),
|
|
103
|
-
tslib_1.__metadata("design:returntype", Promise)
|
|
104
|
-
], InvitationMutation.prototype, "cancelInvitation", null);
|
|
105
|
-
exports.InvitationMutation = InvitationMutation = tslib_1.__decorate([
|
|
106
|
-
(0, type_graphql_1.Resolver)(invitation_js_2.Invitation),
|
|
107
|
-
(0, type_graphql_1.UseMiddleware)(shell_1.SpeakRefusals)
|
|
108
|
-
], InvitationMutation);
|
|
109
|
-
//# sourceMappingURL=invitation-mutation.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"invitation-mutation.js","sourceRoot":"","sources":["../../../server/service/invitation/invitation-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Mutation, Resolver, UseMiddleware } from 'type-graphql'\nimport { In } from 'typeorm'\n\nimport { getRepository, SpeakRefusals } from '@things-factory/shell'\n\nimport { cancelInvitation, inviteByEmail, mailInvitation } from '../../controllers/invitation.js'\nimport { InvitationStatus, mayAccept } from '../../utils/invitation-state.js'\nimport { Role } from '../role/role.js'\nimport { Invitation } from './invitation.js'\n\n/* Both doors carry what the user screens ask for — inviting somebody is changing who is here. */\nconst ADMIN = '@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true)'\n\n@Resolver(Invitation)\n@UseMiddleware(SpeakRefusals)\nexport class InvitationMutation {\n /**\n * Invite somebody into this domain by email.\n *\n * ⚠ The domain is the caller's own, never an argument. It used to be `reference`, a string the\n * caller passed, which is how a door that reads by `{ email, reference, type }` ends up\n * answering about other tenants. The domain comes from the request now.\n */\n @Directive(ADMIN)\n @Directive('@transaction')\n @Mutation(returns => Invitation, { description: \"To invite someone into this domain by email\" })\n async sendInvitation(\n @Arg('email') email: string,\n @Ctx() context: ResolverContext,\n @Arg('roleIds', type => [String], { nullable: true }) roleIds?: string[]\n ): Promise<Invitation> {\n const { domain, user, tx } = context.state\n\n /*\n * Roles are looked up inside this domain and its parent — the same reach a role picker\n * offers. An id from anywhere else simply does not match, so a role cannot be smuggled in.\n */\n const roles = roleIds?.length\n ? await getRepository(Role, tx).find({\n where: {\n id: In(roleIds),\n domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }\n }\n })\n : []\n\n return await inviteByEmail({ email, domain, roles, user, context, tx })\n }\n\n /** Send the mail again for an invitation that is still open. A new token, a new expiry. */\n @Directive(ADMIN)\n @Directive('@transaction')\n @Mutation(returns => Boolean, { description: 'To send a pending invitation again' })\n async resendInvitation(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {\n const { domain, user, tx } = context.state\n\n const invitation = await getRepository(Invitation, tx).findOne({\n where: { id, domain: { id: domain.id } },\n relations: ['domain']\n })\n\n if (!invitation || !mayAccept(invitation)) {\n return false\n }\n\n /*\n * A fresh token. Re-mailing the old one would leave two live links for one invitation, and\n * the older mail is the one more likely to have been forwarded.\n */\n const reissued = await inviteByEmail({\n email: invitation.email,\n domain: invitation.domain,\n roles: invitation.roles,\n user,\n context,\n tx\n })\n\n return !!reissued\n }\n\n /**\n * Withdraw a pending invitation.\n *\n * The row stays and says it was cancelled. It used to be deleted by `{ email, reference, type }`\n * — which erased the fact that anybody had been invited, and took an id from any tenant.\n */\n @Directive(ADMIN)\n @Directive('@transaction')\n @Mutation(returns => Boolean, { description: 'To withdraw a pending invitation' })\n async cancelInvitation(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {\n const { domain, user, tx } = context.state\n\n return await cancelInvitation(id, domain, user, tx)\n }\n}\n"]}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { ListParam } from '@things-factory/shell';
|
|
2
|
-
import { Invitation } from './invitation.js';
|
|
3
|
-
import { InvitationList } from './invitation-types.js';
|
|
4
|
-
export declare class InvitationQuery {
|
|
5
|
-
invitation(id: string, context: ResolverContext): Promise<Invitation>;
|
|
6
|
-
/**
|
|
7
|
-
* Who was invited to this domain, by whom, when, and whether they came in.
|
|
8
|
-
*
|
|
9
|
-
* ⚠ This is why the rows are kept. Accepting used to delete the row, so the moment somebody
|
|
10
|
-
* joined, the fact that they had been invited stopped existing — there was nothing to put on a
|
|
11
|
-
* screen, so no screen was built, so nothing called any of this.
|
|
12
|
-
*/
|
|
13
|
-
invitations(params: ListParam, context: ResolverContext): Promise<InvitationList>;
|
|
14
|
-
/**
|
|
15
|
-
* What state this invitation is in, read against the clock.
|
|
16
|
-
*
|
|
17
|
-
* ⚠ Not the stored column on its own. A pending row whose expiry has passed **is** expired
|
|
18
|
-
* whether or not anything has been round to write that down — otherwise the list would show a
|
|
19
|
-
* live invitation for a token that no longer opens anything, which is the more dangerous of
|
|
20
|
-
* the two ways to be wrong here.
|
|
21
|
-
*/
|
|
22
|
-
state(invitation: Invitation): Promise<string>;
|
|
23
|
-
}
|