@contember/engine-tenant-api 1.3.0-rc.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/src/model/authorization/AclSchemaAccessNodeFactory.d.ts.map +1 -1
  2. package/dist/src/model/authorization/AclSchemaAccessNodeFactory.js +4 -0
  3. package/dist/src/model/authorization/AclSchemaAccessNodeFactory.js.map +1 -1
  4. package/dist/src/model/mailing/UserMailer.d.ts +3 -0
  5. package/dist/src/model/mailing/UserMailer.d.ts.map +1 -1
  6. package/dist/src/model/mailing/UserMailer.js.map +1 -1
  7. package/dist/src/model/queries/membership/ProjectMembersQuery.d.ts.map +1 -1
  8. package/dist/src/model/queries/membership/ProjectMembersQuery.js +14 -10
  9. package/dist/src/model/queries/membership/ProjectMembersQuery.js.map +1 -1
  10. package/dist/src/model/service/InviteManager.d.ts.map +1 -1
  11. package/dist/src/model/service/InviteManager.js +6 -2
  12. package/dist/src/model/service/InviteManager.js.map +1 -1
  13. package/dist/src/model/service/PasswordResetManager.d.ts.map +1 -1
  14. package/dist/src/model/service/PasswordResetManager.js +1 -0
  15. package/dist/src/model/service/PasswordResetManager.js.map +1 -1
  16. package/dist/src/model/service/SignUpManager.d.ts.map +1 -1
  17. package/dist/src/model/service/SignUpManager.js +4 -0
  18. package/dist/src/model/service/SignUpManager.js.map +1 -1
  19. package/dist/src/model/service/idp/providers/AppleProvider.d.ts +2 -0
  20. package/dist/src/model/service/idp/providers/AppleProvider.d.ts.map +1 -1
  21. package/dist/src/model/service/idp/providers/AppleProvider.js +3 -1
  22. package/dist/src/model/service/idp/providers/AppleProvider.js.map +1 -1
  23. package/dist/src/model/service/idp/providers/FacebookProvider.d.ts +2 -0
  24. package/dist/src/model/service/idp/providers/FacebookProvider.d.ts.map +1 -1
  25. package/dist/src/model/service/idp/providers/FacebookProvider.js +3 -0
  26. package/dist/src/model/service/idp/providers/FacebookProvider.js.map +1 -1
  27. package/dist/src/model/service/idp/providers/OIDCProvider.d.ts +1 -0
  28. package/dist/src/model/service/idp/providers/OIDCProvider.d.ts.map +1 -1
  29. package/dist/src/model/service/idp/providers/OIDCProvider.js +5 -3
  30. package/dist/src/model/service/idp/providers/OIDCProvider.js.map +1 -1
  31. package/dist/src/model/service/idp/providers/OIDCTypes.d.ts +4 -0
  32. package/dist/src/model/service/idp/providers/OIDCTypes.d.ts.map +1 -1
  33. package/dist/src/model/service/idp/providers/OIDCTypes.js +1 -0
  34. package/dist/src/model/service/idp/providers/OIDCTypes.js.map +1 -1
  35. package/dist/src/model/utils/email.d.ts +1 -0
  36. package/dist/src/model/utils/email.d.ts.map +1 -1
  37. package/dist/src/model/utils/email.js +7 -1
  38. package/dist/src/model/utils/email.js.map +1 -1
  39. package/dist/src/resolvers/types/ProjectTypeResolver.d.ts.map +1 -1
  40. package/dist/src/resolvers/types/ProjectTypeResolver.js +5 -2
  41. package/dist/src/resolvers/types/ProjectTypeResolver.js.map +1 -1
  42. package/dist/src/schema/index.d.ts +8 -6
  43. package/dist/src/schema/index.d.ts.map +1 -1
  44. package/dist/src/schema/tenant.graphql.d.ts.map +1 -1
  45. package/dist/src/schema/tenant.graphql.js +5 -1
  46. package/dist/src/schema/tenant.graphql.js.map +1 -1
  47. package/dist/src/tsconfig.tsbuildinfo +1 -1
  48. package/dist/tests/cases/integration/mocked/listProjectMembersQuery.test.d.ts +2 -0
  49. package/dist/tests/cases/integration/mocked/listProjectMembersQuery.test.d.ts.map +1 -0
  50. package/dist/tests/cases/integration/mocked/listProjectMembersQuery.test.js +82 -0
  51. package/dist/tests/cases/integration/mocked/listProjectMembersQuery.test.js.map +1 -0
  52. package/dist/tests/cases/unit/authorization/membershipAwareAccessNode.test.js +19 -1
  53. package/dist/tests/cases/unit/authorization/membershipAwareAccessNode.test.js.map +1 -1
  54. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  55. package/package.json +12 -12
  56. package/src/model/authorization/AclSchemaAccessNodeFactory.ts +6 -0
  57. package/src/model/mailing/UserMailer.ts +4 -4
  58. package/src/model/queries/membership/ProjectMembersQuery.ts +17 -9
  59. package/src/model/service/InviteManager.ts +10 -5
  60. package/src/model/service/PasswordResetManager.ts +2 -1
  61. package/src/model/service/SignUpManager.ts +4 -0
  62. package/src/model/service/idp/providers/AppleProvider.ts +11 -5
  63. package/src/model/service/idp/providers/FacebookProvider.ts +11 -5
  64. package/src/model/service/idp/providers/OIDCProvider.ts +11 -5
  65. package/src/model/service/idp/providers/OIDCTypes.ts +1 -0
  66. package/src/model/utils/email.ts +6 -2
  67. package/src/resolvers/types/ProjectTypeResolver.ts +3 -0
  68. package/src/schema/index.ts +9 -5
  69. package/src/schema/tenant.graphql.ts +5 -1
  70. package/tests/cases/integration/mocked/listProjectMembersQuery.test.ts +83 -0
  71. package/tests/cases/unit/authorization/membershipAwareAccessNode.test.ts +25 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contember/engine-tenant-api",
3
- "version": "1.3.0-rc.2",
3
+ "version": "1.3.0",
4
4
  "license": "Apache-2.0",
5
5
  "main": "dist/src/index.js",
6
6
  "typings": "dist/src/index.d.ts",
@@ -13,16 +13,16 @@
13
13
  "test": "vitest --dir ./tests/cases --no-threads"
14
14
  },
15
15
  "dependencies": {
16
- "@contember/authorization": "1.3.0-rc.2",
17
- "@contember/config-loader": "1.3.0-rc.2",
18
- "@contember/database": "1.3.0-rc.2",
19
- "@contember/database-migrations": "1.3.0-rc.2",
20
- "@contember/dic": "1.3.0-rc.2",
21
- "@contember/graphql-utils": "1.3.0-rc.2",
22
- "@contember/logger": "1.3.0-rc.2",
23
- "@contember/schema": "1.3.0-rc.2",
24
- "@contember/schema-utils": "1.3.0-rc.2",
25
- "@contember/typesafe": "1.3.0-rc.2",
16
+ "@contember/authorization": "1.3.0",
17
+ "@contember/config-loader": "1.3.0",
18
+ "@contember/database": "1.3.0",
19
+ "@contember/database-migrations": "1.3.0",
20
+ "@contember/dic": "1.3.0",
21
+ "@contember/graphql-utils": "1.3.0",
22
+ "@contember/logger": "1.3.0",
23
+ "@contember/schema": "1.3.0",
24
+ "@contember/schema-utils": "1.3.0",
25
+ "@contember/typesafe": "1.3.0",
26
26
  "@graphql-tools/utils": "^8.6.13",
27
27
  "graphql-tag": "^2.12.5",
28
28
  "jose": "^4.10",
@@ -32,7 +32,7 @@
32
32
  "otpauth": "^7.0.6"
33
33
  },
34
34
  "devDependencies": {
35
- "@contember/database-tester": "1.3.0-rc.2",
35
+ "@contember/database-tester": "1.3.0",
36
36
  "@graphql-codegen/cli": "^2.6.2",
37
37
  "@graphql-codegen/typescript": "^2.5.1",
38
38
  "@graphql-codegen/typescript-operations": "^2.4.2",
@@ -38,6 +38,12 @@ export class AclSchemaAccessNodeFactory {
38
38
  node.allow(PermissionActions.PROJECT_UPDATE_MEMBER([]), verifier)
39
39
  node.allow(PermissionActions.PROJECT_REMOVE_MEMBER([]), verifier)
40
40
  }
41
+
42
+ if (membershipRoles.some(it => it.tenant?.view)) {
43
+ const verifier = createVerifier(it => it.view ?? {})
44
+ node.allow(PermissionActions.PROJECT_VIEW_MEMBER([]), verifier)
45
+ }
46
+
41
47
  return node
42
48
  }
43
49
  }
@@ -11,11 +11,11 @@ export class UserMailer {
11
11
  constructor(
12
12
  private readonly mailer: Mailer,
13
13
  private readonly templateRenderer: TemplateRenderer,
14
- ) {}
14
+ ) { }
15
15
 
16
16
  async sendNewUserInvitedMail(
17
17
  dbContext: DatabaseContext,
18
- mailArguments: { email: string; password: string | null; token: string | null; project: string },
18
+ mailArguments: { email: string; password: string | null; token: string | null; project: string; projectSlug: string },
19
19
  customMailOptions: { projectId: string; variant: string },
20
20
  ): Promise<void> {
21
21
  const template = (await this.getCustomTemplate(dbContext, { type: MailType.newUserInvited, ...customMailOptions })) || {
@@ -27,7 +27,7 @@ export class UserMailer {
27
27
 
28
28
  async sendExistingUserInvitedEmail(
29
29
  dbContext: DatabaseContext,
30
- mailArguments: { email: string; project: string },
30
+ mailArguments: { email: string; project: string; projectSlug: string },
31
31
  customMailOptions: { projectId: string; variant: string },
32
32
  ): Promise<void> {
33
33
  const template = (await this.getCustomTemplate(dbContext, { type: MailType.existingUserInvited, ...customMailOptions })) || {
@@ -39,7 +39,7 @@ export class UserMailer {
39
39
 
40
40
  async sendPasswordResetEmail(
41
41
  dbContext: DatabaseContext,
42
- mailArguments: { email: string; token: string; project?: string },
42
+ mailArguments: { email: string; token: string; project?: string; projectSlug?: string },
43
43
  customMailOptions: { projectId?: string; variant: string },
44
44
  ): Promise<void> {
45
45
  const template = (await this.getCustomTemplate(dbContext, { type: MailType.passwordReset, ...customMailOptions })) || {
@@ -23,22 +23,30 @@ export class ProjectMembersQuery extends DatabaseQuery<ProjectMembersQueryResult
23
23
  .where({ project_id: this.projectId }),
24
24
  ))
25
25
  .match(qb => {
26
- const { email, memberType } = this.projectMemberInput.filter ?? {}
27
- if (email && memberType === 'API_KEY') {
26
+ const identityId = this.projectMemberInput.filter?.identityId
27
+ return identityId ? qb.where(it => it.in(['identity', 'id'], identityId)) : qb
28
+ })
29
+ .match(qb => {
30
+ const { email, memberType, personId } = this.projectMemberInput.filter ?? {}
31
+ if ((email || personId) && memberType === 'API_KEY') {
28
32
  throw new ImplementationException()
29
33
  }
34
+
30
35
  const personQuery = SelectBuilder.create()
31
36
  .from('person')
32
37
  .where(expr => expr.columnsEq(['person', 'identity_id'], ['identity', 'id']))
33
- if (email) {
34
- return qb.where(it => it.exists(personQuery.where({ email })))
38
+ .match(qb => email ? qb.where(it => it.in('email', email)) : qb)
39
+ .match(qb => personId ? qb.where(it => it.in(['person', 'id'], personId)) : qb)
40
+
41
+
42
+ if (email || personId || memberType === 'PERSON') {
43
+ return qb.where(it => it.exists(personQuery))
35
44
  }
36
- switch (memberType) {
37
- case 'API_KEY':
38
- return qb.where(it => it.not(it => it.exists(personQuery)))
39
- case 'PERSON':
40
- return qb.where(it => it.exists(personQuery))
45
+
46
+ if (memberType === 'API_KEY') {
47
+ return qb.where(it => it.not(it => it.exists(personQuery)))
41
48
  }
49
+
42
50
  return qb
43
51
  })
44
52
  .limit(this.projectMemberInput.limit ?? undefined, this.projectMemberInput.offset ?? undefined)
@@ -12,10 +12,11 @@ import { InviteErrorCode, InviteMethod } from '../../schema'
12
12
  import { TenantRole } from '../authorization'
13
13
  import { UserMailer } from '../mailing'
14
14
  import { createAppendMembershipVariables } from './membershipUtils'
15
- import { Response, ResponseOk } from '../utils/Response'
15
+ import { Response, ResponseError, ResponseOk } from '../utils/Response'
16
16
  import { DatabaseContext, TokenHash } from '../utils'
17
17
  import { NoPassword, PasswordPlain } from '../dtos'
18
18
  import { Acl } from '@contember/schema'
19
+ import { validateEmail } from '../utils/email'
19
20
 
20
21
  export interface InviteData {
21
22
  email: string
@@ -36,7 +37,7 @@ export class InviteManager {
36
37
  private readonly providers: Providers,
37
38
  private readonly mailer: UserMailer,
38
39
  private readonly projectSchemaResolver: ProjectSchemaResolver,
39
- ) {}
40
+ ) { }
40
41
 
41
42
  async invite(
42
43
  dbContext: DatabaseContext,
@@ -52,6 +53,10 @@ export class InviteManager {
52
53
  passwordResetTokenHash,
53
54
  }: InviteData,
54
55
  ): Promise<InviteResponse> {
56
+ if (!validateEmail(email.trim())) {
57
+ return new ResponseError('INVALID_EMAIL_FORMAT', 'E-mail address is not in a valid format')
58
+ }
59
+
55
60
  return await dbContext.transaction(async trx => {
56
61
  let person: Omit<PersonRow, 'roles'> | null = await trx.queryHandler.fetch(PersonQuery.byEmail(email))
57
62
  const isNew = !person
@@ -89,11 +94,11 @@ export class InviteManager {
89
94
  if (isNew) {
90
95
  await this.mailer.sendNewUserInvitedMail(
91
96
  trx,
92
- { email, project: project.name, password: generatedPassword, token: resetToken },
97
+ { email, project: project.name, projectSlug: project.slug, password: generatedPassword, token: resetToken },
93
98
  customMailOptions,
94
99
  )
95
100
  } else {
96
- await this.mailer.sendExistingUserInvitedEmail(trx, { email, project: project.name }, customMailOptions)
101
+ await this.mailer.sendExistingUserInvitedEmail(trx, { email, project: project.name, projectSlug: project.slug }, customMailOptions)
97
102
  }
98
103
  }
99
104
  return new ResponseOk(new InviteResult(person, isNew))
@@ -110,5 +115,5 @@ export class InviteManager {
110
115
  export type InviteResponse = Response<InviteResult, InviteErrorCode>
111
116
 
112
117
  export class InviteResult {
113
- constructor(public readonly person: Omit<PersonRow, 'roles'>, public readonly isNew: boolean) {}
118
+ constructor(public readonly person: Omit<PersonRow, 'roles'>, public readonly isNew: boolean) { }
114
119
  }
@@ -17,7 +17,7 @@ export class PasswordResetManager {
17
17
  constructor(
18
18
  private readonly mailer: UserMailer,
19
19
  private readonly projectManager: ProjectManager,
20
- ) {}
20
+ ) { }
21
21
 
22
22
  public async createPasswordResetRequest(
23
23
  dbContext: DatabaseContext,
@@ -45,6 +45,7 @@ export class PasswordResetManager {
45
45
  email: person.email,
46
46
  token: result.token,
47
47
  project: project?.name,
48
+ projectSlug: project?.slug,
48
49
  },
49
50
  {
50
51
  variant: mailOptions.mailVariant || '',
@@ -6,6 +6,7 @@ import { getPasswordWeaknessMessage } from '../utils/password'
6
6
  import { Response, ResponseError, ResponseOk } from '../utils/Response'
7
7
  import { DatabaseContext } from '../utils'
8
8
  import { MaybePassword } from '../dtos'
9
+ import { validateEmail } from '../utils/email'
9
10
 
10
11
  type SignUpUser = {
11
12
  email: string
@@ -16,6 +17,9 @@ type SignUpUser = {
16
17
 
17
18
  export class SignUpManager {
18
19
  async signUp(dbContext: DatabaseContext, { email, password, roles = [] }: SignUpUser): Promise<SignUpResponse> {
20
+ if (!validateEmail(email.trim())) {
21
+ return new ResponseError('INVALID_EMAIL_FORMAT', 'E-mail address is not in a valid format')
22
+ }
19
23
  if (await this.isEmailAlreadyUsed(dbContext, email)) {
20
24
  return new ResponseError('EMAIL_ALREADY_EXISTS', `User with email ${email} already exists`)
21
25
  }
@@ -56,10 +56,16 @@ export class AppleProvider implements IdentityProviderHandler<AppleConfiguration
56
56
  .setExpirationTime('10m')
57
57
  .sign(privateKey)
58
58
 
59
- return new this.issuer.Client({
60
- client_id: configuration.clientId,
61
- client_secret: jwt,
62
- response_types: [configuration.responseType || 'code'],
63
- })
59
+ return new this.issuer.Client(
60
+ {
61
+ client_id: configuration.clientId,
62
+ client_secret: jwt,
63
+ response_types: [configuration.responseType || 'code'],
64
+ },
65
+ undefined,
66
+ {
67
+ additionalAuthorizedParties: [...(configuration.additionalAuthorizedParties ?? [])],
68
+ },
69
+ )
64
70
  }
65
71
  }
@@ -116,10 +116,16 @@ export class FacebookProvider implements IdentityProviderHandler<FacebookConfigu
116
116
  }
117
117
 
118
118
  private async createOIDCClient(configuration: FacebookConfiguration): Promise<Client> {
119
- return new this.facebookIssuer.Client({
120
- client_id: configuration.clientId,
121
- client_secret: configuration.clientSecret,
122
- response_types: [configuration.responseType || 'code'],
123
- })
119
+ return new this.facebookIssuer.Client(
120
+ {
121
+ client_id: configuration.clientId,
122
+ client_secret: configuration.clientSecret,
123
+ response_types: [configuration.responseType || 'code'],
124
+ },
125
+ undefined,
126
+ {
127
+ additionalAuthorizedParties: [...(configuration.additionalAuthorizedParties ?? [])],
128
+ },
129
+ )
124
130
  }
125
131
  }
@@ -31,10 +31,16 @@ export class OIDCProvider implements IdentityProviderHandler<OIDCConfiguration>
31
31
  private async createOIDCClient(configuration: OIDCConfiguration): Promise<Client> {
32
32
  this.issuerCache[configuration.url] ??= await Issuer.discover(configuration.url)
33
33
 
34
- return new this.issuerCache[configuration.url].Client({
35
- client_id: configuration.clientId,
36
- client_secret: configuration.clientSecret,
37
- response_types: [configuration.responseType || 'code'],
38
- })
34
+ return new this.issuerCache[configuration.url].Client(
35
+ {
36
+ client_id: configuration.clientId,
37
+ client_secret: configuration.clientSecret,
38
+ response_types: [configuration.responseType || 'code'],
39
+ },
40
+ undefined,
41
+ {
42
+ additionalAuthorizedParties: [...(configuration.additionalAuthorizedParties ?? [])],
43
+ },
44
+ )
39
45
  }
40
46
  }
@@ -9,6 +9,7 @@ export interface OIDCSessionData {
9
9
  export const OIDCConfigurationOptions = Typesafe.partial({
10
10
  responseType: Typesafe.enumeration<ResponseType>('code', 'code id_token', 'code id_token token', 'code token', 'id_token', 'id_token token', 'none'),
11
11
  claims: Typesafe.string,
12
+ additionalAuthorizedParties: Typesafe.array(Typesafe.string),
12
13
  })
13
14
  export const BaseOIDCConfiguration = Typesafe.intersection(
14
15
  Typesafe.object({
@@ -1,5 +1,9 @@
1
- import { Literal } from '@contember/database'
2
-
3
1
  export const normalizeEmail = (email: string) => {
4
2
  return email.replaceAll(/\s/g, '').toLocaleLowerCase()
5
3
  }
4
+
5
+ export const validateEmail = (email: string): boolean => {
6
+ return email.length >= 3
7
+ && email.includes('@')
8
+ && !!email.match(/^[^\s@]+@[\w_-]+(\.[\w_-]+)*$/)
9
+ }
@@ -24,6 +24,9 @@ export class ProjectTypeResolver implements ProjectResolvers {
24
24
  if (args.input?.filter?.email && args.input.filter.memberType === 'API_KEY') {
25
25
  throw new UserInputError(`Cannot use email filter for ApiKey member type.`)
26
26
  }
27
+ if (args.input?.filter?.personId && args.input.filter.memberType === 'API_KEY') {
28
+ throw new UserInputError(`Cannot use personId filter for ApiKey member type.`)
29
+ }
27
30
  if (args.memberType && args.input) {
28
31
  throw new UserInputError(`Cannot use both deprecated memberType parameter and new input parameter`)
29
32
  }
@@ -275,9 +275,9 @@ export type CreateSessionTokenError = {
275
275
  }
276
276
 
277
277
  export type CreateSessionTokenErrorCode =
278
+ | 'PERSON_DISABLED'
278
279
  | 'UNKNOWN_EMAIL'
279
280
  | 'UNKNOWN_PERSON_ID'
280
- | 'PERSON_DISABLED'
281
281
 
282
282
  export type CreateSessionTokenResponse = {
283
283
  readonly __typename?: 'CreateSessionTokenResponse'
@@ -352,7 +352,7 @@ export type DisablePersonError = {
352
352
  }
353
353
 
354
354
  export type DisablePersonErrorCode =
355
- 'PERSON_ALREADY_DISABLED'
355
+ | 'PERSON_ALREADY_DISABLED'
356
356
  | 'PERSON_NOT_FOUND'
357
357
 
358
358
  export type DisablePersonResponse = {
@@ -461,6 +461,7 @@ export type InviteError = {
461
461
 
462
462
  export type InviteErrorCode =
463
463
  | 'ALREADY_MEMBER'
464
+ | 'INVALID_EMAIL_FORMAT'
464
465
  | 'INVALID_MEMBERSHIP'
465
466
  | 'PROJECT_NOT_FOUND'
466
467
  | 'ROLE_NOT_FOUND'
@@ -852,8 +853,10 @@ export type ProjectIdentityRelation = {
852
853
  }
853
854
 
854
855
  export type ProjectMembersFilter = {
855
- readonly email?: InputMaybe<Scalars['String']>
856
+ readonly email?: InputMaybe<ReadonlyArray<Scalars['String']>>
857
+ readonly identityId?: InputMaybe<ReadonlyArray<Scalars['String']>>
856
858
  readonly memberType?: InputMaybe<MemberType>
859
+ readonly personId?: InputMaybe<ReadonlyArray<Scalars['String']>>
857
860
  }
858
861
 
859
862
  export type ProjectMembersInput = {
@@ -1031,8 +1034,8 @@ export type SignInErrorCode =
1031
1034
  | 'INVALID_PASSWORD'
1032
1035
  | 'NO_PASSWORD_SET'
1033
1036
  | 'OTP_REQUIRED'
1034
- | 'UNKNOWN_EMAIL'
1035
1037
  | 'PERSON_DISABLED'
1038
+ | 'UNKNOWN_EMAIL'
1036
1039
 
1037
1040
  export type SignInIdpError = {
1038
1041
  readonly __typename?: 'SignInIDPError'
@@ -1046,8 +1049,8 @@ export type SignInIdpErrorCode =
1046
1049
  | 'IDP_VALIDATION_FAILED'
1047
1050
  | 'INVALID_IDP_RESPONSE'
1048
1051
  | 'PERSON_ALREADY_EXISTS'
1049
- | 'PERSON_NOT_FOUND'
1050
1052
  | 'PERSON_DISABLED'
1053
+ | 'PERSON_NOT_FOUND'
1051
1054
 
1052
1055
  export type SignInIdpResponse = {
1053
1056
  readonly __typename?: 'SignInIDPResponse'
@@ -1109,6 +1112,7 @@ export type SignUpError = {
1109
1112
 
1110
1113
  export type SignUpErrorCode =
1111
1114
  | 'EMAIL_ALREADY_EXISTS'
1115
+ | 'INVALID_EMAIL_FORMAT'
1112
1116
  | 'TOO_WEAK'
1113
1117
 
1114
1118
  export type SignUpResponse = {
@@ -116,6 +116,7 @@ const schema: DocumentNode = gql`
116
116
 
117
117
  enum SignUpErrorCode {
118
118
  EMAIL_ALREADY_EXISTS
119
+ INVALID_EMAIL_FORMAT
119
120
  TOO_WEAK
120
121
  }
121
122
 
@@ -393,6 +394,7 @@ const schema: DocumentNode = gql`
393
394
  PROJECT_NOT_FOUND
394
395
  ALREADY_MEMBER
395
396
  INVALID_MEMBERSHIP
397
+ INVALID_EMAIL_FORMAT
396
398
 
397
399
  ROLE_NOT_FOUND @deprecated
398
400
  VARIABLE_NOT_FOUND @deprecated
@@ -684,7 +686,9 @@ const schema: DocumentNode = gql`
684
686
 
685
687
  input ProjectMembersFilter {
686
688
  memberType: MemberType
687
- email: String
689
+ email: [String!]
690
+ identityId: [String!]
691
+ personId: [String!]
688
692
  }
689
693
 
690
694
  enum MemberType {
@@ -0,0 +1,83 @@
1
+ import { executeTenantTest } from '../../../src/testTenant'
2
+ import { GQL } from '../../../src/tags'
3
+ import { testUuid } from '../../../src/testUuid'
4
+ import { test } from 'vitest'
5
+ import { sqlTransaction } from './sql/sqlTransaction'
6
+
7
+ test('list project members by email query', async () => {
8
+ await executeTenantTest({
9
+ query: {
10
+ query: GQL`
11
+ query {
12
+ projectBySlug(slug: "sandbox") {
13
+ members(
14
+ input: { filter: { email: ["foo@localhost", "bar@localhost"], memberType: PERSON } }
15
+ ) {
16
+ identity {
17
+ id
18
+ }
19
+ memberships {
20
+ role
21
+ variables {
22
+ name
23
+ values
24
+ }
25
+ }
26
+ }
27
+ }
28
+ }`,
29
+ variables: {},
30
+ },
31
+ executes: [
32
+ {
33
+ sql: `SELECT "id", "name", "slug", "config", "updated_at" AS "updatedAt"
34
+ FROM "tenant"."project"
35
+ WHERE "slug" = ?`,
36
+ parameters: ['sandbox'],
37
+ response: { rows: [{ id: testUuid(1), name: 'sandbox', slug: 'sandbox', config: {}, updatedAt: new Date() }] },
38
+ },
39
+ ...sqlTransaction(
40
+ {
41
+ sql: `select "id", "description" from "tenant"."identity"
42
+ where exists (select ?::int from "tenant"."project_membership"
43
+ where "project_membership"."identity_id" = "identity"."id" and "project_id" = ?)
44
+ and exists (select ?::int from "tenant"."person" where "person"."identity_id" = "identity"."id" and "email" in (?, ?))`,
45
+ parameters: [1, testUuid(1), 1, 'foo@localhost', 'bar@localhost'],
46
+ response: {
47
+ rows: [
48
+ { id: testUuid(2), description: 'foobar' },
49
+ ],
50
+ },
51
+ },
52
+ {
53
+ sql: `with
54
+ "memberships" as (select "project_membership"."id", "project_membership"."role", "project_membership"."identity_id" from "tenant"."project_membership" where "identity_id" in (?) and "project_id" = ?),
55
+ "variables" as (select "membership_id", json_agg(json_build_object('name', variable, 'values', value)) as "variables" from "tenant"."project_membership_variable" inner join "memberships" on "project_membership_variable"."membership_id" = "memberships"."id" group by "membership_id")
56
+ select "role", coalesce(variables, '[]'::json) as "variables", "identity_id" as "identityId" from "memberships" left join "variables" on "memberships"."id" = "variables"."membership_id"`,
57
+ parameters: [testUuid(2), testUuid(1)],
58
+ response: {
59
+ rows: [
60
+ {
61
+ role: 'foo',
62
+ variables: [{ values: ['x', 'y'], name: 'xyz' }],
63
+ identityId: testUuid(2),
64
+ },
65
+ ],
66
+ },
67
+ },
68
+ ),
69
+ ],
70
+ return: {
71
+ data: {
72
+ projectBySlug: {
73
+ members: [
74
+ {
75
+ identity: { id: testUuid(2) },
76
+ memberships: [{ role: 'foo', variables: [{ name: 'xyz', values: ['x', 'y'] }] }],
77
+ },
78
+ ],
79
+ },
80
+ },
81
+ },
82
+ })
83
+ })
@@ -68,7 +68,7 @@ const aclSchemaForInviteOnly: Acl.Schema = {
68
68
  }
69
69
 
70
70
 
71
- test('admin can invite public. cannat manage public, cannot invite lorem', async () => {
71
+ test('admin can invite public. cannot manage public, cannot invite lorem', async () => {
72
72
  const node = new AclSchemaAccessNodeFactory().create(aclSchemaForInviteOnly, [{ role: 'admin', variables: [] }])
73
73
 
74
74
  assert.ok(await node.isAllowed(aclEvaluator, PermissionActions.PERSON_INVITE([{ role: 'public', variables: [] }])))
@@ -77,3 +77,27 @@ test('admin can invite public. cannat manage public, cannot invite lorem', async
77
77
 
78
78
  assert.notOk(await node.isAllowed(aclEvaluator, PermissionActions.PROJECT_REMOVE_MEMBER([{ role: 'public', variables: [] }])))
79
79
  })
80
+
81
+
82
+ const aclSchemaForViewOnly: Acl.Schema = {
83
+ roles: {
84
+ admin: {
85
+ entities: {},
86
+ variables: {},
87
+ tenant: {
88
+ view: {
89
+ lorem: true,
90
+ },
91
+ },
92
+ },
93
+ },
94
+ }
95
+
96
+
97
+ test('admin can view lorem, but cannot manage it', async () => {
98
+ const node = new AclSchemaAccessNodeFactory().create(aclSchemaForViewOnly, [{ role: 'admin', variables: [] }])
99
+
100
+ assert.ok(await node.isAllowed(aclEvaluator, PermissionActions.PROJECT_VIEW_MEMBER([{ role: 'lorem', variables: [] }])))
101
+
102
+ assert.notOk(await node.isAllowed(aclEvaluator, PermissionActions.PROJECT_REMOVE_MEMBER([{ role: 'lorem', variables: [] }])))
103
+ })