@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.
- package/.env.json +2 -2
- package/index.ts +3 -0
- package/package.json +4 -4
- package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +63 -2
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +3 -0
- 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 +86 -109
- 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/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 +136 -123
- package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +8 -8
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +2 -1
- package/src/helpers/AdminPermissionChecker.ts +54 -3
- package/src/helpers/AuthenticatedStructures.ts +88 -23
- 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
|
@@ -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(
|
|
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
|
|
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:
|
|
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
|
|
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 =>
|
|
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
|
}
|
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
|
+
})
|