@stamhoofd/backend 2.77.3 → 2.77.5

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 CHANGED
@@ -19,6 +19,7 @@ import { AuditLogService } from './src/services/AuditLogService';
19
19
  import { DocumentService } from './src/services/DocumentService';
20
20
  import { FileSignService } from './src/services/FileSignService';
21
21
  import { PlatformMembershipService } from './src/services/PlatformMembershipService';
22
+ import { UniqueUserService } from './src/services/UniqueUserService';
22
23
 
23
24
  process.on('unhandledRejection', (error: Error) => {
24
25
  console.error('unhandledRejection');
@@ -49,13 +50,21 @@ const seeds = async () => {
49
50
  console.error(e);
50
51
  }
51
52
  };
53
+ const bootTime = process.hrtime();
52
54
 
53
55
  const start = async () => {
54
56
  console.log('Running server at v' + Version);
55
57
  loadLogger();
58
+
56
59
  await GlobalHelper.load();
60
+ await UniqueUserService.check();
61
+
62
+ // Init platform shared struct: otherwise permissions won't work with missing responsibilities
63
+ await Platform.getSharedStruct();
57
64
 
58
65
  const router = new Router();
66
+
67
+ // Note: we should load endpoints one by once to have a reliable order of url matching
59
68
  await router.loadAllEndpoints(__dirname + '/src/endpoints/global/*');
60
69
  await router.loadAllEndpoints(__dirname + '/src/endpoints/admin/*');
61
70
  await router.loadAllEndpoints(__dirname + '/src/endpoints/auth');
@@ -95,9 +104,6 @@ const start = async () => {
95
104
  // Add CORS headers
96
105
  routerServer.addResponseMiddleware(CORSMiddleware);
97
106
 
98
- // Init platform shared struct: otherwise permissions won't work with missing responsibilities
99
- await Platform.getSharedStruct();
100
-
101
107
  // Register Excel loaders
102
108
  await import('./src/excel-loaders/members');
103
109
  await import('./src/excel-loaders/payments');
@@ -111,6 +117,9 @@ const start = async () => {
111
117
 
112
118
  routerServer.listen(STAMHOOFD.PORT ?? 9090);
113
119
 
120
+ const hrend = process.hrtime(bootTime);
121
+ console.log('🟢 HTTP server started in ' + Math.ceil(hrend[0] * 1000 + hrend[1] / 1000000) + 'ms');
122
+
114
123
  resumeEmails().catch(console.error);
115
124
 
116
125
  if (routerServer.server) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.77.3",
3
+ "version": "2.77.5",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -37,14 +37,14 @@
37
37
  "@simonbackx/simple-encoding": "2.20.0",
38
38
  "@simonbackx/simple-endpoints": "1.19.1",
39
39
  "@simonbackx/simple-logging": "^1.0.1",
40
- "@stamhoofd/backend-i18n": "2.77.3",
41
- "@stamhoofd/backend-middleware": "2.77.3",
42
- "@stamhoofd/email": "2.77.3",
43
- "@stamhoofd/models": "2.77.3",
44
- "@stamhoofd/queues": "2.77.3",
45
- "@stamhoofd/sql": "2.77.3",
46
- "@stamhoofd/structures": "2.77.3",
47
- "@stamhoofd/utility": "2.77.3",
40
+ "@stamhoofd/backend-i18n": "2.77.5",
41
+ "@stamhoofd/backend-middleware": "2.77.5",
42
+ "@stamhoofd/email": "2.77.5",
43
+ "@stamhoofd/models": "2.77.5",
44
+ "@stamhoofd/queues": "2.77.5",
45
+ "@stamhoofd/sql": "2.77.5",
46
+ "@stamhoofd/structures": "2.77.5",
47
+ "@stamhoofd/utility": "2.77.5",
48
48
  "archiver": "^7.0.1",
49
49
  "aws-sdk": "^2.885.0",
50
50
  "axios": "1.6.8",
@@ -64,5 +64,5 @@
64
64
  "publishConfig": {
65
65
  "access": "public"
66
66
  },
67
- "gitHead": "73e6c9aec7a8e3feed99014f54fdae6890b346ce"
67
+ "gitHead": "0406aa72c5040bf58c903fa27ce31a24c78b370e"
68
68
  }
@@ -25,9 +25,19 @@ Email.recipientLoaders.set(EmailRecipientFilterType.Members, {
25
25
 
26
26
  count: async (query: LimitedFilteredRequest) => {
27
27
  query.filter = mergeFilters([query.filter, {
28
- email: {
29
- $neq: null,
30
- },
28
+ $and: [
29
+ {
30
+ email: {
31
+ $neq: null,
32
+ },
33
+ },
34
+ {
35
+ email: {
36
+ $neq: '',
37
+ },
38
+ },
39
+ ],
40
+
31
41
  }]);
32
42
  const q = await GetMembersEndpoint.buildQuery(query);
33
43
  return await q.count();
@@ -11,6 +11,7 @@ import { PeriodHelper } from '../../../helpers/PeriodHelper';
11
11
  import { SetupStepUpdater } from '../../../helpers/SetupStepUpdater';
12
12
  import { TagHelper } from '../../../helpers/TagHelper';
13
13
  import { PlatformMembershipService } from '../../../services/PlatformMembershipService';
14
+ import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer';
14
15
 
15
16
  type Params = Record<string, never>;
16
17
  type Query = undefined;
@@ -49,6 +50,7 @@ export class PatchPlatformEndpoint extends Endpoint<
49
50
  }
50
51
 
51
52
  const platform = await Platform.getShared();
53
+ let shouldUpdateUserPermissions = false;
52
54
 
53
55
  if (request.body.privateConfig) {
54
56
  // Did we patch roles?
@@ -62,6 +64,7 @@ export class PatchPlatformEndpoint extends Endpoint<
62
64
  platform.privateConfig.roles,
63
65
  request.body.privateConfig.roles,
64
66
  );
67
+ shouldUpdateUserPermissions = true;
65
68
  }
66
69
 
67
70
  if (request.body.privateConfig.emails) {
@@ -234,6 +237,10 @@ export class PatchPlatformEndpoint extends Endpoint<
234
237
  SetupStepUpdater.updateSetupStepsForAllOrganizationsInCurrentPeriod().catch(console.error);
235
238
  }
236
239
 
240
+ if (shouldUpdateUserPermissions) {
241
+ await MemberUserSyncer.updatePermissionsForPlatform();
242
+ }
243
+
237
244
  return new Response(await Platform.getSharedPrivateStruct());
238
245
  }
239
246
 
@@ -11,6 +11,7 @@ import { Context } from '../../../../helpers/Context';
11
11
  import { SetupStepUpdater } from '../../../../helpers/SetupStepUpdater';
12
12
  import { TagHelper } from '../../../../helpers/TagHelper';
13
13
  import { ViesHelper } from '../../../../helpers/ViesHelper';
14
+ import { MemberUserSyncer } from '../../../../helpers/MemberUserSyncer';
14
15
 
15
16
  type Params = Record<string, never>;
16
17
  type Query = undefined;
@@ -65,6 +66,7 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
65
66
 
66
67
  const errors = new SimpleErrors();
67
68
  let shouldUpdateSetupSteps = false;
69
+ let shouldUpdateUserPermissions = false;
68
70
  let updateTags = false;
69
71
 
70
72
  if (await Context.auth.hasFullAccess(organization.id)) {
@@ -128,6 +130,10 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
128
130
  organization.privateMeta.balanceNotificationSettings = patchObject(organization.privateMeta.balanceNotificationSettings, request.body.privateMeta.balanceNotificationSettings);
129
131
  organization.privateMeta.recordAnswers = request.body.privateMeta.recordAnswers.applyTo(organization.privateMeta.recordAnswers);
130
132
 
133
+ if (request.body.privateMeta.responsibilities || request.body.privateMeta.roles) {
134
+ shouldUpdateUserPermissions = true;
135
+ }
136
+
131
137
  if (request.body.privateMeta.mollieProfile !== undefined) {
132
138
  organization.privateMeta.mollieProfile = patchObject(organization.privateMeta.mollieProfile, request.body.privateMeta.mollieProfile);
133
139
  }
@@ -392,6 +398,10 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
392
398
  await SetupStepUpdater.updateForOrganization(organization);
393
399
  }
394
400
 
401
+ if (shouldUpdateUserPermissions) {
402
+ await MemberUserSyncer.updatePermissionsForOrganization(organization.id);
403
+ }
404
+
395
405
  if (updateTags) {
396
406
  await TagHelper.updateOrganizations();
397
407
  }
@@ -852,8 +852,6 @@ export class AdminPermissionChecker {
852
852
  * Return a list of RecordSettings the current user can view or edit
853
853
  */
854
854
  async getAccessibleRecordCategories(member: MemberWithRegistrations, level: PermissionLevel = PermissionLevel.Read): Promise<RecordCategory[]> {
855
- const isUserManager = this.isUserManager(member);
856
-
857
855
  // First list all organizations this member is part of
858
856
  const organizations: Organization[] = [];
859
857
 
@@ -875,14 +873,6 @@ export class AdminPermissionChecker {
875
873
  // Check if we have access to their data
876
874
  const recordCategories: RecordCategory[] = [];
877
875
  for (const organization of organizations) {
878
- if (isUserManager) {
879
- for (const category of organization.meta.recordsConfiguration.recordCategories) {
880
- if (category.checkPermissionForUserManager(level)) {
881
- recordCategories.push(category);
882
- }
883
- }
884
- }
885
-
886
876
  const permissions = await this.getOrganizationPermissions(organization);
887
877
 
888
878
  if (!permissions) {
@@ -891,7 +881,7 @@ export class AdminPermissionChecker {
891
881
 
892
882
  // Now add all records of this organization
893
883
  for (const category of organization.meta.recordsConfiguration.recordCategories) {
894
- if (isUserManager && recordCategories.find(c => c.id === category.id)) {
884
+ if (recordCategories.find(c => c.id === category.id)) {
895
885
  // Already added
896
886
  continue;
897
887
  }
@@ -916,7 +906,7 @@ export class AdminPermissionChecker {
916
906
 
917
907
  // Platform data
918
908
  const platformPermissions = this.platformPermissions;
919
- if (platformPermissions || isUserManager) {
909
+ if (platformPermissions) {
920
910
  for (const category of this.platform.config.recordsConfiguration.recordCategories) {
921
911
  if (recordCategories.find(c => c.id === category.id)) {
922
912
  // Already added
@@ -926,11 +916,6 @@ export class AdminPermissionChecker {
926
916
  if (platformPermissions?.hasResourceAccess(PermissionsResourceType.RecordCategories, category.id, level)) {
927
917
  recordCategories.push(category);
928
918
  }
929
- else if (isUserManager) {
930
- if (category.checkPermissionForUserManager(level)) {
931
- recordCategories.push(category);
932
- }
933
- }
934
919
  }
935
920
  }
936
921
 
@@ -1069,21 +1054,45 @@ export class AdminPermissionChecker {
1069
1054
  async getAccessibleRecordSet(member: MemberWithRegistrations, level: PermissionLevel = PermissionLevel.Read): Promise<Set<string>> {
1070
1055
  const categories = await this.getAccessibleRecordCategories(member, level);
1071
1056
  const set = new Set<string>();
1057
+
1058
+ for (const category of categories) {
1059
+ for (const record of category.getAllRecords()) {
1060
+ set.add(record.id);
1061
+ }
1062
+ }
1063
+
1072
1064
  const isUserManager = this.isUserManager(member);
1073
1065
 
1066
+ // Also include those we can access as user manager
1074
1067
  if (isUserManager) {
1075
- for (const category of categories) {
1076
- for (const record of category.getAllRecords()) {
1077
- if (record.checkPermissionForUserManager(level)) {
1078
- set.add(record.id);
1068
+ const allCategories = this.platform.config.recordsConfiguration.recordCategories.slice();
1069
+
1070
+ // First list all organizations this member is part of
1071
+ const organizations: Organization[] = [];
1072
+
1073
+ if (member.organizationId) {
1074
+ if (this.checkScope(member.organizationId)) {
1075
+ organizations.push(await this.getOrganization(member.organizationId));
1076
+ }
1077
+ }
1078
+
1079
+ for (const registration of member.registrations) {
1080
+ if (this.checkScope(registration.organizationId)) {
1081
+ if (!organizations.find(o => o.id === registration.organizationId)) {
1082
+ organizations.push(await this.getOrganization(registration.organizationId));
1079
1083
  }
1080
1084
  }
1081
1085
  }
1082
- }
1083
- else {
1084
- for (const category of categories) {
1086
+
1087
+ for (const organization of organizations) {
1088
+ allCategories.push(...organization.meta.recordsConfiguration.recordCategories);
1089
+ }
1090
+
1091
+ for (const category of allCategories) {
1085
1092
  for (const record of category.getAllRecords()) {
1086
- set.add(record.id);
1093
+ if (record.checkPermissionForUserManager(level)) {
1094
+ set.add(record.id);
1095
+ }
1087
1096
  }
1088
1097
  }
1089
1098
  }
@@ -1,6 +1,6 @@
1
- import { CachedBalance, Member, MemberResponsibilityRecord, MemberWithRegistrations, User } from '@stamhoofd/models';
1
+ import { CachedBalance, Member, MemberResponsibilityRecord, MemberWithRegistrations, Organization, Platform, User } from '@stamhoofd/models';
2
2
  import { SQL } from '@stamhoofd/sql';
3
- import { AuditLogSource, MemberDetails, Permissions, UserPermissions } from '@stamhoofd/structures';
3
+ import { AuditLogSource, MemberDetails, PermissionRole, Permissions, UserPermissions } from '@stamhoofd/structures';
4
4
  import crypto from 'crypto';
5
5
  import basex from 'base-x';
6
6
  import { AuditLogService } from '../services/AuditLogService';
@@ -129,6 +129,20 @@ export class MemberUserSyncerStatic {
129
129
  return MemberResponsibilityRecord.fromRows(rows, MemberResponsibilityRecord.table);
130
130
  }
131
131
 
132
+ async updatePermissionsForOrganization(organizationId: string) {
133
+ const admins = await User.getAdmins(organizationId);
134
+ for (const admin of admins) {
135
+ await this.updateInheritedPermissions(admin);
136
+ }
137
+ }
138
+
139
+ async updatePermissionsForPlatform() {
140
+ const admins = await User.getPlatformAdmins();
141
+ for (const admin of admins) {
142
+ await this.updateInheritedPermissions(admin);
143
+ }
144
+ }
145
+
132
146
  async updateInheritedPermissions(user: User) {
133
147
  const responsibilities = user.memberId ? (await this.getResponsibilitiesForMembers([user.memberId])) : [];
134
148
 
@@ -173,6 +187,54 @@ export class MemberUserSyncerStatic {
173
187
  }
174
188
  }
175
189
 
190
+ // Update roles (remove roles from permissions that no longer exist)
191
+ for (const organizationId of user.permissions.organizationPermissions.keys()) {
192
+ const organizationPermissions = user.permissions.organizationPermissions.get(organizationId);
193
+ if (!organizationPermissions) {
194
+ continue;
195
+ }
196
+
197
+ const roles = organizationPermissions.roles;
198
+ if (roles.length === 0) {
199
+ continue;
200
+ }
201
+
202
+ const organization = await Organization.getByID(organizationId);
203
+ if (!organization) {
204
+ // Delete key
205
+ user.permissions.organizationPermissions.delete(organizationId);
206
+ continue;
207
+ }
208
+
209
+ const availableRoles = organization.privateMeta.roles;
210
+ const newRoles: PermissionRole[] = [];
211
+ for (const role of roles) {
212
+ const roleInfo = availableRoles.find(r => r.id === role.id);
213
+ if (roleInfo) {
214
+ role.name = roleInfo.name;
215
+ newRoles.push(role);
216
+ }
217
+ }
218
+ organizationPermissions.roles = newRoles;
219
+ }
220
+
221
+ const globalPermissions = user.permissions.globalPermissions;
222
+ if (globalPermissions) {
223
+ const roles = globalPermissions.roles;
224
+ if (roles.length > 0) {
225
+ const availableRoles = (await Platform.getSharedPrivateStruct()).privateConfig.roles;
226
+ const newRoles: PermissionRole[] = [];
227
+ for (const role of roles) {
228
+ const roleInfo = availableRoles.find(r => r.id === role.id);
229
+ if (roleInfo) {
230
+ role.name = roleInfo.name;
231
+ newRoles.push(role);
232
+ }
233
+ }
234
+ globalPermissions.roles = newRoles;
235
+ }
236
+ }
237
+
176
238
  // Platform permissions
177
239
  user.permissions.clearEmptyPermissions();
178
240
 
@@ -33,7 +33,6 @@ export const MembershipCharger = {
33
33
  while (true) {
34
34
  const memberships = await MemberPlatformMembership.select()
35
35
  .where('id', SQLWhereSign.Greater, lastId)
36
- .where('balanceItemId', null)
37
36
  .where('deletedAt', null)
38
37
  .where('locked', false)
39
38
  .where(SQL.where('trialUntil', null).or('trialUntil', SQLWhereSign.LessEqual, new Date()))
@@ -57,6 +56,11 @@ export const MembershipCharger = {
57
56
  for (const membership of memberships) {
58
57
  // charge
59
58
  if (membership.balanceItemId) {
59
+ if (!membership.locked) {
60
+ // Lock this membership
61
+ membership.locked = true;
62
+ await membership.save();
63
+ }
60
64
  continue;
61
65
  }
62
66
 
@@ -0,0 +1,61 @@
1
+ import { Database } from '@simonbackx/simple-database';
2
+ import { logger, StyledText } from '@simonbackx/simple-logging';
3
+ import chalk from 'chalk';
4
+
5
+ const UNIQUE_KEY_NAME = 'email';
6
+
7
+ /**
8
+ * This service is responsible for creating MySQL unique constraints on boot depending on the environment configuration.
9
+ * If STAMHOOFD.userMode = 'platform' then we'll create a unique constraint on the email column of the user table.
10
+ * If not, we'll delete the constraint if it exists.
11
+ */
12
+ export class UniqueUserService {
13
+ static async hasUniqueConstraint() {
14
+ const [results] = await Database.select('SHOW INDEX FROM `users` WHERE Key_name = ?', [UNIQUE_KEY_NAME]);
15
+ return results.length > 0;
16
+ }
17
+
18
+ static async check() {
19
+ await logger.setContext({
20
+ prefixes: [
21
+ new StyledText(`[UniqueUserService] `).addClass('unique-user-service', 'tag'),
22
+ ],
23
+ tags: ['unique-user-service'],
24
+ }, async () => {
25
+ if (STAMHOOFD.userMode === 'platform') {
26
+ if (!(await this.hasUniqueConstraint())) {
27
+ console.warn('Unique constraint is missing. Creating it now...');
28
+ await this.createConstraint();
29
+ }
30
+ }
31
+ else {
32
+ if (await this.hasUniqueConstraint()) {
33
+ console.warn('Unique constraint exists but should be removed. Deleting it now...');
34
+ await this.dropConstraint();
35
+ }
36
+ }
37
+ });
38
+ }
39
+
40
+ static async createConstraint() {
41
+ try {
42
+ await Database.statement('ALTER TABLE `users` ADD UNIQUE INDEX `' + UNIQUE_KEY_NAME + '` (`email`) USING BTREE;');
43
+ console.log('Unique constraint created.');
44
+ }
45
+ catch (e) {
46
+ console.error(chalk.red('Failed to create unique constraint on email column of users table:'));
47
+ console.error(e);
48
+ }
49
+ }
50
+
51
+ static async dropConstraint() {
52
+ try {
53
+ await Database.statement('ALTER TABLE `users` DROP INDEX `' + UNIQUE_KEY_NAME + '`;');
54
+ console.log('Unique constraint dropped.');
55
+ }
56
+ catch (e) {
57
+ console.error(chalk.red('Failed to drop unique constraint on email column of users table:'));
58
+ console.error(e);
59
+ }
60
+ }
61
+ }