@stamhoofd/backend 2.9.0 → 2.14.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.
@@ -814,7 +814,7 @@ export class AdminPermissionChecker {
814
814
  async hasFinancialMemberAccess(member: MemberWithRegistrations, level: PermissionLevel = PermissionLevel.Read): Promise<boolean> {
815
815
  const isUserManager = this.isUserManager(member)
816
816
 
817
- if (isUserManager) {
817
+ if (isUserManager && level === PermissionLevel.Read) {
818
818
  return true;
819
819
  }
820
820
 
@@ -846,6 +846,15 @@ export class AdminPermissionChecker {
846
846
  continue;
847
847
  }
848
848
 
849
+ if (isUserManager) {
850
+ // Requirements are higher: you need financial access to write your own financial
851
+ // data changes
852
+ if (permissions.hasAccessRight(AccessRight.OrganizationManagePayments)) {
853
+ return true;
854
+ }
855
+ continue;
856
+ }
857
+
849
858
  if (permissions.hasAccessRight(level === PermissionLevel.Read ? AccessRight.MemberReadFinancialData : AccessRight.MemberWriteFinancialData)) {
850
859
  return true;
851
860
  }
@@ -854,8 +863,16 @@ export class AdminPermissionChecker {
854
863
  // Platform data
855
864
  const platformPermissions = this.platformPermissions
856
865
  if (platformPermissions) {
857
- if (platformPermissions.hasAccessRight(level === PermissionLevel.Read ? AccessRight.MemberReadFinancialData : AccessRight.MemberWriteFinancialData)) {
858
- return true;
866
+ if (isUserManager) {
867
+ // Requirements are higher: you need financial access to write your own financial
868
+ // data changes
869
+ if (platformPermissions.hasAccessRight(AccessRight.OrganizationManagePayments)) {
870
+ return true;
871
+ }
872
+ } else {
873
+ if (platformPermissions.hasAccessRight(level === PermissionLevel.Read ? AccessRight.MemberReadFinancialData : AccessRight.MemberWriteFinancialData)) {
874
+ return true;
875
+ }
859
876
  }
860
877
  }
861
878
 
@@ -950,94 +967,97 @@ export class AdminPermissionChecker {
950
967
  const hasRecordAnswers = !!data.details.recordAnswers;
951
968
  const hasNotes = data.details.notes !== undefined;
952
969
  const isSetFinancialSupportTrue = data.details.requiresFinancialSupport?.value === true;
970
+ const isUserManager = this.isUserManager(member);
953
971
 
954
- if(hasRecordAnswers || hasNotes || isSetFinancialSupportTrue) {
955
- const isUserManager = this.isUserManager(member);
972
+ if (hasRecordAnswers) {
973
+ if (!(data.details.recordAnswers instanceof PatchMap)) {
974
+ throw new SimpleError({
975
+ code: 'invalid_request',
976
+ message: 'Cannot PUT recordAnswers',
977
+ statusCode: 400
978
+ })
979
+ }
980
+
981
+ const records = isUserManager ? new Set() : await this.getAccessibleRecordSet(member, PermissionLevel.Write)
956
982
 
957
- if (hasRecordAnswers) {
958
- if (!(data.details.recordAnswers instanceof PatchMap)) {
959
- throw new SimpleError({
960
- code: 'invalid_request',
961
- message: 'Cannot PUT recordAnswers',
962
- statusCode: 400
963
- })
964
- }
965
-
966
- const records = isUserManager ? new Set() : await this.getAccessibleRecordSet(member, PermissionLevel.Write)
967
-
968
- for (const [key, value] of data.details.recordAnswers.entries()) {
969
- let name: string | undefined = undefined
970
- if (value) {
971
- if (value.isPatch()) {
972
- throw new SimpleError({
973
- code: 'invalid_request',
974
- message: 'Cannot PATCH a record answer object',
975
- statusCode: 400
976
- })
977
- }
978
-
979
- const id = value.settings.id
980
-
981
- if (id !== key) {
982
- throw new SimpleError({
983
- code: 'invalid_request',
984
- message: 'Record answer key does not match record id',
985
- statusCode: 400
986
- })
987
- }
988
-
989
- name = value.settings.name
983
+ for (const [key, value] of data.details.recordAnswers.entries()) {
984
+ let name: string | undefined = undefined
985
+ if (value) {
986
+ if (value.isPatch()) {
987
+ throw new SimpleError({
988
+ code: 'invalid_request',
989
+ message: 'Cannot PATCH a record answer object',
990
+ statusCode: 400
991
+ })
990
992
  }
991
993
 
992
- if (!isUserManager && !records.has(key)) {
994
+ const id = value.settings.id
995
+
996
+ if (id !== key) {
993
997
  throw new SimpleError({
994
- code: 'permission_denied',
995
- message: `Je hebt geen toegangsrechten om het antwoord op ${name ?? 'deze vraag'} aan te passen voor dit lid`,
998
+ code: 'invalid_request',
999
+ message: 'Record answer key does not match record id',
996
1000
  statusCode: 400
997
1001
  })
998
1002
  }
1003
+
1004
+ name = value.settings.name
999
1005
  }
1000
- }
1001
1006
 
1002
- if(hasNotes && isUserManager) {
1003
- throw new SimpleError({
1004
- code: 'permission_denied',
1005
- message: 'Cannot edit notes',
1006
- statusCode: 400
1007
- })
1007
+ if (!isUserManager && !records.has(key)) {
1008
+ throw new SimpleError({
1009
+ code: 'permission_denied',
1010
+ message: `Je hebt geen toegangsrechten om het antwoord op ${name ?? 'deze vraag'} aan te passen voor dit lid`,
1011
+ statusCode: 400
1012
+ })
1013
+ }
1008
1014
  }
1015
+ }
1009
1016
 
1010
- if(isSetFinancialSupportTrue) {
1011
- const financialSupport = this.platform.config.recordsConfiguration.financialSupport;
1012
- const preventSelfAssignment = financialSupport?.preventSelfAssignment === true;
1017
+ if (hasNotes && isUserManager) {
1018
+ throw new SimpleError({
1019
+ code: 'permission_denied',
1020
+ message: 'Cannot edit notes',
1021
+ statusCode: 400
1022
+ })
1023
+ }
1013
1024
 
1025
+ // Has financial write access?
1026
+ if (!await this.hasFinancialMemberAccess(member, PermissionLevel.Write)) {
1027
+ if (isUserManager && isSetFinancialSupportTrue) {
1028
+ const financialSupportSettings = this.platform.config.financialSupport;
1029
+ const preventSelfAssignment = financialSupportSettings?.preventSelfAssignment === true;
1030
+
1014
1031
  if(preventSelfAssignment) {
1015
1032
  throw new SimpleError({
1016
1033
  code: 'permission_denied',
1017
- message: 'Je hebt geen toegangsrechten om de financiële status van dit lid aan te passen',
1018
- human: financialSupport.preventSelfAssignmentText ?? FinancialSupportSettings.defaultPreventSelfAssignmentText,
1034
+ message: 'No permissions to enable financial support for your own members',
1035
+ human: financialSupportSettings.preventSelfAssignmentText ?? FinancialSupportSettings.defaultPreventSelfAssignmentText,
1019
1036
  statusCode: 400
1020
1037
  });
1021
1038
  }
1022
1039
  }
1023
- }
1024
-
1025
- // Has financial write access?
1026
- if (!await this.hasFinancialMemberAccess(member, PermissionLevel.Write)) {
1040
+
1027
1041
  if (data.details.requiresFinancialSupport) {
1028
- throw new SimpleError({
1029
- code: 'permission_denied',
1030
- message: 'Je hebt geen toegangsrechten om de financiële status van dit lid aan te passen',
1031
- statusCode: 400
1032
- })
1042
+ if (isUserManager) {
1043
+ // Already handled
1044
+ } else {
1045
+ throw new SimpleError({
1046
+ code: 'permission_denied',
1047
+ message: 'Je hebt geen toegangsrechten om de financiële status van dit lid aan te passen',
1048
+ statusCode: 400
1049
+ })
1050
+ }
1033
1051
  }
1034
1052
 
1035
- if (data.details.uitpasNumber) {
1036
- throw new SimpleError({
1037
- code: 'permission_denied',
1038
- message: 'Je hebt geen toegangsrechten om het UiTPAS-nummer van dit lid aan te passen',
1039
- statusCode: 400
1040
- })
1053
+ if (!isUserManager) {
1054
+ if (data.details.uitpasNumber) {
1055
+ throw new SimpleError({
1056
+ code: 'permission_denied',
1057
+ message: 'Je hebt geen toegangsrechten om het UiTPAS-nummer van dit lid aan te passen',
1058
+ statusCode: 400
1059
+ })
1060
+ }
1041
1061
  }
1042
1062
 
1043
1063
  if (data.outstandingBalance) {
@@ -36,8 +36,14 @@ export class MemberUserSyncerStatic {
36
36
  } else {
37
37
  // Only auto unlink users that do not have an account
38
38
  for (const user of member.users) {
39
- if (!user.hasAccount() && !userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
40
- await this.unlinkUser(user, member)
39
+ if (!userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
40
+ if (!user.hasAccount()) {
41
+ await this.unlinkUser(user, member)
42
+ } else {
43
+ // Make sure only linked as a parent, not as user self
44
+ // This makes sure we don't inherit permissions and aren't counted as 'begin' the member
45
+ await this.linkUser(user.email, member, true)
46
+ }
41
47
  }
42
48
  }
43
49
  }
@@ -0,0 +1,151 @@
1
+ import { AutoEncoderPatchType } from '@simonbackx/simple-encoding';
2
+ import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
3
+ import { Company, Country } from '@stamhoofd/structures';
4
+ import axios from 'axios';
5
+ import * as jsvat from 'jsvat-next'; // has no default export, so we need the wildcard
6
+
7
+ export class ViesHelperStatic {
8
+ testMode = false;
9
+
10
+ async request(method: "GET" | "POST", url: string, content: any) {
11
+
12
+ const json = content ? JSON.stringify(content) : "";
13
+
14
+ console.log("[VIES REQUEST]", method, url, content ? "\n [VIES REQUEST] " : undefined, json)
15
+
16
+ const response = await axios.request({
17
+ method,
18
+ url,
19
+ headers: {
20
+ 'Content-Type': json.length > 0 ? 'application/json' : "text/plain",
21
+ },
22
+ data: json
23
+
24
+ })
25
+ console.log("[VIES RESPONSE]", method, url, "\n[VIES RESPONSE]", JSON.stringify(response.data))
26
+ return response.data
27
+ }
28
+
29
+ async checkCompany(company: Company, patch: AutoEncoderPatchType<Company>|Company) {
30
+ if (!company.address) {
31
+ // Not allowed to set
32
+ patch.companyNumber = null;
33
+ patch.VATNumber = null;
34
+ return;
35
+ }
36
+
37
+ if (company.VATNumber !== null) {
38
+ // Changed VAT number
39
+ patch.VATNumber = await ViesHelper.checkVATNumber(company.address.country, company.VATNumber)
40
+
41
+ if (company.address.country === Country.Belgium) {
42
+ patch.companyNumber = company.VATNumber
43
+ }
44
+ }
45
+
46
+ if (company.companyNumber) {
47
+ if (company.VATNumber !== null && company.address.country === Country.Belgium) {
48
+ // Already validated
49
+ } else {
50
+ // Need to validate
51
+ const result = await ViesHelper.checkCompanyNumber(company.address.country, company.companyNumber)
52
+ patch.companyNumber = result.companyNumber
53
+ if (result.VATNumber !== undefined) {
54
+ patch.VATNumber = result.VATNumber
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ async checkCompanyNumber(country: Country, companyNumber: string): Promise<{companyNumber: string|null, VATNumber?: string|null}> {
61
+ if (country !== Country.Belgium) {
62
+ // Not supported
63
+ return {
64
+ companyNumber
65
+ };
66
+ }
67
+
68
+ // In Belgium, the company number syntax is the same as VAT number
69
+
70
+ const result = jsvat.checkVAT(companyNumber, [jsvat.belgium]);
71
+
72
+ if (!result.isValid) {
73
+ throw new SimpleError({
74
+ "code": "invalid_field",
75
+ "message": "Ongeldig ondernemingsnummer: " + companyNumber,
76
+ "field": "companyNumber"
77
+ })
78
+ }
79
+
80
+ // If this is a valid VAT number, we can assume it's a valid company number
81
+ try {
82
+ const corrected = await this.checkVATNumber(Country.Belgium, companyNumber);
83
+
84
+ // this is a VAT number, not a company number
85
+ return {
86
+ companyNumber: corrected,
87
+ VATNumber: corrected
88
+ }
89
+ } catch (e) {
90
+ if (isSimpleError(e) || isSimpleErrors(e)) {
91
+ // Ignore: normal that it is not a valid VAT number
92
+ } else {
93
+ // Other errors should be thrown
94
+ throw e;
95
+ }
96
+ }
97
+
98
+ return {
99
+ companyNumber: result.value ?? companyNumber,
100
+
101
+ // VATNumber should always be set to null if it is not a valid VAT number
102
+ VATNumber: null
103
+ };
104
+ }
105
+
106
+ async checkVATNumber(country: Country, vatNumber: string): Promise<string> {
107
+ const result = jsvat.checkVAT(vatNumber, country === Country.Belgium ? [jsvat.belgium] : [jsvat.netherlands]);
108
+
109
+ if (!result.isValid) {
110
+ throw new SimpleError({
111
+ "code": "invalid_field",
112
+ "message": "Ongeldig BTW-nummer: " + vatNumber,
113
+ "field": "VATNumber"
114
+ })
115
+ }
116
+
117
+ const formatted = result.value ?? vatNumber;
118
+
119
+ try {
120
+ const cleaned = formatted.substring(2).replace(/(\.\-\s)+/g, "")
121
+ const response = await this.request("POST", "https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number", {
122
+ countryCode: country,
123
+ vatNumber: cleaned
124
+ });
125
+
126
+ if (typeof response !== 'object' || response === null || typeof response.valid !== 'boolean') {
127
+ // APi error
128
+ throw new Error("Invalid response from VIES")
129
+ }
130
+
131
+ if (!response.valid) {
132
+ throw new SimpleError({
133
+ "code": "invalid_field",
134
+ "message": "Het opgegeven BTW-nummer is ongeldig of niet BTW-plichtig: " + formatted,
135
+ "field": "VATNumber"
136
+ })
137
+ }
138
+ } catch (e) {
139
+ if (isSimpleError(e) || isSimpleErrors(e)) {
140
+ throw e;
141
+ }
142
+ // Unavailable: ignore for now
143
+ console.error('VIES error', e);
144
+ }
145
+
146
+ return formatted;
147
+ }
148
+
149
+ }
150
+
151
+ export const ViesHelper = new ViesHelperStatic();
package/.env.json DELETED
@@ -1,65 +0,0 @@
1
- {
2
- "environment": "development",
3
- "domains": {
4
- "dashboard": "dashboard.stamhoofd",
5
- "registration": {
6
- "": "be.stamhoofd",
7
- "BE": "be.stamhoofd",
8
- "NL": "nl.stamhoofd"
9
- },
10
- "marketing": {
11
- "": "www.be.stamhoofd",
12
- "BE": "www.be.stamhoofd",
13
- "NL": "www.nl.stamhoofd"
14
- },
15
- "webshop": {
16
- "": "shop.be.stamhoofd",
17
- "BE": "shop.be.stamhoofd",
18
- "NL": "shop.nl.stamhoofd"
19
- },
20
- "webshopPrefix": "shop",
21
- "legacyWebshop": "shop.stamhoofd",
22
- "api": "api.stamhoofd",
23
- "demoApi": "api.stamhoofd",
24
- "rendererApi": "renderer.stamhoofd"
25
- },
26
- "translationNamespace": "digit",
27
- "userMode": "platform",
28
-
29
- "PORT": 9091,
30
- "DB_HOST": "127.0.0.1",
31
- "DB_USER": "root",
32
- "DB_PASS": "root",
33
- "DB_DATABASE": "ksa-stamhoofd",
34
-
35
- "SMTP_HOST": "0.0.0.0",
36
- "SMTP_USERNAME": "username",
37
- "SMTP_PASSWORD": "password",
38
- "SMTP_PORT": 1025,
39
-
40
- "TRANSACTIONAL_SMTP_HOST": "0.0.0.0",
41
- "TRANSACTIONAL_SMTP_USERNAME": "username",
42
- "TRANSACTIONAL_SMTP_PASSWORD": "password",
43
- "TRANSACTIONAL_SMTP_PORT": 1025,
44
-
45
- "AWS_ACCESS_KEY_ID": "",
46
- "AWS_SECRET_ACCESS_KEY": "",
47
- "AWS_REGION": "",
48
-
49
- "SPACES_ENDPOINT": "",
50
- "SPACES_BUCKET": "",
51
- "SPACES_KEY": "",
52
- "SPACES_SECRET": "",
53
-
54
- "MOLLIE_CLIENT_ID": "",
55
- "MOLLIE_SECRET": "",
56
- "MOLLIE_API_KEY": "",
57
- "MOLLIE_ORGANIZATION_TOKEN": "",
58
-
59
- "LATEST_IOS_VERSION": 0,
60
- "LATEST_ANDROID_VERSION": 0,
61
-
62
- "NOLT_SSO_SECRET_KEY": "",
63
- "INTERNAL_SECRET_KEY": "",
64
- "CRONS_DISABLED": false
65
- }