@getstrata/bootstrap 0.2.66 → 0.4.2
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/CHANGELOG.md +45 -10
- package/README.md +10 -10
- package/dist/_.._/_.._/index.html +2 -2
- package/dist/bootstrap/createSpaRoutes.d.ts +9 -2
- package/dist/bootstrap/createWebRoutes.d.ts +7 -3
- package/dist/bootstrap/dogfoodApp.d.ts +7 -0
- package/dist/bootstrap/httpKernel.d.ts +4 -4
- package/dist/bootstrap/public-api.d.ts +2 -1
- package/dist/bootstrap/schemaTarget.d.ts +5 -0
- package/dist/bootstrap/server.d.ts +1 -1
- package/dist/bootstrap/web/index.d.ts +1 -1
- package/dist/bootstrap/web/routing.d.ts +2 -2
- package/dist/bootstrap/web/session.d.ts +18 -3
- package/dist/entries/buildModuleRoutes.js +8 -3
- package/dist/entries/buildWebModuleRoutes.js +8 -3
- package/dist/entries/cache/modelCacheTags.js +6 -1
- package/dist/entries/context.js +52 -166
- package/dist/entries/createRoutes.js +83 -888
- package/dist/entries/createSpaRoutes.js +48 -24
- package/dist/entries/createWebRoutes.js +13 -7
- package/dist/entries/dependencies.js +52 -166
- package/dist/entries/discoverModules.js +6 -1
- package/dist/entries/httpKernel.js +2 -2
- package/dist/entries/listeners/invalidateCacheOnModelWrite.js +6 -1
- package/dist/entries/providers/view.js +5 -160
- package/dist/entries/providers.js +52 -166
- package/dist/entries/secretsGuard.js +13 -8
- package/dist/entries/web/routing.js +4 -3
- package/dist/entries/web/session.js +58 -25
- package/dist/index.js +132 -206
- package/package.json +5 -5
- package/dist/bootstrap/scimRoutes.d.ts +0 -24
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
var __jsonParse = (a) => JSON.parse(a);
|
|
3
3
|
|
|
4
4
|
// ../../src/bootstrap/providers/auth.ts
|
|
5
|
+
import { BasicAuthGuard } from "@getstrata/core/auth/basicAuthGuard";
|
|
5
6
|
import {
|
|
6
7
|
AuthManager,
|
|
7
8
|
CompositeGuard,
|
|
8
9
|
DatabaseTokenGuard,
|
|
9
10
|
GuestGuard
|
|
10
11
|
} from "@getstrata/core/auth/guard";
|
|
12
|
+
import { JwtGuard } from "@getstrata/core/auth/jwtGuard";
|
|
11
13
|
import { SessionGuard } from "@getstrata/core/auth/sessionGuard";
|
|
12
14
|
|
|
13
15
|
// ../../src/config/auth.ts
|
|
14
16
|
var authConfig = {
|
|
15
|
-
allowDevHeaders:
|
|
17
|
+
allowDevHeaders: process.env.AUTH_DEV_HEADERS === "true",
|
|
16
18
|
tokenDefaultAbilities: ["*"]
|
|
17
19
|
};
|
|
18
20
|
|
|
@@ -47,11 +49,22 @@ var authProvider = {
|
|
|
47
49
|
name: "core.auth",
|
|
48
50
|
register({ container, config }) {
|
|
49
51
|
config.set("auth.allowDevHeaders", authConfig.allowDevHeaders);
|
|
50
|
-
const
|
|
52
|
+
const apiGuard = new DatabaseTokenGuard(container);
|
|
53
|
+
const sessionGuard = new SessionGuard(container);
|
|
54
|
+
const jwtGuard = new JwtGuard;
|
|
55
|
+
const basicGuard = new BasicAuthGuard(container);
|
|
56
|
+
const guards = [apiGuard, jwtGuard, basicGuard, sessionGuard];
|
|
51
57
|
if (authConfig.allowDevHeaders) {
|
|
52
58
|
guards.push(new GuestGuard);
|
|
53
59
|
}
|
|
54
|
-
|
|
60
|
+
const auth = new AuthManager(new CompositeGuard(guards));
|
|
61
|
+
auth.registerGuard("api", apiGuard);
|
|
62
|
+
auth.registerGuard("access_token", apiGuard);
|
|
63
|
+
auth.registerGuard("jwt", jwtGuard);
|
|
64
|
+
auth.registerGuard("basic", basicGuard);
|
|
65
|
+
auth.registerGuard("web", sessionGuard);
|
|
66
|
+
auth.registerGuard("session", sessionGuard);
|
|
67
|
+
container.set(CORE_AUTH_TOKEN, auth);
|
|
55
68
|
}
|
|
56
69
|
};
|
|
57
70
|
var auth_default = authProvider;
|
|
@@ -112,8 +125,12 @@ var queueConfig = {
|
|
|
112
125
|
};
|
|
113
126
|
// ../../src/bootstrap/env.ts
|
|
114
127
|
import { defineEnvSchema } from "@getstrata/core/config/envSchema";
|
|
128
|
+
import { DEFAULT_SPA_PREFIX, FRONTEND_MODE_PATTERN } from "@getstrata/core/runtime/frontendMode";
|
|
115
129
|
var appEnvSchema = defineEnvSchema({
|
|
116
|
-
DATABASE_URL: {
|
|
130
|
+
DATABASE_URL: {
|
|
131
|
+
required: true,
|
|
132
|
+
pattern: /^(postgres(ql)?|mysql|sqlite):\/\//i
|
|
133
|
+
},
|
|
117
134
|
PORT: {
|
|
118
135
|
integer: true,
|
|
119
136
|
minimum: 1,
|
|
@@ -141,9 +158,21 @@ var appEnvSchema = defineEnvSchema({
|
|
|
141
158
|
pattern: /^(sync|async|redis)$/
|
|
142
159
|
},
|
|
143
160
|
AUTH_DEV_HEADERS: {
|
|
144
|
-
default: "
|
|
161
|
+
default: "false",
|
|
145
162
|
pattern: /^(true|false|0|1)$/
|
|
146
163
|
},
|
|
164
|
+
DB_CONNECTION: {
|
|
165
|
+
default: "",
|
|
166
|
+
pattern: /^(pgsql|postgres|postgresql|mysql|mariadb|sqlite)?$/i
|
|
167
|
+
},
|
|
168
|
+
AUTH_DEFAULT_GUARD: {
|
|
169
|
+
default: "web"
|
|
170
|
+
},
|
|
171
|
+
JWT_TTL_SECONDS: {
|
|
172
|
+
integer: true,
|
|
173
|
+
minimum: 60,
|
|
174
|
+
default: "3600"
|
|
175
|
+
},
|
|
147
176
|
APP_ENV: {
|
|
148
177
|
default: "local"
|
|
149
178
|
},
|
|
@@ -153,6 +182,13 @@ var appEnvSchema = defineEnvSchema({
|
|
|
153
182
|
APP_URL: {
|
|
154
183
|
default: "http://localhost:3000"
|
|
155
184
|
},
|
|
185
|
+
FRONTEND_MODE: {
|
|
186
|
+
default: "api",
|
|
187
|
+
pattern: FRONTEND_MODE_PATTERN
|
|
188
|
+
},
|
|
189
|
+
SPA_PREFIX: {
|
|
190
|
+
default: DEFAULT_SPA_PREFIX
|
|
191
|
+
},
|
|
156
192
|
API_PREFIX: {
|
|
157
193
|
default: "/api/v1"
|
|
158
194
|
},
|
|
@@ -306,7 +342,12 @@ function readDiscoverModulesState() {
|
|
|
306
342
|
return state;
|
|
307
343
|
}
|
|
308
344
|
function configureModulesDirectory(modulesDir) {
|
|
309
|
-
readDiscoverModulesState()
|
|
345
|
+
const state = readDiscoverModulesState();
|
|
346
|
+
if (state.configuredModulesDir !== modulesDir) {
|
|
347
|
+
state.appModules.length = 0;
|
|
348
|
+
state.modulesReady = undefined;
|
|
349
|
+
}
|
|
350
|
+
state.configuredModulesDir = modulesDir;
|
|
310
351
|
}
|
|
311
352
|
function resolveModulesDirectory(options) {
|
|
312
353
|
const state = readDiscoverModulesState();
|
|
@@ -470,7 +511,6 @@ var storageProvider = {
|
|
|
470
511
|
var storage_default = storageProvider;
|
|
471
512
|
|
|
472
513
|
// ../../src/bootstrap/providers/view.ts
|
|
473
|
-
import { resolveMembershipLookup } from "@getstrata/core/auth/membershipContext";
|
|
474
514
|
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
475
515
|
import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
|
|
476
516
|
import { isViewsEnabled } from "@getstrata/core/runtime/frontendMode";
|
|
@@ -482,148 +522,6 @@ import {
|
|
|
482
522
|
errorTemplateName,
|
|
483
523
|
resolveWebLayoutData
|
|
484
524
|
} from "@getstrata/core/view";
|
|
485
|
-
|
|
486
|
-
// ../../src/modules/organization/repository.ts
|
|
487
|
-
import { BaseRepository } from "@getstrata/core/database/baseRepository";
|
|
488
|
-
import { NotFoundError } from "@getstrata/core/errors/http";
|
|
489
|
-
import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
|
|
490
|
-
|
|
491
|
-
// ../../src/modules/organization/table.ts
|
|
492
|
-
import { defineTable } from "@getstrata/core/database/table";
|
|
493
|
-
|
|
494
|
-
// ../../src/domain/workhub.ts
|
|
495
|
-
var ORGANIZATION_TABLE = "organization";
|
|
496
|
-
|
|
497
|
-
// ../../src/modules/organization/table.ts
|
|
498
|
-
var organizationTable = defineTable({
|
|
499
|
-
name: ORGANIZATION_TABLE,
|
|
500
|
-
primaryKey: "id",
|
|
501
|
-
columns: ["id", "tenant_id", "name", "slug", "created_at", "updated_at", "deleted_at"],
|
|
502
|
-
softDeletes: true,
|
|
503
|
-
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
// ../../src/modules/organization/repository.ts
|
|
507
|
-
class OrganizationRepository extends BaseRepository {
|
|
508
|
-
constructor() {
|
|
509
|
-
super(organizationTable);
|
|
510
|
-
}
|
|
511
|
-
async findBySlug(slug) {
|
|
512
|
-
return await this.firstOrNull({ slug });
|
|
513
|
-
}
|
|
514
|
-
async listForTenant(options) {
|
|
515
|
-
return await this.findAll({
|
|
516
|
-
limit: options.limit,
|
|
517
|
-
offset: options.offset,
|
|
518
|
-
where: { tenant_id: options.tenantId ?? currentTenantId() }
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
async countForTenant(tenantId = currentTenantId()) {
|
|
522
|
-
return await this.countWhere({ tenant_id: tenantId });
|
|
523
|
-
}
|
|
524
|
-
async findForTenantOrThrow(id, tenantId = currentTenantId()) {
|
|
525
|
-
const organization = await this.findById(id);
|
|
526
|
-
if (!organization || organization.tenant_id !== tenantId) {
|
|
527
|
-
throw new NotFoundError(`SCIM group ${id} not found.`);
|
|
528
|
-
}
|
|
529
|
-
return organization;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
var repository_default = OrganizationRepository;
|
|
533
|
-
|
|
534
|
-
// ../../src/modules/user/repository.ts
|
|
535
|
-
import {
|
|
536
|
-
emailLookupForQuery,
|
|
537
|
-
protectEmail,
|
|
538
|
-
revealEmail
|
|
539
|
-
} from "@getstrata/core/crypto/fieldEncryption";
|
|
540
|
-
import { revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
|
|
541
|
-
import { BaseRepository as BaseRepository2 } from "@getstrata/core/database/baseRepository";
|
|
542
|
-
import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
|
|
543
|
-
|
|
544
|
-
// ../../src/modules/user/table.ts
|
|
545
|
-
import { defineTable as defineTable2 } from "@getstrata/core/database/table";
|
|
546
|
-
var userTable = defineTable2({
|
|
547
|
-
name: "users",
|
|
548
|
-
primaryKey: "id",
|
|
549
|
-
columns: [
|
|
550
|
-
"id",
|
|
551
|
-
"name",
|
|
552
|
-
"email",
|
|
553
|
-
"email_lookup",
|
|
554
|
-
"role",
|
|
555
|
-
"tenant_id",
|
|
556
|
-
"password_hash",
|
|
557
|
-
"email_verified_at",
|
|
558
|
-
"mfa_secret",
|
|
559
|
-
"mfa_enabled",
|
|
560
|
-
"mfa_recovery_codes",
|
|
561
|
-
"profile_photo_path",
|
|
562
|
-
"session_valid_after",
|
|
563
|
-
"current_organization_id",
|
|
564
|
-
"created_at",
|
|
565
|
-
"updated_at"
|
|
566
|
-
],
|
|
567
|
-
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
// ../../src/modules/user/repository.ts
|
|
571
|
-
class UserRepository extends BaseRepository2 {
|
|
572
|
-
constructor() {
|
|
573
|
-
super(userTable);
|
|
574
|
-
}
|
|
575
|
-
decode(record) {
|
|
576
|
-
return {
|
|
577
|
-
...record,
|
|
578
|
-
email: revealEmail(record.email),
|
|
579
|
-
mfa_secret: revealMfaSecret(record.mfa_secret)
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
async findById(id) {
|
|
583
|
-
const record = await super.findById(id);
|
|
584
|
-
return record ? this.decode(record) : null;
|
|
585
|
-
}
|
|
586
|
-
async findAll(options = {}) {
|
|
587
|
-
const records = await super.findAll(options);
|
|
588
|
-
return records.map((record) => this.decode(record));
|
|
589
|
-
}
|
|
590
|
-
async create(values) {
|
|
591
|
-
const email = values.email;
|
|
592
|
-
if (!email) {
|
|
593
|
-
throw new Error("Email is required.");
|
|
594
|
-
}
|
|
595
|
-
const protectedEmail = protectEmail(email);
|
|
596
|
-
const record = await super.create({
|
|
597
|
-
...values,
|
|
598
|
-
tenant_id: values.tenant_id ?? currentTenantId2(),
|
|
599
|
-
email: protectedEmail.storedEmail,
|
|
600
|
-
email_lookup: protectedEmail.emailLookup,
|
|
601
|
-
password_hash: values.password_hash ?? ""
|
|
602
|
-
});
|
|
603
|
-
return this.decode(record);
|
|
604
|
-
}
|
|
605
|
-
async updateByIdOrThrow(id, values, errorFactory) {
|
|
606
|
-
const changes = { ...values };
|
|
607
|
-
if (values.email !== undefined) {
|
|
608
|
-
const protectedEmail = protectEmail(values.email);
|
|
609
|
-
changes.email = protectedEmail.storedEmail;
|
|
610
|
-
changes.email_lookup = protectedEmail.emailLookup;
|
|
611
|
-
}
|
|
612
|
-
const record = await super.updateByIdOrThrow(id, changes, errorFactory);
|
|
613
|
-
return this.decode(record);
|
|
614
|
-
}
|
|
615
|
-
async findByEmail(email) {
|
|
616
|
-
const records = await this.findWhere({ email_lookup: emailLookupForQuery(email) }, { limit: 1 });
|
|
617
|
-
const record = records[0];
|
|
618
|
-
return record ? this.decode(record) : null;
|
|
619
|
-
}
|
|
620
|
-
async countForTenant(tenantId = currentTenantId2()) {
|
|
621
|
-
return await this.countWhere({ tenant_id: tenantId });
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
var repository_default2 = UserRepository;
|
|
625
|
-
|
|
626
|
-
// ../../src/bootstrap/providers/view.ts
|
|
627
525
|
var CORE_VIEW_TOKEN = "core.view";
|
|
628
526
|
var VIEW_DIRECTORY_CONFIG_KEY = "view.directory";
|
|
629
527
|
var viewProvider = {
|
|
@@ -637,23 +535,11 @@ var viewProvider = {
|
|
|
637
535
|
const engine = new EtaViewEngine(viewsDirectory, (request) => resolveWebLayoutData(container, request ?? currentRequestMeta().request));
|
|
638
536
|
container.set(CORE_VIEW_TOKEN, engine);
|
|
639
537
|
configureWebLayoutData({
|
|
640
|
-
extra: async (
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
try {
|
|
646
|
-
const record = await new repository_default2().findByIdOrThrow(user.id);
|
|
647
|
-
const memberships = await resolveMembershipLookup().listForUser(record.id);
|
|
648
|
-
const organizationsRepo = new repository_default;
|
|
649
|
-
const organizations = (await Promise.all(memberships.map((membership) => organizationsRepo.findById(membership.organization_id)))).filter((organization) => Boolean(organization && !organization.deleted_at));
|
|
650
|
-
const currentId = record.current_organization_id ?? null;
|
|
651
|
-
const currentOrganization = organizations.find((organization) => organization.id === currentId) ?? organizations[0] ?? null;
|
|
652
|
-
return { appName, currentOrganization, organizations };
|
|
653
|
-
} catch {
|
|
654
|
-
return { appName, currentOrganization: null, organizations: [] };
|
|
655
|
-
}
|
|
656
|
-
}
|
|
538
|
+
extra: async () => ({
|
|
539
|
+
appName: appDisplayName(),
|
|
540
|
+
currentOrganization: null,
|
|
541
|
+
organizations: []
|
|
542
|
+
})
|
|
657
543
|
});
|
|
658
544
|
configureWebErrorView({
|
|
659
545
|
render: async (input) => engine.render(errorTemplateName(input.status), {
|
|
@@ -2,15 +2,21 @@
|
|
|
2
2
|
var __jsonParse = (a) => JSON.parse(a);
|
|
3
3
|
|
|
4
4
|
// ../../src/bootstrap/secretsGuard.ts
|
|
5
|
-
|
|
6
|
-
var
|
|
7
|
-
var
|
|
5
|
+
import { isViewsMode, parseFrontendMode } from "@getstrata/core/runtime/frontendMode";
|
|
6
|
+
var PUBLISHED_TEST_ADMIN_API_TOKEN = "strata-admin-test-token";
|
|
7
|
+
var PUBLISHED_TEST_MEMBER_API_TOKEN = "strata-member-test-token";
|
|
8
|
+
var PUBLISHED_TEST_SCIM_BEARER_TOKEN = "strata-scim-test-token";
|
|
8
9
|
var MIN_SESSION_SECRET_LENGTH = 32;
|
|
9
10
|
var PUBLISHED_TEST_TOKENS = new Set([
|
|
10
11
|
PUBLISHED_TEST_ADMIN_API_TOKEN,
|
|
11
|
-
PUBLISHED_TEST_MEMBER_API_TOKEN
|
|
12
|
+
PUBLISHED_TEST_MEMBER_API_TOKEN,
|
|
13
|
+
"workhub-admin-test-token",
|
|
14
|
+
"workhub-member-test-token"
|
|
15
|
+
]);
|
|
16
|
+
var PUBLISHED_TEST_SCIM_TOKENS = new Set([
|
|
17
|
+
PUBLISHED_TEST_SCIM_BEARER_TOKEN,
|
|
18
|
+
"workhub-scim-test-token"
|
|
12
19
|
]);
|
|
13
|
-
var PUBLISHED_TEST_SCIM_TOKENS = new Set([PUBLISHED_TEST_SCIM_BEARER_TOKEN]);
|
|
14
20
|
function isEnabled(value, defaultEnabled) {
|
|
15
21
|
if (value === undefined) {
|
|
16
22
|
return defaultEnabled;
|
|
@@ -34,7 +40,7 @@ function assertAuthDevHeadersDisabled(env) {
|
|
|
34
40
|
function assertSessionSecret(env) {
|
|
35
41
|
const secret = env.SESSION_SECRET?.trim() ?? "";
|
|
36
42
|
if (secret.length < MIN_SESSION_SECRET_LENGTH) {
|
|
37
|
-
throw new Error("Production startup blocked: set SESSION_SECRET when FRONTEND_MODE=server-htmx (32+ characters).");
|
|
43
|
+
throw new Error("Production startup blocked: set SESSION_SECRET when FRONTEND_MODE=server-htmx or hybrid (32+ characters).");
|
|
38
44
|
}
|
|
39
45
|
}
|
|
40
46
|
function assertPublishedTestTokensRotated(env) {
|
|
@@ -96,8 +102,7 @@ function assertProductionSecrets(env = process.env) {
|
|
|
96
102
|
assertTokenAuthProductionSecrets(env);
|
|
97
103
|
}
|
|
98
104
|
assertFeatureProductionSecrets(env);
|
|
99
|
-
|
|
100
|
-
if (frontendMode === "server-htmx") {
|
|
105
|
+
if (isViewsMode(parseFrontendMode(env.FRONTEND_MODE))) {
|
|
101
106
|
assertSessionSecret(env);
|
|
102
107
|
}
|
|
103
108
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
var __jsonParse = (a) => JSON.parse(a);
|
|
3
3
|
|
|
4
4
|
// ../../src/bootstrap/web/routing.ts
|
|
5
|
+
import { requestPrefersJson } from "@getstrata/core/http/contentNegotiation";
|
|
5
6
|
import { withErrorHandling as withErrorHandling2 } from "@getstrata/core/http/response";
|
|
6
7
|
|
|
7
8
|
// ../../src/bootstrap/http/securedRouteModelBinding.ts
|
|
@@ -33,7 +34,7 @@ import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/require
|
|
|
33
34
|
import { createRequirePasswordConfirmMiddleware } from "@getstrata/core/http/requirePasswordConfirmMiddleware";
|
|
34
35
|
import { createRequireVerifiedMiddleware } from "@getstrata/core/http/requireVerifiedMiddleware";
|
|
35
36
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
36
|
-
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
37
|
+
import { withErrorHandling, withJsonErrorHandling } from "@getstrata/core/http/response";
|
|
37
38
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
38
39
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
39
40
|
import { createValidateSignatureMiddleware } from "@getstrata/core/http/signedUrl";
|
|
@@ -181,7 +182,7 @@ class HttpKernel {
|
|
|
181
182
|
return withMiddleware(...middleware)(handler);
|
|
182
183
|
}
|
|
183
184
|
wrapApi(handler) {
|
|
184
|
-
return this.wrap(["api", "authenticated"], handler);
|
|
185
|
+
return withJsonErrorHandling(this.wrap(["api", "authenticated"], handler));
|
|
185
186
|
}
|
|
186
187
|
wrapWeb(handler) {
|
|
187
188
|
return withErrorHandling(this.wrap("web", handler));
|
|
@@ -366,7 +367,7 @@ function wrapWebThrottle(kernel, scope, handler, onThrottled) {
|
|
|
366
367
|
const throttled = scope === "login" ? kernel.wrapLogin(handler) : kernel.wrapRegister(handler);
|
|
367
368
|
return async (request) => {
|
|
368
369
|
const response = await throttled(request);
|
|
369
|
-
if (response.status === 429) {
|
|
370
|
+
if (response.status === 429 && !requestPrefersJson(request)) {
|
|
370
371
|
return onThrottled(request);
|
|
371
372
|
}
|
|
372
373
|
return response;
|
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
var __jsonParse = (a) => JSON.parse(a);
|
|
3
3
|
|
|
4
4
|
// ../../src/bootstrap/web/session.ts
|
|
5
|
-
import {
|
|
5
|
+
import { createHmac, randomBytes } from "crypto";
|
|
6
6
|
import { AuthManager } from "@getstrata/core/auth/guard";
|
|
7
7
|
import { getBoundDatabaseConnection } from "@getstrata/core/database/boundConnection";
|
|
8
8
|
import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
|
|
9
|
+
import { currentSqlDialect } from "@getstrata/core/database/dialect";
|
|
9
10
|
import { readRequestCookie } from "@getstrata/core/http/cookies";
|
|
11
|
+
import { timingSafeCompareString } from "@getstrata/core/security/timingSafeCompare";
|
|
12
|
+
function sqlPlaceholder(index) {
|
|
13
|
+
return currentSqlDialect().placeholder(index);
|
|
14
|
+
}
|
|
15
|
+
function sqlNow() {
|
|
16
|
+
return currentSqlDialect().nowExpression();
|
|
17
|
+
}
|
|
10
18
|
function isSqlClient(value) {
|
|
11
19
|
return typeof value.unsafe === "function";
|
|
12
20
|
}
|
|
@@ -29,22 +37,34 @@ function defaultMapSessionUser(user) {
|
|
|
29
37
|
role: user.is_admin ? "admin" : "member"
|
|
30
38
|
};
|
|
31
39
|
}
|
|
40
|
+
function sessionDisplayName(row, email) {
|
|
41
|
+
if (typeof row.name === "string" && row.name.trim() !== "") {
|
|
42
|
+
return row.name;
|
|
43
|
+
}
|
|
44
|
+
const first = typeof row.first_name === "string" ? row.first_name.trim() : "";
|
|
45
|
+
const last = typeof row.last_name === "string" ? row.last_name.trim() : "";
|
|
46
|
+
const composed = `${first} ${last}`.trim();
|
|
47
|
+
return composed || email;
|
|
48
|
+
}
|
|
49
|
+
function mapSessionUserRow(row) {
|
|
50
|
+
const email = typeof row.email === "string" ? row.email : "";
|
|
51
|
+
return {
|
|
52
|
+
id: Number(row.user_id ?? row.id),
|
|
53
|
+
name: sessionDisplayName(row, email),
|
|
54
|
+
email,
|
|
55
|
+
learn_subscriber: Boolean(row.learn_subscriber),
|
|
56
|
+
is_admin: Boolean(row.is_admin)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
32
59
|
async function defaultLoadSessionUser(sql, sessionId) {
|
|
33
|
-
const rows = await sql.unsafe(`SELECT s.
|
|
34
|
-
COALESCE(u.is_admin, false) AS is_admin
|
|
60
|
+
const rows = await sql.unsafe(`SELECT s.user_id, s.expires_at, u.*
|
|
35
61
|
FROM sessions s
|
|
36
62
|
INNER JOIN users u ON u.id = s.user_id
|
|
37
|
-
WHERE s.id = $1 AND s.expires_at >
|
|
63
|
+
WHERE s.id = ${sqlPlaceholder(1)} AND s.expires_at > ${sqlNow()}`, [sessionId]);
|
|
38
64
|
const row = rows[0];
|
|
39
65
|
if (!row)
|
|
40
66
|
return null;
|
|
41
|
-
return
|
|
42
|
-
id: row.user_id,
|
|
43
|
-
name: row.name,
|
|
44
|
-
email: row.email,
|
|
45
|
-
learn_subscriber: row.learn_subscriber,
|
|
46
|
-
is_admin: row.is_admin
|
|
47
|
-
};
|
|
67
|
+
return mapSessionUserRow(row);
|
|
48
68
|
}
|
|
49
69
|
function redirectWithCookie(location, setCookie, status) {
|
|
50
70
|
return new Response(null, {
|
|
@@ -82,7 +102,7 @@ class CookieSessionStore {
|
|
|
82
102
|
if (!raw)
|
|
83
103
|
return null;
|
|
84
104
|
const [sessionId, signature] = raw.split(".");
|
|
85
|
-
if (!sessionId || !signature || signature
|
|
105
|
+
if (!sessionId || !signature || !timingSafeCompareString(signature, this.sign(sessionId))) {
|
|
86
106
|
return null;
|
|
87
107
|
}
|
|
88
108
|
return sessionId;
|
|
@@ -96,18 +116,28 @@ class CookieSessionStore {
|
|
|
96
116
|
sql() {
|
|
97
117
|
return resolveSql(this.sqlSource);
|
|
98
118
|
}
|
|
99
|
-
async create(user) {
|
|
119
|
+
async create(user, meta = {}) {
|
|
100
120
|
const id = randomBytes(32).toString("hex");
|
|
101
121
|
const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
|
|
102
|
-
await this.sql().unsafe(`INSERT INTO sessions (id, user_id, expires_at
|
|
103
|
-
|
|
104
|
-
user.id,
|
|
105
|
-
expires
|
|
106
|
-
]);
|
|
122
|
+
await this.sql().unsafe(`INSERT INTO sessions (id, user_id, expires_at, user_agent, ip_address, last_active_at)
|
|
123
|
+
VALUES (${sqlPlaceholder(1)}, ${sqlPlaceholder(2)}, ${sqlPlaceholder(3)}, ${sqlPlaceholder(4)}, ${sqlPlaceholder(5)}, ${sqlNow()})`, [id, user.id, expires, meta.userAgent ?? null, meta.ipAddress ?? null]);
|
|
107
124
|
return id;
|
|
108
125
|
}
|
|
109
126
|
async destroy(sessionId) {
|
|
110
|
-
await this.sql().unsafe(`DELETE FROM sessions WHERE id = $1`, [sessionId]);
|
|
127
|
+
await this.sql().unsafe(`DELETE FROM sessions WHERE id = ${sqlPlaceholder(1)}`, [sessionId]);
|
|
128
|
+
}
|
|
129
|
+
async destroyOtherSessions(userId, keepSessionId) {
|
|
130
|
+
await this.sql().unsafe(`DELETE FROM sessions WHERE user_id = ${sqlPlaceholder(1)} AND id <> ${sqlPlaceholder(2)}`, [userId, keepSessionId]);
|
|
131
|
+
}
|
|
132
|
+
async listForUser(userId) {
|
|
133
|
+
const dialect = currentSqlDialect();
|
|
134
|
+
return this.sql().unsafe(`SELECT id, user_id, user_agent, ip_address, last_active_at, expires_at
|
|
135
|
+
FROM sessions
|
|
136
|
+
WHERE user_id = ${dialect.placeholder(1)} AND expires_at > ${dialect.nowExpression()}
|
|
137
|
+
ORDER BY last_active_at DESC${dialect.nullsLastSuffix()}, expires_at DESC`, [userId]);
|
|
138
|
+
}
|
|
139
|
+
async touch(sessionId) {
|
|
140
|
+
await this.sql().unsafe(`UPDATE sessions SET last_active_at = ${sqlNow()} WHERE id = ${sqlPlaceholder(1)}`, [sessionId]);
|
|
111
141
|
}
|
|
112
142
|
async read(request) {
|
|
113
143
|
const sessionId = this.sessionIdFromRequest(request);
|
|
@@ -116,7 +146,7 @@ class CookieSessionStore {
|
|
|
116
146
|
return this.loadSessionUser(this.sql(), sessionId);
|
|
117
147
|
}
|
|
118
148
|
sign(value) {
|
|
119
|
-
return
|
|
149
|
+
return createHmac("sha256", this.secret).update(value).digest("hex").slice(0, 32);
|
|
120
150
|
}
|
|
121
151
|
}
|
|
122
152
|
|
|
@@ -139,11 +169,14 @@ class CookieSessionGuard {
|
|
|
139
169
|
class CookieSessionAuthManager extends AuthManager {
|
|
140
170
|
store;
|
|
141
171
|
constructor(store, mapUser = defaultMapSessionUser) {
|
|
142
|
-
|
|
172
|
+
const guard = new CookieSessionGuard(store, mapUser);
|
|
173
|
+
super(guard);
|
|
143
174
|
this.store = store;
|
|
175
|
+
this.registerGuard("web", guard);
|
|
176
|
+
this.registerGuard("session", guard);
|
|
144
177
|
}
|
|
145
|
-
async signIn(user) {
|
|
146
|
-
const sessionId = await this.store.create(user);
|
|
178
|
+
async signIn(user, meta = {}) {
|
|
179
|
+
const sessionId = await this.store.create(user, meta);
|
|
147
180
|
return { sessionId, setCookie: this.store.cookieHeader(user, sessionId) };
|
|
148
181
|
}
|
|
149
182
|
async signOut(request) {
|
|
@@ -153,8 +186,8 @@ class CookieSessionAuthManager extends AuthManager {
|
|
|
153
186
|
}
|
|
154
187
|
return { setCookie: this.store.clearCookieHeader() };
|
|
155
188
|
}
|
|
156
|
-
async signInRedirect(user, location, status = 302) {
|
|
157
|
-
const { setCookie } = await this.signIn(user);
|
|
189
|
+
async signInRedirect(user, location, status = 302, meta = {}) {
|
|
190
|
+
const { setCookie } = await this.signIn(user, meta);
|
|
158
191
|
return redirectWithCookie(location, setCookie, status);
|
|
159
192
|
}
|
|
160
193
|
async signOutRedirect(request, location, status = 302) {
|