@stamhoofd/backend 2.2.0 → 2.4.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 (33) hide show
  1. package/.env.json +2 -2
  2. package/index.ts +3 -0
  3. package/package.json +4 -4
  4. package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +63 -2
  5. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +3 -0
  6. package/src/endpoints/auth/CreateAdminEndpoint.ts +6 -3
  7. package/src/endpoints/auth/GetOtherUserEndpoint.ts +41 -0
  8. package/src/endpoints/auth/GetUserEndpoint.ts +6 -28
  9. package/src/endpoints/auth/PatchUserEndpoint.ts +25 -6
  10. package/src/endpoints/auth/SignupEndpoint.ts +2 -2
  11. package/src/endpoints/global/email/CreateEmailEndpoint.ts +120 -0
  12. package/src/endpoints/global/email/GetEmailEndpoint.ts +51 -0
  13. package/src/endpoints/global/email/PatchEmailEndpoint.ts +108 -0
  14. package/src/endpoints/global/events/GetEventsEndpoint.ts +223 -0
  15. package/src/endpoints/global/events/PatchEventsEndpoint.ts +319 -0
  16. package/src/endpoints/global/members/GetMembersEndpoint.ts +124 -48
  17. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +86 -109
  18. package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +2 -1
  19. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +9 -0
  20. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +3 -2
  21. package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +1 -1
  22. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +43 -25
  23. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +26 -7
  24. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +23 -22
  25. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +136 -123
  26. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +8 -8
  27. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +2 -1
  28. package/src/helpers/AdminPermissionChecker.ts +54 -3
  29. package/src/helpers/AuthenticatedStructures.ts +88 -23
  30. package/src/helpers/Context.ts +4 -0
  31. package/src/helpers/EmailResumer.ts +17 -0
  32. package/src/helpers/MemberUserSyncer.ts +221 -0
  33. package/src/seeds/1722256498-group-update-occupancy.ts +52 -0
@@ -1,6 +1,6 @@
1
1
  import { AutoEncoderPatchType, PatchMap } from "@simonbackx/simple-encoding"
2
2
  import { SimpleError } from "@simonbackx/simple-errors"
3
- import { BalanceItem, Document, DocumentTemplate, EmailTemplate, Group, Member, MemberWithRegistrations, Order, Organization, Payment, Registration, User, Webshop } from "@stamhoofd/models"
3
+ import { BalanceItem, Document, DocumentTemplate, EmailTemplate, Event, Group, Member, MemberWithRegistrations, Order, Organization, Payment, Registration, User, Webshop } from "@stamhoofd/models"
4
4
  import { AccessRight, GroupCategory, GroupStatus, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordCategory } from "@stamhoofd/structures"
5
5
  import { Formatter } from "@stamhoofd/utility"
6
6
 
@@ -11,6 +11,10 @@ import { Formatter } from "@stamhoofd/utility"
11
11
  export class AdminPermissionChecker {
12
12
  organization: Organization|null
13
13
  user: User
14
+ /**
15
+ * The member that is linked to this user = is this user
16
+ */
17
+ member: MemberWithRegistrations|null = null
14
18
  platform: PlatformStruct
15
19
 
16
20
  organizationCache: Map<string, Organization|Promise<Organization|undefined>> = new Map()
@@ -143,6 +147,13 @@ export class AdminPermissionChecker {
143
147
  if (!this.checkScope(group.organizationId)) {
144
148
  return false
145
149
  }
150
+ const organization = await this.getOrganization(group.organizationId)
151
+
152
+ if (group.periodId !== organization.periodId) {
153
+ if (!await this.hasFullAccess(group.organizationId)) {
154
+ return false
155
+ }
156
+ }
146
157
 
147
158
  if (group.deletedAt || group.status === GroupStatus.Archived) {
148
159
  return await this.canAccessArchivedGroups(group.organizationId);
@@ -160,7 +171,6 @@ export class AdminPermissionChecker {
160
171
  }
161
172
 
162
173
  // Check parent categories
163
- const organization = await this.getOrganization(group.organizationId)
164
174
  const parentCategories = group.getParentCategories(organization.meta.categories)
165
175
  for (const category of parentCategories) {
166
176
  if (organizationPermissions.hasResourceAccess(PermissionsResourceType.GroupCategories, category.id, permissionLevel)) {
@@ -171,6 +181,28 @@ export class AdminPermissionChecker {
171
181
  return false;
172
182
  }
173
183
 
184
+ async canAccessEvent(event: Event, permissionLevel: PermissionLevel = PermissionLevel.Read): Promise<boolean> {
185
+ // Check permissions aren't scoped to a specific organization, and they mismatch
186
+ if (!this.checkScope(event.organizationId)) {
187
+ return false
188
+ }
189
+
190
+ if (permissionLevel !== PermissionLevel.Read) {
191
+ if (event.organizationId) {
192
+ // Need full access for now
193
+ if (!await this.hasFullAccess(event.organizationId)) {
194
+ return false
195
+ }
196
+ } else {
197
+ if (!this.hasPlatformFullAccess()) {
198
+ return false
199
+ }
200
+ }
201
+ }
202
+
203
+ return true;
204
+ }
205
+
174
206
  async canAccessArchivedGroups(organizationId: string) {
175
207
  return await this.hasFullAccess(organizationId)
176
208
  }
@@ -221,7 +253,8 @@ export class AdminPermissionChecker {
221
253
  return false;
222
254
  }
223
255
 
224
- if (organizationPermissions.hasAccess(permissionLevel)) {
256
+ if (organizationPermissions.hasAccess(PermissionLevel.Full)) {
257
+ // Only full permissions; because non-full doesn't have access to other periods
225
258
  return true;
226
259
  }
227
260
 
@@ -853,6 +886,24 @@ export class AdminPermissionChecker {
853
886
  return set
854
887
  }
855
888
 
889
+
890
+ async getAccessibleGroups(organizationId: string, level: PermissionLevel = PermissionLevel.Read): Promise<string[] | 'all'> {
891
+ if (await this.hasFullAccess(organizationId)) {
892
+ return 'all'
893
+ }
894
+
895
+ const groups = await this.getOrganizationGroups(organizationId)
896
+ const accessibleGroups: string[] = []
897
+
898
+ for (const group of groups) {
899
+ if (await this.canAccessGroup(group, level)) {
900
+ accessibleGroups.push(group.id)
901
+ }
902
+ }
903
+ return accessibleGroups
904
+ }
905
+
906
+
856
907
  /**
857
908
  * Changes data inline
858
909
  */
@@ -1,8 +1,9 @@
1
1
  import { SimpleError } from "@simonbackx/simple-errors";
2
- import { Group, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, User, Webshop } from "@stamhoofd/models";
3
- import { MemberPlatformMembership as MemberPlatformMembershipStruct, MemberResponsibilityRecord as MemberResponsibilityRecordStruct, MemberWithRegistrationsBlob, MembersBlob, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, PrivateWebshop, User as UserStruct, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
2
+ import { Event, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, User, Webshop } from "@stamhoofd/models";
3
+ import { Event as EventStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberResponsibilityRecord as MemberResponsibilityRecordStruct, MemberWithRegistrationsBlob, MembersBlob, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, PrivateWebshop, User as UserStruct, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
4
4
 
5
5
  import { Context } from "./Context";
6
+ import { Formatter } from "@stamhoofd/utility";
6
7
 
7
8
  /**
8
9
  * Builds authenticated structures for the current user
@@ -66,7 +67,6 @@ export class AuthenticatedStructures {
66
67
 
67
68
  static async organization(organization: Organization): Promise<OrganizationStruct> {
68
69
  if (await Context.optionalAuth?.canAccessPrivateOrganizationData(organization)) {
69
- const groups = await Group.getAll(organization.id, organization.periodId)
70
70
  const webshops = await Webshop.where({ organizationId: organization.id }, { select: Webshop.selectColumnsWithout(undefined, "products", "categories")})
71
71
  const webshopStructures: WebshopPreview[] = []
72
72
 
@@ -77,20 +77,7 @@ export class AuthenticatedStructures {
77
77
  webshopStructures.push(WebshopPreview.create(w))
78
78
  }
79
79
 
80
- const oPeriods = await OrganizationRegistrationPeriod.where({ periodId: organization.periodId }, {limit: 1})
81
- let oPeriod = oPeriods[0];
82
- const period = (await RegistrationPeriod.getByID(organization.periodId))!
83
-
84
- if (!oPeriod) {
85
- const organizationPeriod = new OrganizationRegistrationPeriod();
86
- organizationPeriod.organizationId = organization.id;
87
- organizationPeriod.periodId = period.id
88
- organizationPeriod.settings.categories = organization.meta.categories
89
- organizationPeriod.settings.rootCategoryId = organization.meta.rootCategoryId
90
- await organizationPeriod.save();
91
-
92
- oPeriod = organizationPeriod
93
- }
80
+ const {groups, organizationPeriod, period} = await organization.getPeriod({emptyGroups: false})
94
81
 
95
82
  return OrganizationStruct.create({
96
83
  id: organization.id,
@@ -103,7 +90,7 @@ export class AuthenticatedStructures {
103
90
  privateMeta: organization.privateMeta,
104
91
  webshops: webshopStructures,
105
92
  createdAt: organization.createdAt,
106
- period: oPeriod.getStructure(period, groups)
93
+ period: organizationPeriod.getPrivateStructure(period, groups)
107
94
  })
108
95
  }
109
96
 
@@ -112,19 +99,64 @@ export class AuthenticatedStructures {
112
99
 
113
100
  static async adminOrganizations(organizations: Organization[]): Promise<OrganizationStruct[]> {
114
101
  const structs: OrganizationStruct[] = [];
115
- const admins = await User.getAdmins(organizations.map(o => o.id))
116
102
 
117
103
  for (const organization of organizations) {
118
104
  const base = await organization.getStructure({emptyGroups: true})
119
- base.admins = admins.filter(a => a.permissions?.organizationPermissions.has(organization.id)).map(a => UserStruct.create({...a, hasAccount: a.hasAccount()}))
120
105
  structs.push(base)
121
106
  }
122
107
 
123
108
  return structs
124
109
  }
125
110
 
126
- static async membersBlob(members: MemberWithRegistrations[], includeContextOrganization = false): Promise<MembersBlob> {
111
+ static async userWithMembers(user: User): Promise<UserWithMembers> {
112
+ const members = await Member.getMembersWithRegistrationForUser(user)
113
+
114
+ return UserWithMembers.create({
115
+ ...user,
116
+ hasAccount: user.hasAccount(),
117
+ members: await this.membersBlob(members, false, user)
118
+ })
119
+ }
120
+
121
+ /**
122
+ * This version only returns connected members that are 1:1, skips other members
123
+ */
124
+ static async usersWithMembers(users: User[]): Promise<UserWithMembers[]> {
125
+ const structs: UserWithMembers[] = [];
126
+ const memberIds = Formatter.uniqueArray(users.map(u => u.memberId).filter(id => id !== null) as string[])
127
+ const members = memberIds.length > 0 ? await Member.getBlobByIds(...memberIds) : []
128
+
129
+ for (const user of users) {
130
+ const filteredMembers = user.memberId ? members.filter(m => m.id === user.memberId) : []
131
+ structs.push(UserWithMembers.create({
132
+ ...user,
133
+ hasAccount: user.hasAccount(),
134
+ members: await this.membersBlob(filteredMembers, false)
135
+ }))
136
+ }
137
+
138
+ return structs
139
+ }
140
+
141
+ static async membersBlob(members: MemberWithRegistrations[], includeContextOrganization = false, includeUser?: User): Promise<MembersBlob> {
142
+ if (members.length === 0 && !includeUser) {
143
+ return MembersBlob.create({members: [], organizations: []})
144
+ }
127
145
  const organizations = new Map<string, Organization>()
146
+
147
+ if (includeUser) {
148
+ for (const organizationId of includeUser.permissions?.organizationPermissions.keys() ?? []) {
149
+ if (includeContextOrganization || organizationId !== Context.auth.organization?.id) {
150
+ const found = organizations.get(organizationId);
151
+ if (!found) {
152
+ const organization = await Context.auth.getOrganization(organizationId)
153
+ organizations.set(organization.id, organization)
154
+ }
155
+ }
156
+ }
157
+ }
158
+
159
+
128
160
  const memberBlobs: MemberWithRegistrationsBlob[] = []
129
161
  for (const member of members) {
130
162
  for (const registration of member.registrations) {
@@ -137,6 +169,7 @@ export class AuthenticatedStructures {
137
169
  }
138
170
  }
139
171
 
172
+
140
173
  const blob = member.getStructureWithRegistrations()
141
174
  memberBlobs.push(
142
175
  await Context.auth.filterMemberData(member, blob)
@@ -145,10 +178,22 @@ export class AuthenticatedStructures {
145
178
 
146
179
  // Load responsibilities
147
180
  const responsibilities = members.length > 0 ? await MemberResponsibilityRecord.where({ memberId: { sign: 'IN', value: members.map(m => m.id) } }) : []
148
- const platformMemberships = members.length > 0 ? await MemberPlatformMembership.where({ memberId: { sign: 'IN', value: members.map(m => m.id) } }) : []
181
+ const platformMemberships = members.length > 0 ? await MemberPlatformMembership.where({ deletedAt: null, memberId: { sign: 'IN', value: members.map(m => m.id) } }) : []
182
+
183
+ // Load missing organizations
184
+ const organizationIds = Formatter.uniqueArray(responsibilities.map(r => r.organizationId).filter(id => id !== null) as string[])
185
+ for (const id of organizationIds) {
186
+ if (includeContextOrganization || id !== Context.auth.organization?.id) {
187
+ const found = organizations.get(id);
188
+ if (!found) {
189
+ const organization = await Context.auth.getOrganization(id)
190
+ organizations.set(organization.id, organization)
191
+ }
192
+ }
193
+ }
149
194
 
150
195
  for (const blob of memberBlobs) {
151
- blob.responsibilities = responsibilities.filter(r => r.memberId == blob.id).map(r => MemberResponsibilityRecordStruct.create(r))
196
+ blob.responsibilities = responsibilities.filter(r => r.memberId == blob.id).map(r => r.getStructure())
152
197
  blob.platformMemberships = platformMemberships.filter(r => r.memberId == blob.id).map(r => MemberPlatformMembershipStruct.create(r))
153
198
  }
154
199
 
@@ -157,4 +202,24 @@ export class AuthenticatedStructures {
157
202
  organizations: await Promise.all([...organizations.values()].map(o => this.organization(o)))
158
203
  })
159
204
  }
205
+
206
+ static async events(events: Event[]): Promise<EventStruct[]> {
207
+ // Load groups
208
+ const groupIds = events.map(e => e.groupId).filter(id => id !== null) as string[]
209
+ const groups = groupIds.length > 0 ? await Group.getByIDs(...groupIds) : []
210
+
211
+ const result: EventStruct[] = []
212
+
213
+ for (const event of events) {
214
+ const group = groups.find(g => g.id == event.groupId) ?? null
215
+
216
+ if (group && await Context.auth.canAccessGroup(group)) {
217
+ result.push(event.getPrivateStructure(group))
218
+ } else {
219
+ result.push(event.getStructure(group))
220
+ }
221
+ }
222
+
223
+ return result
224
+ }
160
225
  }
@@ -185,6 +185,10 @@ export class ContextInstance {
185
185
 
186
186
  const user = token.user
187
187
  this.user = user
188
+
189
+ // Load member of user
190
+ // todo
191
+
188
192
  this.#auth = new AdminPermissionChecker(user, await Platform.getSharedPrivateStruct(), this.organization);
189
193
 
190
194
  return {user, token};
@@ -0,0 +1,17 @@
1
+ import { Email } from "@stamhoofd/models";
2
+ import { SQL } from "@stamhoofd/sql";
3
+ import { EmailStatus } from "@stamhoofd/structures";
4
+
5
+ export async function resumeEmails() {
6
+ const query = SQL.select()
7
+ .from(SQL.table(Email.table))
8
+ .where(SQL.column('status'), EmailStatus.Sending);
9
+
10
+ const result = await query.fetch();
11
+ const emails = Email.fromRows(result, Email.table);
12
+
13
+ for (const email of emails) {
14
+ console.log('Resuming email that has sending status on boot', email.id);
15
+ await email.send()
16
+ }
17
+ }
@@ -0,0 +1,221 @@
1
+ import { Member, MemberResponsibilityRecord, MemberWithRegistrations, User } from "@stamhoofd/models";
2
+ import { SQL } from "@stamhoofd/sql";
3
+ import { Permissions, UserPermissions } from "@stamhoofd/structures";
4
+
5
+ export class MemberUserSyncerStatic {
6
+ /**
7
+ * Call when:
8
+ * - responsibilities have changed
9
+ * - email addresses have changed
10
+ */
11
+ async onChangeMember(member: MemberWithRegistrations) {
12
+ const userEmails = [...member.details.alternativeEmails]
13
+
14
+ if (member.details.email) {
15
+ userEmails.push(member.details.email)
16
+ }
17
+
18
+ const parentEmails = member.details.parentsHaveAccess ? member.details.parents.flatMap(p => p.email ? [p.email, ...p.alternativeEmails] : p.alternativeEmails) : []
19
+
20
+ // Make sure all these users have access to the member
21
+ for (const email of userEmails) {
22
+ // Link users that are found with these email addresses.
23
+ await this.linkUser(email, member, false)
24
+ }
25
+
26
+ for (const email of parentEmails) {
27
+ // Link parents
28
+ await this.linkUser(email, member, true)
29
+ }
30
+
31
+ // Remove access of users that are not in this list
32
+ for (const user of member.users) {
33
+ if (!userEmails.includes(user.email) && !parentEmails.includes(user.email)) {
34
+ await this.unlinkUser(user, member)
35
+ }
36
+ }
37
+
38
+ }
39
+
40
+ async onDeleteMember(member: MemberWithRegistrations) {
41
+ for (const u of member.users) {
42
+ console.log("Unlinking user "+u.email+" from deleted member "+member.id)
43
+ await this.unlinkUser(u, member)
44
+ }
45
+ }
46
+
47
+ async getResponsibilitiesForMembers(memberIds: string[]) {
48
+ const rows = await SQL.select()
49
+ .from(SQL.table(MemberResponsibilityRecord.table))
50
+ .where(SQL.column("memberId"), memberIds)
51
+ .where(SQL.column("endDate"), null)
52
+ .fetch();
53
+
54
+ return MemberResponsibilityRecord.fromRows(rows, MemberResponsibilityRecord.table)
55
+ }
56
+
57
+ async updateInheritedPermissions(user: User) {
58
+ const responsibilities = user.memberId ? (await this.getResponsibilitiesForMembers([user.memberId])) : []
59
+
60
+ // Check if the member has active registrations
61
+ // otherwise -> do not inherit any permissions
62
+
63
+ user.permissions = user.permissions ?? UserPermissions.create({})
64
+
65
+ // Group responsibilities by organization
66
+ const responsibilitiesByOrganization: Map<string|null, MemberResponsibilityRecord[]> = new Map()
67
+
68
+ responsibilitiesByOrganization.set(null, [])
69
+
70
+ for (const organizationId of user.permissions.organizationPermissions.keys()) {
71
+ // Make sure we reset responsibilities for organizations that are not in the list
72
+ responsibilitiesByOrganization.set(organizationId, [])
73
+ }
74
+
75
+ for (const responsibility of responsibilities) {
76
+ const v = responsibilitiesByOrganization.get(responsibility.organizationId) ?? []
77
+ responsibilitiesByOrganization.set(responsibility.organizationId, v)
78
+ v.push(responsibility)
79
+ }
80
+
81
+ for (const organizationId of responsibilitiesByOrganization.keys()) {
82
+ if (organizationId === null) {
83
+ const patch = user.permissions.convertPlatformPatch(
84
+ Permissions.patch({
85
+ responsibilities: (responsibilitiesByOrganization.get(organizationId) ?? []).map(r => r.getStructure()) as any
86
+ })
87
+ )
88
+ user.permissions = user.permissions.patch(patch)
89
+ } else {
90
+ const patch = user.permissions.convertPatch(
91
+ Permissions.patch({
92
+ responsibilities: (responsibilitiesByOrganization.get(organizationId) ?? []).map(r => r.getStructure()) as any
93
+ }),
94
+ organizationId
95
+ )
96
+ user.permissions = user.permissions.patch(patch)
97
+ }
98
+
99
+ }
100
+
101
+ // Platform permissions
102
+ user.permissions.clearEmptyPermissions()
103
+
104
+ if (user.permissions.isEmpty) {
105
+ user.permissions = null;
106
+ }
107
+
108
+ await user.save()
109
+ }
110
+
111
+ async unlinkUser(user: User, member: MemberWithRegistrations) {
112
+ console.log("Removing access for "+ user.id +" to member "+member.id)
113
+ await Member.users.reverse("members").unlink(user, member)
114
+
115
+ if (user.memberId === member.id) {
116
+ user.memberId = null;
117
+ await user.save()
118
+ }
119
+
120
+ // Update model relation to correct response
121
+ const existingIndex = member.users.findIndex(u => u.id === user.id)
122
+ if (existingIndex !== -1) {
123
+ member.users.splice(existingIndex, 1)
124
+ }
125
+
126
+ await this.updateInheritedPermissions(user)
127
+ }
128
+
129
+ async linkUser(email: string, member: MemberWithRegistrations, asParent: boolean) {
130
+ let user = member.users.find(u => u.email.toLocaleLowerCase() === email.toLocaleLowerCase()) ?? await User.getForAuthentication(member.organizationId, email, {allowWithoutAccount: true})
131
+
132
+ if (user) {
133
+ console.log("Giving an existing user access to a member: " + user.id + ' - ' + member.id)
134
+ if (!asParent) {
135
+ if (user.memberId && user.memberId !== member.id) {
136
+ console.error('Found conflicting user with multiple members', user.id, 'members', user.memberId, 'to', member.id)
137
+
138
+ const otherMember = await Member.getWithRegistrations(user.memberId)
139
+
140
+ if (otherMember) {
141
+ if (otherMember.registrations.length > 0 && member.registrations.length === 0) {
142
+ // Choose the other member
143
+ // don't make changes
144
+ console.error('Resolved to current member - no changes made')
145
+ return
146
+ }
147
+
148
+ const responsibilities = await this.getResponsibilitiesForMembers([otherMember.id, member.id])
149
+ const responsibilitiesOther = responsibilities.filter(r => r.memberId === otherMember.id)
150
+ const responsibilitiesCurrent = responsibilities.filter(r => r.memberId === member.id)
151
+
152
+ if (responsibilitiesOther.length >= responsibilitiesCurrent.length) {
153
+ console.error('Resolved to current member because of more responsibilities - no changes made')
154
+ return
155
+ }
156
+ }
157
+ }
158
+ user.firstName = member.details.firstName
159
+ user.lastName = member.details.lastName
160
+ user.memberId = member.id;
161
+ await this.updateInheritedPermissions(user)
162
+ } else {
163
+ if (user.memberId === member.id) {
164
+ // Unlink: parents are never 'equal' to the member
165
+ user.memberId = null;
166
+ await this.updateInheritedPermissions(user)
167
+ }
168
+
169
+ if (!user.firstName && !user.lastName) {
170
+ const parents = member.details.parents.filter(p => p.email === email)
171
+ if (parents.length === 1) {
172
+ user.firstName = parents[0].firstName
173
+ user.lastName = parents[0].lastName
174
+ await user.save()
175
+ }
176
+ }
177
+
178
+ if (user.firstName === member.details.firstName && user.lastName === member.details.lastName) {
179
+ user.firstName = null;
180
+ user.lastName = null;
181
+ await user.save()
182
+ }
183
+ }
184
+ } else {
185
+ // Create a new placeholder user
186
+ user = new User()
187
+ user.organizationId = member.organizationId
188
+ user.email = email
189
+
190
+ if (!asParent) {
191
+ user.firstName = member.details.firstName
192
+ user.lastName = member.details.lastName
193
+ user.memberId = member.id;
194
+ await this.updateInheritedPermissions(user)
195
+ } else {
196
+ const parents = member.details.parents.filter(p => p.email === email)
197
+ if (parents.length === 1) {
198
+ user.firstName = parents[0].firstName
199
+ user.lastName = parents[0].lastName
200
+ }
201
+
202
+ if (user.firstName === member.details.firstName && user.lastName === member.details.lastName) {
203
+ user.firstName = null;
204
+ user.lastName = null;
205
+ }
206
+
207
+ await user.save()
208
+ }
209
+
210
+ console.log("Created new (placeholder) user that has access to a member: "+user.id)
211
+ }
212
+
213
+ // Update model relation to correct response
214
+ if (!member.users.find(u => u.id === user.id)) {
215
+ await Member.users.reverse("members").link(user, [member])
216
+ member.users.push(user)
217
+ }
218
+ }
219
+ }
220
+
221
+ export const MemberUserSyncer = new MemberUserSyncerStatic()
@@ -0,0 +1,52 @@
1
+ import { Migration } from '@simonbackx/simple-database';
2
+ import { Group } from '@stamhoofd/models';
3
+
4
+ export default new Migration(async () => {
5
+ if (STAMHOOFD.environment == "test") {
6
+ console.log("skipped in tests")
7
+ return;
8
+ }
9
+
10
+ if(STAMHOOFD.userMode !== "platform") {
11
+ console.log("skipped seed group-update-occupancy because usermode not platform")
12
+ return;
13
+ }
14
+
15
+ process.stdout.write('\n');
16
+ let c = 0;
17
+ let id: string = '';
18
+
19
+ while(true) {
20
+ const rawGroups = await Group.where({
21
+ id: {
22
+ value: id,
23
+ sign: '>'
24
+ }
25
+ }, {limit: 100, sort: ['id']});
26
+
27
+ const groups = await Group.getByIDs(...rawGroups.map(g => g.id));
28
+
29
+ for (const group of groups) {
30
+ await group.updateOccupancy();
31
+ await group.save();
32
+
33
+ c++;
34
+
35
+ if (c%1000 === 0) {
36
+ process.stdout.write('.');
37
+ }
38
+ if (c%10000 === 0) {
39
+ process.stdout.write('\n');
40
+ }
41
+ }
42
+
43
+ if (groups.length === 0) {
44
+ break;
45
+ }
46
+
47
+ id = groups[groups.length - 1].id;
48
+ }
49
+
50
+ // Do something here
51
+ return Promise.resolve()
52
+ })