@getstrata/bootstrap 0.2.61 → 0.2.63
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 +8 -0
- package/README.md +1 -1
- package/dist/bootstrap/httpKernel.d.ts +6 -2
- package/dist/entries/buildModuleRoutes.js +8 -1
- package/dist/entries/buildWebModuleRoutes.js +8 -1
- package/dist/entries/context.js +164 -0
- package/dist/entries/createRoutes.js +99 -8
- package/dist/entries/createWebRoutes.js +8 -1
- package/dist/entries/dependencies.js +164 -0
- package/dist/entries/httpKernel.js +8 -1
- package/dist/entries/providers/view.js +164 -0
- package/dist/entries/providers.js +164 -0
- package/dist/entries/web/routing.js +8 -1
- package/dist/index.js +172 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @getstrata/bootstrap changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.63
|
|
4
|
+
|
|
5
|
+
- `HttpKernel.wrapWebGuest()` `home` accepts `string | ((user) => string | Promise<string>)` so signed-in guest redirects can follow the current team. Default remains `/organizations`.
|
|
6
|
+
|
|
7
|
+
## 0.2.62
|
|
8
|
+
|
|
9
|
+
- `HttpKernel.wrapWebPasswordConfirm()` is Laravel `password.confirm` for HTML routes. Peer `@getstrata/core` `^0.5.75`.
|
|
10
|
+
|
|
3
11
|
## 0.2.61
|
|
4
12
|
|
|
5
13
|
- `HttpKernel.wrapVerified()` / `wrapWebVerified()` / `wrapWebAuthenticatedAllowUnverified()`. When `FEATURE_EMAIL_VERIFICATION=true`, `wrapWebAuthenticated` and `wrapWebAbility` require a verified email. Peer `@getstrata/core` `^0.5.74`.
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ import { createHttpKernel, createAppContext, coreProviders } from "@getstrata/bo
|
|
|
32
32
|
|
|
33
33
|
Sibling HTMX apps should bind `createCookieSessionAuthManager` from `@getstrata/bootstrap/web/session` instead of HMAC `SessionGuard`. Use `signIn` / `signOut` (or the redirect helpers) instead of calling `CookieSessionStore` from controllers. Pass `mapUser` to map roles in the app. Pass `loadSessionUser` when the default `learn_subscriber` / `is_admin` SELECT does not match your schema. WorkHub’s table is `sessions` (`0031_create_sessions`); its loader is `loadWorkhubSessionUser` (maps `users.role`, decrypts email). WorkHub web login itself stays on HMAC `SessionGuard`.
|
|
34
34
|
|
|
35
|
-
`wrapWeb` applies the web group (CSRF + flash) and `withErrorHandling`, so CSRF `ForbiddenError` becomes an HTML 403 in `FRONTEND_MODE=server-htmx`. `wrapWebGuest` is Laravel `guest` / `RedirectIfAuthenticated` (signed-in users go to `/organizations` by default). `wrapWebLogin` / `wrapWebRegister` include that web group plus throttle — do not wrap them with `wrapWeb` again. The throttle callback should return an HTML form at 429 (WorkHub’s `/login` and `/register` do).
|
|
35
|
+
`wrapWeb` applies the web group (CSRF + flash) and `withErrorHandling`, so CSRF `ForbiddenError` becomes an HTML 403 in `FRONTEND_MODE=server-htmx`. `wrapWebGuest` is Laravel `guest` / `RedirectIfAuthenticated` (signed-in users go to `/organizations` by default; pass a string or `(user) => path` to override). `wrapWebLogin` / `wrapWebRegister` include that web group plus throttle — do not wrap them with `wrapWeb` again. The throttle callback should return an HTML form at 429 (WorkHub’s `/login` and `/register` do).
|
|
36
36
|
|
|
37
37
|
These subpaths remain WorkHub-oriented and are not a generic starter API: `@getstrata/bootstrap/createRoutes` (includes SCIM), `@getstrata/bootstrap/schedule`, and `@getstrata/bootstrap/createWebRoutes` (redirects `/` to `/organizations`).
|
|
38
38
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import type { AuthUser } from "@getstrata/core/auth/authContext";
|
|
1
2
|
import type { Policy } from "@getstrata/core/auth/policy";
|
|
2
3
|
import type { Middleware, RouteHandler } from "@getstrata/core/http/middleware";
|
|
3
4
|
import type { AppDependencies } from "./contracts";
|
|
4
5
|
type MiddlewareGroupName = "api" | "authenticated" | "web";
|
|
6
|
+
type WebGuestHome = string | ((user: AuthUser) => string | Promise<string>);
|
|
5
7
|
declare class HttpKernel {
|
|
6
8
|
private readonly dependencies;
|
|
7
9
|
constructor(dependencies: AppDependencies);
|
|
@@ -11,12 +13,14 @@ declare class HttpKernel {
|
|
|
11
13
|
wrapApi(handler: RouteHandler): RouteHandler;
|
|
12
14
|
wrapWeb(handler: RouteHandler): RouteHandler;
|
|
13
15
|
/** Laravel `guest` / `RedirectIfAuthenticated` — signed-in users go to `home`. */
|
|
14
|
-
wrapWebGuest(handler: RouteHandler, home?:
|
|
16
|
+
wrapWebGuest(handler: RouteHandler, home?: WebGuestHome): RouteHandler;
|
|
15
17
|
wrapWebPublicRead(handler: RouteHandler): RouteHandler;
|
|
16
18
|
wrapWebAuthenticated(handler: RouteHandler): RouteHandler;
|
|
17
19
|
/** Signed-in HTML without Laravel `verified` (logout, verification notice). */
|
|
18
20
|
wrapWebAuthenticatedAllowUnverified(handler: RouteHandler): RouteHandler;
|
|
19
21
|
wrapWebVerified(handler: RouteHandler): RouteHandler;
|
|
22
|
+
/** Laravel `password.confirm` — requires a fresh signed confirmation cookie. */
|
|
23
|
+
wrapWebPasswordConfirm(handler: RouteHandler): RouteHandler;
|
|
20
24
|
wrapWebAbility(ability: string, handler: RouteHandler): RouteHandler;
|
|
21
25
|
wrapWebGlobalAdmin(handler: RouteHandler): RouteHandler;
|
|
22
26
|
wrapAuthenticated(handler: RouteHandler): RouteHandler;
|
|
@@ -34,5 +38,5 @@ declare class HttpKernel {
|
|
|
34
38
|
private wrapThrottle;
|
|
35
39
|
}
|
|
36
40
|
declare function createHttpKernel(dependencies: AppDependencies): HttpKernel;
|
|
37
|
-
export type { MiddlewareGroupName };
|
|
41
|
+
export type { MiddlewareGroupName, WebGuestHome };
|
|
38
42
|
export { createHttpKernel, HttpKernel };
|
|
@@ -25,6 +25,7 @@ import { requestIdMiddleware } from "@getstrata/core/http/middleware";
|
|
|
25
25
|
import { createRequireAbilityMiddleware } from "@getstrata/core/http/requireAbilityMiddleware";
|
|
26
26
|
import { createRequireAuthMiddleware } from "@getstrata/core/http/requireAuthMiddleware";
|
|
27
27
|
import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/requireGlobalAdminMiddleware";
|
|
28
|
+
import { createRequirePasswordConfirmMiddleware } from "@getstrata/core/http/requirePasswordConfirmMiddleware";
|
|
28
29
|
import { createRequireVerifiedMiddleware } from "@getstrata/core/http/requireVerifiedMiddleware";
|
|
29
30
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
30
31
|
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
@@ -188,7 +189,8 @@ class HttpKernel {
|
|
|
188
189
|
if (isEmailVerificationRequired() && !hasVerifiedEmail(user)) {
|
|
189
190
|
return Response.redirect("/email/verify", 302);
|
|
190
191
|
}
|
|
191
|
-
|
|
192
|
+
const location = typeof home === "function" ? await home(user) : home;
|
|
193
|
+
return Response.redirect(location, 302);
|
|
192
194
|
}
|
|
193
195
|
return handler(request);
|
|
194
196
|
});
|
|
@@ -208,6 +210,11 @@ class HttpKernel {
|
|
|
208
210
|
wrapWebVerified(handler) {
|
|
209
211
|
return this.wrapWebAuth(handler, { verified: true });
|
|
210
212
|
}
|
|
213
|
+
wrapWebPasswordConfirm(handler) {
|
|
214
|
+
return this.wrapWebAuth(withMiddleware(createRequirePasswordConfirmMiddleware())(handler), {
|
|
215
|
+
verified: true
|
|
216
|
+
});
|
|
217
|
+
}
|
|
211
218
|
wrapWebAbility(ability, handler) {
|
|
212
219
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
213
220
|
const abilityChecker = resolveAbilityChecker(this.dependencies.container);
|
|
@@ -28,6 +28,7 @@ import { requestIdMiddleware } from "@getstrata/core/http/middleware";
|
|
|
28
28
|
import { createRequireAbilityMiddleware } from "@getstrata/core/http/requireAbilityMiddleware";
|
|
29
29
|
import { createRequireAuthMiddleware } from "@getstrata/core/http/requireAuthMiddleware";
|
|
30
30
|
import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/requireGlobalAdminMiddleware";
|
|
31
|
+
import { createRequirePasswordConfirmMiddleware } from "@getstrata/core/http/requirePasswordConfirmMiddleware";
|
|
31
32
|
import { createRequireVerifiedMiddleware } from "@getstrata/core/http/requireVerifiedMiddleware";
|
|
32
33
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
33
34
|
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
@@ -191,7 +192,8 @@ class HttpKernel {
|
|
|
191
192
|
if (isEmailVerificationRequired() && !hasVerifiedEmail(user)) {
|
|
192
193
|
return Response.redirect("/email/verify", 302);
|
|
193
194
|
}
|
|
194
|
-
|
|
195
|
+
const location = typeof home === "function" ? await home(user) : home;
|
|
196
|
+
return Response.redirect(location, 302);
|
|
195
197
|
}
|
|
196
198
|
return handler(request);
|
|
197
199
|
});
|
|
@@ -211,6 +213,11 @@ class HttpKernel {
|
|
|
211
213
|
wrapWebVerified(handler) {
|
|
212
214
|
return this.wrapWebAuth(handler, { verified: true });
|
|
213
215
|
}
|
|
216
|
+
wrapWebPasswordConfirm(handler) {
|
|
217
|
+
return this.wrapWebAuth(withMiddleware(createRequirePasswordConfirmMiddleware())(handler), {
|
|
218
|
+
verified: true
|
|
219
|
+
});
|
|
220
|
+
}
|
|
214
221
|
wrapWebAbility(ability, handler) {
|
|
215
222
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
216
223
|
const abilityChecker = resolveAbilityChecker(this.dependencies.container);
|
package/dist/entries/context.js
CHANGED
|
@@ -475,15 +475,160 @@ var storageProvider = {
|
|
|
475
475
|
var storage_default = storageProvider;
|
|
476
476
|
|
|
477
477
|
// ../../src/bootstrap/providers/view.ts
|
|
478
|
+
import { resolveMembershipLookup } from "@getstrata/core/auth/membershipContext";
|
|
478
479
|
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
480
|
+
import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
|
|
479
481
|
import { isViewsEnabled } from "@getstrata/core/runtime/frontendMode";
|
|
480
482
|
import {
|
|
481
483
|
configureWebErrorView,
|
|
484
|
+
configureWebLayoutData,
|
|
482
485
|
DEFAULT_VIEWS_DIRECTORY,
|
|
483
486
|
EtaViewEngine,
|
|
484
487
|
errorTemplateName,
|
|
485
488
|
resolveWebLayoutData
|
|
486
489
|
} from "@getstrata/core/view";
|
|
490
|
+
|
|
491
|
+
// ../../src/modules/organization/repository.ts
|
|
492
|
+
import { BaseRepository } from "@getstrata/core/database/baseRepository";
|
|
493
|
+
import { NotFoundError } from "@getstrata/core/errors/http";
|
|
494
|
+
import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
|
|
495
|
+
|
|
496
|
+
// ../../src/modules/organization/table.ts
|
|
497
|
+
import { defineTable } from "@getstrata/core/database/table";
|
|
498
|
+
|
|
499
|
+
// ../../src/domain/workhub.ts
|
|
500
|
+
var ORGANIZATION_TABLE = "organization";
|
|
501
|
+
|
|
502
|
+
// ../../src/modules/organization/table.ts
|
|
503
|
+
var organizationTable = defineTable({
|
|
504
|
+
name: ORGANIZATION_TABLE,
|
|
505
|
+
primaryKey: "id",
|
|
506
|
+
columns: ["id", "tenant_id", "name", "slug", "created_at", "updated_at", "deleted_at"],
|
|
507
|
+
softDeletes: true,
|
|
508
|
+
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// ../../src/modules/organization/repository.ts
|
|
512
|
+
class OrganizationRepository extends BaseRepository {
|
|
513
|
+
constructor() {
|
|
514
|
+
super(organizationTable);
|
|
515
|
+
}
|
|
516
|
+
async findBySlug(slug) {
|
|
517
|
+
return await this.firstOrNull({ slug });
|
|
518
|
+
}
|
|
519
|
+
async listForTenant(options) {
|
|
520
|
+
return await this.findAll({
|
|
521
|
+
limit: options.limit,
|
|
522
|
+
offset: options.offset,
|
|
523
|
+
where: { tenant_id: options.tenantId ?? currentTenantId() }
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
async countForTenant(tenantId = currentTenantId()) {
|
|
527
|
+
return await this.countWhere({ tenant_id: tenantId });
|
|
528
|
+
}
|
|
529
|
+
async findForTenantOrThrow(id, tenantId = currentTenantId()) {
|
|
530
|
+
const organization = await this.findById(id);
|
|
531
|
+
if (!organization || organization.tenant_id !== tenantId) {
|
|
532
|
+
throw new NotFoundError(`SCIM group ${id} not found.`);
|
|
533
|
+
}
|
|
534
|
+
return organization;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
var repository_default = OrganizationRepository;
|
|
538
|
+
|
|
539
|
+
// ../../src/modules/user/repository.ts
|
|
540
|
+
import {
|
|
541
|
+
emailLookupForQuery,
|
|
542
|
+
protectEmail,
|
|
543
|
+
revealEmail
|
|
544
|
+
} from "@getstrata/core/crypto/fieldEncryption";
|
|
545
|
+
import { revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
|
|
546
|
+
import { BaseRepository as BaseRepository2 } from "@getstrata/core/database/baseRepository";
|
|
547
|
+
import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
|
|
548
|
+
|
|
549
|
+
// ../../src/modules/user/table.ts
|
|
550
|
+
import { defineTable as defineTable2 } from "@getstrata/core/database/table";
|
|
551
|
+
var userTable = defineTable2({
|
|
552
|
+
name: "users",
|
|
553
|
+
primaryKey: "id",
|
|
554
|
+
columns: [
|
|
555
|
+
"id",
|
|
556
|
+
"name",
|
|
557
|
+
"email",
|
|
558
|
+
"email_lookup",
|
|
559
|
+
"role",
|
|
560
|
+
"tenant_id",
|
|
561
|
+
"password_hash",
|
|
562
|
+
"email_verified_at",
|
|
563
|
+
"mfa_secret",
|
|
564
|
+
"mfa_enabled",
|
|
565
|
+
"mfa_recovery_codes",
|
|
566
|
+
"profile_photo_path",
|
|
567
|
+
"session_valid_after",
|
|
568
|
+
"current_organization_id",
|
|
569
|
+
"created_at",
|
|
570
|
+
"updated_at"
|
|
571
|
+
],
|
|
572
|
+
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// ../../src/modules/user/repository.ts
|
|
576
|
+
class UserRepository extends BaseRepository2 {
|
|
577
|
+
constructor() {
|
|
578
|
+
super(userTable);
|
|
579
|
+
}
|
|
580
|
+
decode(record) {
|
|
581
|
+
return {
|
|
582
|
+
...record,
|
|
583
|
+
email: revealEmail(record.email),
|
|
584
|
+
mfa_secret: revealMfaSecret(record.mfa_secret)
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
async findById(id) {
|
|
588
|
+
const record = await super.findById(id);
|
|
589
|
+
return record ? this.decode(record) : null;
|
|
590
|
+
}
|
|
591
|
+
async findAll(options = {}) {
|
|
592
|
+
const records = await super.findAll(options);
|
|
593
|
+
return records.map((record) => this.decode(record));
|
|
594
|
+
}
|
|
595
|
+
async create(values) {
|
|
596
|
+
const email = values.email;
|
|
597
|
+
if (!email) {
|
|
598
|
+
throw new Error("Email is required.");
|
|
599
|
+
}
|
|
600
|
+
const protectedEmail = protectEmail(email);
|
|
601
|
+
const record = await super.create({
|
|
602
|
+
...values,
|
|
603
|
+
tenant_id: values.tenant_id ?? currentTenantId2(),
|
|
604
|
+
email: protectedEmail.storedEmail,
|
|
605
|
+
email_lookup: protectedEmail.emailLookup,
|
|
606
|
+
password_hash: values.password_hash ?? ""
|
|
607
|
+
});
|
|
608
|
+
return this.decode(record);
|
|
609
|
+
}
|
|
610
|
+
async updateByIdOrThrow(id, values, errorFactory) {
|
|
611
|
+
const changes = { ...values };
|
|
612
|
+
if (values.email !== undefined) {
|
|
613
|
+
const protectedEmail = protectEmail(values.email);
|
|
614
|
+
changes.email = protectedEmail.storedEmail;
|
|
615
|
+
changes.email_lookup = protectedEmail.emailLookup;
|
|
616
|
+
}
|
|
617
|
+
const record = await super.updateByIdOrThrow(id, changes, errorFactory);
|
|
618
|
+
return this.decode(record);
|
|
619
|
+
}
|
|
620
|
+
async findByEmail(email) {
|
|
621
|
+
const records = await this.findWhere({ email_lookup: emailLookupForQuery(email) }, { limit: 1 });
|
|
622
|
+
const record = records[0];
|
|
623
|
+
return record ? this.decode(record) : null;
|
|
624
|
+
}
|
|
625
|
+
async countForTenant(tenantId = currentTenantId2()) {
|
|
626
|
+
return await this.countWhere({ tenant_id: tenantId });
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
var repository_default2 = UserRepository;
|
|
630
|
+
|
|
631
|
+
// ../../src/bootstrap/providers/view.ts
|
|
487
632
|
var CORE_VIEW_TOKEN = "core.view";
|
|
488
633
|
var VIEW_DIRECTORY_CONFIG_KEY = "view.directory";
|
|
489
634
|
var viewProvider = {
|
|
@@ -496,6 +641,25 @@ var viewProvider = {
|
|
|
496
641
|
config.set(VIEW_DIRECTORY_CONFIG_KEY, viewsDirectory);
|
|
497
642
|
const engine = new EtaViewEngine(viewsDirectory, (request) => resolveWebLayoutData(container, request ?? currentRequestMeta().request));
|
|
498
643
|
container.set(CORE_VIEW_TOKEN, engine);
|
|
644
|
+
configureWebLayoutData({
|
|
645
|
+
extra: async (user) => {
|
|
646
|
+
const appName = appDisplayName();
|
|
647
|
+
if (!user || typeof user.id !== "number") {
|
|
648
|
+
return { appName, currentOrganization: null, organizations: [] };
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
const record = await new repository_default2().findByIdOrThrow(user.id);
|
|
652
|
+
const memberships = await resolveMembershipLookup().listForUser(record.id);
|
|
653
|
+
const organizationsRepo = new repository_default;
|
|
654
|
+
const organizations = (await Promise.all(memberships.map((membership) => organizationsRepo.findById(membership.organization_id)))).filter((organization) => Boolean(organization && !organization.deleted_at));
|
|
655
|
+
const currentId = record.current_organization_id ?? null;
|
|
656
|
+
const currentOrganization = organizations.find((organization) => organization.id === currentId) ?? organizations[0] ?? null;
|
|
657
|
+
return { appName, currentOrganization, organizations };
|
|
658
|
+
} catch {
|
|
659
|
+
return { appName, currentOrganization: null, organizations: [] };
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
});
|
|
499
663
|
configureWebErrorView({
|
|
500
664
|
render: async (input) => engine.render(errorTemplateName(input.status), {
|
|
501
665
|
title: input.title,
|
|
@@ -65,6 +65,7 @@ import { requestIdMiddleware } from "@getstrata/core/http/middleware";
|
|
|
65
65
|
import { createRequireAbilityMiddleware } from "@getstrata/core/http/requireAbilityMiddleware";
|
|
66
66
|
import { createRequireAuthMiddleware } from "@getstrata/core/http/requireAuthMiddleware";
|
|
67
67
|
import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/requireGlobalAdminMiddleware";
|
|
68
|
+
import { createRequirePasswordConfirmMiddleware } from "@getstrata/core/http/requirePasswordConfirmMiddleware";
|
|
68
69
|
import { createRequireVerifiedMiddleware } from "@getstrata/core/http/requireVerifiedMiddleware";
|
|
69
70
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
70
71
|
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
@@ -228,7 +229,8 @@ class HttpKernel {
|
|
|
228
229
|
if (isEmailVerificationRequired() && !hasVerifiedEmail(user)) {
|
|
229
230
|
return Response.redirect("/email/verify", 302);
|
|
230
231
|
}
|
|
231
|
-
|
|
232
|
+
const location = typeof home === "function" ? await home(user) : home;
|
|
233
|
+
return Response.redirect(location, 302);
|
|
232
234
|
}
|
|
233
235
|
return handler(request);
|
|
234
236
|
});
|
|
@@ -248,6 +250,11 @@ class HttpKernel {
|
|
|
248
250
|
wrapWebVerified(handler) {
|
|
249
251
|
return this.wrapWebAuth(handler, { verified: true });
|
|
250
252
|
}
|
|
253
|
+
wrapWebPasswordConfirm(handler) {
|
|
254
|
+
return this.wrapWebAuth(withMiddleware(createRequirePasswordConfirmMiddleware())(handler), {
|
|
255
|
+
verified: true
|
|
256
|
+
});
|
|
257
|
+
}
|
|
251
258
|
wrapWebAbility(ability, handler) {
|
|
252
259
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
253
260
|
const abilityChecker = resolveAbilityChecker(this.dependencies.container);
|
|
@@ -791,7 +798,7 @@ function assertScimIfMatch(request, etagSource) {
|
|
|
791
798
|
// ../../src/modules/scim/service.ts
|
|
792
799
|
import { hashPassword as hashPassword3 } from "@getstrata/core/auth/password";
|
|
793
800
|
import { resolveService } from "@getstrata/core/contracts/di";
|
|
794
|
-
import { NotFoundError as
|
|
801
|
+
import { NotFoundError as NotFoundError7 } from "@getstrata/core/errors/http";
|
|
795
802
|
import { currentTenantId as currentTenantId4 } from "@getstrata/core/tenant/tenantContext";
|
|
796
803
|
|
|
797
804
|
// ../../src/domain/scim.ts
|
|
@@ -1044,6 +1051,7 @@ import {
|
|
|
1044
1051
|
import { OidcProvider } from "@getstrata/core/auth/oauth/oidcProvider";
|
|
1045
1052
|
import { GitHubOAuthProvider, MockOAuthProvider } from "@getstrata/core/auth/oauth/providers";
|
|
1046
1053
|
import { SamlProvider } from "@getstrata/core/auth/oauth/samlProvider";
|
|
1054
|
+
import { getRequiredDependency } from "@getstrata/core/contracts/di";
|
|
1047
1055
|
|
|
1048
1056
|
// ../../src/modules/user/apiTokenRepository.ts
|
|
1049
1057
|
import { BaseRepository as BaseRepository2 } from "@getstrata/core/database/baseRepository";
|
|
@@ -1067,9 +1075,16 @@ var apiTokenTable = defineTable2({
|
|
|
1067
1075
|
});
|
|
1068
1076
|
|
|
1069
1077
|
// ../../src/modules/user/authService.ts
|
|
1078
|
+
import { currentAuthUser } from "@getstrata/core/auth/authContext";
|
|
1070
1079
|
import { hashPassword, verifyPassword } from "@getstrata/core/auth/password";
|
|
1080
|
+
import { normalizeEmail } from "@getstrata/core/crypto/fieldEncryption";
|
|
1071
1081
|
import { protectMfaSecret, revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
|
|
1072
|
-
import { UnauthorizedError, ValidationError } from "@getstrata/core/errors/http";
|
|
1082
|
+
import { UnauthorizedError as UnauthorizedError2, ValidationError } from "@getstrata/core/errors/http";
|
|
1083
|
+
import {
|
|
1084
|
+
generateRecoveryCodes,
|
|
1085
|
+
hashRecoveryCode,
|
|
1086
|
+
recoveryCodeMatches
|
|
1087
|
+
} from "@getstrata/core/security/recoveryCodes";
|
|
1073
1088
|
import { logSecurityEvent } from "@getstrata/core/security/securityEvents";
|
|
1074
1089
|
import { resolveDefaultTokenExpiryDays } from "@getstrata/core/security/tokenExpiry";
|
|
1075
1090
|
import { buildOtpauthUrl, generateTotpSecret, verifyTotp } from "@getstrata/core/security/totp";
|
|
@@ -1078,6 +1093,7 @@ import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tena
|
|
|
1078
1093
|
// ../../src/core/auth/abilityCatalog.ts
|
|
1079
1094
|
var MEMBER_ABILITIES = [
|
|
1080
1095
|
"organizations:read",
|
|
1096
|
+
"organizations:create",
|
|
1081
1097
|
"projects:read",
|
|
1082
1098
|
"projects:create",
|
|
1083
1099
|
"tasks:read",
|
|
@@ -1087,7 +1103,8 @@ var MEMBER_ABILITIES = [
|
|
|
1087
1103
|
"attachments:read",
|
|
1088
1104
|
"attachments:create",
|
|
1089
1105
|
"auth:tokens:read",
|
|
1090
|
-
"auth:tokens:write"
|
|
1106
|
+
"auth:tokens:write",
|
|
1107
|
+
"auth:tokens:delete"
|
|
1091
1108
|
];
|
|
1092
1109
|
var ADMIN_ABILITIES = [
|
|
1093
1110
|
...MEMBER_ABILITIES,
|
|
@@ -1105,6 +1122,14 @@ var ADMIN_ABILITIES = [
|
|
|
1105
1122
|
"webhooks:write",
|
|
1106
1123
|
"audit:read"
|
|
1107
1124
|
];
|
|
1125
|
+
// ../../src/modules/user/mfaRequiredError.ts
|
|
1126
|
+
import { UnauthorizedError } from "@getstrata/core/errors/http";
|
|
1127
|
+
|
|
1128
|
+
// ../../src/modules/user/currentOrganizationService.ts
|
|
1129
|
+
import { assertResourceInCurrentTenant } from "@getstrata/core/auth/membershipScope";
|
|
1130
|
+
import { resolveMembershipService } from "@getstrata/core/auth/membershipService";
|
|
1131
|
+
import { NotFoundError as NotFoundError3 } from "@getstrata/core/errors/http";
|
|
1132
|
+
|
|
1108
1133
|
// ../../src/modules/user/notificationRepository.ts
|
|
1109
1134
|
import { BaseRepository as BaseRepository3 } from "@getstrata/core/database/baseRepository";
|
|
1110
1135
|
|
|
@@ -1118,7 +1143,7 @@ var notificationTable = defineTable3({
|
|
|
1118
1143
|
});
|
|
1119
1144
|
|
|
1120
1145
|
// ../../src/modules/user/notificationService.ts
|
|
1121
|
-
import { NotFoundError as
|
|
1146
|
+
import { NotFoundError as NotFoundError4 } from "@getstrata/core/errors/http";
|
|
1122
1147
|
|
|
1123
1148
|
// ../../src/modules/user/oauthIdentityRepository.ts
|
|
1124
1149
|
import { BaseRepository as BaseRepository4 } from "@getstrata/core/database/baseRepository";
|
|
@@ -1137,11 +1162,16 @@ import { ValidationError as ValidationError2 } from "@getstrata/core/errors/http
|
|
|
1137
1162
|
import { absoluteTemporarySignedUrl } from "@getstrata/core/http/signedUrl";
|
|
1138
1163
|
import { mailer } from "@getstrata/core/mail/mailer";
|
|
1139
1164
|
import { sendMarkdownMail } from "@getstrata/core/mail/markdownMailable";
|
|
1165
|
+
import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
|
|
1140
1166
|
import { logSecurityEvent as logSecurityEvent2 } from "@getstrata/core/security/securityEvents";
|
|
1141
1167
|
import { timingSafeCompareString } from "@getstrata/core/security/timingSafeCompare";
|
|
1142
1168
|
var RESET_TTL_SECONDS = 60 * 60;
|
|
1143
1169
|
var VERIFY_TTL_SECONDS = 60 * 60 * 24;
|
|
1144
1170
|
|
|
1171
|
+
// ../../src/modules/user/profilePhotoService.ts
|
|
1172
|
+
import { BadRequestError, NotFoundError as NotFoundError5 } from "@getstrata/core/errors/http";
|
|
1173
|
+
import { isImageMimeType, resizeImageContents } from "@getstrata/core/media/imageTransform";
|
|
1174
|
+
|
|
1145
1175
|
// ../../src/modules/user/repository.ts
|
|
1146
1176
|
import {
|
|
1147
1177
|
emailLookupForQuery,
|
|
@@ -1168,15 +1198,76 @@ var userTable = defineTable5({
|
|
|
1168
1198
|
"email_verified_at",
|
|
1169
1199
|
"mfa_secret",
|
|
1170
1200
|
"mfa_enabled",
|
|
1201
|
+
"mfa_recovery_codes",
|
|
1202
|
+
"profile_photo_path",
|
|
1203
|
+
"session_valid_after",
|
|
1204
|
+
"current_organization_id",
|
|
1171
1205
|
"created_at",
|
|
1172
1206
|
"updated_at"
|
|
1173
1207
|
],
|
|
1174
1208
|
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
1175
1209
|
});
|
|
1176
1210
|
|
|
1211
|
+
// ../../src/modules/user/repository.ts
|
|
1212
|
+
class UserRepository extends BaseRepository5 {
|
|
1213
|
+
constructor() {
|
|
1214
|
+
super(userTable);
|
|
1215
|
+
}
|
|
1216
|
+
decode(record) {
|
|
1217
|
+
return {
|
|
1218
|
+
...record,
|
|
1219
|
+
email: revealEmail(record.email),
|
|
1220
|
+
mfa_secret: revealMfaSecret2(record.mfa_secret)
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
async findById(id) {
|
|
1224
|
+
const record = await super.findById(id);
|
|
1225
|
+
return record ? this.decode(record) : null;
|
|
1226
|
+
}
|
|
1227
|
+
async findAll(options = {}) {
|
|
1228
|
+
const records = await super.findAll(options);
|
|
1229
|
+
return records.map((record) => this.decode(record));
|
|
1230
|
+
}
|
|
1231
|
+
async create(values) {
|
|
1232
|
+
const email = values.email;
|
|
1233
|
+
if (!email) {
|
|
1234
|
+
throw new Error("Email is required.");
|
|
1235
|
+
}
|
|
1236
|
+
const protectedEmail = protectEmail(email);
|
|
1237
|
+
const record = await super.create({
|
|
1238
|
+
...values,
|
|
1239
|
+
tenant_id: values.tenant_id ?? currentTenantId3(),
|
|
1240
|
+
email: protectedEmail.storedEmail,
|
|
1241
|
+
email_lookup: protectedEmail.emailLookup,
|
|
1242
|
+
password_hash: values.password_hash ?? ""
|
|
1243
|
+
});
|
|
1244
|
+
return this.decode(record);
|
|
1245
|
+
}
|
|
1246
|
+
async updateByIdOrThrow(id, values, errorFactory) {
|
|
1247
|
+
const changes = { ...values };
|
|
1248
|
+
if (values.email !== undefined) {
|
|
1249
|
+
const protectedEmail = protectEmail(values.email);
|
|
1250
|
+
changes.email = protectedEmail.storedEmail;
|
|
1251
|
+
changes.email_lookup = protectedEmail.emailLookup;
|
|
1252
|
+
}
|
|
1253
|
+
const record = await super.updateByIdOrThrow(id, changes, errorFactory);
|
|
1254
|
+
return this.decode(record);
|
|
1255
|
+
}
|
|
1256
|
+
async findByEmail(email) {
|
|
1257
|
+
const records = await this.findWhere({ email_lookup: emailLookupForQuery(email) }, { limit: 1 });
|
|
1258
|
+
const record = records[0];
|
|
1259
|
+
return record ? this.decode(record) : null;
|
|
1260
|
+
}
|
|
1261
|
+
async countForTenant(tenantId = currentTenantId3()) {
|
|
1262
|
+
return await this.countWhere({ tenant_id: tenantId });
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
var repository_default2 = UserRepository;
|
|
1266
|
+
|
|
1177
1267
|
// ../../src/modules/user/tokenService.ts
|
|
1268
|
+
import { ADMIN_ABILITIES as ADMIN_ABILITIES2 } from "@getstrata/core/auth/abilityCatalog";
|
|
1178
1269
|
import { hashApiToken as hashApiToken2 } from "@getstrata/core/auth/tokenHash";
|
|
1179
|
-
import { ForbiddenError, NotFoundError as
|
|
1270
|
+
import { ForbiddenError, NotFoundError as NotFoundError6, ValidationError as ValidationError3 } from "@getstrata/core/errors/http";
|
|
1180
1271
|
import { resolveDefaultTokenExpiryDays as resolveDefaultTokenExpiryDays2 } from "@getstrata/core/security/tokenExpiry";
|
|
1181
1272
|
|
|
1182
1273
|
// ../../src/modules/user/provider.ts
|
|
@@ -1234,7 +1325,7 @@ class ScimService {
|
|
|
1234
1325
|
async findUserRecord(id) {
|
|
1235
1326
|
const user = await this.users.findById(id);
|
|
1236
1327
|
if (!user || user.tenant_id !== currentTenantId4()) {
|
|
1237
|
-
throw new
|
|
1328
|
+
throw new NotFoundError7(`SCIM user ${id} not found.`);
|
|
1238
1329
|
}
|
|
1239
1330
|
return user;
|
|
1240
1331
|
}
|
|
@@ -1276,7 +1367,7 @@ class ScimService {
|
|
|
1276
1367
|
const user = await this.findUserRecord(id);
|
|
1277
1368
|
const deleted = await this.users.deleteById(id);
|
|
1278
1369
|
if (!deleted) {
|
|
1279
|
-
throw new
|
|
1370
|
+
throw new NotFoundError7(`SCIM user ${id} not found.`);
|
|
1280
1371
|
}
|
|
1281
1372
|
return { id: user.id, updated_at: user.updated_at };
|
|
1282
1373
|
}
|
|
@@ -33,6 +33,7 @@ import { requestIdMiddleware } from "@getstrata/core/http/middleware";
|
|
|
33
33
|
import { createRequireAbilityMiddleware } from "@getstrata/core/http/requireAbilityMiddleware";
|
|
34
34
|
import { createRequireAuthMiddleware } from "@getstrata/core/http/requireAuthMiddleware";
|
|
35
35
|
import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/requireGlobalAdminMiddleware";
|
|
36
|
+
import { createRequirePasswordConfirmMiddleware } from "@getstrata/core/http/requirePasswordConfirmMiddleware";
|
|
36
37
|
import { createRequireVerifiedMiddleware } from "@getstrata/core/http/requireVerifiedMiddleware";
|
|
37
38
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
38
39
|
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
@@ -196,7 +197,8 @@ class HttpKernel {
|
|
|
196
197
|
if (isEmailVerificationRequired() && !hasVerifiedEmail(user)) {
|
|
197
198
|
return Response.redirect("/email/verify", 302);
|
|
198
199
|
}
|
|
199
|
-
|
|
200
|
+
const location = typeof home === "function" ? await home(user) : home;
|
|
201
|
+
return Response.redirect(location, 302);
|
|
200
202
|
}
|
|
201
203
|
return handler(request);
|
|
202
204
|
});
|
|
@@ -216,6 +218,11 @@ class HttpKernel {
|
|
|
216
218
|
wrapWebVerified(handler) {
|
|
217
219
|
return this.wrapWebAuth(handler, { verified: true });
|
|
218
220
|
}
|
|
221
|
+
wrapWebPasswordConfirm(handler) {
|
|
222
|
+
return this.wrapWebAuth(withMiddleware(createRequirePasswordConfirmMiddleware())(handler), {
|
|
223
|
+
verified: true
|
|
224
|
+
});
|
|
225
|
+
}
|
|
219
226
|
wrapWebAbility(ability, handler) {
|
|
220
227
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
221
228
|
const abilityChecker = resolveAbilityChecker(this.dependencies.container);
|