@stamhoofd/backend 2.19.0 → 2.21.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 (22) hide show
  1. package/.env.template.json +1 -1
  2. package/package.json +6 -6
  3. package/src/crons.ts +3 -67
  4. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +33 -29
  5. package/src/endpoints/global/files/ExportToExcelEndpoint.ts +3 -8
  6. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +10 -1
  7. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.test.ts +0 -47
  8. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +14 -16
  9. package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +8 -0
  10. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +1 -1
  11. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +171 -36
  12. package/src/endpoints/organization/dashboard/nolt/CreateNoltTokenEndpoint.ts +2 -1
  13. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -1
  14. package/src/endpoints/organization/dashboard/registration-periods/SetupStepReviewEndpoint.ts +5 -2
  15. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +28 -32
  16. package/src/helpers/AdminPermissionChecker.ts +22 -5
  17. package/src/helpers/AuthenticatedStructures.ts +3 -2
  18. package/src/helpers/SetupStepsUpdater.ts +115 -13
  19. package/src/endpoints/global/payments/ExchangeSTPaymentEndpoint.ts +0 -153
  20. package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.test.ts +0 -64
  21. package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.ts +0 -84
  22. package/src/endpoints/organization/dashboard/organization/GetRegisterCodeEndpoint.ts +0 -65
@@ -58,7 +58,7 @@
58
58
  "LATEST_IOS_VERSION": 0,
59
59
  "LATEST_ANDROID_VERSION": 0,
60
60
 
61
- "NOLT_SSO_SECRET_KEY": "",
61
+ "NOLT_SSO_SECRET_KEY": "optional",
62
62
  "INTERNAL_SECRET_KEY": "",
63
63
  "CRONS_DISABLED": false,
64
64
  "WHITELISTED_EMAIL_DESTINATIONS": ["*"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.19.0",
3
+ "version": "2.21.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -38,11 +38,11 @@
38
38
  "@simonbackx/simple-logging": "^1.0.1",
39
39
  "@stamhoofd/backend-i18n": "^2.17.0",
40
40
  "@stamhoofd/backend-middleware": "^2.17.0",
41
- "@stamhoofd/email": "^2.17.0",
42
- "@stamhoofd/models": "^2.19.0",
41
+ "@stamhoofd/email": "^2.21.0",
42
+ "@stamhoofd/models": "^2.21.0",
43
43
  "@stamhoofd/queues": "^2.17.3",
44
- "@stamhoofd/sql": "^2.18.0",
45
- "@stamhoofd/structures": "^2.19.0",
44
+ "@stamhoofd/sql": "^2.20.0",
45
+ "@stamhoofd/structures": "^2.21.0",
46
46
  "@stamhoofd/utility": "^2.17.0",
47
47
  "archiver": "^7.0.1",
48
48
  "aws-sdk": "^2.885.0",
@@ -60,5 +60,5 @@
60
60
  "postmark": "4.0.2",
61
61
  "stripe": "^16.6.0"
62
62
  },
63
- "gitHead": "876e7b976122e1c2f67d4cceebe92ec2af2bc35a"
63
+ "gitHead": "e6707735a221da68b4e6e669f60f0427ac8acc5d"
64
64
  }
package/src/crons.ts CHANGED
@@ -1,21 +1,14 @@
1
1
  import { Database } from '@simonbackx/simple-database';
2
2
  import { logger, StyledText } from "@simonbackx/simple-logging";
3
3
  import { I18n } from '@stamhoofd/backend-i18n';
4
- import { Email } from '@stamhoofd/email';
5
- import { EmailAddress } from '@stamhoofd/email';
6
- import { Group, STPackage, Webshop } from '@stamhoofd/models';
7
- import { Organization } from '@stamhoofd/models';
8
- import { Payment } from '@stamhoofd/models';
9
- import { Registration } from '@stamhoofd/models';
10
- import { STInvoice } from '@stamhoofd/models';
11
- import { STPendingInvoice } from '@stamhoofd/models';
4
+ import { Email, EmailAddress } from '@stamhoofd/email';
5
+ import { Group, Organization, Payment, Registration, STPackage, STPendingInvoice, Webshop } from '@stamhoofd/models';
12
6
  import { QueueHandler } from '@stamhoofd/queues';
13
7
  import { PaymentMethod, PaymentProvider, PaymentStatus } from '@stamhoofd/structures';
14
8
  import { Formatter, sleep } from '@stamhoofd/utility';
15
9
  import AWS from 'aws-sdk';
16
10
  import { DateTime } from 'luxon';
17
11
 
18
- import { ExchangeSTPaymentEndpoint } from './endpoints/global/payments/ExchangeSTPaymentEndpoint';
19
12
  import { ExchangePaymentEndpoint } from './endpoints/organization/shared/ExchangePaymentEndpoint';
20
13
  import { checkSettlements } from './helpers/CheckSettlements';
21
14
  import { ForwardHandler } from './helpers/ForwardHandler';
@@ -471,12 +464,7 @@ async function checkPayments() {
471
464
  continue;
472
465
  }
473
466
  } else {
474
- // Try stamhoofd payment
475
- const invoices = await STInvoice.where({ paymentId: payment.id })
476
- if (invoices.length === 1) {
477
- await ExchangeSTPaymentEndpoint.pollStatus(payment, invoices[0])
478
- continue
479
- }
467
+ // deprecated
480
468
  }
481
469
 
482
470
  // Check expired
@@ -599,52 +587,6 @@ async function checkReservedUntil() {
599
587
  }
600
588
  }
601
589
 
602
-
603
- // Wait for midnight before checking billing
604
- let lastBillingCheck: Date | null = new Date()
605
- let lastBillingId = ""
606
- async function checkBilling() {
607
- if (STAMHOOFD.environment === "development") {
608
- return
609
- }
610
-
611
- console.log("[BILLING] Checking billing...")
612
-
613
- // Wait for the next day before doing a new check
614
- if (lastBillingCheck && Formatter.dateIso(lastBillingCheck) === Formatter.dateIso(new Date())) {
615
- console.log("[BILLING] Billing check done for today")
616
- return
617
- }
618
-
619
- const organizations = await Organization.where({ id: { sign: '>', value: lastBillingId } }, {
620
- limit: 10,
621
- sort: ["id"]
622
- })
623
-
624
- if (organizations.length == 0) {
625
- // Wait again until next day
626
- lastBillingId = ""
627
- lastBillingCheck = new Date()
628
- return
629
- }
630
-
631
- for (const organization of organizations) {
632
- console.log("[BILLING] Checking billing for "+organization.name)
633
-
634
- try {
635
- await QueueHandler.schedule("billing/invoices-"+organization.id, async () => {
636
- await STPendingInvoice.addAutomaticItems(organization)
637
- });
638
- } catch (e) {
639
- console.error(e)
640
- }
641
-
642
- }
643
-
644
- lastBillingId = organizations[organizations.length - 1].id
645
-
646
- }
647
-
648
590
  let lastDripCheck: Date | null = null
649
591
  let lastDripId = ""
650
592
  async function checkDrips() {
@@ -723,12 +665,6 @@ registeredCronJobs.push({
723
665
  running: false
724
666
  });
725
667
 
726
- registeredCronJobs.push({
727
- name: 'checkBilling',
728
- method: checkBilling,
729
- running: false
730
- });
731
-
732
668
  registeredCronJobs.push({
733
669
  name: 'checkReservedUntil',
734
670
  method: checkReservedUntil,
@@ -1,8 +1,7 @@
1
1
  import { Decoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
- import { Email } from '@stamhoofd/email';
4
- import { PasswordToken, User } from '@stamhoofd/models';
5
- import { ForgotPasswordRequest } from '@stamhoofd/structures';
3
+ import { PasswordToken, sendEmailTemplate, User } from '@stamhoofd/models';
4
+ import { EmailTemplateType, ForgotPasswordRequest, Recipient, Replacement } from '@stamhoofd/structures';
6
5
 
7
6
  import { Context } from '../../helpers/Context';
8
7
 
@@ -29,41 +28,46 @@ export class ForgotPasswordEndpoint extends Endpoint<Params, Query, Body, Respon
29
28
  }
30
29
 
31
30
  async handle(request: DecodedRequest<Params, Query, Body>) {
32
- // for now we care more about UX, so we show a mesage if the user doesn't exist
33
31
  const organization = await Context.setOptionalOrganizationScope()
34
32
  const user = await User.getForAuthentication(organization?.id ?? null, request.body.email, {allowWithoutAccount: true});
35
33
 
36
- const { from, replyTo } = {
37
- from: organization ? organization.getStrongEmail(request.i18n) : Email.getInternalEmailFor(request.i18n),
38
- replyTo: undefined
39
- }
40
- const name = organization ? organization.name : request.i18n.t("shared.platformName");
41
-
42
34
  if (!user) {
43
- // Send email
44
- Email.send({
45
- from,
46
- replyTo,
47
- to: request.body.email,
48
- subject: "["+name+"] Wachtwoord vergeten",
49
- type: "transactional",
50
- text: "Hallo, \n\nJe gaf aan dat je jouw wachtwoord bent vergeten, maar er bestaat geen account op het e-mailadres dat je hebt ingegeven ("+request.body.email+"). Niet zeker meer welk e-mailadres je kan gebruiken? Wij sturen altijd e-mails naar een e-mailadres waarop je een account hebt. Lukt dat niet? Dan moet je je eerst registreren.\n\nMet vriendelijke groeten,\n"+(name),
51
- });
35
+ // Create e-mail builder
36
+ await sendEmailTemplate(organization, {
37
+ recipients: [
38
+ Recipient.create({
39
+ email: request.body.email
40
+ })
41
+ ],
42
+ template: {
43
+ type: EmailTemplateType.ForgotPasswordButNoAccount,
44
+ },
45
+ type: 'transactional'
46
+ })
52
47
 
53
48
  return new Response(undefined)
54
49
  }
55
50
 
56
51
  const recoveryUrl = await PasswordToken.getPasswordRecoveryUrl(user, organization, request.i18n)
57
-
58
- // Send email
59
- Email.send({
60
- from,
61
- replyTo,
62
- to: user.email,
63
- subject: "Wachtwoord vergeten",
64
- type: "transactional",
65
- text: (user.firstName ? "Hey "+user.firstName : "Hey") + ", \n\nJe gaf aan dat je jouw wachtwoord bent vergeten. Je kan een nieuw wachtwoord instellen door op de volgende link te klikken of door deze te kopiëren in de URL-balk van je browser:\n"+recoveryUrl+"\n\nWachtwoord al teruggevonden of heb je helemaal niet aangeduid dat je je wachtwoord vergeten bent? Dan mag je deze e-mail gewoon negeren.\n\nMet vriendelijke groeten,\n"+(user.permissions ? request.i18n.t("shared.platformName") : name)
66
- });
52
+
53
+ // Create e-mail builder
54
+ await sendEmailTemplate(organization, {
55
+ recipients: [
56
+ Recipient.create({
57
+ email: request.body.email,
58
+ replacements: [
59
+ Replacement.create({
60
+ token: 'resetUrl',
61
+ value: recoveryUrl
62
+ })
63
+ ]
64
+ })
65
+ ],
66
+ template: {
67
+ type: EmailTemplateType.ForgotPassword,
68
+ },
69
+ type: 'transactional'
70
+ })
67
71
 
68
72
  return new Response(undefined);
69
73
  }
@@ -4,7 +4,7 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
4
4
  import { SimpleError } from '@simonbackx/simple-errors';
5
5
  import { Email } from '@stamhoofd/email';
6
6
  import { ArchiverWriterAdapter, exportToExcel, XlsxTransformerSheet, XlsxWriter } from '@stamhoofd/excel-writer';
7
- import { getEmailBuilderForTemplate, Platform, RateLimiter } from '@stamhoofd/models';
7
+ import { getEmailBuilderForTemplate, Platform, RateLimiter, sendEmailTemplate } from '@stamhoofd/models';
8
8
  import { EmailTemplateType, ExcelExportRequest, ExcelExportResponse, ExcelExportType, LimitedFilteredRequest, PaginatedResponse, Recipient, Replacement, Version } from '@stamhoofd/structures';
9
9
  import { sleep } from "@stamhoofd/utility";
10
10
  import { Context } from '../../../helpers/Context';
@@ -90,7 +90,7 @@ export class ExportToExcelEndpoint extends Endpoint<Params, Query, Body, Respons
90
90
  const result = await Promise.race([
91
91
  this.job(loader, request.body, request.params.type).then(async (url: string) => {
92
92
  if (sendEmail) {
93
- const builder = await getEmailBuilderForTemplate(organization, {
93
+ await sendEmailTemplate(null, {
94
94
  template: {
95
95
  type: EmailTemplateType.ExcelExportSucceeded
96
96
  },
@@ -99,13 +99,8 @@ export class ExportToExcelEndpoint extends Endpoint<Params, Query, Body, Respons
99
99
  token: 'downloadUrl',
100
100
  value: url
101
101
  }))
102
- ],
103
- from: Email.getInternalEmailFor(Context.i18n)
102
+ ]
104
103
  })
105
-
106
- if (builder) {
107
- Email.schedule(builder)
108
- }
109
104
 
110
105
  }
111
106
 
@@ -9,6 +9,7 @@ import { Formatter } from '@stamhoofd/utility';
9
9
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
10
10
  import { Context } from '../../../helpers/Context';
11
11
  import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer';
12
+ import { SetupStepUpdater } from '../../../helpers/SetupStepsUpdater';
12
13
 
13
14
  type Params = Record<string, never>;
14
15
  type Query = undefined;
@@ -150,6 +151,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
150
151
  await MemberUserSyncer.onChangeMember(member)
151
152
  }
152
153
 
154
+ let shouldUpdateSetupSteps = false;
155
+
153
156
  // Loop all members one by one
154
157
  for (let patch of request.body.getPatches()) {
155
158
  const member = members.find(m => m.id === patch.id) ?? await Member.getWithRegistrations(patch.id)
@@ -223,6 +226,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
223
226
  }
224
227
 
225
228
  await responsibilityRecord.save()
229
+ shouldUpdateSetupSteps = true;
226
230
  }
227
231
 
228
232
  // Create responsibilities
@@ -323,6 +327,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
323
327
  model.startDate = put.startDate
324
328
 
325
329
  await model.save()
330
+ shouldUpdateSetupSteps = true;
326
331
  }
327
332
 
328
333
  // Auto link users based on data
@@ -443,7 +448,6 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
443
448
  updateMembershipMemberIds.add(member.id)
444
449
  }
445
450
 
446
-
447
451
  if (!members.find(m => m.id === member.id)) {
448
452
  members.push(member)
449
453
  }
@@ -460,6 +464,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
460
464
  await User.deleteForDeletedMember(member.id)
461
465
  await BalanceItem.deleteForDeletedMember(member.id)
462
466
  await member.delete()
467
+ shouldUpdateSetupSteps = true
463
468
 
464
469
  // Update occupancy of this member because we removed registrations
465
470
  const groupIds = member.registrations.flatMap(r => r.groupId)
@@ -498,6 +503,10 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
498
503
  }
499
504
  }
500
505
 
506
+ if(shouldUpdateSetupSteps && organization) {
507
+ SetupStepUpdater.updateForOrganization(organization).catch(console.error);
508
+ }
509
+
501
510
  return new Response(
502
511
  await AuthenticatedStructures.membersBlob(members)
503
512
  );
@@ -1,15 +1,10 @@
1
1
 
2
2
  import { Request } from "@simonbackx/simple-endpoints";
3
- import { Organization, OrganizationFactory, RegisterCodeFactory, STCredit } from "@stamhoofd/models";
4
3
  import { Address, Country, CreateOrganization, NewUser, Organization as OrganizationStruct, Version } from "@stamhoofd/structures";
5
4
 
6
5
  import { testServer } from "../../../../tests/helpers/TestServer";
7
6
  import { CreateOrganizationEndpoint } from "./CreateOrganizationEndpoint";
8
7
 
9
- function expect_toBeDefined<T>(arg: T): asserts arg is NonNullable<T> {
10
- expect(arg).toBeDefined();
11
- }
12
-
13
8
  describe("Endpoint.CreateOrganization", () => {
14
9
  // Test endpoint
15
10
  const endpoint = new CreateOrganizationEndpoint();
@@ -60,46 +55,4 @@ describe("Endpoint.CreateOrganization", () => {
60
55
 
61
56
  await expect(testServer.test(endpoint, r)).rejects.toThrow(/name/);
62
57
  });
63
-
64
- test("Can create an organization with a register code and apply the discount", async () => {
65
- const otherOrganization = await new OrganizationFactory({}).create();
66
- const code = await new RegisterCodeFactory({organization: otherOrganization}).create();
67
- const uri = 'my-organization-with-a-discount';
68
-
69
- const r = Request.buildJson(
70
- "POST",
71
- "/organizations",
72
- "todo-host.be",
73
- CreateOrganization.create({
74
- organization: OrganizationStruct.create({
75
- name: "My organization with a discount",
76
- uri,
77
- address: Address.create({
78
- street: "My street",
79
- number: "1",
80
- postalCode: "9000",
81
- city: "Gent",
82
- country: Country.Belgium
83
- }),
84
- }),
85
- user: NewUser.create({
86
- email: "voorbeeld@stamhoofd.be",
87
- password: "My user password",
88
- }),
89
- registerCode: code.code
90
- })
91
- );
92
-
93
- const response = await testServer.test(endpoint, r);
94
- expect(response.body.token).not.toBeEmpty();
95
-
96
- const organization = await Organization.getByURI(uri);
97
- expect_toBeDefined(organization);
98
-
99
- // Check if this organization has an open register code
100
- const credits = await STCredit.getForOrganization(organization.id);
101
- expect(credits.length).toBe(1);
102
- expect(credits[0].change).toBe(code.value);
103
- });
104
-
105
58
  });
@@ -1,9 +1,7 @@
1
- import { Model } from '@simonbackx/simple-database';
2
1
  import { Decoder } from '@simonbackx/simple-encoding';
3
2
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
4
3
  import { SimpleError } from '@simonbackx/simple-errors';
5
- import { Email, EmailInterfaceBase } from '@stamhoofd/email';
6
- import { EmailVerificationCode, Organization, RegisterCode, User } from '@stamhoofd/models';
4
+ import { EmailVerificationCode, Organization, User } from '@stamhoofd/models';
7
5
  import { CreateOrganization, PermissionLevel, Permissions, SignupResponse, UserPermissions } from "@stamhoofd/structures";
8
6
  import { Formatter } from "@stamhoofd/utility";
9
7
 
@@ -83,14 +81,14 @@ export class CreateOrganizationEndpoint extends Endpoint<Params, Query, Body, Re
83
81
  organization.name = request.body.organization.name;
84
82
 
85
83
  // Delay save until after organization is saved, but do validations before the organization is saved
86
- let registerCodeModels: Model[] = []
87
- let delayEmails: EmailInterfaceBase[] = []
84
+ // let registerCodeModels: Model[] = []
85
+ // let delayEmails: EmailInterfaceBase[] = []
88
86
 
89
- if (request.body.registerCode) {
90
- const applied = await RegisterCode.applyRegisterCode(organization, request.body.registerCode)
91
- registerCodeModels = applied.models
92
- delayEmails = applied.emails
93
- }
87
+ //if (request.body.registerCode) {
88
+ // const applied = await RegisterCode.applyRegisterCode(organization, request.body.registerCode)
89
+ // registerCodeModels = applied.models
90
+ // delayEmails = applied.emails
91
+ //}
94
92
 
95
93
  organization.uri = uri;
96
94
  organization.meta = request.body.organization.meta
@@ -126,16 +124,16 @@ export class CreateOrganizationEndpoint extends Endpoint<Params, Query, Body, Re
126
124
  user.permissions.organizationPermissions.set(organization.id, Permissions.create({ level: PermissionLevel.Full }))
127
125
  await user.save()
128
126
 
129
- for (const model of registerCodeModels) {
130
- await model.save()
131
- }
127
+ // for (const model of registerCodeModels) {
128
+ // await model.save()
129
+ // }
132
130
 
133
131
  const code = await EmailVerificationCode.createFor(user, user.email)
134
132
  code.send(user, organization, request.i18n)
135
133
 
136
- for (const email of delayEmails) {
137
- Email.sendInternal(email, organization.i18n)
138
- }
134
+ // for (const email of delayEmails) {
135
+ // Email.sendInternal(email, organization.i18n)
136
+ // }
139
137
 
140
138
  return new Response(SignupResponse.create({
141
139
  token: code.token
@@ -45,6 +45,14 @@ export class GetOrganizationFromUriEndpoint extends Endpoint<Params, Query, Body
45
45
  statusCode: 404
46
46
  })
47
47
  }
48
+
49
+ if (!organization.active) {
50
+ throw new SimpleError({
51
+ code: "archived_organization",
52
+ message: "This organization has been archived",
53
+ statusCode: 404
54
+ })
55
+ }
48
56
  return new Response(await AuthenticatedStructures.organization(organization));
49
57
  }
50
58
  }
@@ -45,7 +45,7 @@ export class SearchOrganizationEndpoint extends Endpoint<Params, Query, Body, Re
45
45
  };
46
46
 
47
47
  // We had to add an order by in the query to fix the limit. MySQL doesn't want to limit the results correctly if we don't explicitly sort the results on their relevance
48
- const organizations = await Organization.where({ searchIndex: match }, {
48
+ const organizations = await Organization.where({ searchIndex: match, active: 1 }, {
49
49
  limit: 15,
50
50
  sort: [
51
51
  {