@stamhoofd/backend 2.3.1 → 2.5.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.
- package/index.ts +3 -0
- package/package.json +4 -4
- package/src/endpoints/admin/invoices/GetInvoicesEndpoint.ts +1 -1
- package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +63 -2
- package/src/endpoints/auth/CreateAdminEndpoint.ts +6 -3
- package/src/endpoints/auth/GetOtherUserEndpoint.ts +41 -0
- package/src/endpoints/auth/GetUserEndpoint.ts +6 -28
- package/src/endpoints/auth/PatchUserEndpoint.ts +25 -6
- package/src/endpoints/auth/SignupEndpoint.ts +2 -2
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +120 -0
- package/src/endpoints/global/email/GetEmailEndpoint.ts +51 -0
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +108 -0
- package/src/endpoints/global/events/GetEventsEndpoint.ts +223 -0
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +319 -0
- package/src/endpoints/global/members/GetMembersEndpoint.ts +124 -48
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +107 -117
- package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +3 -2
- package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +2 -1
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +2 -1
- package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +2 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +9 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +3 -2
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +345 -176
- package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +5 -5
- package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +43 -25
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +26 -7
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +23 -22
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +210 -121
- package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +8 -8
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +2 -1
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +2 -1
- package/src/helpers/AdminPermissionChecker.ts +60 -3
- package/src/helpers/AuthenticatedStructures.ts +164 -37
- package/src/helpers/Context.ts +4 -0
- package/src/helpers/EmailResumer.ts +17 -0
- package/src/helpers/MemberUserSyncer.ts +221 -0
- package/src/seeds/1722256498-group-update-occupancy.ts +52 -0
- package/src/seeds/1722344160-update-membership.ts +57 -0
|
@@ -1,8 +1,10 @@
|
|
|
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
|
+
import { OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, GroupCategory, GroupPrivateSettings, GroupSettings, GroupStatus, Group as GroupStruct, GroupType } from '@stamhoofd/structures';
|
|
4
5
|
|
|
5
6
|
import { Context } from "./Context";
|
|
7
|
+
import { Formatter } from "@stamhoofd/utility";
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Builds authenticated structures for the current user
|
|
@@ -51,10 +53,71 @@ export class AuthenticatedStructures {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
static async group(group: Group) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
return (await this.groups([group]))[0]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static async groups(groups: Group[]) {
|
|
60
|
+
const waitingListIds = Formatter.uniqueArray(groups.map(g => g.waitingListId).filter(id => id !== null) as string[])
|
|
61
|
+
const waitingLists = waitingListIds.length > 0 ? await Group.getByIDs(...waitingListIds) : []
|
|
62
|
+
|
|
63
|
+
const structs: GroupStruct[] = []
|
|
64
|
+
for (const group of groups) {
|
|
65
|
+
const waitingList = waitingLists.find(g => g.id == group.waitingListId) ?? null
|
|
66
|
+
const waitingListStruct = waitingList ? GroupStruct.create(waitingList) : null
|
|
67
|
+
if (waitingList && waitingListStruct && !await Context.optionalAuth?.canAccessGroup(waitingList)) {
|
|
68
|
+
waitingListStruct.privateSettings = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const struct = GroupStruct.create({
|
|
72
|
+
...group,
|
|
73
|
+
waitingList: waitingListStruct
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
if (!await Context.optionalAuth?.canAccessGroup(group)) {
|
|
77
|
+
struct.privateSettings = null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
structs.push(struct)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return structs;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static async organizationRegistrationPeriods(organizationRegistrationPeriods: OrganizationRegistrationPeriod[]) {
|
|
87
|
+
if (organizationRegistrationPeriods.length === 0) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const periodIds = Formatter.uniqueArray(organizationRegistrationPeriods.map(p => p.periodId))
|
|
92
|
+
const periods = await RegistrationPeriod.getByIDs(...periodIds)
|
|
93
|
+
|
|
94
|
+
const groupIds = Formatter.uniqueArray(organizationRegistrationPeriods.flatMap(p => p.settings.categories.flatMap(c => c.groupIds)))
|
|
95
|
+
const groups = groupIds.length ? await Group.getByIDs(...groupIds) : []
|
|
96
|
+
|
|
97
|
+
const groupStructs = await this.groups(groups)
|
|
98
|
+
|
|
99
|
+
const structs: OrganizationRegistrationPeriodStruct[] = []
|
|
100
|
+
for (const organizationPeriod of organizationRegistrationPeriods) {
|
|
101
|
+
const period = periods.find(p => p.id == organizationPeriod.periodId) ?? null
|
|
102
|
+
if (!period) {
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
const groupIds = Formatter.uniqueArray(organizationPeriod.settings.categories.flatMap(c => c.groupIds))
|
|
106
|
+
|
|
107
|
+
structs.push(
|
|
108
|
+
OrganizationRegistrationPeriodStruct.create({
|
|
109
|
+
...organizationPeriod,
|
|
110
|
+
period: period.getStructure(),
|
|
111
|
+
groups: groupStructs.filter(gg => groupIds.includes(gg.id)).sort(GroupStruct.defaultSort)
|
|
112
|
+
})
|
|
113
|
+
)
|
|
56
114
|
}
|
|
57
|
-
|
|
115
|
+
|
|
116
|
+
return structs
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static async organizationRegistrationPeriod(organizationRegistrationPeriod: OrganizationRegistrationPeriod) {
|
|
120
|
+
return (await this.organizationRegistrationPeriods([organizationRegistrationPeriod]))[0]
|
|
58
121
|
}
|
|
59
122
|
|
|
60
123
|
static async webshop(webshop: Webshop) {
|
|
@@ -65,8 +128,9 @@ export class AuthenticatedStructures {
|
|
|
65
128
|
}
|
|
66
129
|
|
|
67
130
|
static async organization(organization: Organization): Promise<OrganizationStruct> {
|
|
131
|
+
const organizationPeriod = await organization.getPeriod()
|
|
132
|
+
|
|
68
133
|
if (await Context.optionalAuth?.canAccessPrivateOrganizationData(organization)) {
|
|
69
|
-
const groups = await Group.getAll(organization.id, organization.periodId)
|
|
70
134
|
const webshops = await Webshop.where({ organizationId: organization.id }, { select: Webshop.selectColumnsWithout(undefined, "products", "categories")})
|
|
71
135
|
const webshopStructures: WebshopPreview[] = []
|
|
72
136
|
|
|
@@ -77,54 +141,82 @@ export class AuthenticatedStructures {
|
|
|
77
141
|
webshopStructures.push(WebshopPreview.create(w))
|
|
78
142
|
}
|
|
79
143
|
|
|
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
|
-
}
|
|
94
|
-
|
|
95
144
|
return OrganizationStruct.create({
|
|
96
|
-
|
|
97
|
-
name: organization.name,
|
|
98
|
-
meta: organization.meta,
|
|
99
|
-
address: organization.address,
|
|
100
|
-
registerDomain: organization.registerDomain,
|
|
101
|
-
uri: organization.uri,
|
|
102
|
-
website: organization.website,
|
|
145
|
+
...organization.getBaseStructure(),
|
|
103
146
|
privateMeta: organization.privateMeta,
|
|
104
147
|
webshops: webshopStructures,
|
|
105
|
-
|
|
106
|
-
period: oPeriod.getStructure(period, groups)
|
|
148
|
+
period: await this.organizationRegistrationPeriod(organizationPeriod)
|
|
107
149
|
})
|
|
108
150
|
}
|
|
109
151
|
|
|
110
|
-
return
|
|
152
|
+
return OrganizationStruct.create({
|
|
153
|
+
...organization.getBaseStructure(),
|
|
154
|
+
period: await this.organizationRegistrationPeriod(organizationPeriod)
|
|
155
|
+
})
|
|
111
156
|
}
|
|
112
157
|
|
|
113
158
|
static async adminOrganizations(organizations: Organization[]): Promise<OrganizationStruct[]> {
|
|
114
159
|
const structs: OrganizationStruct[] = [];
|
|
115
|
-
const admins = await User.getAdmins(organizations.map(o => o.id))
|
|
116
160
|
|
|
117
161
|
for (const organization of organizations) {
|
|
118
|
-
const base =
|
|
119
|
-
base.admins = admins.filter(a => a.permissions?.organizationPermissions.has(organization.id)).map(a => UserStruct.create({...a, hasAccount: a.hasAccount()}))
|
|
162
|
+
const base = organization.getBaseStructure()
|
|
120
163
|
structs.push(base)
|
|
121
164
|
}
|
|
122
165
|
|
|
166
|
+
return Promise.resolve(structs)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
static async userWithMembers(user: User): Promise<UserWithMembers> {
|
|
170
|
+
const members = await Member.getMembersWithRegistrationForUser(user)
|
|
171
|
+
|
|
172
|
+
return UserWithMembers.create({
|
|
173
|
+
...user,
|
|
174
|
+
hasAccount: user.hasAccount(),
|
|
175
|
+
|
|
176
|
+
// Always include the current context organization - because it is possible we switch organization and we don't want to refetch every time
|
|
177
|
+
members: await this.membersBlob(members, true, user)
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* This version only returns connected members that are 1:1, skips other members
|
|
183
|
+
*/
|
|
184
|
+
static async usersWithMembers(users: User[]): Promise<UserWithMembers[]> {
|
|
185
|
+
const structs: UserWithMembers[] = [];
|
|
186
|
+
const memberIds = Formatter.uniqueArray(users.map(u => u.memberId).filter(id => id !== null) as string[])
|
|
187
|
+
const members = memberIds.length > 0 ? await Member.getBlobByIds(...memberIds) : []
|
|
188
|
+
|
|
189
|
+
for (const user of users) {
|
|
190
|
+
const filteredMembers = user.memberId ? members.filter(m => m.id === user.memberId) : []
|
|
191
|
+
structs.push(UserWithMembers.create({
|
|
192
|
+
...user,
|
|
193
|
+
hasAccount: user.hasAccount(),
|
|
194
|
+
members: await this.membersBlob(filteredMembers, false)
|
|
195
|
+
}))
|
|
196
|
+
}
|
|
197
|
+
|
|
123
198
|
return structs
|
|
124
199
|
}
|
|
125
200
|
|
|
126
|
-
static async membersBlob(members: MemberWithRegistrations[], includeContextOrganization = false): Promise<MembersBlob> {
|
|
201
|
+
static async membersBlob(members: MemberWithRegistrations[], includeContextOrganization = false, includeUser?: User): Promise<MembersBlob> {
|
|
202
|
+
if (members.length === 0 && !includeUser) {
|
|
203
|
+
return MembersBlob.create({members: [], organizations: []})
|
|
204
|
+
}
|
|
127
205
|
const organizations = new Map<string, Organization>()
|
|
206
|
+
|
|
207
|
+
if (includeUser) {
|
|
208
|
+
for (const organizationId of includeUser.permissions?.organizationPermissions.keys() ?? []) {
|
|
209
|
+
if (includeContextOrganization || organizationId !== Context.auth.organization?.id) {
|
|
210
|
+
const found = organizations.get(organizationId);
|
|
211
|
+
if (!found) {
|
|
212
|
+
const organization = await Context.auth.getOrganization(organizationId)
|
|
213
|
+
organizations.set(organization.id, organization)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
128
220
|
const memberBlobs: MemberWithRegistrationsBlob[] = []
|
|
129
221
|
for (const member of members) {
|
|
130
222
|
for (const registration of member.registrations) {
|
|
@@ -137,6 +229,7 @@ export class AuthenticatedStructures {
|
|
|
137
229
|
}
|
|
138
230
|
}
|
|
139
231
|
|
|
232
|
+
|
|
140
233
|
const blob = member.getStructureWithRegistrations()
|
|
141
234
|
memberBlobs.push(
|
|
142
235
|
await Context.auth.filterMemberData(member, blob)
|
|
@@ -145,10 +238,22 @@ export class AuthenticatedStructures {
|
|
|
145
238
|
|
|
146
239
|
// Load responsibilities
|
|
147
240
|
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) } }) : []
|
|
241
|
+
const platformMemberships = members.length > 0 ? await MemberPlatformMembership.where({ deletedAt: null, memberId: { sign: 'IN', value: members.map(m => m.id) } }) : []
|
|
242
|
+
|
|
243
|
+
// Load missing organizations
|
|
244
|
+
const organizationIds = Formatter.uniqueArray(responsibilities.map(r => r.organizationId).filter(id => id !== null) as string[])
|
|
245
|
+
for (const id of organizationIds) {
|
|
246
|
+
if (includeContextOrganization || id !== Context.auth.organization?.id) {
|
|
247
|
+
const found = organizations.get(id);
|
|
248
|
+
if (!found) {
|
|
249
|
+
const organization = await Context.auth.getOrganization(id)
|
|
250
|
+
organizations.set(organization.id, organization)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
149
254
|
|
|
150
255
|
for (const blob of memberBlobs) {
|
|
151
|
-
blob.responsibilities = responsibilities.filter(r => r.memberId == blob.id).map(r =>
|
|
256
|
+
blob.responsibilities = responsibilities.filter(r => r.memberId == blob.id).map(r => r.getStructure())
|
|
152
257
|
blob.platformMemberships = platformMemberships.filter(r => r.memberId == blob.id).map(r => MemberPlatformMembershipStruct.create(r))
|
|
153
258
|
}
|
|
154
259
|
|
|
@@ -157,4 +262,26 @@ export class AuthenticatedStructures {
|
|
|
157
262
|
organizations: await Promise.all([...organizations.values()].map(o => this.organization(o)))
|
|
158
263
|
})
|
|
159
264
|
}
|
|
265
|
+
|
|
266
|
+
static async events(events: Event[]): Promise<EventStruct[]> {
|
|
267
|
+
// Load groups
|
|
268
|
+
const groupIds = events.map(e => e.groupId).filter(id => id !== null) as string[]
|
|
269
|
+
const groups = groupIds.length > 0 ? await Group.getByIDs(...groupIds) : []
|
|
270
|
+
const groupStructs = await this.groups(groups)
|
|
271
|
+
|
|
272
|
+
const result: EventStruct[] = []
|
|
273
|
+
|
|
274
|
+
for (const event of events) {
|
|
275
|
+
const group = groupStructs.find(g => g.id == event.groupId) ?? null
|
|
276
|
+
|
|
277
|
+
const struct = EventStruct.create({
|
|
278
|
+
...event,
|
|
279
|
+
group
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
result.push(struct)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return result
|
|
286
|
+
}
|
|
160
287
|
}
|
package/src/helpers/Context.ts
CHANGED
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Member } 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 update-membership 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 rawMembers = await Member.where({
|
|
21
|
+
id: {
|
|
22
|
+
value: id,
|
|
23
|
+
sign: '>'
|
|
24
|
+
}
|
|
25
|
+
}, {limit: 100, sort: ['id']});
|
|
26
|
+
|
|
27
|
+
// const members = await Member.getByIDs(...rawMembers.map(m => m.id));
|
|
28
|
+
|
|
29
|
+
for (const member of rawMembers) {
|
|
30
|
+
const memberWithRegistrations = await Member.getWithRegistrations(member.id);
|
|
31
|
+
if(memberWithRegistrations) {
|
|
32
|
+
await memberWithRegistrations.updateMemberships();
|
|
33
|
+
await memberWithRegistrations.save();
|
|
34
|
+
} else {
|
|
35
|
+
throw new Error("Member with registrations not found: " + member.id);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
c++;
|
|
39
|
+
|
|
40
|
+
if (c%1000 === 0) {
|
|
41
|
+
process.stdout.write('.');
|
|
42
|
+
}
|
|
43
|
+
if (c%10000 === 0) {
|
|
44
|
+
process.stdout.write('\n');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (rawMembers.length === 0) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
id = rawMembers[rawMembers.length - 1].id;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Do something here
|
|
56
|
+
return Promise.resolve()
|
|
57
|
+
})
|