@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 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?: string): RouteHandler;
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
- return Response.redirect(home, 302);
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
- return Response.redirect(home, 302);
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);
@@ -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
- return Response.redirect(home, 302);
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 NotFoundError5 } from "@getstrata/core/errors/http";
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 NotFoundError3 } from "@getstrata/core/errors/http";
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 NotFoundError4 } from "@getstrata/core/errors/http";
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 NotFoundError5(`SCIM user ${id} not found.`);
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 NotFoundError5(`SCIM user ${id} not found.`);
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
- return Response.redirect(home, 302);
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);