@stamhoofd/backend 2.144.0 → 2.145.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/package.json +19 -19
- package/src/crons/mollie-chargebacks.ts +31 -29
- package/src/crons/mollie-refunds.ts +61 -59
- package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +16 -5
- package/src/endpoints/auth/CreateTokenEndpoint.test.ts +210 -4
- package/src/endpoints/auth/CreateTokenEndpoint.ts +9 -20
- package/src/endpoints/auth/DeleteTokenEndpoint.test.ts +29 -0
- package/src/endpoints/auth/DeleteTokenEndpoint.ts +2 -1
- package/src/endpoints/auth/GetUserEndpoint.test.ts +32 -11
- package/src/endpoints/auth/MFA.security.test.ts +9 -7
- package/src/endpoints/auth/MFA.test.ts +42 -15
- package/src/endpoints/auth/PatchUserEndpoint.ts +3 -2
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +5 -4
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +4 -3
- package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +6 -5
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +5 -4
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +5 -4
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +2 -1
- package/src/endpoints/global/events/PatchEventsEndpoint.test.ts +2 -1
- package/src/endpoints/global/files/UploadFile.test.ts +2 -1
- package/src/endpoints/global/files/UploadImage.test.ts +2 -1
- package/src/endpoints/global/files/upload-security.test.ts +3 -2
- package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +4 -3
- package/src/endpoints/global/members/GetMembersEndpoint.test.ts +26 -25
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +57 -56
- package/src/endpoints/global/members/SendMemberSecurityCodeEndpoint.test.ts +4 -3
- package/src/endpoints/global/platform/GetPlatformEndpoint.test.ts +6 -4
- package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +6 -6
- package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.test.ts +9 -8
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +14 -13
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +24 -23
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +4 -3
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +2 -1
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +2 -1
- package/src/endpoints/global/webshops/GetWebshopsEndpoint.test.ts +20 -19
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemBreakdownEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/billing/ApplyRegisterCodeEndpoint.test.ts +3 -2
- package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +4 -3
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +0 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +23 -22
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +4 -4
- package/src/endpoints/organization/dashboard/payments/GetPaymentBreakdownEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.test.ts +4 -3
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/registration-periods/MoveRegistrationPeriods.test.ts +7 -6
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.test.ts +9 -8
- package/src/endpoints/organization/dashboard/settlements/SettlementsExportEndpoint.test.ts +3 -2
- package/src/endpoints/organization/dashboard/settlements/SettlementsSyncEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +2 -1
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.test.ts +3 -2
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +6 -5
- package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.test.ts +16 -15
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/webshops/WebshopReservedPathSegments.test.ts +2 -1
- package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +4 -3
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +6 -5
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +94 -8
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +18 -18
- package/src/helpers/Context.ts +13 -6
- package/src/helpers/ContextStorage.ts +5 -0
- package/src/helpers/StripeSettlementSync.ts +10 -7
- package/src/helpers/TwoFactorHelper.ts +21 -22
- package/src/services/AdminSessionService.ts +5 -3
- package/src/services/InvoiceService.ts +2 -2
- package/src/services/MollieService.ts +168 -126
- package/src/services/PaymentService.ts +2 -2
- package/src/services/SSOService.test.ts +25 -1
- package/src/services/SSOService.ts +17 -6
- package/src/services/SessionService.test.ts +319 -0
- package/src/services/SessionService.ts +357 -0
- package/src/services/WebshopCrowdfundingService.test.ts +4 -3
- package/src/sql-filters/registrations.ts +59 -1
- package/tests/actions/patchUserMember.ts +2 -1
- package/tests/e2e/api-rate-limits.test.ts +2 -1
- package/tests/e2e/bundle-discounts.test.ts +4 -3
- package/tests/e2e/charge-members.test.ts +6 -5
- package/tests/e2e/documents.test.ts +4 -3
- package/tests/e2e/private-files.test.ts +9 -8
- package/tests/e2e/register.test.ts +4 -3
- package/tests/e2e/stock.test.ts +4 -3
- package/tests/e2e/tickets.test.ts +4 -3
- package/tests/helpers/ExportSlice.ts +2 -1
- package/tests/helpers/MollieMocker.ts +85 -23
- package/tests/init/initAdmin.ts +2 -1
- package/tests/init/initPlatformAdmin.ts +2 -1
- package/src/helpers/ViesHelper.test.ts +0 -401
- package/src/helpers/ViesHelper.ts +0 -222
- package/src/services/PeppolDirectoryService.test.ts +0 -139
- package/src/services/PeppolDirectoryService.ts +0 -111
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { Database } from '@simonbackx/simple-database';
|
|
2
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
3
|
+
import { Token, User, UserSession } from '@stamhoofd/models';
|
|
4
|
+
import type { SessionDurations, SessionType } from '@stamhoofd/models/constants/sessions.js';
|
|
5
|
+
import { ACCESS_TOKEN_DURATION, SESSION_DURATIONS, SSO_HANDOFF_TOKEN_DURATION } from '@stamhoofd/models/constants/sessions.js';
|
|
6
|
+
import { SQLWhereSign } from '@stamhoofd/sql';
|
|
7
|
+
import { SessionClientType, SessionDeviceType, SessionLoginMethod, SessionOS } from '@stamhoofd/structures';
|
|
8
|
+
import type { SessionMetaData } from '@stamhoofd/structures';
|
|
9
|
+
import crypto from 'crypto';
|
|
10
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
11
|
+
|
|
12
|
+
import { contextStorage } from '../helpers/ContextStorage.js';
|
|
13
|
+
|
|
14
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
15
|
+
const MAX_SESSIONS = 15;
|
|
16
|
+
const API_TOKEN_DURATION = 5 * 365 * DAY;
|
|
17
|
+
|
|
18
|
+
async function randomBytes(size: number): Promise<Buffer> {
|
|
19
|
+
return await new Promise((resolve, reject) => {
|
|
20
|
+
crypto.randomBytes(size, (error, buffer) => error ? reject(error) : resolve(buffer));
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class SessionService {
|
|
25
|
+
/**
|
|
26
|
+
* @param authenticatedAt Pass the current date when a primary credential was verified
|
|
27
|
+
* as part of this request, so the session counts as "fresh" for sensitive actions.
|
|
28
|
+
*/
|
|
29
|
+
static async createSession<U extends User>(user: U, { loginMethod = SessionLoginMethod.Password, authenticatedAt = null }: { loginMethod?: SessionLoginMethod; authenticatedAt?: Date | null } = {}): Promise<Token & { user: U }> {
|
|
30
|
+
const tokenId = uuidv4();
|
|
31
|
+
const session = await this.createUserSession(user, {
|
|
32
|
+
tokenId,
|
|
33
|
+
clientType: this.getClientType(),
|
|
34
|
+
loginMethod,
|
|
35
|
+
metaData: this.getMetaData(),
|
|
36
|
+
});
|
|
37
|
+
const token = await this.createToken(user, { session, tokenId });
|
|
38
|
+
token.authenticatedAt = authenticatedAt;
|
|
39
|
+
this.applyLimits({ token, session, user });
|
|
40
|
+
await token.save();
|
|
41
|
+
return token;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Used by the SSO callback: that is a browser redirect, so the only way to hand the
|
|
46
|
+
* session to the client is in the URL. Only the refresh token travels along, and the
|
|
47
|
+
* client exchanges it for a usable access token.
|
|
48
|
+
*/
|
|
49
|
+
static async createSSOHandoff<U extends User>(user: U, { clientType = this.getClientType(), metaData = this.getMetaData() }: { clientType?: SessionClientType; metaData?: SessionMetaData } = {}): Promise<Token & { user: U }> {
|
|
50
|
+
const tokenId = uuidv4();
|
|
51
|
+
const session = await this.createUserSession(user, { tokenId, clientType, loginMethod: SessionLoginMethod.SSO, metaData });
|
|
52
|
+
const token = await this.createToken(user, { session, tokenId });
|
|
53
|
+
|
|
54
|
+
token.accessTokenValidUntil = new Date(Date.now() - 31 * DAY);
|
|
55
|
+
token.accessTokenValidUntil.setMilliseconds(0);
|
|
56
|
+
this.applyLimits({ token, session, user });
|
|
57
|
+
token.refreshTokenValidUntil = new Date(Math.min(token.refreshTokenValidUntil.getTime(), Date.now() + SSO_HANDOFF_TOKEN_DURATION));
|
|
58
|
+
token.refreshTokenValidUntil.setMilliseconds(0);
|
|
59
|
+
await token.save();
|
|
60
|
+
return token;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static async createApiSession<U extends User>(user: U): Promise<Token & { user: U }> {
|
|
64
|
+
const tokenId = uuidv4();
|
|
65
|
+
const session = await this.createUserSession(user, {
|
|
66
|
+
tokenId,
|
|
67
|
+
clientType: SessionClientType.Browser,
|
|
68
|
+
loginMethod: SessionLoginMethod.Password,
|
|
69
|
+
metaData: this.getFallbackMetaData(),
|
|
70
|
+
});
|
|
71
|
+
const token = await this.createToken(user, { session, tokenId });
|
|
72
|
+
token.accessTokenValidUntil = new Date(Date.now() + API_TOKEN_DURATION);
|
|
73
|
+
token.accessTokenValidUntil.setMilliseconds(0);
|
|
74
|
+
token.refreshTokenValidUntil = new Date(token.accessTokenValidUntil);
|
|
75
|
+
await token.save();
|
|
76
|
+
return token;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The new token continues the session of the old one, so it keeps its start date and
|
|
81
|
+
* cannot outlive it. Using the session is what postpones the inactivity limit.
|
|
82
|
+
*/
|
|
83
|
+
static async rotateSession<U extends User>(oldToken: Token & { user: U }): Promise<Token & { user: U }> {
|
|
84
|
+
const session = await UserSession.getByID(oldToken.sessionId);
|
|
85
|
+
if (!session || session.userId !== oldToken.userId) {
|
|
86
|
+
throw this.invalidRefreshTokenError();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const durations = this.getDurations(session, oldToken.user);
|
|
90
|
+
if (durations.session !== null && session.startedAt.getTime() + durations.session <= Date.now()) {
|
|
91
|
+
await oldToken.delete();
|
|
92
|
+
throw this.invalidRefreshTokenError();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const isSSOHandoff = this.isSSOHandoff(oldToken, session);
|
|
96
|
+
const token = await this.createToken(oldToken.user, { session });
|
|
97
|
+
|
|
98
|
+
this.applyLimits({ token, session, user: oldToken.user });
|
|
99
|
+
await token.save();
|
|
100
|
+
|
|
101
|
+
const didRotate = isSSOHandoff
|
|
102
|
+
? await this.consumeSSOHandoff({ sessionId: session.id, handoffTokenId: oldToken.id, tokenId: token.id })
|
|
103
|
+
: await this.replaceLastUsedToken({ sessionId: session.id, previousTokenId: oldToken.id, tokenId: token.id });
|
|
104
|
+
if (!didRotate) {
|
|
105
|
+
await token.delete();
|
|
106
|
+
throw this.invalidRefreshTokenError();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const metaData = this.getMetaData();
|
|
110
|
+
session.appVersion = metaData.appVersion ?? session.appVersion;
|
|
111
|
+
session.nativeAppVersion = metaData.nativeAppVersion ?? session.nativeAppVersion;
|
|
112
|
+
session.osVersion = metaData.osVersion ?? session.osVersion;
|
|
113
|
+
await session.save();
|
|
114
|
+
return token;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private static isSSOHandoff(token: Token, session: UserSession): boolean {
|
|
118
|
+
// Handoff access tokens expire before they are created; normal access tokens do not.
|
|
119
|
+
return session.loginMethod === SessionLoginMethod.SSO && token.accessTokenValidUntil < token.createdAt;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static async getByRefreshToken(refreshToken: string) {
|
|
123
|
+
const token = await Token.getByRefreshToken(refreshToken);
|
|
124
|
+
if (!token || token.user.isApiUser) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
if (token.refreshTokenValidUntil < new Date()) {
|
|
128
|
+
console.error('Detected an expired refresh token, deleting all sessions and tokens for user', token.userId);
|
|
129
|
+
await UserSession.delete().where('userId', token.userId).delete();
|
|
130
|
+
await Token.delete().where('userId', token.userId).delete();
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
return token;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
static async activateToken(token: Token): Promise<boolean> {
|
|
137
|
+
const [result] = await Database.update(
|
|
138
|
+
'UPDATE `user_sessions` SET `lastActiveTokenId` = ? WHERE `id` = ? AND `lastUsedTokenId` = ?',
|
|
139
|
+
[token.id, token.sessionId, token.id],
|
|
140
|
+
);
|
|
141
|
+
if (result.affectedRows > 0) {
|
|
142
|
+
await Database.delete('DELETE FROM `tokens` WHERE `sessionId` = ? AND `id` != ?', [token.sessionId, token.id]);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const [sessions] = await Database.select('SELECT `id` FROM `user_sessions` WHERE `id` = ? AND `lastActiveTokenId` = ? LIMIT 1', [token.sessionId, token.id]);
|
|
147
|
+
return sessions.length > 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static async endSession(token: Token): Promise<void> {
|
|
151
|
+
await UserSession.delete().where('id', token.sessionId).delete();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
static async deleteOtherSessions({ userId, keepAccessToken }: { userId: string; keepAccessToken: string | null }): Promise<number> {
|
|
155
|
+
const user = await User.getByID(userId);
|
|
156
|
+
if (user?.isApiUser) {
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
return await this.deleteForUsers({ userIds: [userId], keepAccessToken });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
static async deleteForUsers({ userIds, keepAccessToken }: { userIds: string[]; keepAccessToken: string | null }): Promise<number> {
|
|
163
|
+
if (userIds.length === 0) {
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
const query = Token.delete().where('userId', userIds);
|
|
167
|
+
if (keepAccessToken) {
|
|
168
|
+
query.where(Token.primary.name, SQLWhereSign.NotEqual, keepAccessToken);
|
|
169
|
+
}
|
|
170
|
+
const { affectedRows } = await query.delete();
|
|
171
|
+
return affectedRows;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
static async clearFor({ userId, keepAccessToken }: { userId: string; keepAccessToken: string }): Promise<void> {
|
|
175
|
+
await Database.delete('DELETE FROM `tokens` WHERE `userId` = ? AND `accessToken` != ?', [userId, keepAccessToken]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private static invalidRefreshTokenError() {
|
|
179
|
+
return new SimpleError({
|
|
180
|
+
code: 'invalid_refresh_token',
|
|
181
|
+
message: 'Invalid refresh token',
|
|
182
|
+
statusCode: 400,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private static async createToken<U extends User>(user: U, { session, tokenId = uuidv4() }: { session: UserSession; tokenId?: string }): Promise<Token & { user: U }> {
|
|
187
|
+
if (user.isSystemUser) {
|
|
188
|
+
throw new SimpleError({
|
|
189
|
+
code: 'internal_error',
|
|
190
|
+
message: 'Cannot create token for system user',
|
|
191
|
+
statusCode: 500,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const token = new Token().setRelation(Token.user, user);
|
|
196
|
+
token.id = tokenId;
|
|
197
|
+
token.sessionId = session.id;
|
|
198
|
+
token.accessTokenValidUntil = new Date(Date.now() + ACCESS_TOKEN_DURATION);
|
|
199
|
+
token.accessTokenValidUntil.setMilliseconds(0);
|
|
200
|
+
token.refreshTokenValidUntil = new Date(token.accessTokenValidUntil);
|
|
201
|
+
token.accessToken = (await randomBytes(192)).toString('base64').toUpperCase();
|
|
202
|
+
token.refreshToken = (await randomBytes(192)).toString('base64').toUpperCase();
|
|
203
|
+
return token;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private static async createUserSession(user: User, { tokenId, clientType, loginMethod, metaData }: { tokenId: string; clientType: SessionClientType; loginMethod: SessionLoginMethod; metaData: SessionMetaData }): Promise<UserSession> {
|
|
207
|
+
const session = new UserSession();
|
|
208
|
+
session.id = uuidv4();
|
|
209
|
+
session.userId = user.id;
|
|
210
|
+
session.clientType = clientType;
|
|
211
|
+
session.loginMethod = loginMethod;
|
|
212
|
+
session.startedAt = new Date();
|
|
213
|
+
session.startedAt.setMilliseconds(0);
|
|
214
|
+
session.lastUsedTokenId = session.lastActiveTokenId = tokenId;
|
|
215
|
+
this.applyMetaData(session, metaData);
|
|
216
|
+
|
|
217
|
+
await Database.delete(
|
|
218
|
+
'DELETE FROM `user_sessions` WHERE `id` IN (SELECT `id` FROM (SELECT `id` FROM `user_sessions` WHERE `userId` = ? ORDER BY `startedAt` DESC LIMIT ? OFFSET ?) old_sessions)',
|
|
219
|
+
[user.id, MAX_SESSIONS - 1, MAX_SESSIONS - 1],
|
|
220
|
+
);
|
|
221
|
+
await session.save();
|
|
222
|
+
return session;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private static applyMetaData(session: UserSession, metaData: SessionMetaData): void {
|
|
226
|
+
session.deviceType = metaData.deviceType;
|
|
227
|
+
session.deviceName = metaData.deviceName;
|
|
228
|
+
session.osName = metaData.osName;
|
|
229
|
+
session.osVersion = metaData.osVersion;
|
|
230
|
+
session.appVersion = metaData.appVersion;
|
|
231
|
+
session.nativeAppVersion = metaData.nativeAppVersion;
|
|
232
|
+
session.browserName = metaData.browserName;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private static async replaceLastUsedToken({ sessionId, previousTokenId, tokenId }: { sessionId: string; previousTokenId: string; tokenId: string }): Promise<boolean> {
|
|
236
|
+
const [result] = await Database.update(
|
|
237
|
+
'UPDATE `user_sessions` SET `lastUsedTokenId` = ? WHERE `id` = ? AND (`lastUsedTokenId` = ? OR `lastActiveTokenId` = ?)',
|
|
238
|
+
[tokenId, sessionId, previousTokenId, previousTokenId],
|
|
239
|
+
);
|
|
240
|
+
if (result.affectedRows === 0) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
await Database.delete('DELETE FROM `tokens` WHERE `sessionId` = ? AND `id` != ? AND `id` != (SELECT `lastActiveTokenId` FROM `user_sessions` WHERE `id` = ?)', [sessionId, tokenId, sessionId]);
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private static async consumeSSOHandoff({ sessionId, handoffTokenId, tokenId }: { sessionId: string; handoffTokenId: string; tokenId: string }): Promise<boolean> {
|
|
248
|
+
const [result] = await Database.update(
|
|
249
|
+
'UPDATE `user_sessions` SET `lastUsedTokenId` = ? WHERE `id` = ? AND `lastUsedTokenId` = ?',
|
|
250
|
+
[tokenId, sessionId, handoffTokenId],
|
|
251
|
+
);
|
|
252
|
+
if (result.affectedRows === 0) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
await Database.delete('DELETE FROM `tokens` WHERE `sessionId` = ? AND `id` != ?', [sessionId, tokenId]);
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Only read when a session is created: it is a client provided header, and a rotation
|
|
261
|
+
* has to keep the value of the login instead of trusting the header again.
|
|
262
|
+
*/
|
|
263
|
+
private static getClientType(): SessionClientType {
|
|
264
|
+
const platform = contextStorage.getStore()?.request.headers['x-platform'];
|
|
265
|
+
if (platform === 'ios') {
|
|
266
|
+
return SessionClientType.iOS;
|
|
267
|
+
}
|
|
268
|
+
if (platform === 'android') {
|
|
269
|
+
return SessionClientType.Android;
|
|
270
|
+
}
|
|
271
|
+
return SessionClientType.Browser;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Recalculated on every rotation, so a user that becomes an administrator during a
|
|
276
|
+
* session is limited from that moment on (counted from their login, as if they had the
|
|
277
|
+
* permissions all along).
|
|
278
|
+
*/
|
|
279
|
+
private static getDurations(session: UserSession, user: User): SessionDurations {
|
|
280
|
+
return SESSION_DURATIONS[this.getSessionType(session, user)][session.clientType];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private static getSessionType(session: UserSession, user: User): SessionType {
|
|
284
|
+
// Before the permissions: whoever signs in through the identity provider stays
|
|
285
|
+
// dependent on it, member or administrator.
|
|
286
|
+
if (session.loginMethod === SessionLoginMethod.SSO) {
|
|
287
|
+
return 'sso';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const permissions = user.permissions;
|
|
291
|
+
if (!permissions || permissions.isEmpty) {
|
|
292
|
+
return 'user';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (permissions.globalPermissions !== null) {
|
|
296
|
+
return 'platformAdmin';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return 'admin';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private static applyLimits({ token, session, user }: { token: Token; session: UserSession; user: User }) {
|
|
303
|
+
const durations = this.getDurations(session, user);
|
|
304
|
+
const sessionEndsAt = durations.session !== null ? (session.startedAt.getTime() + durations.session) : (Date.now() + durations.refreshToken);
|
|
305
|
+
|
|
306
|
+
token.refreshTokenValidUntil = new Date(Math.min(Date.now() + durations.refreshToken, sessionEndsAt));
|
|
307
|
+
token.refreshTokenValidUntil.setMilliseconds(0);
|
|
308
|
+
|
|
309
|
+
token.accessTokenValidUntil = new Date(Math.min(token.accessTokenValidUntil.getTime(), sessionEndsAt));
|
|
310
|
+
token.accessTokenValidUntil.setMilliseconds(0);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
static getMetaData(): SessionMetaData {
|
|
314
|
+
const value = contextStorage.getStore()?.request.headers['x-session-metadata'];
|
|
315
|
+
return this.parseMetaData(typeof value === 'string' ? value : null);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
static parseMetaData(value: string | null): SessionMetaData {
|
|
319
|
+
const fallback = this.getFallbackMetaData();
|
|
320
|
+
if (typeof value !== 'string' || value.length > 4096) {
|
|
321
|
+
return fallback;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
let parsed: Partial<SessionMetaData>;
|
|
326
|
+
try {
|
|
327
|
+
parsed = JSON.parse(value) as Partial<SessionMetaData>;
|
|
328
|
+
} catch {
|
|
329
|
+
parsed = JSON.parse(decodeURIComponent(value)) as Partial<SessionMetaData>;
|
|
330
|
+
}
|
|
331
|
+
const string = (candidate: unknown, max: number): string | null => typeof candidate === 'string' && candidate.length > 0 ? candidate.slice(0, max) : null;
|
|
332
|
+
return {
|
|
333
|
+
deviceType: Object.values(SessionDeviceType).includes(parsed.deviceType as SessionDeviceType) ? parsed.deviceType as SessionDeviceType : fallback.deviceType,
|
|
334
|
+
deviceName: string(parsed.deviceName, 255),
|
|
335
|
+
osName: Object.values(SessionOS).includes(parsed.osName as SessionOS) ? parsed.osName as SessionOS : null,
|
|
336
|
+
osVersion: string(parsed.osVersion, 64),
|
|
337
|
+
appVersion: string(parsed.appVersion, 64),
|
|
338
|
+
nativeAppVersion: string(parsed.nativeAppVersion, 64),
|
|
339
|
+
browserName: string(parsed.browserName, 64),
|
|
340
|
+
};
|
|
341
|
+
} catch {
|
|
342
|
+
return fallback;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private static getFallbackMetaData(): SessionMetaData {
|
|
347
|
+
return {
|
|
348
|
+
deviceType: SessionDeviceType.Desktop,
|
|
349
|
+
deviceName: null,
|
|
350
|
+
osName: null,
|
|
351
|
+
osVersion: null,
|
|
352
|
+
appVersion: null,
|
|
353
|
+
nativeAppVersion: null,
|
|
354
|
+
browserName: null,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { PatchableArrayAutoEncoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { PatchableArray } from '@simonbackx/simple-encoding';
|
|
3
3
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
4
|
-
import type { Organization, StripeAccount } from '@stamhoofd/models';
|
|
5
|
-
import { BalanceItem, BalanceItemFactory, BalanceItemPayment, Order, OrderFactory, OrganizationFactory, Payment,
|
|
4
|
+
import type { Organization, StripeAccount, Token } from '@stamhoofd/models';
|
|
5
|
+
import { BalanceItem, BalanceItemFactory, BalanceItemPayment, Order, OrderFactory, OrganizationFactory, Payment, UserFactory, Webshop, WebshopFactory } from '@stamhoofd/models';
|
|
6
6
|
import type { OrderResponse } from '@stamhoofd/structures';
|
|
7
7
|
import { BalanceItemStatus, Cart, CartItem, Customer, OrderData, OrderStatus, PaymentConfiguration, PaymentMethod, PaymentStatus, PermissionLevel, Permissions, PrivateOrder, PrivatePaymentConfiguration, PrivateWebshop, Product, ProductPrice, TransferSettings, WebshopCrowdfunding, WebshopMetaData, WebshopPrivateMetaData } from '@stamhoofd/structures';
|
|
8
8
|
import { v4 as uuidv4 } from 'uuid';
|
|
9
|
+
import { SessionService } from './SessionService.js';
|
|
9
10
|
|
|
10
11
|
import type { StripeObject } from '../../tests/helpers/StripeMocker.js';
|
|
11
12
|
import { StripeMocker } from '../../tests/helpers/StripeMocker.js';
|
|
@@ -234,7 +235,7 @@ describe('WebshopCrowdfundingService', () => {
|
|
|
234
235
|
level: PermissionLevel.Full,
|
|
235
236
|
}),
|
|
236
237
|
}).create();
|
|
237
|
-
token = await
|
|
238
|
+
token = await SessionService.createSession(user);
|
|
238
239
|
});
|
|
239
240
|
|
|
240
241
|
afterAll(() => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Group, Member, Organization, Registration } from '@stamhoofd/models';
|
|
2
2
|
import type { SQLFilterDefinitions } from '@stamhoofd/sql';
|
|
3
|
-
import { baseSQLFilterCompilers, createColumnFilter, createJoinedRelationFilter, SQL, SQLValueType } from '@stamhoofd/sql';
|
|
3
|
+
import { baseSQLFilterCompilers, createColumnFilter, createExistsFilter, createJoinedRelationFilter, SQL, SQLValueType } from '@stamhoofd/sql';
|
|
4
4
|
import { SQLTranslatedString } from '../helpers/SQLTranslatedString.js';
|
|
5
5
|
import { baseRegistrationFilterCompilers } from './base-registration-filter-compilers.js';
|
|
6
6
|
import { memberFilterCompilers } from './members.js';
|
|
@@ -55,6 +55,64 @@ export const registrationFilterCompilers: SQLFilterDefinitions = {
|
|
|
55
55
|
}),
|
|
56
56
|
},
|
|
57
57
|
),
|
|
58
|
+
/**
|
|
59
|
+
* There is a bug in MySQL 8 that is fixed in 9.3
|
|
60
|
+
* where EXISTS (select * from json_table(...)) does not work
|
|
61
|
+
* To fix this, we do a double select with join inside the select
|
|
62
|
+
* It is a bit slower, but it works for now.
|
|
63
|
+
*/
|
|
64
|
+
options: createExistsFilter(
|
|
65
|
+
SQL.select()
|
|
66
|
+
.from(SQL.table('registrations', 'innerRegistrations'))
|
|
67
|
+
.join(
|
|
68
|
+
SQL.join(
|
|
69
|
+
SQL.jsonTable(
|
|
70
|
+
SQL.jsonExtract(SQL.column('innerRegistrations', 'options'), '$.value'),
|
|
71
|
+
'options',
|
|
72
|
+
)
|
|
73
|
+
.addColumn(
|
|
74
|
+
'amount',
|
|
75
|
+
'INT',
|
|
76
|
+
'$.amount',
|
|
77
|
+
)
|
|
78
|
+
.addColumn(
|
|
79
|
+
'optionId',
|
|
80
|
+
'TEXT',
|
|
81
|
+
'$.option.id',
|
|
82
|
+
)
|
|
83
|
+
.addColumn(
|
|
84
|
+
'optionMenuId',
|
|
85
|
+
'TEXT',
|
|
86
|
+
'$.optionMenu.id',
|
|
87
|
+
),
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
.where(SQL.column('innerRegistrations', 'id'), SQL.column('registrations', 'id')),
|
|
91
|
+
{
|
|
92
|
+
...baseSQLFilterCompilers,
|
|
93
|
+
amount: createColumnFilter({
|
|
94
|
+
expression: SQL.column('options', 'amount'),
|
|
95
|
+
type: SQLValueType.Number,
|
|
96
|
+
nullable: false,
|
|
97
|
+
}),
|
|
98
|
+
option: {
|
|
99
|
+
...baseSQLFilterCompilers,
|
|
100
|
+
id: createColumnFilter({
|
|
101
|
+
expression: SQL.column('options', 'optionId'),
|
|
102
|
+
type: SQLValueType.String,
|
|
103
|
+
nullable: false,
|
|
104
|
+
}),
|
|
105
|
+
},
|
|
106
|
+
optionMenu: {
|
|
107
|
+
...baseSQLFilterCompilers,
|
|
108
|
+
id: createColumnFilter({
|
|
109
|
+
expression: SQL.column('options', 'optionMenuId'),
|
|
110
|
+
type: SQLValueType.String,
|
|
111
|
+
nullable: false,
|
|
112
|
+
}),
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
),
|
|
58
116
|
organization: createJoinedRelationFilter(
|
|
59
117
|
organizationJoin,
|
|
60
118
|
organizationFilterCompilers,
|
|
@@ -6,11 +6,12 @@ import { Token } from '@stamhoofd/models';
|
|
|
6
6
|
import type { MemberWithRegistrationsBlob } from '@stamhoofd/structures';
|
|
7
7
|
import { PatchUserMembersEndpoint } from '../../src/endpoints/global/registration/PatchUserMembersEndpoint.js';
|
|
8
8
|
import { testServer } from '../helpers/TestServer.js';
|
|
9
|
+
import { SessionService } from '../../src/services/SessionService.js';
|
|
9
10
|
|
|
10
11
|
export async function patchUserMember({ patch, organization, user }: { patch: AutoEncoderPatchType<MemberWithRegistrationsBlob>; organization: Organization; user: User }) {
|
|
11
12
|
expect(patch.id).toEqual(expect.any(String));
|
|
12
13
|
|
|
13
|
-
const token = await
|
|
14
|
+
const token = await SessionService.createSession(user);
|
|
14
15
|
|
|
15
16
|
const arr = new PatchableArray();
|
|
16
17
|
arr.addPatch(patch);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
2
2
|
import type { Organization } from '@stamhoofd/models';
|
|
3
3
|
import { OrganizationFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
4
|
+
import { SessionService } from '../../src/services/SessionService.js';
|
|
4
5
|
|
|
5
6
|
import { ApiUser, ApiUserRateLimits, PermissionLevel, Permissions, UserMeta, UserPermissions } from '@stamhoofd/structures';
|
|
6
7
|
import { STExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
@@ -29,7 +30,7 @@ describe('E2E.APIRateLimits', () => {
|
|
|
29
30
|
level: PermissionLevel.Full,
|
|
30
31
|
}),
|
|
31
32
|
}).create();
|
|
32
|
-
const token = await
|
|
33
|
+
const token = await SessionService.createSession(user, { authenticatedAt: new Date() });
|
|
33
34
|
|
|
34
35
|
const createRequest = Request.buildJson('POST', '/api-keys', organization.getApiHost(), ApiUser.create({
|
|
35
36
|
permissions: UserPermissions.create({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import type { BalanceItem, Organization, RegistrationPeriod } from '@stamhoofd/models';
|
|
3
|
-
import { BalanceItemFactory, GroupFactory, MemberFactory, OrganizationFactory, OrganizationRegistrationPeriodFactory, Registration, RegistrationFactory, RegistrationPeriodFactory,
|
|
2
|
+
import type { BalanceItem, Organization, RegistrationPeriod, Token } from '@stamhoofd/models';
|
|
3
|
+
import { BalanceItemFactory, GroupFactory, MemberFactory, OrganizationFactory, OrganizationRegistrationPeriodFactory, Registration, RegistrationFactory, RegistrationPeriodFactory, UserFactory } from '@stamhoofd/models';
|
|
4
4
|
import { AccessRight, AppliedRegistrationDiscount, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, BooleanStatus, GroupPriceDiscount, GroupPriceDiscountType, IDRegisterCart, IDRegisterCheckout, IDRegisterItem, PaymentMethod, PermissionLevel, Permissions, PermissionsResourceType, ReduceablePrice, ResourcePermissions } from '@stamhoofd/structures';
|
|
5
5
|
import { STExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
6
6
|
import { RegisterMembersEndpoint } from '../../src/endpoints/global/registration/RegisterMembersEndpoint.js';
|
|
@@ -12,6 +12,7 @@ import { initBundleDiscount } from '../init/initBundleDiscount.js';
|
|
|
12
12
|
import { initPermissionRole } from '../init/initPermissionRole.js';
|
|
13
13
|
import { initStripe } from '../init/initStripe.js';
|
|
14
14
|
import { initMembershipOrganization } from '../init/initMembershipOrganization.js';
|
|
15
|
+
import { SessionService } from '../../src/services/SessionService.js';
|
|
15
16
|
|
|
16
17
|
const baseUrl = `/members/register`;
|
|
17
18
|
|
|
@@ -64,7 +65,7 @@ describe('E2E.Bundle Discounts', () => {
|
|
|
64
65
|
permissions: null,
|
|
65
66
|
}).create();
|
|
66
67
|
|
|
67
|
-
const token = await
|
|
68
|
+
const token = await SessionService.createSession(user);
|
|
68
69
|
|
|
69
70
|
const member = await new MemberFactory({ organization, user })
|
|
70
71
|
.create();
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import type { Organization, RegistrationPeriod } from '@stamhoofd/models';
|
|
3
|
-
import { GroupFactory, MemberFactory, OrganizationFactory, RegistrationFactory, RegistrationPeriodFactory,
|
|
2
|
+
import type { Organization, RegistrationPeriod, Token } from '@stamhoofd/models';
|
|
3
|
+
import { GroupFactory, MemberFactory, OrganizationFactory, RegistrationFactory, RegistrationPeriodFactory, UserFactory } from '@stamhoofd/models';
|
|
4
4
|
import type { BalanceItemWithPayments, StamhoofdFilter } from '@stamhoofd/structures';
|
|
5
5
|
import { AccessRight, ChargeRequest, LimitedFilteredRequest, PermissionLevel, PermissionRoleDetailed, Permissions, PermissionsResourceType, ResourcePermissions, Version } from '@stamhoofd/structures';
|
|
6
6
|
import { STExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
7
7
|
import { ChargeMembersEndpoint } from '../../src/endpoints/admin/members/ChargeMembersEndpoint.js';
|
|
8
8
|
import { testServer } from '../helpers/TestServer.js';
|
|
9
9
|
import { GetReceivableBalanceEndpoint } from '../../src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.js';
|
|
10
|
+
import { SessionService } from '../../src/services/SessionService.js';
|
|
10
11
|
|
|
11
12
|
describe('E2E.ChargeMembers', () => {
|
|
12
13
|
const chargeMembersEndpoint = new ChargeMembersEndpoint();
|
|
@@ -45,7 +46,7 @@ describe('E2E.ChargeMembers', () => {
|
|
|
45
46
|
})
|
|
46
47
|
.create();
|
|
47
48
|
|
|
48
|
-
const token = await
|
|
49
|
+
const token = await SessionService.createSession(user);
|
|
49
50
|
|
|
50
51
|
return { organization, user, token };
|
|
51
52
|
};
|
|
@@ -148,7 +149,7 @@ describe('E2E.ChargeMembers', () => {
|
|
|
148
149
|
for (const userFactory of testUserFactories) {
|
|
149
150
|
const user = await userFactory.create();
|
|
150
151
|
|
|
151
|
-
const token = await
|
|
152
|
+
const token = await SessionService.createSession(user);
|
|
152
153
|
|
|
153
154
|
await expect(postCharge(filter, organization, body, token))
|
|
154
155
|
.rejects
|
|
@@ -234,7 +235,7 @@ describe('E2E.ChargeMembers', () => {
|
|
|
234
235
|
})
|
|
235
236
|
.create();
|
|
236
237
|
|
|
237
|
-
const otherFinancialDirectorToken = await
|
|
238
|
+
const otherFinancialDirectorToken = await SessionService.createSession(otherFinancialDirector);
|
|
238
239
|
|
|
239
240
|
const body = ChargeRequest.create({
|
|
240
241
|
description: 'test description',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import type { Group, Member, Organization, RegistrationPeriod } from '@stamhoofd/models';
|
|
3
|
-
import { Document, DocumentTemplateFactory, GroupFactory, MemberFactory, OrganizationFactory, OrganizationRegistrationPeriodFactory, Registration, RegistrationPeriodFactory,
|
|
2
|
+
import type { Group, Member, Organization, RegistrationPeriod, Token } from '@stamhoofd/models';
|
|
3
|
+
import { Document, DocumentTemplateFactory, GroupFactory, MemberFactory, OrganizationFactory, OrganizationRegistrationPeriodFactory, Registration, RegistrationPeriodFactory, UserFactory } from '@stamhoofd/models';
|
|
4
4
|
import { IDRegisterCart, IDRegisterCheckout, IDRegisterItem, MemberDetails, MemberWithRegistrationsBlob, PaymentMethod } from '@stamhoofd/structures';
|
|
5
5
|
import { TestUtils } from '@stamhoofd/test-utils';
|
|
6
6
|
import { Formatter } from '@stamhoofd/utility';
|
|
@@ -14,6 +14,7 @@ import { initStripe } from '../init/initStripe.js';
|
|
|
14
14
|
import { registrationUpdateQueue } from '../../src/services/BalanceItemService.js';
|
|
15
15
|
import { DocumentRenderService } from '../../src/services/DocumentRenderService.js';
|
|
16
16
|
import { initMembershipOrganization } from '../init/initMembershipOrganization.js';
|
|
17
|
+
import { SessionService } from '../../src/services/SessionService.js';
|
|
17
18
|
|
|
18
19
|
const baseUrl = `/members/register`;
|
|
19
20
|
|
|
@@ -73,7 +74,7 @@ describe('E2E.Documents', () => {
|
|
|
73
74
|
permissions: null,
|
|
74
75
|
}).create();
|
|
75
76
|
|
|
76
|
-
const token = await
|
|
77
|
+
const token = await SessionService.createSession(user);
|
|
77
78
|
|
|
78
79
|
const member = await new MemberFactory({ organization, user })
|
|
79
80
|
.create();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { File, MemberDetails, MemberWithRegistrationsBlob, RecordCategory, RecordFileAnswer, RecordSettings, RecordType, TranslatedString, Version } from '@stamhoofd/structures';
|
|
2
|
+
import { SessionService } from '../../src/services/SessionService.js';
|
|
2
3
|
|
|
3
4
|
import type { PatchableArrayAutoEncoder } from '@simonbackx/simple-encoding';
|
|
4
5
|
import { encodeObject, EncodeMedium, PatchableArray } from '@simonbackx/simple-encoding';
|
|
@@ -48,7 +49,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
48
49
|
level: PermissionLevel.Full,
|
|
49
50
|
}),
|
|
50
51
|
}).create();
|
|
51
|
-
const token = await
|
|
52
|
+
const token = await SessionService.createSession(user);
|
|
52
53
|
|
|
53
54
|
// Create a user member
|
|
54
55
|
const member = MemberWithRegistrationsBlob.create({
|
|
@@ -91,7 +92,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
91
92
|
level: PermissionLevel.Full,
|
|
92
93
|
}),
|
|
93
94
|
}).create();
|
|
94
|
-
const token = await
|
|
95
|
+
const token = await SessionService.createSession(user);
|
|
95
96
|
|
|
96
97
|
// Create a user member
|
|
97
98
|
const member = MemberWithRegistrationsBlob.create({
|
|
@@ -135,7 +136,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
135
136
|
level: PermissionLevel.Full,
|
|
136
137
|
}),
|
|
137
138
|
}).create();
|
|
138
|
-
const token = await
|
|
139
|
+
const token = await SessionService.createSession(user);
|
|
139
140
|
|
|
140
141
|
// Create a user member
|
|
141
142
|
const member = MemberWithRegistrationsBlob.create({
|
|
@@ -197,7 +198,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
197
198
|
level: PermissionLevel.Full,
|
|
198
199
|
}),
|
|
199
200
|
}).create();
|
|
200
|
-
const token = await
|
|
201
|
+
const token = await SessionService.createSession(user);
|
|
201
202
|
|
|
202
203
|
// Public files are not signed, so a malicious user can put anything in them. The url of a file ends up
|
|
203
204
|
// in a link (and in the source of an image), so a javascript: url would run on our own domain.
|
|
@@ -260,7 +261,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
260
261
|
level: PermissionLevel.Full,
|
|
261
262
|
}),
|
|
262
263
|
}).create();
|
|
263
|
-
const token = await
|
|
264
|
+
const token = await SessionService.createSession(user);
|
|
264
265
|
|
|
265
266
|
// Create a user member
|
|
266
267
|
const member = MemberWithRegistrationsBlob.create({
|
|
@@ -324,7 +325,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
324
325
|
level: PermissionLevel.Full,
|
|
325
326
|
}),
|
|
326
327
|
}).create();
|
|
327
|
-
const token = await
|
|
328
|
+
const token = await SessionService.createSession(user);
|
|
328
329
|
|
|
329
330
|
// Create a user member
|
|
330
331
|
const member = MemberWithRegistrationsBlob.create({
|
|
@@ -404,7 +405,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
404
405
|
level: PermissionLevel.Full,
|
|
405
406
|
}),
|
|
406
407
|
}).create();
|
|
407
|
-
const token = await
|
|
408
|
+
const token = await SessionService.createSession(user);
|
|
408
409
|
|
|
409
410
|
// Create a user member
|
|
410
411
|
const member = MemberWithRegistrationsBlob.create({
|
|
@@ -662,7 +663,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
662
663
|
level: PermissionLevel.Full,
|
|
663
664
|
}),
|
|
664
665
|
}).create();
|
|
665
|
-
const token = await
|
|
666
|
+
const token = await SessionService.createSession(user);
|
|
666
667
|
|
|
667
668
|
// Create a user member
|
|
668
669
|
const member = MemberWithRegistrationsBlob.create({
|